Add context menu for Fullscreen mode actions. Issue #1344

This commit is contained in:
Maurice Parker 2019-11-25 19:43:43 -06:00
parent b1471d4d20
commit 323b160b7f
8 changed files with 344 additions and 21 deletions

View File

@ -151,6 +151,7 @@
51B62E68233186730085F949 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B62E67233186730085F949 /* IconView.swift */; };
51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; };
51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; };
51C266EA238C334800F53014 /* ContextMenuPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C266E9238C334800F53014 /* ContextMenuPreviewViewController.swift */; };
51C451A9226377C200C03939 /* ArticlesDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8407167F2262A61100344432 /* ArticlesDatabase.framework */; };
51C451AA226377C200C03939 /* ArticlesDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8407167F2262A61100344432 /* ArticlesDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
51C451B9226377C900C03939 /* Articles.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 840716732262A60F00344432 /* Articles.framework */; };
@ -1321,6 +1322,7 @@
51B62E67233186730085F949 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = "<group>"; };
51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleActivityItemSource.swift; sourceTree = "<group>"; };
51BB7C302335ACDE008E8144 /* page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
51C266E9238C334800F53014 /* ContextMenuPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuPreviewViewController.swift; sourceTree = "<group>"; };
51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboard-Extensions.swift"; sourceTree = "<group>"; };
51C45250226506F400C03939 /* String-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String-Extensions.swift"; sourceTree = "<group>"; };
51C45254226507D200C03939 /* AppAssets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppAssets.swift; sourceTree = "<group>"; };
@ -1948,6 +1950,7 @@
51C4527E2265092C00C03939 /* ArticleViewController.swift */,
517630222336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift */,
51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */,
51C266E9238C334800F53014 /* ContextMenuPreviewViewController.swift */,
5142192923522B5500E07E2C /* ImageViewController.swift */,
514219362352510100E07E2C /* ImageScrollView.swift */,
518651D9235621840078E021 /* ImageTransition.swift */,
@ -2963,7 +2966,7 @@
};
513C5CE5232571C2003D4054 = {
CreatedOnToolsVersion = 11.0;
DevelopmentTeam = 8EQFQ9RY84;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
};
518B2ED12351B3DD00400001 = {
@ -2973,7 +2976,7 @@
TestTargetID = 840D617B2029031C009BC708;
};
6581C73220CED60000F4AD34 = {
DevelopmentTeam = 8EQFQ9RY84;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
};
65ED3FA2235DEF6C0081F399 = {
@ -2986,7 +2989,7 @@
};
840D617B2029031C009BC708 = {
CreatedOnToolsVersion = 9.3;
DevelopmentTeam = 8EQFQ9RY84;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.BackgroundModes = {
@ -2996,7 +2999,7 @@
};
849C645F1ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = 8EQFQ9RY84;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.HardenedRuntime = {
@ -3006,7 +3009,7 @@
};
849C64701ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = 8EQFQ9RY84;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
TestTargetID = 849C645F1ED37A5D003D8FC0;
};
@ -4027,6 +4030,7 @@
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */,
5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */,
518651DA235621840078E021 /* ImageTransition.swift in Sources */,
51C266EA238C334800F53014 /* ContextMenuPreviewViewController.swift in Sources */,
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */,
514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,

View File

@ -92,6 +92,50 @@ extension Article {
return FaviconGenerator.favicon(webFeed)
}
func byline() -> String {
guard let authors = authors ?? webFeed?.authors, !authors.isEmpty else {
return ""
}
// If the author's name is the same as the feed, then we don't want to display it.
// This code assumes that multiple authors would never match the feed name so that
// if there feed owner has an article co-author all authors are given the byline.
if authors.count == 1, let author = authors.first {
if author.name == webFeed?.nameForDisplay {
return ""
}
}
var byline = ""
var isFirstAuthor = true
for author in authors {
if !isFirstAuthor {
byline += ", "
}
isFirstAuthor = false
if let emailAddress = author.emailAddress, emailAddress.contains(" ") {
byline += emailAddress // probably name plus email address
}
else if let name = author.name, let emailAddress = author.emailAddress {
byline += "\(name) <\(emailAddress)>"
}
else if let name = author.name {
byline += name
}
else if let emailAddress = author.emailAddress {
byline += "<\(emailAddress)>"
}
else if let url = author.url {
byline += url
}
}
return byline
}
}
// MARK: Path

