Compare commits
2 Commits
40a742139b
...
864fd77ecc
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 864fd77ecc | |
Shadowfacts | 78da04162f |
|
@ -117,6 +117,7 @@
|
||||||
D63F9C6B241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */; };
|
D63F9C6B241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */; };
|
||||||
D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */; };
|
D63F9C6C241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */; };
|
||||||
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
D63F9C6E241D2D85004C03CF /* CompositionAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */; };
|
||||||
|
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */; };
|
||||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
|
||||||
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; };
|
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */; };
|
||||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
||||||
|
@ -424,6 +425,7 @@
|
||||||
D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
D63F9C69241C50B9004C03CF /* ComposeAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeAttachmentTableViewCell.xib; sourceTree = "<group>"; };
|
D63F9C6A241C50B9004C03CF /* ComposeAttachmentTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeAttachmentTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
D63F9C6D241D2D85004C03CF /* CompositionAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionAttachment.swift; sourceTree = "<group>"; };
|
||||||
|
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectImageButton.swift; sourceTree = "<group>"; };
|
||||||
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
|
||||||
D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
D641C772213CAA25004B4513 /* NotificationsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1219,6 +1221,7 @@
|
||||||
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
D627943A23A55BA600D38C68 /* NavigableTableViewCell.swift */,
|
||||||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */,
|
||||||
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
D627944623A6AC9300D38C68 /* BasicTableViewCell.xib */,
|
||||||
|
D6403CC124A6B72D00E81C55 /* VisualEffectImageButton.swift */,
|
||||||
D67C57A721E2649B00C3118B /* Account Detail */,
|
D67C57A721E2649B00C3118B /* Account Detail */,
|
||||||
D67C57B021E28F9400C3118B /* Compose Status Reply */,
|
D67C57B021E28F9400C3118B /* Compose Status Reply */,
|
||||||
D626494023C122C800612E6E /* Asset Picker */,
|
D626494023C122C800612E6E /* Asset Picker */,
|
||||||
|
@ -1491,7 +1494,7 @@
|
||||||
D6D4DDC4212518A000E1C4BB /* Project object */ = {
|
D6D4DDC4212518A000E1C4BB /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 1000;
|
LastSwiftUpdateCheck = 1200;
|
||||||
LastUpgradeCheck = 1020;
|
LastUpgradeCheck = 1020;
|
||||||
ORGANIZATIONNAME = Shadowfacts;
|
ORGANIZATIONNAME = Shadowfacts;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
@ -1504,6 +1507,9 @@
|
||||||
LastSwiftMigration = 1020;
|
LastSwiftMigration = 1020;
|
||||||
TestTargetID = D6D4DDCB212518A000E1C4BB;
|
TestTargetID = D6D4DDCB212518A000E1C4BB;
|
||||||
};
|
};
|
||||||
|
D68E526124A3F9AF0054355A = {
|
||||||
|
CreatedOnToolsVersion = 12.0;
|
||||||
|
};
|
||||||
D6D4DDCB212518A000E1C4BB = {
|
D6D4DDCB212518A000E1C4BB = {
|
||||||
CreatedOnToolsVersion = 10.0;
|
CreatedOnToolsVersion = 10.0;
|
||||||
LastSwiftMigration = 1020;
|
LastSwiftMigration = 1020;
|
||||||
|
@ -1765,6 +1771,7 @@
|
||||||
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
D6B053A623BD2D0C00A066FA /* AssetCollectionViewController.swift in Sources */,
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||||
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
|
D627FF7B217E951500CC0648 /* DraftsTableViewController.swift in Sources */,
|
||||||
|
D6403CC224A6B72D00E81C55 /* VisualEffectImageButton.swift in Sources */,
|
||||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
||||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
||||||
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
|
D6A3BC8F2321FFB900FD64D5 /* StatusActionAccountListTableViewController.swift in Sources */,
|
||||||
|
|
|
@ -19,6 +19,8 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
var resultsController: SearchResultsViewController!
|
var resultsController: SearchResultsViewController!
|
||||||
var searchController: UISearchController!
|
var searchController: UISearchController!
|
||||||
|
|
||||||
|
var searchControllerStatusOnAppearance: Bool? = nil
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
@ -109,6 +111,18 @@ class ExploreViewController: EnhancedTableViewController {
|
||||||
reloadLists()
|
reloadLists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
// this is a workaround for the issue that setting isActive on a search controller that is not visible
|
||||||
|
// does not cause it to automatically become active once it becomes visible
|
||||||
|
// see FB7814561
|
||||||
|
if let active = searchControllerStatusOnAppearance {
|
||||||
|
searchController.isActive = active
|
||||||
|
searchControllerStatusOnAppearance = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func reloadLists() {
|
func reloadLists() {
|
||||||
let request = Client.getLists()
|
let request = Client.getLists()
|
||||||
mastodonController.run(request) { (response) in
|
mastodonController.run(request) { (response) in
|
||||||
|
|
|
@ -18,12 +18,45 @@ protocol MainSidebarViewControllerDelegate: class {
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
class MainSidebarViewController: UIViewController {
|
class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
weak var mastodonController: MastodonController!
|
private weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
weak var sidebarDelegate: MainSidebarViewControllerDelegate?
|
weak var sidebarDelegate: MainSidebarViewControllerDelegate?
|
||||||
|
|
||||||
var collectionView: UICollectionView!
|
private var collectionView: UICollectionView!
|
||||||
var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
|
var allItems: [Item] {
|
||||||
|
[
|
||||||
|
.tab(.timelines),
|
||||||
|
.tab(.notifications),
|
||||||
|
.tab(.myProfile),
|
||||||
|
] + exploreTabItems
|
||||||
|
}
|
||||||
|
|
||||||
|
var exploreTabItems: [Item] {
|
||||||
|
var items: [Item] = [.search, .bookmarks]
|
||||||
|
let snapshot = dataSource.snapshot()
|
||||||
|
for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
|
||||||
|
items.append(.list(list))
|
||||||
|
}
|
||||||
|
for case let .savedHashtag(hashtag) in snapshot.itemIdentifiers(inSection: .savedHashtags) {
|
||||||
|
items.append(.savedHashtag(hashtag))
|
||||||
|
}
|
||||||
|
for case let .savedInstance(instance) in snapshot.itemIdentifiers(inSection: .savedInstances) {
|
||||||
|
items.append(.savedInstance(instance))
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var previouslySelectedItem: Item?
|
||||||
|
var selectedItem: Item? {
|
||||||
|
guard let indexPath = collectionView.indexPathsForSelectedItems?.first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return dataSource.itemIdentifier(for: indexPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) var itemLastSelectedTimestamps = [Item: Date]()
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
@ -53,15 +86,16 @@ class MainSidebarViewController: UIViewController {
|
||||||
|
|
||||||
applyInitialSnapshot()
|
applyInitialSnapshot()
|
||||||
|
|
||||||
select(.tab(.timelines), animated: false)
|
select(item: .tab(.timelines), animated: false)
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedHashtags), name: .savedHashtagsChanged, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(_ item: Item, animated: Bool) {
|
func select(item: Item, animated: Bool) {
|
||||||
guard let indexPath = dataSource.indexPath(for: item) else { return }
|
guard let indexPath = dataSource.indexPath(for: item) else { return }
|
||||||
collectionView.selectItem(at: indexPath, animated: animated, scrollPosition: .top)
|
collectionView.selectItem(at: indexPath, animated: animated, scrollPosition: .top)
|
||||||
|
itemLastSelectedTimestamps[item] = Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
|
@ -301,32 +335,35 @@ fileprivate extension MainTabBarViewController.Tab {
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
extension MainSidebarViewController: UICollectionViewDelegate {
|
extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
previouslySelectedItem = selectedItem
|
||||||
return false
|
return true
|
||||||
}
|
|
||||||
switch item {
|
|
||||||
case .tab(.compose):
|
|
||||||
sidebarDelegate?.sidebarRequestPresentCompose(self)
|
|
||||||
return false
|
|
||||||
case .addList:
|
|
||||||
showAddList()
|
|
||||||
return false
|
|
||||||
case .addSavedHashtag:
|
|
||||||
showAddSavedHashtag()
|
|
||||||
return false
|
|
||||||
case .addSavedInstance:
|
|
||||||
showAddSavedInstance()
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||||
collectionView.deselectItem(at: indexPath, animated: true)
|
collectionView.deselectItem(at: indexPath, animated: true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sidebarDelegate?.sidebar(self, didSelectItem: item)
|
itemLastSelectedTimestamps[item] = Date()
|
||||||
|
if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) {
|
||||||
|
if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) {
|
||||||
|
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)
|
||||||
|
}
|
||||||
|
switch item {
|
||||||
|
case .tab(.compose):
|
||||||
|
sidebarDelegate?.sidebarRequestPresentCompose(self)
|
||||||
|
case .addList:
|
||||||
|
showAddList()
|
||||||
|
case .addSavedHashtag:
|
||||||
|
showAddSavedHashtag()
|
||||||
|
case .addSavedInstance:
|
||||||
|
showAddSavedInstance()
|
||||||
|
default:
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sidebarDelegate?.sidebar(self, didSelectItem: item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,10 @@ class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
private var sidebar: MainSidebarViewController!
|
private var sidebar: MainSidebarViewController!
|
||||||
|
|
||||||
private var detailViewControllers: [MainSidebarViewController.Item: UIViewController] = [:]
|
// Keep track of navigation stacks per-item so that we can only ever use a single navigation controller
|
||||||
|
private var navigationStacks: [MainSidebarViewController.Item: [UIViewController]] = [:]
|
||||||
|
|
||||||
|
private var tabBarViewController: MainTabBarViewController!
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
@ -34,36 +37,234 @@ class MainSplitViewController: UISplitViewController {
|
||||||
preferredSplitBehavior = .tile
|
preferredSplitBehavior = .tile
|
||||||
presentsWithGesture = false
|
presentsWithGesture = false
|
||||||
showsSecondaryOnlyButton = false
|
showsSecondaryOnlyButton = false
|
||||||
|
delegate = self
|
||||||
|
|
||||||
sidebar = MainSidebarViewController(mastodonController: mastodonController)
|
sidebar = MainSidebarViewController(mastodonController: mastodonController)
|
||||||
sidebar.sidebarDelegate = self
|
sidebar.sidebarDelegate = self
|
||||||
setViewController(sidebar, for: .primary)
|
setViewController(sidebar, for: .primary)
|
||||||
|
|
||||||
|
setViewController(EnhancedNavigationViewController(), for: .secondary)
|
||||||
select(item: .tab(.timelines))
|
select(item: .tab(.timelines))
|
||||||
setViewController(MainTabBarViewController(mastodonController: mastodonController), for: .compact)
|
|
||||||
|
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)
|
||||||
|
setViewController(tabBarViewController, for: .compact)
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(item: MainSidebarViewController.Item) {
|
func select(item: MainSidebarViewController.Item) {
|
||||||
let itemController = getOrCreateDetailViewController(item: item)
|
let nav = viewController(for: .secondary) as! UINavigationController
|
||||||
setViewController(itemController, for: .secondary)
|
nav.viewControllers = getOrCreateNavigationStack(item: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOrCreateDetailViewController(item: MainSidebarViewController.Item) -> UIViewController? {
|
func getOrCreateNavigationStack(item: MainSidebarViewController.Item) -> [UIViewController] {
|
||||||
if let existing = detailViewControllers[item] {
|
if let existing = navigationStacks[item], existing.count > 0 {
|
||||||
return existing
|
return existing
|
||||||
} else {
|
} else {
|
||||||
guard let new = item.createRootViewController(mastodonController) else { return nil }
|
let new = [item.createRootViewController(mastodonController)!]
|
||||||
let nav = EnhancedNavigationViewController(rootViewController: new)
|
navigationStacks[item] = new
|
||||||
|
return new
|
||||||
// Prevents the navigation bar from going transparent when switching sidebar sections.
|
|
||||||
nav.navigationBar.scrollEdgeAppearance = nav.navigationBar.standardAppearance
|
|
||||||
|
|
||||||
detailViewControllers[item] = nav
|
|
||||||
return nav
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 14.0, *)
|
||||||
|
extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
|
/// Transfer the navigation stack for a sidebar item to a destination navgiation controller.
|
||||||
|
/// - Parameter dropFirst: Remove the first view controller from the item's navigation stack before transferring.
|
||||||
|
/// - Parameter append: Append the item's navigation stack to the destination nav controller's instead of replacing it.
|
||||||
|
private func transferNavigationStack(from item: MainSidebarViewController.Item, to destination: UINavigationController, dropFirst: Bool = false, append: Bool = false) {
|
||||||
|
var itemNavStack: [UIViewController]
|
||||||
|
if item == sidebar.selectedItem {
|
||||||
|
let detailNav = viewController(for: .secondary) as! UINavigationController
|
||||||
|
itemNavStack = detailNav.viewControllers
|
||||||
|
} else {
|
||||||
|
itemNavStack = navigationStacks[item] ?? []
|
||||||
|
navigationStacks.removeValue(forKey: item)
|
||||||
|
}
|
||||||
|
if itemNavStack.isEmpty {
|
||||||
|
itemNavStack = [item.createRootViewController(mastodonController)!]
|
||||||
|
}
|
||||||
|
|
||||||
|
if dropFirst {
|
||||||
|
itemNavStack.remove(at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if append {
|
||||||
|
destination.viewControllers += itemNavStack
|
||||||
|
} else {
|
||||||
|
destination.viewControllers = itemNavStack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitViewControllerDidCollapse(_ svc: UISplitViewController) {
|
||||||
|
// Transfer the nav stacks for all the sidebar items that map 1 <-> 1 with tabs
|
||||||
|
for tab in [MainTabBarViewController.Tab.timelines, .notifications, .myProfile] {
|
||||||
|
let tabNav = tabBarViewController.viewController(for: tab) as! UINavigationController
|
||||||
|
transferNavigationStack(from: .tab(tab), to: tabNav)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since several sidebar items map to the single Explore tab, we only transfer the
|
||||||
|
// navigation stack of the most-recently used one.
|
||||||
|
let mostRecentExploreItem: (MainSidebarViewController.Item, Date)? =
|
||||||
|
sidebar.exploreTabItems.compactMap {
|
||||||
|
if let timestamp = sidebar.itemLastSelectedTimestamps[$0] {
|
||||||
|
return ($0, timestamp)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}.min {
|
||||||
|
$0.1 > $1.1
|
||||||
|
}
|
||||||
|
if let mostRecentExploreItem = mostRecentExploreItem?.0,
|
||||||
|
mostRecentExploreItem != .search {
|
||||||
|
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
||||||
|
// Pop back to root, so we're appending to the Explore VC instead of some other VC
|
||||||
|
exploreNav.popToRootViewController(animated: false)
|
||||||
|
// Append so we don't replace the Explore VC
|
||||||
|
transferNavigationStack(from: mostRecentExploreItem, to: exploreNav, append: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch the tab bar to focus the same item as the sidebar has selected
|
||||||
|
switch sidebar.selectedItem! {
|
||||||
|
case let .tab(tab):
|
||||||
|
// sidebar items that map 1 <-> 1 can be transferred directly
|
||||||
|
tabBarViewController.select(tab: tab)
|
||||||
|
|
||||||
|
case .search:
|
||||||
|
// Search sidebar item maps to the Explore tab with the search controller/results visible
|
||||||
|
// The nav stack can't be copied directly, since the split VC uses a different SearchViewController
|
||||||
|
// so that explore items aren't shown multiple times.
|
||||||
|
|
||||||
|
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
||||||
|
let explore: ExploreViewController
|
||||||
|
if let existing = exploreNav.viewControllers.first as? ExploreViewController {
|
||||||
|
explore = existing
|
||||||
|
exploreNav.popToRootViewController(animated: false)
|
||||||
|
} else {
|
||||||
|
// If the Explore tab hasn't been loaded before, it's root view controller won't be loaded yet, so create and add it manually.
|
||||||
|
explore = ExploreViewController(mastodonController: mastodonController)
|
||||||
|
exploreNav.viewControllers = [explore]
|
||||||
|
}
|
||||||
|
// Make sure viewDidLoad is called so that the searchController/resultsController have been initialized
|
||||||
|
explore.loadViewIfNeeded()
|
||||||
|
|
||||||
|
let nav = viewController(for: .secondary) as! UINavigationController
|
||||||
|
let search = nav.viewControllers.first as! SearchViewController
|
||||||
|
// Copy the search query from the search VC to the Explore VC's search controller.
|
||||||
|
let query = search.searchController.searchBar.text ?? ""
|
||||||
|
explore.searchController.searchBar.text = query
|
||||||
|
// Instruct the explore controller to show its search controller immediately upon its first appearance.
|
||||||
|
// explore.searchController.isActive can't be set directly, see FB7814561
|
||||||
|
explore.searchControllerStatusOnAppearance = !query.isEmpty
|
||||||
|
// Copy the results from the search VC's results controller to avoid the delay introduced by an extra network request
|
||||||
|
explore.resultsController.loadResults(from: search.resultsController)
|
||||||
|
|
||||||
|
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
|
||||||
|
transferNavigationStack(from: .search, to: exploreNav, dropFirst: true, append: true)
|
||||||
|
|
||||||
|
tabBarViewController.select(tab: .explore)
|
||||||
|
|
||||||
|
case .bookmarks, .list(_), .savedHashtag(_), .savedInstance(_):
|
||||||
|
tabBarViewController.select(tab: .explore)
|
||||||
|
// Make sure the Explore VC doesn't show it's search bar when it appears, in case the user was previously
|
||||||
|
// in compact mode and performing a search.
|
||||||
|
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
||||||
|
let explore = exploreNav.viewControllers.first as! ExploreViewController
|
||||||
|
explore.searchControllerStatusOnAppearance = false
|
||||||
|
|
||||||
|
case .listsHeader, .addList, .savedHashtagsHeader, .addSavedHashtag, .savedInstancesHeader, .addSavedInstance:
|
||||||
|
// These items are not selectable in the sidebar collection view, so this code is unreachable.
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transfer a navigation stack from a navigation controller belonging to the tab bar VC to a sidebar item.
|
||||||
|
/// - Parameter skipFirst:The number of view controllers that should be skipped from the source navigation controller.
|
||||||
|
/// - Parameter prepend: An optional view controller to prepend to the beginning of the navigation stack being moved.
|
||||||
|
private func transferNavigationStack(from navController: UINavigationController, to item: MainSidebarViewController.Item, skipFirst: Int = 0, prepend: UIViewController? = nil) {
|
||||||
|
let viewControllersToMove = navController.viewControllers.dropFirst(skipFirst)
|
||||||
|
navController.viewControllers.removeLast(navController.viewControllers.count - skipFirst)
|
||||||
|
|
||||||
|
if let prepend = prepend {
|
||||||
|
navigationStacks[item] = [prepend] + viewControllersToMove
|
||||||
|
} else {
|
||||||
|
navigationStacks[item] = Array(viewControllersToMove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitViewControllerDidExpand(_ svc: UISplitViewController) {
|
||||||
|
// For each sidebar item, transfer the existing navigation stasck from the tab bar controller to ourself.
|
||||||
|
var exploreItem: MainSidebarViewController.Item?
|
||||||
|
for tab in MainTabBarViewController.Tab.allCases {
|
||||||
|
guard let tabNavController = tabBarViewController.viewController(for: tab) as? UINavigationController else { continue }
|
||||||
|
let tabNavigationStack = tabNavController.viewControllers
|
||||||
|
|
||||||
|
switch tab {
|
||||||
|
case .timelines, .notifications, .myProfile:
|
||||||
|
// Items that map 1 <-> 1 to tabs can be transferred directly.
|
||||||
|
let item = MainSidebarViewController.Item.tab(tab)
|
||||||
|
transferNavigationStack(from: tabNavController, to: item)
|
||||||
|
|
||||||
|
case .explore:
|
||||||
|
// The Explore tab is more complicated since it encapsulates a bunch of screens which have top-level sidebar items.
|
||||||
|
|
||||||
|
var toPrepend: UIViewController? = nil
|
||||||
|
|
||||||
|
// If the tab navigation stack has only one item or the search controller is active, it corresponds to the Search item
|
||||||
|
// For other items, the 2nd VC in the nav stack determines which sidebar item they map to.
|
||||||
|
// Search screen has special considerations, all others can be transferred directly.
|
||||||
|
if tabNavigationStack.count == 1 || ((tabNavigationStack.first as? ExploreViewController)?.searchController.isActive ?? false) {
|
||||||
|
exploreItem = .search
|
||||||
|
let searchVC = SearchViewController(mastodonController: mastodonController)
|
||||||
|
searchVC.loadViewIfNeeded()
|
||||||
|
let explore = tabNavigationStack.first as! ExploreViewController
|
||||||
|
if let exploreSearchControler = explore.searchController,
|
||||||
|
let query = exploreSearchControler.searchBar.text {
|
||||||
|
// Transfer query to search VC
|
||||||
|
searchVC.searchController.searchBar.text = query
|
||||||
|
// If there is a query, make the search VC activate itself upon appearing
|
||||||
|
searchVC.searchControllerStatusOnAppearance = !query.isEmpty
|
||||||
|
// Transfer the results from the explore VC, to avoid an extra network request
|
||||||
|
searchVC.resultsController.loadResults(from: explore.resultsController)
|
||||||
|
}
|
||||||
|
// Insert the new search VC at the beginning of the new search nav stack
|
||||||
|
toPrepend = searchVC
|
||||||
|
} else if tabNavigationStack[1] is BookmarksTableViewController {
|
||||||
|
exploreItem = .bookmarks
|
||||||
|
} else if let listVC = tabNavigationStack[1] as? ListTimelineViewController {
|
||||||
|
exploreItem = .list(listVC.list)
|
||||||
|
} else if let hashtagVC = tabNavigationStack[1] as? HashtagTimelineViewController {
|
||||||
|
exploreItem = .savedHashtag(hashtagVC.hashtag)
|
||||||
|
} else if let instanceVC = tabNavigationStack[1] as? InstanceTimelineViewController {
|
||||||
|
exploreItem = .savedInstance(instanceVC.instanceURL)
|
||||||
|
}
|
||||||
|
transferNavigationStack(from: tabNavController, to: exploreItem!, skipFirst: 1, prepend: toPrepend)
|
||||||
|
|
||||||
|
case .compose:
|
||||||
|
// The compose tab can't be activated, this is unreachable.
|
||||||
|
fatalError("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer the selected tab from the tab bar VC to the sidebar
|
||||||
|
switch tabBarViewController.selectedTab {
|
||||||
|
case .timelines, .notifications, .myProfile:
|
||||||
|
// These tabs map 1 <-> 1 with sidebar items
|
||||||
|
let item = MainSidebarViewController.Item.tab(tabBarViewController.selectedTab)
|
||||||
|
sidebar.select(item: item, animated: false)
|
||||||
|
select(item: item)
|
||||||
|
|
||||||
|
case .explore:
|
||||||
|
// If the explore tab is active, the sidebar item is determined above when transferring the explore VC's nav stack
|
||||||
|
sidebar.select(item: exploreItem!, animated: false)
|
||||||
|
select(item: exploreItem!)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
||||||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) {
|
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) {
|
||||||
|
@ -71,6 +272,10 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) {
|
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) {
|
||||||
|
let nav = viewController(for: .secondary) as! UINavigationController
|
||||||
|
if let previous = sidebar.previouslySelectedItem {
|
||||||
|
navigationStacks[previous] = nav.viewControllers
|
||||||
|
}
|
||||||
select(item: item)
|
select(item: item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,10 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var composePlaceholder: UIViewController!
|
private var composePlaceholder: UIViewController!
|
||||||
|
|
||||||
|
var selectedTab: Tab {
|
||||||
|
return Tab(rawValue: selectedIndex)!
|
||||||
|
}
|
||||||
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
@ -65,6 +69,14 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setViewController(_ viewController: UIViewController, forTab tab: Tab) {
|
||||||
|
viewControllers![tab.rawValue] = viewController
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewController(for tab: Tab) -> UIViewController {
|
||||||
|
return viewControllers![tab.rawValue]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainTabBarViewController {
|
extension MainTabBarViewController {
|
||||||
|
@ -91,8 +103,6 @@ extension MainTabBarViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func getTabController(tab: Tab) -> UIViewController? {
|
func getTabController(tab: Tab) -> UIViewController? {
|
||||||
if tab == .compose {
|
if tab == .compose {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -17,7 +17,6 @@ class MyProfileTableViewController: ProfileTableViewController {
|
||||||
title = "My Profile"
|
title = "My Profile"
|
||||||
tabBarItem.image = UIImage(systemName: "person.fill")
|
tabBarItem.image = UIImage(systemName: "person.fill")
|
||||||
|
|
||||||
|
|
||||||
mastodonController.getOwnAccount { (account) in
|
mastodonController.getOwnAccount { (account) in
|
||||||
self.accountID = account.id
|
self.accountID = account.id
|
||||||
|
|
||||||
|
@ -41,14 +40,6 @@ class MyProfileTableViewController: ProfileTableViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func preferencesPressed() {
|
@objc func preferencesPressed() {
|
||||||
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true)
|
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,6 @@ class ProfileTableViewController: EnhancedTableViewController {
|
||||||
self.accountID = accountID
|
self.accountID = accountID
|
||||||
|
|
||||||
super.init(style: .plain)
|
super.init(style: .plain)
|
||||||
|
|
||||||
self.refreshControl = UIRefreshControl()
|
|
||||||
refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged)
|
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composePressed(_:)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
@ -51,6 +47,10 @@ class ProfileTableViewController: EnhancedTableViewController {
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.refreshControl = UIRefreshControl()
|
||||||
|
refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged)
|
||||||
|
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composePressed(_:)))
|
||||||
|
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.estimatedRowHeight = 140
|
tableView.estimatedRowHeight = 140
|
||||||
|
|
|
@ -109,6 +109,15 @@ class SearchResultsViewController: EnhancedTableViewController {
|
||||||
return super.targetViewController(forAction: action, sender: sender)
|
return super.targetViewController(forAction: action, sender: sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadResults(from source: SearchResultsViewController) {
|
||||||
|
currentQuery = source.currentQuery
|
||||||
|
if let sourceDataSource = source.dataSource {
|
||||||
|
dataSource.apply(sourceDataSource.snapshot())
|
||||||
|
}
|
||||||
|
// todo: check if the search needs to be performed before searching
|
||||||
|
// performSearch(query: currentQuery)
|
||||||
|
}
|
||||||
|
|
||||||
func performSearch(query: String?) {
|
func performSearch(query: String?) {
|
||||||
guard let query = query, !query.isEmpty else {
|
guard let query = query, !query.isEmpty else {
|
||||||
self.dataSource.apply(NSDiffableDataSourceSnapshot<Section, Item>())
|
self.dataSource.apply(NSDiffableDataSourceSnapshot<Section, Item>())
|
||||||
|
@ -212,6 +221,20 @@ extension SearchResultsViewController {
|
||||||
case account(String)
|
case account(String)
|
||||||
case hashtag(Hashtag)
|
case hashtag(Hashtag)
|
||||||
case status(String, StatusState)
|
case status(String, StatusState)
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
switch self {
|
||||||
|
case let .account(id):
|
||||||
|
hasher.combine("account")
|
||||||
|
hasher.combine(id)
|
||||||
|
case let .hashtag(hashtag):
|
||||||
|
hasher.combine("hashtag")
|
||||||
|
hasher.combine(hashtag.url)
|
||||||
|
case let .status(id, _):
|
||||||
|
hasher.combine("status")
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
class DataSource: UITableViewDiffableDataSource<Section, Item> {
|
||||||
|
|
|
@ -15,6 +15,8 @@ class SearchViewController: UIViewController {
|
||||||
var resultsController: SearchResultsViewController!
|
var resultsController: SearchResultsViewController!
|
||||||
var searchController: UISearchController!
|
var searchController: UISearchController!
|
||||||
|
|
||||||
|
var searchControllerStatusOnAppearance: Bool? = nil
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
@ -42,5 +44,16 @@ class SearchViewController: UIViewController {
|
||||||
navigationItem.hidesSearchBarWhenScrolling = false
|
navigationItem.hidesSearchBarWhenScrolling = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
// this is a workaround for the issue that setting isActive on a search controller that is not visible
|
||||||
|
// does not cause it to automatically become active once it becomes visible
|
||||||
|
// see FB7814561
|
||||||
|
if let active = searchControllerStatusOnAppearance {
|
||||||
|
searchController.isActive = active
|
||||||
|
searchControllerStatusOnAppearance = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue