- Added a Grid layout option to the feed detail menu.
- In Grid layout, the title length and image position options are replaced by grid columns and grid height options.
- When chosen, the right panes are replaced by a Grid view.
- This is a work in progress.
This commit is contained in:
David Sinclair 2022-08-19 19:21:00 -07:00
parent 7e572477a5
commit 926181327a
9 changed files with 310 additions and 96 deletions

View file

@ -35,6 +35,7 @@ class DetailViewController: BaseViewController {
static let left = "titles_on_left"
static let top = "titles_on_top"
static let bottom = "titles_on_bottom"
static let grid = "titles_in_grid"
}
/// How the feed detail and story pages are laid out.
@ -47,6 +48,9 @@ class DetailViewController: BaseViewController {
/// The story pages are at the top, the feed detail at the bottom.
case bottom
/// Using a grid view for the story titles and story pages.
case grid
}
/// How the feed detail and story pages are laid out.
@ -57,6 +61,8 @@ class DetailViewController: BaseViewController {
return .top
case LayoutValue.bottom:
return .bottom
case LayoutValue.grid:
return .grid
default:
return .left
}
@ -71,6 +77,8 @@ class DetailViewController: BaseViewController {
UserDefaults.standard.set(LayoutValue.top, forKey: Key.layout)
case .bottom:
UserDefaults.standard.set(LayoutValue.bottom, forKey: Key.layout)
case .grid:
UserDefaults.standard.set(LayoutValue.grid, forKey: Key.layout)
default:
UserDefaults.standard.set(LayoutValue.left, forKey: Key.layout)
}
@ -89,6 +97,11 @@ class DetailViewController: BaseViewController {
return layout == .top
}
/// Whether or not using the grid layout; see also the previous property.
@objc var storyTitlesInGrid: Bool {
return layout == .grid
}
/// Preference values.
enum BehaviorValue {
static let auto = "auto"
@ -183,9 +196,12 @@ class DetailViewController: BaseViewController {
/// The feed detail view controller in the supplementary pane, loaded from the storyboard.
var supplementaryFeedDetailViewController: FeedDetailViewController?
/// The feed detail view controller, if using `top` or `bottom` layout. `nil` if using `left` layout.
/// The feed detail view controller, if using `top`, `bottom`, or `grid` layout. `nil` if using `left` layout.
var feedDetailViewController: FeedDetailViewController?
/// The grid detail view controller, if using `grid` layout. `nil` for other layouts.
var gridDetailViewController: GridDetailViewController?
/// The horizontal page view controller. [Not currently used; might be used for #1351 (gestures in vertical scrolling).]
// var horizontalPageViewController: HorizontalPageViewController?
@ -232,10 +248,6 @@ class DetailViewController: BaseViewController {
navigationController?.navigationBar.barStyle = manager.isDarkTheme ? .black : .default
tidyNavigationController()
// DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
// self.findSplitBackButton()
// }
}
/// Adjusts the container when autoscrolling. Only applies to iPhone.
@ -244,46 +256,6 @@ class DetailViewController: BaseViewController {
updateTheme()
}
// @objc func findSplitBackButton() {
// guard let navBar = navigationController?.navigationBar else {
// return
// }
//
// let imageViews = recursiveImageSubviews(of: navBar)
//
// for view in imageViews {
// if let imageView = view as? UIImageView, let image = imageView.image, image.description.contains("BackIndicator"), let button = recursiveButtonSuperview(of: imageView) {
// print("image view: \(imageView), image: \(String(describing: imageView.image)), button: \(button)")
// }
// }
// }
//
// func recursiveImageSubviews(of view: UIView) -> [UIView] {
// var subviews = [UIView]()
//
// for subview in view.subviews {
// if subview is UIImageView {
// subviews.append(subview)
// } else {
// subviews.append(contentsOf: recursiveImageSubviews(of: subview))
// }
// }
//
// return subviews
// }
//
// func recursiveButtonSuperview(of view: UIView) -> UIButton? {
// guard let superview = view.superview else {
// return nil
// }
//
// if let button = superview as? UIButton {
// return button
// }
//
// return recursiveButtonSuperview(of: superview)
// }
override func viewDidLoad() {
super.viewDidLoad()
@ -359,6 +331,11 @@ private extension DetailViewController {
func checkViewControllers() {
let isTop = layout == .top
if layout != .grid, gridDetailViewController != nil {
remove(viewController: gridDetailViewController)
gridDetailViewController = nil
}
if layout == .left {
if feedDetailViewController != nil {
remove(viewController: feedDetailViewController)
@ -371,6 +348,28 @@ private extension DetailViewController {
supplementaryFeedDetailViewController = nil
}
dividerViewBottomConstraint.constant = -13
} else if layout == .grid {
if gridDetailViewController == nil {
gridDetailViewController = Storyboards.shared.controller(withIdentifier: .gridDetail) as? GridDetailViewController
feedDetailViewController = Storyboards.shared.controller(withIdentifier: .feedDetail) as? FeedDetailViewController
add(viewController: gridDetailViewController, top: true)
add(viewController: feedDetailViewController, top: false)
supplementaryFeedDetailNavigationController = appDelegate.feedDetailNavigationController
supplementaryFeedDetailViewController = appDelegate.feedDetailViewController
appDelegate.feedDetailNavigationController = nil
appDelegate.feedDetailViewController = feedDetailViewController
appDelegate.splitViewController.setViewController(nil, for: .supplementary)
} else {
let appropriateSuperview = isTop ? topContainerView : bottomContainerView
if feedDetailViewController?.view.superview != appropriateSuperview {
add(viewController: feedDetailViewController, top: true)
}
}
dividerViewBottomConstraint.constant = -13
} else {
if feedDetailViewController == nil {
@ -396,30 +395,19 @@ private extension DetailViewController {
appDelegate.updateSplitBehavior()
}
guard let storyPagesViewController = storyPagesViewController else {
return
}
let appropriateSuperview = isTop ? bottomContainerView : topContainerView
if storyPagesViewController.view.superview != appropriateSuperview {
add(viewController: storyPagesViewController, top: !isTop)
if layout != .grid {
guard let storyPagesViewController = storyPagesViewController else {
return
}
adjustForAutoscroll()
let appropriateSuperview = isTop ? bottomContainerView : topContainerView
// if isTop {
// bottomContainerView.addSubview(traverseView)
// bottomContainerView.addSubview(autoscrollView)
// } else {
// topContainerView.addSubview(traverseView)
// topContainerView.addSubview(autoscrollView)
// }
if storyPagesViewController.view.superview != appropriateSuperview {
add(viewController: storyPagesViewController, top: !isTop)
adjustForAutoscroll()
}
}
//
// traverseTopContainerBottomConstraint.isActive = !isTop
// traverseBottomContainerBottomConstraint.isActive = isTop
// autoscrollTopContainerBottomConstraint.isActive = !isTop
// autoscrollBottomContainerBottomConstraint.isActive = isTop
}
func add(viewController: UIViewController?, top: Bool) {

View file

@ -2355,42 +2355,62 @@ didEndSwipingSwipingWithState:(MCSwipeTableViewCellState)state
[appDelegate addSplitControlToMenuController:viewController];
NSString *preferenceKey = @"story_titles_position";
NSArray *titles = @[@"Left", @"Top", @"Bottom"];
NSArray *values = @[@"titles_on_left", @"titles_on_top", @"titles_on_bottom"];
NSArray *titles = @[@"Left", @"Top", @"Bottom", @"Grid"];
NSArray *values = @[@"titles_on_left", @"titles_on_top", @"titles_on_bottom", @"titles_in_grid"];
[viewController addSegmentedControlWithTitles:titles values:values preferenceKey:preferenceKey selectionShouldDismiss:YES handler:^(NSUInteger selectedIndex) {
[self.appDelegate.detailViewController updateLayoutWithReload:YES];
}];
if (self.appDelegate.detailViewController.storyTitlesInGrid) {
NSString *preferenceKey = @"grid_columns";
NSArray *titles = @[@"Auto Cols", @"2", @"3", @"4"];
NSArray *values = @[@"auto", @"2", @"3", @"4"];
[viewController addSegmentedControlWithTitles:titles values:values defaultValue:@"auto" preferenceKey:preferenceKey selectionShouldDismiss:NO handler:^(NSUInteger selectedIndex) {
[self.appDelegate.detailViewController updateLayoutWithReload:YES];
}];
preferenceKey = @"grid_height";
titles = @[@"XS", @"Short", @"Medium", @"Tall", @"XL"];
values = @[@"xs", @"short", @"medium", @"tall", @"xl"];
[viewController addSegmentedControlWithTitles:titles values:values defaultValue:@"medium" preferenceKey:preferenceKey selectionShouldDismiss:NO handler:^(NSUInteger selectedIndex) {
[self.appDelegate.detailViewController updateLayoutWithReload:YES];
}];
}
}
NSString *preferenceKey = @"story_list_preview_text_size";
NSArray *titles = @[@"Title", @"content_preview_small.png", @"content_preview_medium.png", @"content_preview_large.png"];
NSArray *values = @[@"title", @"short", @"medium", @"long"];
[viewController addSegmentedControlWithTitles:titles values:values preferenceKey:preferenceKey selectionShouldDismiss:NO handler:^(NSUInteger selectedIndex) {
[self.appDelegate resizePreviewSize];
}];
// Upgrade the prefs; can remove these lines eventually, once most existing users are likely on version 11 or later.
NSString *preview = [[NSUserDefaults standardUserDefaults] stringForKey:@"story_list_preview_images_size"];
if ([preview isEqualToString:@"small"]) {
[[NSUserDefaults standardUserDefaults] setObject:@"small_right" forKey:@"story_list_preview_images_size"];
} else if ([preview isEqualToString:@"large"]) {
[[NSUserDefaults standardUserDefaults] setObject:@"large_right" forKey:@"story_list_preview_images_size"];
if (!self.appDelegate.detailViewController.storyTitlesInGrid) {
NSString *preferenceKey = @"story_list_preview_text_size";
NSArray *titles = @[@"Title", @"content_preview_small.png", @"content_preview_medium.png", @"content_preview_large.png"];
NSArray *values = @[@"title", @"short", @"medium", @"long"];
[viewController addSegmentedControlWithTitles:titles values:values preferenceKey:preferenceKey selectionShouldDismiss:NO handler:^(NSUInteger selectedIndex) {
[self.appDelegate resizePreviewSize];
}];
// Upgrade the prefs; can remove these lines eventually, once most existing users are likely on version 11 or later.
NSString *preview = [[NSUserDefaults standardUserDefaults] stringForKey:@"story_list_preview_images_size"];
if ([preview isEqualToString:@"small"]) {
[[NSUserDefaults standardUserDefaults] setObject:@"small_right" forKey:@"story_list_preview_images_size"];
} else if ([preview isEqualToString:@"large"]) {
[[NSUserDefaults standardUserDefaults] setObject:@"large_right" forKey:@"story_list_preview_images_size"];
}
preferenceKey = @"story_list_preview_images_size";
titles = @[@"No image", @"image_preview_small_left.png", @"image_preview_large_left.png", @"image_preview_large_right.png", @"image_preview_small_right.png"];
values = @[@"none", @"small_left", @"large_left", @"large_right", @"small_right"];
[viewController addSegmentedControlWithTitles:titles values:values preferenceKey:preferenceKey selectionShouldDismiss:NO handler:^(NSUInteger selectedIndex) {
[self.appDelegate resizePreviewSize];
}];
}
preferenceKey = @"story_list_preview_images_size";
titles = @[@"No image", @"image_preview_small_left.png", @"image_preview_large_left.png", @"image_preview_large_right.png", @"image_preview_small_right.png"];
values = @[@"none", @"small_left", @"large_left", @"large_right", @"small_right"];
[viewController addSegmentedControlWithTitles:titles values:values preferenceKey:preferenceKey selectionShouldDismiss:NO handler:^(NSUInteger selectedIndex) {
[self.appDelegate resizePreviewSize];
}];
preferenceKey = @"feed_list_font_size";
titles = @[@"XS", @"S", @"M", @"L", @"XL"];
values = @[@"xs", @"small", @"medium", @"large", @"xl"];
NSString *preferenceKey = @"feed_list_font_size";
NSArray *titles = @[@"XS", @"S", @"M", @"L", @"XL"];
NSArray *values = @[@"xs", @"small", @"medium", @"large", @"xl"];
[viewController addSegmentedControlWithTitles:titles values:values preferenceKey:preferenceKey selectionShouldDismiss:NO handler:^(NSUInteger selectedIndex) {
[self.appDelegate resizeFontSize];

View file

@ -0,0 +1,113 @@
//
// GridDetailViewController.swift
// NewsBlur
//
// Created by David Sinclair on 2022-08-19.
// Copyright © 2022 NewsBlur. All rights reserved.
//
import UIKit
/// A view controller to manage the Grid layout.
class GridDetailViewController: UIViewController {
@IBOutlet var collectionView: UICollectionView!
enum SectionLayoutKind: Int, CaseIterable {
/// Feed cell kind.
case feed
/// Story cell kind.
case story
}
var feedColumns: Int {
guard let pref = UserDefaults.standard.string(forKey: "grid_columns"), let columns = Int(pref) else {
return 4
}
return columns
}
var dataSource: UICollectionViewDiffableDataSource<SectionLayoutKind, Int>! = nil
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = createLayout()
configureDataSource()
}
}
extension GridDetailViewController {
func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int,
layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let sectionLayoutKind = SectionLayoutKind(rawValue: sectionIndex) else {
return nil
}
let columns = sectionLayoutKind == .feed ? self.feedColumns : 1
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2)
let groupHeight = columns == 1 ?
NSCollectionLayoutDimension.absolute(44) :
NSCollectionLayoutDimension.fractionalWidth(0.2)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: groupHeight)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
return section
}
return layout
}
}
extension GridDetailViewController {
func configureDataSource() {
let feedCellRegistration = UICollectionView.CellRegistration<GridFeedCell, Int> { (cell, indexPath, identifier) in
//TODO: 🚧
}
let storyCellRegistration = UICollectionView.CellRegistration<GridStoryCell, Int> { (cell, indexPath, identifier) in
//TODO: 🚧
cell.contentView.backgroundColor = UIColor.red
cell.contentView.layer.borderColor = UIColor.black.cgColor
cell.contentView.layer.borderWidth = 1
cell.contentView.layer.cornerRadius = SectionLayoutKind(rawValue: indexPath.section)! == .feed ? 8 : 0
}
dataSource = UICollectionViewDiffableDataSource<SectionLayoutKind, Int>(collectionView: collectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
return SectionLayoutKind(rawValue: indexPath.section)! == .story ?
collectionView.dequeueConfiguredReusableCell(using: feedCellRegistration, for: indexPath, item: identifier) :
collectionView.dequeueConfiguredReusableCell(using: storyCellRegistration, for: indexPath, item: identifier)
}
let itemsPerSection = 10
var snapshot = NSDiffableDataSourceSnapshot<SectionLayoutKind, Int>()
SectionLayoutKind.allCases.forEach {
snapshot.appendSections([$0])
let itemOffset = $0.rawValue * itemsPerSection
let itemUpperbound = itemOffset + itemsPerSection
snapshot.appendItems(Array(itemOffset..<itemUpperbound))
}
dataSource.apply(snapshot, animatingDifferences: false)
}
}
extension GridDetailViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
}
}

View file

@ -0,0 +1,15 @@
//
// GridFeedCell.swift
// NewsBlur
//
// Created by David Sinclair on 2022-08-19.
// Copyright © 2022 NewsBlur. All rights reserved.
//
import UIKit
/// Collection view cell for a feed detail title.
class GridFeedCell: UICollectionViewCell {
static let reuseIdentifier = "GridFeedCell"
}

View file

@ -0,0 +1,15 @@
//
// GridStoryCell.swift
// NewsBlur
//
// Created by David Sinclair on 2022-08-19.
// Copyright © 2022 NewsBlur. All rights reserved.
//
import UIKit
/// Collection view cell for a story.
class GridStoryCell: UICollectionViewCell {
static let reuseIdentifier = "GridStoryCell"
}

View file

@ -20,6 +20,7 @@ class Storyboards {
/// Main storyboard identifiers.
enum Main: String {
case feedDetail = "FeedDetailViewController"
case gridDetail = "GridDetailViewController"
case horizontalPages = "HorizontalPageViewController"
case verticalPages = "VerticalPageViewController"
// case storyDetail = "StoryDetailViewController" // loading from XIB currently

View file

@ -80,6 +80,9 @@
177551D5238E228A00E27818 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 177551D4238E228A00E27818 /* NotificationCenter.framework */; platformFilter = ios; };
177551DB238E228A00E27818 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 177551D9238E228A00E27818 /* MainInterface.storyboard */; };
177551DF238E228A00E27818 /* Old NewsBlur Latest.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 177551D3238E228A00E27818 /* Old NewsBlur Latest.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
177D017828B056C600F2F2DB /* GridDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177D017728B056C600F2F2DB /* GridDetailViewController.swift */; };
177D017B28B05D2500F2F2DB /* GridFeedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177D017A28B05D2500F2F2DB /* GridFeedCell.swift */; };
177D017D28B05D9500F2F2DB /* GridStoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177D017C28B05D9500F2F2DB /* GridStoryCell.swift */; };
17813FB723AC6E450057FB16 /* WidgetErrorTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 17CE3F0523AC529B003152EF /* WidgetErrorTableViewCell.xib */; };
1787083024F8B3A50000C82B /* StoryDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1787082F24F8B3A50000C82B /* StoryDetailViewController.swift */; };
17876B9E1C9911D40055DD15 /* g_icn_folder_rss_sm.png in Resources */ = {isa = PBXBuildFile; fileRef = 17876B9A1C9911D40055DD15 /* g_icn_folder_rss_sm.png */; };
@ -823,6 +826,9 @@
177551DA238E228A00E27818 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
177551DC238E228A00E27818 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
177551E3238E26BF00E27818 /* Widget Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Widget Extension.entitlements"; sourceTree = "<group>"; };
177D017728B056C600F2F2DB /* GridDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridDetailViewController.swift; sourceTree = "<group>"; };
177D017A28B05D2500F2F2DB /* GridFeedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridFeedCell.swift; sourceTree = "<group>"; };
177D017C28B05D9500F2F2DB /* GridStoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridStoryCell.swift; sourceTree = "<group>"; };
1787082F24F8B3A50000C82B /* StoryDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryDetailViewController.swift; sourceTree = "<group>"; };
17876B9A1C9911D40055DD15 /* g_icn_folder_rss_sm.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = g_icn_folder_rss_sm.png; sourceTree = "<group>"; };
17876B9B1C9911D40055DD15 /* g_icn_folder_rss_sm@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "g_icn_folder_rss_sm@2x.png"; sourceTree = "<group>"; };
@ -1691,6 +1697,7 @@
431B857315A131C300DCE497 /* Feeds */,
431B857215A131B200DCE497 /* Feed-Detail */,
431B857415A1324200DCE497 /* Story */,
177D017928B05CBF00F2F2DB /* Grid */,
431B857115A1317000DCE497 /* FTUX */,
431B857015A1315F00DCE497 /* Social */,
FF3FA8871BB26595001F7C32 /* Activities */,
@ -1802,6 +1809,16 @@
path = "Old Widget Extension";
sourceTree = "<group>";
};
177D017928B05CBF00F2F2DB /* Grid */ = {
isa = PBXGroup;
children = (
177D017728B056C600F2F2DB /* GridDetailViewController.swift */,
177D017A28B05D2500F2F2DB /* GridFeedCell.swift */,
177D017C28B05D9500F2F2DB /* GridStoryCell.swift */,
);
name = Grid;
sourceTree = "<group>";
};
19C28FACFE9D520D11CA2CBB /* Products */ = {
isa = PBXGroup;
children = (
@ -3704,6 +3721,7 @@
FF2924E61E932D2900FCFA63 /* PINDiskCache.m in Sources */,
7843F50511EEB1A000675F64 /* FeedDetailTableCell.m in Sources */,
784B50ED127E3F68008F90EA /* LoginViewController.m in Sources */,
177D017D28B05D9500F2F2DB /* GridStoryCell.swift in Sources */,
FF5ACC241DE5F0C000FBD044 /* NotificationsViewController.m in Sources */,
FFCDD8FE17F6368F000C6483 /* MCSwipeTableViewCell.m in Sources */,
78095EC9128F30B500230C8E /* OriginalStoryViewController.m in Sources */,
@ -3763,6 +3781,7 @@
17432C831C53438D003F8FD6 /* FeedChooserViewController.m in Sources */,
010EDEFA1B2386B7003B79DE /* OnePasswordExtension.m in Sources */,
43ABBCAA15B53A1400EA3111 /* InteractionCell.m in Sources */,
177D017828B056C600F2F2DB /* GridDetailViewController.swift in Sources */,
FF8D1ECF1BAA311000725D8A /* SBJson4StreamTokeniser.m in Sources */,
FF8D1ED81BAA33BA00725D8A /* NSObject+SBJSON.m in Sources */,
172AD274251D9F40000BB264 /* Storyboards.swift in Sources */,
@ -3791,6 +3810,7 @@
176A5C7A24F8BD1B009E8DF9 /* DetailViewController.swift in Sources */,
17432C7F1C533FBC003F8FD6 /* MenuViewController.m in Sources */,
FFF8B3AF1F847505001AB95E /* NBDashboardNavigationBar.m in Sources */,
177D017B28B05D2500F2F2DB /* GridFeedCell.swift in Sources */,
17E635AF1C548C580075338E /* FeedChooserItem.m in Sources */,
FF6A233216448E0700E15989 /* StoryPagesObjCViewController.m in Sources */,
FF67D3B2168924C40057A7DA /* TrainerViewController.m in Sources */,

View file

@ -303,6 +303,46 @@
</objects>
<point key="canvasLocation" x="-546" y="1835"/>
</scene>
<!--Grid Detail View Controller-->
<scene sceneID="OXZ-eV-qQz">
<objects>
<viewController storyboardIdentifier="GridDetailViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="QmF-fv-1Bp" customClass="GridDetailViewController" customModule="NewsBlur" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="JQ6-ZA-aZb">
<rect key="frame" x="0.0" y="0.0" width="1194" height="834"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="vaf-JJ-7oP">
<rect key="frame" x="0.0" y="0.0" width="1194" height="834"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="Fmx-jr-pAt">
<size key="itemSize" width="128" height="128"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells/>
<connections>
<outlet property="delegate" destination="QmF-fv-1Bp" id="UAX-33-p9U"/>
</connections>
</collectionView>
</subviews>
<viewLayoutGuide key="safeArea" id="pwo-OK-kJk"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="vaf-JJ-7oP" firstAttribute="top" secondItem="JQ6-ZA-aZb" secondAttribute="top" id="7F9-cE-mjz"/>
<constraint firstAttribute="trailing" secondItem="vaf-JJ-7oP" secondAttribute="trailing" id="Dp9-b5-7mR"/>
<constraint firstAttribute="bottom" secondItem="vaf-JJ-7oP" secondAttribute="bottom" id="jin-ea-ii1"/>
<constraint firstItem="vaf-JJ-7oP" firstAttribute="leading" secondItem="JQ6-ZA-aZb" secondAttribute="leading" id="uJn-0H-m3i"/>
</constraints>
</view>
<connections>
<outlet property="collectionView" destination="vaf-JJ-7oP" id="BkE-3h-foS"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="5kV-BU-lYP" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="181" y="2678"/>
</scene>
<!--Horizontal Page View Controller-->
<scene sceneID="Dnt-lB-G3h">
<objects>
@ -315,7 +355,7 @@
<placeholder placeholderIdentifier="IBFirstResponder" id="y78-Tv-B24" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<customObject id="9g1-OD-yDA" customClass="HorizontalPageDelegate" customModule="NewsBlur" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="181" y="2603"/>
<point key="canvasLocation" x="181" y="3488"/>
</scene>
<!--Vertical Page View Controller-->
<scene sceneID="jpJ-mR-ccR">
@ -329,7 +369,7 @@
<placeholder placeholderIdentifier="IBFirstResponder" id="rdh-By-bOV" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<customObject id="aFI-Ht-5Fb" customClass="VerticalPageDelegate" customModule="NewsBlur" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="181" y="3363"/>
<point key="canvasLocation" x="181" y="4247"/>
</scene>
<!--Feed Detail View Controller-->
<scene sceneID="P4B-M4-aIa">

View file

@ -216,6 +216,7 @@
<string>Titles on left</string>
<string>Titles on top</string>
<string>Titles on bottom</string>
<string>Titles in grid</string>
</array>
<key>DefaultValue</key>
<string>titles_on_left</string>
@ -224,6 +225,7 @@
<string>titles_on_left</string>
<string>titles_on_top</string>
<string>titles_on_bottom</string>
<string>titles_in_grid</string>
</array>
<key>Key</key>
<string>story_titles_position</string>