View File

@ -43,6 +43,10 @@ struct AppAssets {
return UIImage(named: "articleExtractorOff")!
}()
static var articleExtractorOffSmall: UIImage = {
return UIImage(systemName: "doc.plaintext")!
}()
static var articleExtractorOffTinted: UIImage = {
let image = UIImage(named: "articleExtractorOff")!
return image.maskWithColor(color: AppAssets.primaryAccentColor.cgColor)!
@ -52,6 +56,10 @@ struct AppAssets {
return UIImage(named: "articleExtractorOn")!
}()
static var articleExtractorOnSmall: UIImage = {
return UIImage(systemName: "doc.plaintext")!
}()
static var articleExtractorOnTinted: UIImage = {
let image = UIImage(named: "articleExtractorOn")!
return image.maskWithColor(color: AppAssets.primaryAccentColor.cgColor)!
@ -129,6 +137,18 @@ struct AppAssets {
return UIImage(systemName: "ellipsis.circle")!
}()
static var nextArticleImage: UIImage = {
return UIImage(systemName: "chevron.down")!
}()
static var nextUnreadArticleImage: UIImage = {
return UIImage(systemName: "chevron.down.circle")!
}()
static var prevArticleImage: UIImage = {
return UIImage(systemName: "chevron.up")!
}()
static var openInSidebarImage: UIImage = {
return UIImage(systemName: "arrow.turn.down.left")!
}()

View File

@ -47,6 +47,10 @@ class ArticleViewController: UIViewController {
}()
private var webView: WKWebView!
private lazy var contextMenuInteraction = UIContextMenuInteraction(delegate: self)
private var isFullScreenAvailable: Bool {
return traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed
}
private lazy var transition = ImageTransition(controller: self)
private var clickedImageCompletion: (() -> Void)?
@ -93,6 +97,7 @@ class ArticleViewController: UIViewController {
webView?.evaluateJavaScript("cancelImageLoad();")
webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasClicked)
webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.imageWasShown)
webView.removeInteraction(contextMenuInteraction)
webView.removeFromSuperview()
ArticleViewControllerWebViewProvider.shared.enqueueWebView(webView)
webView = nil
@ -122,6 +127,7 @@ class ArticleViewController: UIViewController {
self.webViewContainer.addChildAndPin(webView)
webView.navigationDelegate = self
webView.uiDelegate = self
self.configureContextMenuInteraction()
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked)
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown)
@ -285,19 +291,8 @@ class ArticleViewController: UIViewController {
coordinator.toggleStarredForCurrentArticle()
}
@IBAction func openBrowser(_ sender: Any) {
coordinator.showBrowserForCurrentArticle()
}
@IBAction func showActivityDialog(_ sender: Any) {
guard let preferredLink = currentArticle?.preferredLink, let url = URL(string: preferredLink) else {
return
}
let itemSource = ArticleActivityItemSource(url: url, subject: currentArticle!.title)
let activityViewController = UIActivityViewController(activityItems: [itemSource], applicationActivities: nil)
activityViewController.popoverPresentationController?.barButtonItem = actionBarButtonItem
present(activityViewController, animated: true)
showActivityDialog()
}
// MARK: Keyboard Shortcuts
@ -357,6 +352,39 @@ extension ArticleViewController: InteractiveNavigationControllerTappable {
}
}
// MARK: UIContextMenuInteractionDelegate
extension ArticleViewController: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: contextMenuPreviewProvider) { [weak self] suggestedActions in
guard let self = self else { return nil }
var actions = [UIAction]()
if let action = self.prevArticleAction() {
actions.append(action)
}
if let action = self.nextArticleAction() {
actions.append(action)
}
actions.append(self.toggleReadAction())
actions.append(self.toggleStarredAction())
if let action = self.nextUnreadArticleAction() {
actions.append(action)
}
actions.append(self.toggleArticleExtractorAction())
actions.append(self.shareAction())
return UIMenu(title: "", children: actions)
}
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
coordinator.showBrowserForCurrentArticle()
}
}
// MARK: WKNavigationDelegate
extension ArticleViewController: WKNavigationDelegate {
@ -492,25 +520,113 @@ private extension ArticleViewController {
}
}
func showActivityDialog() {
guard let preferredLink = currentArticle?.preferredLink, let url = URL(string: preferredLink) else {
return
}
let itemSource = ArticleActivityItemSource(url: url, subject: currentArticle!.title)
let activityViewController = UIActivityViewController(activityItems: [itemSource], applicationActivities: nil)
activityViewController.popoverPresentationController?.barButtonItem = actionBarButtonItem
present(activityViewController, animated: true)
}
func showBars() {
if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed {
if isFullScreenAvailable {
AppDefaults.articleFullscreenEnabled = false
coordinator.showStatusBar()
showNavigationViewConstraint.constant = 0
showToolbarViewConstraint.constant = 0
navigationController?.setNavigationBarHidden(false, animated: true)
navigationController?.setToolbarHidden(false, animated: true)
configureContextMenuInteraction()
}
}
func hideBars() {
if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed {
if isFullScreenAvailable {
AppDefaults.articleFullscreenEnabled = true
coordinator.hideStatusBar()
showNavigationViewConstraint.constant = 44.0
showToolbarViewConstraint.constant = 44.0
navigationController?.setNavigationBarHidden(true, animated: true)
navigationController?.setToolbarHidden(true, animated: true)
configureContextMenuInteraction()
}
}
func configureContextMenuInteraction() {
if isFullScreenAvailable {
if navigationController?.isNavigationBarHidden ?? false {
webView?.addInteraction(contextMenuInteraction)
} else {
webView?.removeInteraction(contextMenuInteraction)
}
}
}
func contextMenuPreviewProvider() -> UIViewController {
let previewProvider = UIStoryboard.main.instantiateController(ofType: ContextMenuPreviewViewController.self)
previewProvider.article = currentArticle
return previewProvider
}
func prevArticleAction() -> UIAction? {
guard coordinator.isPrevArticleAvailable else { return nil }
let title = NSLocalizedString("Previous Article", comment: "Previous Article")
return UIAction(title: title, image: AppAssets.prevArticleImage) { [weak self] action in
self?.coordinator.selectPrevArticle()
}
}
func nextArticleAction() -> UIAction? {
guard coordinator.isNextArticleAvailable else { return nil }
let title = NSLocalizedString("Next Article", comment: "Next Article")
return UIAction(title: title, image: AppAssets.nextArticleImage) { [weak self] action in
self?.coordinator.selectNextArticle()
}
}
func toggleReadAction() -> UIAction {
let read = currentArticle?.status.read ?? false
let title = read ? NSLocalizedString("Mark as Unread", comment: "Mark as Unread") : NSLocalizedString("Mark as Read", comment: "Mark as Read")
let readImage = read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage
return UIAction(title: title, image: readImage) { [weak self] action in
self?.coordinator.toggleReadForCurrentArticle()
}
}
func toggleStarredAction() -> UIAction {
let starred = currentArticle?.status.starred ?? false
let title = starred ? NSLocalizedString("Mark as Unstarred", comment: "Mark as Unstarred") : NSLocalizedString("Mark as Starred", comment: "Mark as Starred")
let starredImage = starred ? AppAssets.starOpenImage : AppAssets.starClosedImage
return UIAction(title: title, image: starredImage) { [weak self] action in
self?.coordinator.toggleStarredForCurrentArticle()
}
}
func nextUnreadArticleAction() -> UIAction? {
guard coordinator.isAnyUnreadAvailable else { return nil }
let title = NSLocalizedString("Next Unread Article", comment: "Next Unread Article")
return UIAction(title: title, image: AppAssets.nextUnreadArticleImage) { [weak self] action in
self?.coordinator.selectNextArticle()
}
}
func toggleArticleExtractorAction() -> UIAction {
let extracted = articleExtractorButton.buttonState == .on
let title = extracted ? NSLocalizedString("Show Feed Article", comment: "Show Feed Article") : NSLocalizedString("Show Reader View", comment: "Show Reader View")
let extractorImage = extracted ? AppAssets.articleExtractorOffSmall : AppAssets.articleExtractorOnSmall
return UIAction(title: title, image: extractorImage) { [weak self] action in
self?.coordinator.toggleArticleExtractor()
}
}
func shareAction() -> UIAction {
let title = NSLocalizedString("Share", comment: "Share")
return UIAction(title: title, image: AppAssets.shareImage) { [weak self] action in
self?.showActivityDialog()
}
}

View File

@ -0,0 +1,50 @@
//
// ContextMenuPreviewViewController.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 11/25/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import Articles
class ContextMenuPreviewViewController: UIViewController {
@IBOutlet weak var blogNameLabel: UILabel!
@IBOutlet weak var blogAuthorLabel: UILabel!
@IBOutlet weak var articleTitleLabel: UILabel!
@IBOutlet weak var dateTimeLabel: UILabel!
var article: Article!
override func viewDidLoad() {
super.viewDidLoad()
blogNameLabel.text = article.webFeed?.nameForDisplay ?? ""
blogAuthorLabel.text = article.byline()
articleTitleLabel.text = article.title ?? ""
let icon = IconView()
icon.iconImage = article.iconImage()
icon.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(icon)
NSLayoutConstraint.activate([
icon.widthAnchor.constraint(equalToConstant: 48),
icon.heightAnchor.constraint(equalToConstant: 48),
icon.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
icon.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20)
])
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .medium
dateTimeLabel.text = dateFormatter.string(from: article.logicalDatePublished)
view.setNeedsLayout()
view.layoutIfNeeded()
preferredContentSize = CGSize(width: view.bounds.width, height: dateTimeLabel.frame.maxY + 8)
}
}

View File

@ -299,6 +299,93 @@
</objects>
<point key="canvasLocation" x="3056.521739130435" y="-759.375"/>
</scene>
<!--Context Menu Preview View Controller-->
<scene sceneID="Tc4-Ma-XSa">
<objects>
<viewController storyboardIdentifier="ContextMenuPreviewViewController" id="CoM-D3-PNS" customClass="ContextMenuPreviewViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="eH6-Fa-Tfi">
<rect key="frame" x="0.0" y="0.0" width="414" height="200"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Blog Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YsT-Lt-Zry">
<rect key="frame" x="20" y="8" width="87" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<color key="textColor" name="primaryAccentColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Blog Author" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7GV-PV-YVq">
<rect key="frame" x="20" y="36.5" width="91" height="21"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Article Title" textAlignment="natural" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iFp-rn-HhQ">
<rect key="frame" x="20" y="74.5" width="136" height="33.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0Hz-Dv-MhU">
<rect key="frame" x="20" y="116" width="44" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9Ms-dt-2M8">
<rect key="frame" x="346" y="8" width="48" height="48"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="width" constant="48" id="d19-Jv-DFz"/>
<constraint firstAttribute="height" constant="48" id="vvL-LM-Qkp"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0ko-zB-cnS">
<rect key="frame" x="20" y="65.5" width="374" height="1"/>
<color key="backgroundColor" systemColor="separatorColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="IVk-Gd-niT"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="9Ms-dt-2M8" firstAttribute="top" secondItem="eH6-Fa-Tfi" secondAttribute="top" constant="8" id="ECM-0Y-axL"/>
<constraint firstItem="0Hz-Dv-MhU" firstAttribute="leading" secondItem="d1t-hb-otl" secondAttribute="leading" constant="20" id="GCs-jq-FwF"/>
<constraint firstItem="iFp-rn-HhQ" firstAttribute="top" secondItem="0ko-zB-cnS" secondAttribute="bottom" constant="8" id="HCu-Fi-dC8"/>
<constraint firstItem="7GV-PV-YVq" firstAttribute="top" secondItem="YsT-Lt-Zry" secondAttribute="bottom" constant="8" id="HCw-VQ-FWp"/>
<constraint firstItem="YsT-Lt-Zry" firstAttribute="top" secondItem="eH6-Fa-Tfi" secondAttribute="top" constant="8" id="IbT-5V-iPB"/>
<constraint firstItem="iFp-rn-HhQ" firstAttribute="leading" secondItem="d1t-hb-otl" secondAttribute="leading" constant="20" id="MyB-pX-SCv"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="iFp-rn-HhQ" secondAttribute="trailing" constant="20" id="NF0-QV-MJa"/>
<constraint firstItem="7GV-PV-YVq" firstAttribute="leading" secondItem="d1t-hb-otl" secondAttribute="leading" constant="20" id="Rh6-Ug-Rkf"/>
<constraint firstAttribute="trailing" secondItem="0ko-zB-cnS" secondAttribute="trailing" constant="20" id="Sfv-FQ-fXh"/>
<constraint firstItem="0Hz-Dv-MhU" firstAttribute="top" secondItem="iFp-rn-HhQ" secondAttribute="bottom" constant="8" id="b1a-tF-MdY"/>
<constraint firstItem="YsT-Lt-Zry" firstAttribute="leading" secondItem="d1t-hb-otl" secondAttribute="leading" constant="20" id="fXj-St-fed"/>
<constraint firstItem="9Ms-dt-2M8" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="7GV-PV-YVq" secondAttribute="trailing" constant="8" id="hkE-jR-WyS"/>
<constraint firstItem="0ko-zB-cnS" firstAttribute="top" relation="greaterThanOrEqual" secondItem="9Ms-dt-2M8" secondAttribute="bottom" constant="8" id="kvc-Go-qdz"/>
<constraint firstItem="d1t-hb-otl" firstAttribute="trailing" secondItem="9Ms-dt-2M8" secondAttribute="trailing" constant="20" id="mO6-1A-xSW"/>
<constraint firstItem="9Ms-dt-2M8" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="YsT-Lt-Zry" secondAttribute="trailing" constant="8" id="pAW-iQ-2lB"/>
<constraint firstItem="0ko-zB-cnS" firstAttribute="top" secondItem="7GV-PV-YVq" secondAttribute="bottom" constant="8" id="rVh-Lq-DrY"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="0Hz-Dv-MhU" secondAttribute="trailing" constant="20" id="sg6-sh-fl5"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="0Hz-Dv-MhU" secondAttribute="bottom" constant="8" id="usR-Xq-BeL"/>
<constraint firstItem="0ko-zB-cnS" firstAttribute="leading" secondItem="eH6-Fa-Tfi" secondAttribute="leading" constant="20" id="wPH-RZ-ZJq"/>
</constraints>
<viewLayoutGuide key="safeArea" id="d1t-hb-otl"/>
</view>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<size key="freeformSize" width="414" height="200"/>
<connections>
<outlet property="articleTitleLabel" destination="iFp-rn-HhQ" id="nxQ-GW-QP3"/>
<outlet property="blogAuthorLabel" destination="7GV-PV-YVq" id="xoY-pG-H7S"/>
<outlet property="blogNameLabel" destination="YsT-Lt-Zry" id="WVx-Mh-Fn7"/>
<outlet property="dateTimeLabel" destination="0Hz-Dv-MhU" id="QaU-do-WRo"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="vGQ-wP-i7Q" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3763.7681159420295" y="-983.03571428571422"/>
</scene>
</scenes>
<resources>
<image name="chevron.down" catalog="system" width="64" height="36"/>

View File

@ -10,6 +10,7 @@
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
"template-rendering-intent" : "template",
"preserves-vector-representation" : true
}
}

View File

@ -10,6 +10,7 @@
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
"template-rendering-intent" : "template",
"preserves-vector-representation" : true
}
}