From dffa5d8f7581c4d8bac10fd11bd026e840b86d3d Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 20 Aug 2024 11:55:19 -0400 Subject: [PATCH 01/11] Lists in new sidebar --- .../Main/NewMainTabBarViewController.swift | 143 +++++++++++++----- 1 file changed, 103 insertions(+), 40 deletions(-) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index d3311cd607..1605873745 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -7,11 +7,19 @@ // import UIKit +import Combine +import Pachyderm @available(iOS 18.0, *) class NewMainTabBarViewController: BaseMainTabBarViewController { private let composePlaceholder = UIViewController() + + private var listsGroup: UITabGroup! + + private var cancellables = Set() + + private var navigationStacks = [String: [UIViewController]]() override func viewDidLoad() { super.viewDidLoad() @@ -26,19 +34,54 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { self.makeViewController(for: tab) } - let topLevelTabs = [ - Tab.home, - .notifications, - .compose, - .explore, - .myProfile - ].map { - UITab(title: $0.title, image: UIImage(systemName: $0.imageName), identifier: $0.rawValue, viewControllerProvider: viewControllerProvider) + let homeTab = UITab(title: "Home", image: UIImage(systemName: "house"), identifier: Tab.home.rawValue, viewControllerProvider: viewControllerProvider) + let notificationsTab = UITab(title: "Notifications", image: UIImage(systemName: "bell"), identifier: Tab.notifications.rawValue, viewControllerProvider: viewControllerProvider) + let composeTab = UITab(title: "Compose", image: UIImage(systemName: "pencil"), identifier: Tab.compose.rawValue, viewControllerProvider: viewControllerProvider) + let exploreTab = UITab(title: "Explore", image: UIImage(systemName: "magnifyingglass"), identifier: Tab.explore.rawValue, viewControllerProvider: viewControllerProvider) + let bookmarksTab = UITab(title: "Bookmarks", image: UIImage(systemName: "bookmark"), identifier: Tab.bookmarks.rawValue, viewControllerProvider: viewControllerProvider) + bookmarksTab.preferredPlacement = .optional + let favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider) + favoritesTab.preferredPlacement = .optional + let myProfileTab = UITab(title: "My Profile", image: UIImage(systemName: "person"), identifier: Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) + + listsGroup = UITabGroup(title: "Lists", image: nil, identifier: Tab.lists.rawValue, children: []) { _ in + // this closure is necessary to prevent UIKit from crashing (FB14860961) + return MultiColumnNavigationController() + } + listsGroup.preferredPlacement = .sidebarOnly + listsGroup.sidebarActions = [ + UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { _ in + fatalError("TODO") + }) + ] + reloadLists(mastodonController.lists) + + if UIDevice.current.userInterfaceIdiom == .phone { + self.tabs = [ + homeTab, + notificationsTab, + composeTab, + exploreTab, + myProfileTab, + ] + } else { + self.tabs = [ + homeTab, + notificationsTab, + exploreTab, + bookmarksTab, + favoritesTab, + myProfileTab, + composeTab, + listsGroup, + ] } - self.tabs = topLevelTabs - setupFastAccountSwitcher() + + mastodonController.$lists + .sink { [unowned self] in self.reloadLists($0) } + .store(in: &cancellables) } private func makeViewController(for tab: UITab) -> UIViewController { @@ -55,9 +98,19 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { return composePlaceholder case .explore: root = ExploreViewController(mastodonController: mastodonController) + case .bookmarks: + root = BookmarksViewController(mastodonController: mastodonController) + case .favorites: + root = FavoritesViewController(mastodonController: mastodonController) case .myProfile: root = MyProfileViewController(mastodonController: mastodonController) + case .lists: + fatalError("unreachable") } + return NewMainTabBarViewController.embedInNavigationController(root) + } + + private static func embedInNavigationController(_ vc: UIViewController) -> UIViewController { let nav: any NavigationControllerProtocol if UIDevice.current.userInterfaceIdiom == .phone { nav = EnhancedNavigationViewController() @@ -72,10 +125,18 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { nav = MultiColumnNavigationController() } } - nav.viewControllers = [root] + nav.viewControllers = [vc] return nav } + private func reloadLists(_ lists: [List]) { + listsGroup.children = lists.map { list in + UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: "list:\(list.id)") { [unowned self] _ in + NewMainTabBarViewController.embedInNavigationController(ListTimelineViewController(for: list, mastodonController: self.mastodonController)) + } + } + } + @objc func handleComposeKeyCommand() { compose(editing: nil) } @@ -94,37 +155,11 @@ extension NewMainTabBarViewController { case notifications case compose case explore + case bookmarks + case favorites case myProfile - var title: String { - switch self { - case .home: - "Home" - case .notifications: - "Notifications" - case .compose: - "Compose" - case .explore: - "Explore" - case .myProfile: - "My Profile" - } - } - - var imageName: String { - switch self { - case .home: - "house" - case .notifications: - "bell" - case .compose: - "pencil" - case .explore: - "magnifyingglass" - case .myProfile: - "person" - } - } + case lists } } @@ -156,6 +191,34 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { if let vc = newTab.viewController as? MultiColumnNavigationController { self.updateViewControllerSafeAreaInsets(vc) } + + // All tabs in a tab group deliberately share the same view controller, so we have to do this ourselves. + // I think this is pretty unfortunate API design--half the time, the tab bar controller takes care of + // this, but the rest of the time it's up to you. + // The managingNavigationController API would theoretically solve this, but split-screen/multi-column + // nav can't straightforwardly be implemented as UINavigationController subclasses. + // Unfortunately this, in turn, means that when switching between tabs in the same group, we don't + // get the new transition animation. + // This would be much less complicated if the controller just used the individual VCs of items in a group. + if let group = newTab.parent, + group.identifier == Tab.lists.rawValue, + let nav = group.viewController as? any NavigationControllerProtocol { + if let multiColumn = nav as? MultiColumnNavigationController { + updateViewControllerSafeAreaInsets(multiColumn) + } + + if let previousTab { + navigationStacks[previousTab.identifier] = nav.viewControllers + } + + if let existing = navigationStacks[newTab.identifier] { + nav.viewControllers = existing + } else if let newNav = newTab.viewController as? any NavigationControllerProtocol { + nav.viewControllers = newNav.viewControllers + } else { + fatalError("unreachable") + } + } } } From fda0c187949d0e7c8d792d616bb90f1bd90c3d10 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 20 Aug 2024 12:31:06 -0400 Subject: [PATCH 02/11] Fix insets with new sidebar --- Tusker.xcodeproj/project.pbxproj | 8 ++-- .../AccountFollowsListViewController.swift | 21 ++++++--- .../AccountListViewController.swift | 35 +++++++++------ ...ConversationCollectionViewController.swift | 22 ++++++---- .../Explore/ExploreViewController.swift | 10 ++++- .../FindInstanceViewController.swift | 0 .../SuggestedProfilesViewController.swift | 4 +- .../TrendingHashtagsViewController.swift | 10 ++++- .../Explore/TrendingLinksViewController.swift | 6 ++- .../TrendingStatusesViewController.swift | 28 +++++++----- .../Explore/TrendsViewController.swift | 12 ++++- ...LocalPredicateStatusesViewController.swift | 44 ++++++++++--------- .../Main/NewMainTabBarViewController.swift | 6 ++- ...otificationsCollectionViewController.swift | 4 +- .../ProfileStatusesViewController.swift | 22 ++++++---- .../Search/SearchResultsViewController.swift | 22 ++++++---- ...nAccountListCollectionViewController.swift | 21 ++++++--- .../StatusEditHistoryViewController.swift | 6 ++- .../Timeline/TimelineViewController.swift | 4 +- 19 files changed, 183 insertions(+), 102 deletions(-) rename Tusker/Screens/{ => Explore}/FindInstanceViewController.swift (100%) diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index f147eef464..25f20be3eb 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -131,6 +131,7 @@ D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; }; D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */; }; D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */; }; + D64A50BC2C74F8F4009D7193 /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */; }; D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; }; D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; }; D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; }; @@ -225,7 +226,6 @@ D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; }; D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; }; D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; }; - D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */; }; D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */; }; D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */; }; D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; }; @@ -564,6 +564,7 @@ D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = ""; }; D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMainTabBarViewController.swift; sourceTree = ""; }; D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMainTabBarViewController.swift; sourceTree = ""; }; + D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInstanceViewController.swift; sourceTree = ""; }; D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = ""; }; D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = ""; }; @@ -661,7 +662,6 @@ D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = ""; }; D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = ""; }; D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = ""; }; - D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FindInstanceViewController.swift; path = Tusker/Screens/FindInstanceViewController.swift; sourceTree = SOURCE_ROOT; }; D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidescreenNavigationPrefsView.swift; sourceTree = ""; }; D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowSceneDelegate+Close.swift"; sourceTree = ""; }; D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = ""; }; @@ -981,7 +981,7 @@ D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */, D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */, D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */, - D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */, + D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */, D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */, D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */, D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */, @@ -2241,6 +2241,7 @@ D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */, + D64A50BC2C74F8F4009D7193 /* FindInstanceViewController.swift in Sources */, D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */, D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */, D698F4672BD079800054DB14 /* AnnouncementsHostingController.swift in Sources */, @@ -2280,7 +2281,6 @@ D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */, D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */, D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */, - D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */, D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */, D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */, D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */, diff --git a/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift b/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift index 32d95c26c4..07cd76707a 100644 --- a/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift +++ b/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift @@ -17,9 +17,7 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll let mastodonController: MastodonController let mode: AccountFollowsViewController.Mode - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! private var state: State = .unloaded @@ -40,7 +38,11 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .appGroupedBackground + var config = UICollectionLayoutListConfiguration(appearance: .plain) config.backgroundColor = .appBackground config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in @@ -65,10 +67,19 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll section.readableContentInset(in: environment) return section } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() } diff --git a/Tusker/Screens/Account List/AccountListViewController.swift b/Tusker/Screens/Account List/AccountListViewController.swift index c598abacb8..f0b533dbd9 100644 --- a/Tusker/Screens/Account List/AccountListViewController.swift +++ b/Tusker/Screens/Account List/AccountListViewController.swift @@ -14,9 +14,7 @@ class AccountListViewController: UIViewController, CollectionViewController { private let mastodonController: MastodonController private let accountIDs: [String] - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! init(accountIDs: [String], mastodonController: MastodonController) { @@ -30,7 +28,11 @@ class AccountListViewController: UIViewController, CollectionViewController { fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .appGroupedBackground + var config = UICollectionLayoutListConfiguration(appearance: .grouped) config.backgroundColor = .appGroupedBackground config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets @@ -40,11 +42,25 @@ class AccountListViewController: UIViewController, CollectionViewController { section.readableContentInset(in: environment) return section } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.accounts]) + snapshot.appendItems(accountIDs) + dataSource.apply(snapshot, animatingDifferences: false) } private func createDataSource() -> UICollectionViewDiffableDataSource { @@ -56,16 +72,7 @@ class AccountListViewController: UIViewController, CollectionViewController { return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: itemIdentifier) } } - - override func viewDidLoad() { - super.viewDidLoad() - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.accounts]) - snapshot.appendItems(accountIDs) - dataSource.apply(snapshot, animatingDifferences: false) - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) diff --git a/Tusker/Screens/Conversation/ConversationCollectionViewController.swift b/Tusker/Screens/Conversation/ConversationCollectionViewController.swift index 8f16309e5e..b3559e3023 100644 --- a/Tusker/Screens/Conversation/ConversationCollectionViewController.swift +++ b/Tusker/Screens/Conversation/ConversationCollectionViewController.swift @@ -19,9 +19,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont var statusIDToScrollToOnLoad: String var showStatusesAutomatically = false - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! init(for mainStatusID: String, state: CollapseState, conversationViewController: ConversationViewController) { @@ -38,7 +36,9 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + var config = UICollectionLayoutListConfiguration(appearance: .plain) config.backgroundColor = .appSecondaryBackground config.leadingSwipeActionsConfigurationProvider = { [unowned self] in @@ -66,13 +66,19 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont return section } viewRespectsSystemMinimumLayoutMargins = false - view = UICollectionView(frame: .zero, collectionViewLayout: layout) - // something about the autoresizing mask breaks resizing the vc - view.translatesAutoresizingMaskIntoConstraints = false + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true - + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + #if !targetEnvironment(macCatalyst) collectionView.refreshControl = UIRefreshControl() collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index 75c58f655b..b9ee9a092e 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -48,12 +48,18 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect configuration.headerMode = .supplementary let layout = UICollectionViewCompositionalLayout.list(using: configuration) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) - collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) - + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() applyInitialSnapshot() diff --git a/Tusker/Screens/FindInstanceViewController.swift b/Tusker/Screens/Explore/FindInstanceViewController.swift similarity index 100% rename from Tusker/Screens/FindInstanceViewController.swift rename to Tusker/Screens/Explore/FindInstanceViewController.swift diff --git a/Tusker/Screens/Explore/SuggestedProfilesViewController.swift b/Tusker/Screens/Explore/SuggestedProfilesViewController.swift index b9c07e770d..b1da2f8950 100644 --- a/Tusker/Screens/Explore/SuggestedProfilesViewController.swift +++ b/Tusker/Screens/Explore/SuggestedProfilesViewController.swift @@ -46,8 +46,8 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle collectionView.allowsFocus = true view.addSubview(collectionView) NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) diff --git a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift index 6e5f8c14ce..28691fa4d8 100644 --- a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift +++ b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift @@ -53,12 +53,18 @@ class TrendingHashtagsViewController: UIViewController, CollectionViewController } let layout = UICollectionViewCompositionalLayout.list(using: config) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) - collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) - + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + let loadingCell = UICollectionView.CellRegistration { cell, indexPath, itemIdentifier in cell.indicator.startAnimating() } diff --git a/Tusker/Screens/Explore/TrendingLinksViewController.swift b/Tusker/Screens/Explore/TrendingLinksViewController.swift index 3859fad272..af73cbdf55 100644 --- a/Tusker/Screens/Explore/TrendingLinksViewController.swift +++ b/Tusker/Screens/Explore/TrendingLinksViewController.swift @@ -40,6 +40,8 @@ class TrendingLinksViewController: UIViewController, CollectionViewController { title = NSLocalizedString("Trending Links", comment: "trending links screen title") + view.backgroundColor = .appGroupedBackground + let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in switch dataSource.sectionIdentifier(for: sectionIndex) { case nil: @@ -80,8 +82,8 @@ class TrendingLinksViewController: UIViewController, CollectionViewController { collectionView.allowsFocus = true view.addSubview(collectionView) NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) diff --git a/Tusker/Screens/Explore/TrendingStatusesViewController.swift b/Tusker/Screens/Explore/TrendingStatusesViewController.swift index 63d23a4805..db6e5cac5d 100644 --- a/Tusker/Screens/Explore/TrendingStatusesViewController.swift +++ b/Tusker/Screens/Explore/TrendingStatusesViewController.swift @@ -14,9 +14,7 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController private let mastodonController: MastodonController let filterer: Filterer - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! private var loaded = false @@ -34,7 +32,9 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + var config = UICollectionLayoutListConfiguration(appearance: .plain) config.leadingSwipeActionsConfigurationProvider = { [unowned self] in (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() @@ -62,12 +62,22 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController section.readableContentInset(in: environment) return section } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true - + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() + + NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController) } private func createDataSource() -> UICollectionViewDiffableDataSource { @@ -96,12 +106,6 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController } } - override func viewDidLoad() { - super.viewDidLoad() - - NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController) - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) diff --git a/Tusker/Screens/Explore/TrendsViewController.swift b/Tusker/Screens/Explore/TrendsViewController.swift index 52c7786d29..4b09e2e79d 100644 --- a/Tusker/Screens/Explore/TrendsViewController.swift +++ b/Tusker/Screens/Explore/TrendsViewController.swift @@ -44,6 +44,8 @@ class TrendsViewController: UIViewController, CollectionViewController { override func viewDidLoad() { super.viewDidLoad() + + view.backgroundColor = .appGroupedBackground let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex] @@ -114,13 +116,19 @@ class TrendsViewController: UIViewController, CollectionViewController { } } collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) - collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.delegate = self collectionView.dragDelegate = self collectionView.backgroundColor = .appGroupedBackground collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) - + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) diff --git a/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift b/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift index a743da7ee0..1d5081b296 100644 --- a/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift +++ b/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift @@ -19,9 +19,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont private let predicateTitle: String private let request: (RequestRange) -> Request<[TryDecode]> - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! private var state = State.unloaded @@ -43,7 +41,9 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + var config = UICollectionLayoutListConfiguration(appearance: .plain) config.backgroundColor = .appBackground config.leadingSwipeActionsConfigurationProvider = { [unowned self] in @@ -71,12 +71,30 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont section.readableContentInset(in: environment) return section } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true - + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() + + #if !targetEnvironment(macCatalyst) + collectionView.refreshControl = UIRefreshControl() + collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) + #endif + + addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh \(predicateTitle)")) + + NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController) + NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext) } private func createDataSource() -> UICollectionViewDiffableDataSource { @@ -97,20 +115,6 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont } } - override func viewDidLoad() { - super.viewDidLoad() - - #if !targetEnvironment(macCatalyst) - collectionView.refreshControl = UIRefreshControl() - collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) - #endif - - addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh \(predicateTitle)")) - - NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController) - NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext) - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 1605873745..76853fc521 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -97,7 +97,11 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { case .compose: return composePlaceholder case .explore: - root = ExploreViewController(mastodonController: mastodonController) + if UIDevice.current.userInterfaceIdiom == .phone { + root = ExploreViewController(mastodonController: mastodonController) + } else { + root = InlineTrendsViewController(mastodonController: mastodonController) + } case .bookmarks: root = BookmarksViewController(mastodonController: mastodonController) case .favorites: diff --git a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift index 561c9534b2..eb042b7240 100644 --- a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift @@ -108,8 +108,8 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift index d1f6d3c920..802b9da37f 100644 --- a/Tusker/Screens/Profile/ProfileStatusesViewController.swift +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -26,9 +26,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie private var older: RequestRange? private var cancellables = Set() - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private(set) var dataSource: UICollectionViewDiffableDataSource! var headerCell: ProfileHeaderCollectionViewCell? { collectionView.cellForItem(at: IndexPath(item: 0, section: 0)) as? ProfileHeaderCollectionViewCell @@ -56,7 +54,9 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + var config = UICollectionLayoutListConfiguration(appearance: .plain) config.backgroundColor = .appBackground config.leadingSwipeActionsConfigurationProvider = { [unowned self] in @@ -103,10 +103,18 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie return section } } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) registerTimelineLikeCells() dataSource = createDataSource() @@ -115,10 +123,6 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie collectionView.refreshControl = UIRefreshControl() collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) #endif - } - - override func viewDidLoad() { - super.viewDidLoad() mastodonController.persistentContainer.accountSubject .receive(on: DispatchQueue.main) diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index 9d22138afb..e7394b977c 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -36,7 +36,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController { weak var delegate: SearchResultsViewControllerDelegate? var tokenHandler: ((String, SearchOperatorType) -> Void)? - var collectionView: UICollectionView! { view as? UICollectionView } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! /// Types of results to search for. @@ -62,7 +62,9 @@ class SearchResultsViewController: UIViewController, CollectionViewController { fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in let sectionIdentifier = self.dataSource.sectionIdentifier(for: sectionIndex)! switch sectionIdentifier { @@ -102,7 +104,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController { return .list(using: config, layoutEnvironment: environment) } } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true @@ -110,12 +112,16 @@ class SearchResultsViewController: UIViewController, CollectionViewController { #if !os(visionOS) collectionView.keyboardDismissMode = .interactive #endif - + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() - } - - override func viewDidLoad() { - super.viewDidLoad() searchCancellable = searchSubject .debounce(for: .seconds(1), scheduler: RunLoop.main) diff --git a/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift b/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift index 6fe62d02ae..da96e3947d 100644 --- a/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift +++ b/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift @@ -19,9 +19,7 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect private var needsInaccurateCountWarning = false - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! private var state: State = .unloaded @@ -45,7 +43,11 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .appGroupedBackground + var accountsConfig = UICollectionLayoutListConfiguration(appearance: .grouped) accountsConfig.backgroundColor = .appGroupedBackground accountsConfig.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in @@ -85,10 +87,19 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect section.readableContentInset(in: environment) return section } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() } diff --git a/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift b/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift index 2d2f0389f0..0ff810e4d1 100644 --- a/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift +++ b/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift @@ -35,6 +35,8 @@ class StatusEditHistoryViewController: UIViewController, CollectionViewControlle override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .appGroupedBackground + var config = UICollectionLayoutListConfiguration(appearance: .grouped) config.backgroundColor = .appGroupedBackground config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in @@ -62,8 +64,8 @@ class StatusEditHistoryViewController: UIViewController, CollectionViewControlle collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index 1978195698..df2f089354 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -123,8 +123,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) From 3d9a1086b6cea4c83794d28be628285a0a5bc450 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 20 Aug 2024 12:31:29 -0400 Subject: [PATCH 03/11] Remove dead code --- Tusker.xcodeproj/project.pbxproj | 8 - .../FeaturedProfileCollectionViewCell.swift | 152 ------------------ .../FeaturedProfileCollectionViewCell.xib | 113 ------------- 3 files changed, 273 deletions(-) delete mode 100644 Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift delete mode 100644 Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.xib diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 25f20be3eb..2613721661 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -218,8 +218,6 @@ D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; }; D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */; }; D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; }; - D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; }; - D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */; }; D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; }; D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; }; D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; }; @@ -654,8 +652,6 @@ D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = ""; }; D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.swift; sourceTree = ""; }; D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = ""; }; - D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = ""; }; - D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeaturedProfileCollectionViewCell.xib; sourceTree = ""; }; D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = ""; }; D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = ""; }; D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = ""; }; @@ -991,8 +987,6 @@ D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */, D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */, D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.swift */, - D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */, - D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */, D6C3F4FA299035650009FCFF /* TrendsViewController.swift */, D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */, D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */, @@ -2020,7 +2014,6 @@ buildActionMask = 2147483647; files = ( D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */, - D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */, D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */, D691296E2BA75ADF005C58ED /* PrivacyInfo.xcprivacy in Resources */, D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */, @@ -2261,7 +2254,6 @@ D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */, D6187BED2BFA840B00B3A281 /* FollowRequestNotificationViewController.swift in Sources */, D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */, - D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */, D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */, D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */, D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */, diff --git a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift b/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift deleted file mode 100644 index b253e068f4..0000000000 --- a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// FeaturedProfileCollectionViewCell.swift -// Tusker -// -// Created by Shadowfacts on 2/6/21. -// Copyright © 2021 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm - -class FeaturedProfileCollectionViewCell: UICollectionViewCell { - - @IBOutlet weak var clippingView: UIView! - @IBOutlet weak var headerImageView: UIImageView! - @IBOutlet weak var avatarContainerView: UIView! - @IBOutlet weak var avatarImageView: UIImageView! - @IBOutlet weak var displayNameLabel: AccountDisplayNameLabel! - @IBOutlet weak var noteTextView: StatusContentTextView! - - var account: Account? - - private var accountImagesTask: Task? - - deinit { - accountImagesTask?.cancel() - } - - override func awakeFromNib() { - super.awakeFromNib() - - avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView) - avatarContainerView.layer.cornerCurve = .continuous - avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) - avatarImageView.layer.cornerCurve = .continuous - - displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) - displayNameLabel.adjustsFontForContentSizeCategory = true - - noteTextView.adjustsFontForContentSizeCategory = true - noteTextView.textContainer.lineBreakMode = .byTruncatingTail - noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4) - - backgroundColor = .clear - clippingView.backgroundColor = .appBackground - clippingView.layer.cornerRadius = 5 - clippingView.layer.cornerCurve = .continuous - clippingView.layer.borderWidth = 1 - clippingView.layer.masksToBounds = true - layer.shadowOpacity = 0.2 - layer.shadowRadius = 8 - layer.shadowOffset = .zero - layer.masksToBounds = false - updateLayerColors() - - NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) - } - - func updateUI(account: Account) { - self.account = account - - displayNameLabel.updateForAccountDisplayName(account: account) - - noteTextView.setBodyTextFromHTML(account.note) - noteTextView.setEmojis(account.emojis, identifier: account.id) - - avatarImageView.image = nil - headerImageView.image = nil - - accountImagesTask?.cancel() - accountImagesTask = Task { - await updateImages(account: account) - } - } - - private nonisolated func updateImages(account: Account) async { - await withTaskGroup(of: Void.self) { group in - group.addTask { - guard let avatar = account.avatar, - let image = await ImageCache.avatars.get(avatar).1 else { - return - } - await MainActor.run { - self.avatarImageView.image = image - } - } - group.addTask { - guard let header = account.header, - let image = await ImageCache.headers.get(header).1 else { - return - } - await MainActor.run { - self.headerImageView.image = image - } - } - await group.waitForAll() - } - } - - private func updateLayerColors() { - if traitCollection.userInterfaceStyle == .dark { - clippingView.layer.borderColor = UIColor.darkGray.withAlphaComponent(0.5).cgColor - layer.shadowColor = UIColor.darkGray.cgColor - } else { - clippingView.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor - layer.shadowColor = UIColor.black.cgColor - } - } - - // Unneeded on visionOS because there is no light/dark mode - #if !os(visionOS) - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - updateLayerColors() - } - #endif - - override func layoutSubviews() { - super.layoutSubviews() - - layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: 5, cornerHeight: 5, transform: nil) - } - - @objc private func preferencesChanged() { - avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView) - avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) - - if let account = account { - displayNameLabel.updateForAccountDisplayName(account: account) - } - } - - // MARK: Accessibility - - override var isAccessibilityElement: Bool { - get { true } - set {} - } - - override var accessibilityAttributedLabel: NSAttributedString? { - get { - guard let account else { - return nil - } - let s = NSMutableAttributedString(string: "\(account.displayNameWithoutCustomEmoji), ") - s.append(noteTextView.attributedText) - return s - } - set {} - } - -} diff --git a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.xib b/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.xib deleted file mode 100644 index 0c1d02e434..0000000000 --- a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.xib +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 67e9c1245e4777201e0dfc887449befa5f72c37a Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 20 Aug 2024 22:39:40 -0400 Subject: [PATCH 04/11] Size class switching fixes for new tab/side bar --- Tusker.xcodeproj/project.pbxproj | 4 + .../Main/BaseMainTabBarViewController.swift | 7 +- .../Main/NewMainTabBarViewController.swift | 161 +++++++++++++----- .../AdaptableNavigationController.swift | 136 +++++++++++++++ 4 files changed, 258 insertions(+), 50 deletions(-) create mode 100644 Tusker/Screens/Utilities/AdaptableNavigationController.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 2613721661..c4656be107 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -132,6 +132,7 @@ D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */; }; D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */; }; D64A50BC2C74F8F4009D7193 /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */; }; + D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */; }; D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; }; D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; }; D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; }; @@ -563,6 +564,7 @@ D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMainTabBarViewController.swift; sourceTree = ""; }; D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMainTabBarViewController.swift; sourceTree = ""; }; D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInstanceViewController.swift; sourceTree = ""; }; + D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptableNavigationController.swift; sourceTree = ""; }; D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = ""; }; D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = ""; }; @@ -1556,6 +1558,7 @@ D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */, D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */, D61F759129365C6C00C0B37F /* CollectionViewController.swift */, + D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */, ); path = Utilities; sourceTree = ""; @@ -2341,6 +2344,7 @@ D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, + D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */, D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */, D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */, D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */, diff --git a/Tusker/Screens/Main/BaseMainTabBarViewController.swift b/Tusker/Screens/Main/BaseMainTabBarViewController.swift index 02ddc472a0..1885ff0ab5 100644 --- a/Tusker/Screens/Main/BaseMainTabBarViewController.swift +++ b/Tusker/Screens/Main/BaseMainTabBarViewController.swift @@ -71,11 +71,12 @@ class BaseMainTabBarViewController: UITabBarController { private func repositionFastSwitcherIndicator() { guard let myProfileButton = findMyProfileTabBarButton(), - myProfileButton.window != nil else { + myProfileButton.window != nil, + let fastSwitcherIndicator else { fastSwitcherIndicator?.isHidden = true return } - fastSwitcherIndicator?.isHidden = false + fastSwitcherIndicator.isHidden = false NSLayoutConstraint.deactivate(fastSwitcherConstraints) let isPortrait = view.bounds.width < view.bounds.height if traitCollection.horizontalSizeClass == .compact && isPortrait { @@ -156,7 +157,7 @@ extension BaseMainTabBarViewController: StateRestorableViewController { let compose = presentedNav.viewControllers.first as? ComposeHostingController { let draft = compose.controller.draft activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID) - } else if let vc = (selectedViewController as! UINavigationController).topViewController as? StateRestorableViewController { + } else if let vc = (selectedViewController as? any NavigationControllerProtocol)?.topViewController as? StateRestorableViewController { activity = vc.stateRestorationActivity() } if activity == nil { diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 76853fc521..7ff101d684 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -15,11 +15,19 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { private let composePlaceholder = UIViewController() + private var homeTab: UITab! + private var notificationsTab: UITab! + private var composeTab: UITab! + private var exploreTab: UITab! + private var bookmarksTab: UITab! + private var favoritesTab: UITab! + private var myProfileTab: UITab! private var listsGroup: UITabGroup! private var cancellables = Set() private var navigationStacks = [String: [UIViewController]]() + private var isCompact: Bool? override func viewDidLoad() { super.viewDidLoad() @@ -34,19 +42,19 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { self.makeViewController(for: tab) } - let homeTab = UITab(title: "Home", image: UIImage(systemName: "house"), identifier: Tab.home.rawValue, viewControllerProvider: viewControllerProvider) - let notificationsTab = UITab(title: "Notifications", image: UIImage(systemName: "bell"), identifier: Tab.notifications.rawValue, viewControllerProvider: viewControllerProvider) - let composeTab = UITab(title: "Compose", image: UIImage(systemName: "pencil"), identifier: Tab.compose.rawValue, viewControllerProvider: viewControllerProvider) - let exploreTab = UITab(title: "Explore", image: UIImage(systemName: "magnifyingglass"), identifier: Tab.explore.rawValue, viewControllerProvider: viewControllerProvider) - let bookmarksTab = UITab(title: "Bookmarks", image: UIImage(systemName: "bookmark"), identifier: Tab.bookmarks.rawValue, viewControllerProvider: viewControllerProvider) + homeTab = UITab(title: "Home", image: UIImage(systemName: "house"), identifier: Tab.home.rawValue, viewControllerProvider: viewControllerProvider) + notificationsTab = UITab(title: "Notifications", image: UIImage(systemName: "bell"), identifier: Tab.notifications.rawValue, viewControllerProvider: viewControllerProvider) + composeTab = UITab(title: "Compose", image: UIImage(systemName: "pencil"), identifier: Tab.compose.rawValue, viewControllerProvider: viewControllerProvider) + exploreTab = UITab(title: "Explore", image: UIImage(systemName: "magnifyingglass"), identifier: Tab.explore.rawValue, viewControllerProvider: viewControllerProvider) + bookmarksTab = UITab(title: "Bookmarks", image: UIImage(systemName: "bookmark"), identifier: Tab.bookmarks.rawValue, viewControllerProvider: viewControllerProvider) bookmarksTab.preferredPlacement = .optional - let favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider) + favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider) favoritesTab.preferredPlacement = .optional - let myProfileTab = UITab(title: "My Profile", image: UIImage(systemName: "person"), identifier: Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) + myProfileTab = UITab(title: "My Profile", image: UIImage(systemName: "person"), identifier: Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) listsGroup = UITabGroup(title: "Lists", image: nil, identifier: Tab.lists.rawValue, children: []) { _ in // this closure is necessary to prevent UIKit from crashing (FB14860961) - return MultiColumnNavigationController() + return AdaptableNavigationController() } listsGroup.preferredPlacement = .sidebarOnly listsGroup.sidebarActions = [ @@ -65,6 +73,72 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { myProfileTab, ] } else { + self.updatePadTabs() + registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: NewMainTabBarViewController, previousTraitCollection) in + self.updatePadTabs() + + let vcToUpdate = self.selectedTab!.parent?.viewController ?? self.selectedTab!.viewController! + self.updateViewControllerSafeAreaInsets(vcToUpdate) + } + } + + setupFastAccountSwitcher() + + mastodonController.$lists + .sink { [unowned self] in self.reloadLists($0) } + .store(in: &cancellables) + } + + private func updatePadTabs() { + let wasCompact = isCompact + + if self.traitCollection.horizontalSizeClass == .compact { + isCompact = true + + var exploreNavStack: [UIViewController]? = nil + if selectedTab?.parent == listsGroup { + let nav = listsGroup.viewController as! any NavigationControllerProtocol + exploreNavStack = nav.viewControllers + nav.viewControllers = [] + } + + self.tabs = [ + homeTab, + notificationsTab, + composeTab, + exploreTab, + myProfileTab, + ] + + if let exploreNavStack { + selectedTab = exploreTab + let nav = exploreTab.viewController as! any NavigationControllerProtocol + nav.viewControllers = exploreNavStack + } + } else { + isCompact = false + + var newTab: (UITab, [UIViewController])? = nil + if wasCompact == true, + selectedTab == exploreTab { + let nav = exploreTab.viewController as! any NavigationControllerProtocol + // skip over the ExploreViewController + if nav.viewControllers.count > 1 { + switch nav.viewControllers[1] { + case let listVC as ListTimelineViewController: + if let tab = listsGroup.tab(forIdentifier: "list:\(listVC.list.id)") { + newTab = (tab, Array(nav.viewControllers[1...])) + nav.viewControllers = [ + nav.viewControllers[0], // leave the ExploreVC in place + InlineTrendsViewController(mastodonController: mastodonController), // re-insert an InlineTrendsVC + ] + } + default: + break + } + } + } + self.tabs = [ homeTab, notificationsTab, @@ -75,13 +149,17 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { composeTab, listsGroup, ] + + if let (tab, navStack) = newTab { + let nav = tab.parent!.viewController as! any NavigationControllerProtocol + nav.viewControllers = navStack + // Setting the tab now seems to be clobbered by the UITabBarController itself updating in response + // to the size class change. So wait until it finishes to do so. + DispatchQueue.main.async { + self.selectedTab = tab + } + } } - - setupFastAccountSwitcher() - - mastodonController.$lists - .sink { [unowned self] in self.reloadLists($0) } - .store(in: &cancellables) } private func makeViewController(for tab: UITab) -> UIViewController { @@ -100,7 +178,11 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { if UIDevice.current.userInterfaceIdiom == .phone { root = ExploreViewController(mastodonController: mastodonController) } else { - root = InlineTrendsViewController(mastodonController: mastodonController) + let nav = AdaptableNavigationController(viewControllersToPrependInCompact: [ + ExploreViewController(mastodonController: mastodonController) + ]) + nav.viewControllers = [InlineTrendsViewController(mastodonController: mastodonController)] + return nav } case .bookmarks: root = BookmarksViewController(mastodonController: mastodonController) @@ -111,24 +193,11 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { case .lists: fatalError("unreachable") } - return NewMainTabBarViewController.embedInNavigationController(root) + return embedInNavigationController(root) } - private static func embedInNavigationController(_ vc: UIViewController) -> UIViewController { - let nav: any NavigationControllerProtocol - if UIDevice.current.userInterfaceIdiom == .phone { - nav = EnhancedNavigationViewController() - } else { - // TODO: need to figure out how to update the navigation controller if the pref changes - switch Preferences.shared.widescreenNavigationMode { - case .stack: - nav = EnhancedNavigationViewController() - case .splitScreen: - nav = SplitNavigationController() - case .multiColumn: - nav = MultiColumnNavigationController() - } - } + private func embedInNavigationController(_ vc: UIViewController) -> UIViewController { + let nav = AdaptableNavigationController() nav.viewControllers = [vc] return nav } @@ -136,7 +205,7 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { private func reloadLists(_ lists: [List]) { listsGroup.children = lists.map { list in UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: "list:\(list.id)") { [unowned self] _ in - NewMainTabBarViewController.embedInNavigationController(ListTimelineViewController(for: list, mastodonController: self.mastodonController)) + return ListTimelineViewController(for: list, mastodonController: self.mastodonController) } } } @@ -145,7 +214,10 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { compose(editing: nil) } - fileprivate func updateViewControllerSafeAreaInsets(_ vc: MultiColumnNavigationController) { + fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) { + guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else { + return + } // When in sidebar mode, for multi column mode, don't leave an inset for the floating tab bar, because it leaves a massive gap. // The floating tab bar seems to always be 88pt tall, regardless of, e.g., Dynamic Type size. vc.additionalSafeAreaInsets = UIEdgeInsets(top: sidebar.isHidden ? 0 : -88, left: 0, bottom: 0, right: 0) @@ -192,10 +264,8 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { } func tabBarController(_ tabBarController: UITabBarController, didSelectTab newTab: UITab, previousTab: UITab?) { - if let vc = newTab.viewController as? MultiColumnNavigationController { - self.updateViewControllerSafeAreaInsets(vc) - } - + self.updateViewControllerSafeAreaInsets(newTab.viewController!) + // All tabs in a tab group deliberately share the same view controller, so we have to do this ourselves. // I think this is pretty unfortunate API design--half the time, the tab bar controller takes care of // this, but the rest of the time it's up to you. @@ -207,9 +277,7 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { if let group = newTab.parent, group.identifier == Tab.lists.rawValue, let nav = group.viewController as? any NavigationControllerProtocol { - if let multiColumn = nav as? MultiColumnNavigationController { - updateViewControllerSafeAreaInsets(multiColumn) - } + updateViewControllerSafeAreaInsets(nav) if let previousTab { navigationStacks[previousTab.identifier] = nav.viewControllers @@ -217,8 +285,8 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { if let existing = navigationStacks[newTab.identifier] { nav.viewControllers = existing - } else if let newNav = newTab.viewController as? any NavigationControllerProtocol { - nav.viewControllers = newNav.viewControllers + } else if let newVC = newTab.viewController { + nav.viewControllers = [newVC] } else { fatalError("unreachable") } @@ -229,11 +297,10 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { @available(iOS 18.0, *) extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) { - if let vc = selectedViewController as? MultiColumnNavigationController { - animator.addAnimations { - self.updateViewControllerSafeAreaInsets(vc) - vc.view.layoutIfNeeded() - } + let vc = selectedTab!.parent?.viewController ?? selectedTab!.viewController! + animator.addAnimations { + self.updateViewControllerSafeAreaInsets(vc) + vc.view.layoutIfNeeded() } } } diff --git a/Tusker/Screens/Utilities/AdaptableNavigationController.swift b/Tusker/Screens/Utilities/AdaptableNavigationController.swift new file mode 100644 index 0000000000..0a39fcabf1 --- /dev/null +++ b/Tusker/Screens/Utilities/AdaptableNavigationController.swift @@ -0,0 +1,136 @@ +// +// AdaptableNavigationController.swift +// Tusker +// +// Created by Shadowfacts on 8/20/24. +// Copyright © 2024 Shadowfacts. All rights reserved. +// + +import UIKit +import Combine + +@available(iOS 17.0, *) +class AdaptableNavigationController: UIViewController { + + private let viewControllersToPrependInCompact: [UIViewController] + + private var initialViewControllers: [UIViewController] = [] + private lazy var regular = makeRegularNavigationController() + private lazy var compact = makeCompactNavigationController() + private var _current: (any NavigationControllerProtocol)? + var current: any NavigationControllerProtocol { + traitCollection.horizontalSizeClass == .regular ? regular : compact + } + + init(viewControllersToPrependInCompact: [UIViewController] = []) { + self.viewControllersToPrependInCompact = viewControllersToPrependInCompact + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + updateNavigationController() + registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: AdaptableNavigationController, previousTraitCollection) in + self.updateNavigationController() + } + } + + private func updateNavigationController() { + let isTransferring: Bool + var stack: [UIViewController] + if let _current { + _current.removeViewAndController() + stack = _current.viewControllers + isTransferring = true + } else { + stack = initialViewControllers + initialViewControllers = [] + isTransferring = false + } + + if traitCollection.horizontalSizeClass == .regular { + if isTransferring { + stack.removeFirst(viewControllersToPrependInCompact.count) + } + } else { + stack.insert(contentsOf: viewControllersToPrependInCompact, at: 0) + } + + _current = current + current.viewControllers = stack + + addChild(current) + current.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(current.view) + NSLayoutConstraint.activate([ + current.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + current.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + current.view.topAnchor.constraint(equalTo: view.topAnchor), + current.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + current.didMove(toParent: self) + } + + private func makeRegularNavigationController() -> any NavigationControllerProtocol { + // TODO: need to figure out how to update the navigation controller if the pref changes + switch Preferences.shared.widescreenNavigationMode { + case .stack: + return EnhancedNavigationViewController() + case .splitScreen: + return SplitNavigationController() + case .multiColumn: + return MultiColumnNavigationController() + } + } + + private func makeCompactNavigationController() -> any NavigationControllerProtocol { + EnhancedNavigationViewController() + } +} + +@available(iOS 17.0, *) +extension AdaptableNavigationController: NavigationControllerProtocol { + var viewControllers: [UIViewController] { + get { + _current?.viewControllers ?? initialViewControllers + } + set { + if let _current { + _current.viewControllers = newValue + } else { + initialViewControllers = newValue + } + } + } + + var topViewController: UIViewController? { + if let _current { + return _current.topViewController + } else { + return initialViewControllers.last + } + } + + func popToRootViewController(animated: Bool) -> [UIViewController]? { + if let _current { + return _current.popToRootViewController(animated: animated) + } else { + defer { initialViewControllers = [] } + return initialViewControllers + } + } + + func pushViewController(_ vc: UIViewController, animated: Bool) { + if let _current { + _current.pushViewController(vc, animated: animated) + } else { + initialViewControllers.append(vc) + } + } +} From cb32c66a595554b11b6126756d4a770b5123d746 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 14:48:47 -0400 Subject: [PATCH 05/11] Support fast account switching with new sidebar --- Tusker/Box.swift | 6 +- .../FastAccountSwitcherViewController.swift | 6 +- ...ountSwitchingContainerViewController.swift | 11 +- .../Main/BaseMainTabBarViewController.swift | 21 ++-- .../Main/MainSplitViewController.swift | 6 +- .../Main/NewMainTabBarViewController.swift | 109 ++++++++++++++++++ 6 files changed, 148 insertions(+), 11 deletions(-) diff --git a/Tusker/Box.swift b/Tusker/Box.swift index d1dce9f6d7..f39690d63e 100644 --- a/Tusker/Box.swift +++ b/Tusker/Box.swift @@ -9,10 +9,14 @@ import Foundation @propertyWrapper -class Box { +final class Box { var wrappedValue: Value init(wrappedValue: Value) { self.wrappedValue = wrappedValue } + + var projectedValue: Box { + self + } } diff --git a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift index 692cf23a9e..f4df41f359 100644 --- a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift +++ b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift @@ -11,6 +11,7 @@ import UserAccounts @MainActor protocol FastAccountSwitcherViewControllerDelegate: AnyObject { + func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) /// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached. func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool @@ -31,7 +32,7 @@ class FastAccountSwitcherViewController: UIViewController { #endif private var touchBeganFeedbackWorkItem: DispatchWorkItem? - var itemOrientation: ItemOrientation = .iconsTrailing + private var itemOrientation: ItemOrientation = .iconsTrailing init() { super.init(nibName: "FastAccountSwitcherViewController", bundle: .main) @@ -60,6 +61,9 @@ class FastAccountSwitcherViewController: UIViewController { } func show() { + if let delegate { + itemOrientation = delegate.fastAccountSwitcherItemOrientation(self) + } createAccountViews() // add after creating account views so that the presenter can align based on them delegate?.fastAccountSwitcherAddToViewHierarchy(self) diff --git a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift index d2a472117e..45a7d2639e 100644 --- a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift +++ b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift @@ -39,7 +39,16 @@ class AccountSwitchingContainerViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - embedChild(root) + addChild(root) + root.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(root.view) + NSLayoutConstraint.activate([ + root.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + root.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + root.view.topAnchor.constraint(equalTo: view.topAnchor), + root.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + root.didMove(toParent: self) } override func didReceiveMemoryWarning() { diff --git a/Tusker/Screens/Main/BaseMainTabBarViewController.swift b/Tusker/Screens/Main/BaseMainTabBarViewController.swift index 1885ff0ab5..990ef8c0a1 100644 --- a/Tusker/Screens/Main/BaseMainTabBarViewController.swift +++ b/Tusker/Screens/Main/BaseMainTabBarViewController.swift @@ -8,7 +8,7 @@ import UIKit -class BaseMainTabBarViewController: UITabBarController { +class BaseMainTabBarViewController: UITabBarController, FastAccountSwitcherViewControllerDelegate { let mastodonController: MastodonController @@ -114,12 +114,15 @@ class BaseMainTabBarViewController: UITabBarController { fastAccountSwitcher.hide() } #endif // !os(visionOS) - -} - -#if !os(visionOS) -extension BaseMainTabBarViewController: FastAccountSwitcherViewControllerDelegate { + + // MARK: FastAccountSwitcherViewControllerDelegate + + func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation { + return .iconsTrailing + } + func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { + #if !os(visionOS) fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(fastAccountSwitcher.view) NSLayoutConstraint.activate([ @@ -134,17 +137,21 @@ extension BaseMainTabBarViewController: FastAccountSwitcherViewControllerDelegat fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), ]) + #endif // !os(visionOS) } func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { + #if !os(visionOS) guard let myProfileButton = findMyProfileTabBarButton() else { return false } let locationInButton = myProfileButton.convert(point, from: tabBar) return myProfileButton.bounds.contains(locationInButton) + #else + return false + #endif // !os(visionOS) } } -#endif // !os(visionOS) extension BaseMainTabBarViewController: TuskerNavigationDelegate { var apiController: MastodonController! { mastodonController } diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index 14f8a35b97..23cf89734f 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -93,7 +93,6 @@ class MainSplitViewController: UISplitViewController { if UIDevice.current.userInterfaceIdiom != .mac { let switcher = FastAccountSwitcherViewController() fastAccountSwitcher = switcher - switcher.itemOrientation = .iconsLeading switcher.view.translatesAutoresizingMaskIntoConstraints = false switcher.delegate = self // accessing .view unconditionally loads the view, which we don't want to happen @@ -664,6 +663,10 @@ extension MainSplitViewController: BackgroundableViewController { } extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate { + func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation { + return .iconsLeading + } + func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { view.addSubview(fastAccountSwitcher.view) let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)! @@ -677,6 +680,7 @@ extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate { fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } + func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { guard !isCollapsed, let cell = sidebar.myProfileCell() else { diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 7ff101d684..4726f44d0b 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -28,6 +28,8 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { private var navigationStacks = [String: [UIViewController]]() private var isCompact: Bool? + @Box fileprivate var myProfileCell: UIView? + private var sidebarTapRecognizer: UITapGestureRecognizer? override func viewDidLoad() { super.viewDidLoad() @@ -202,6 +204,17 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { return nav } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if sidebarTapRecognizer == nil, + let sidebarView = findSidebarView() { + sidebarTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped)) + sidebarTapRecognizer!.cancelsTouchesInView = false + sidebarView.addGestureRecognizer(sidebarTapRecognizer!) + } + } + private func reloadLists(_ lists: [List]) { listsGroup.children = lists.map { list in UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: "list:\(list.id)") { [unowned self] _ in @@ -214,6 +227,10 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { compose(editing: nil) } + @objc private func sidebarTapped() { + fastAccountSwitcher?.hide() + } + fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) { guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else { return @@ -222,6 +239,57 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { // The floating tab bar seems to always be 88pt tall, regardless of, e.g., Dynamic Type size. vc.additionalSafeAreaInsets = UIEdgeInsets(top: sidebar.isHidden ? 0 : -88, left: 0, bottom: 0, right: 0) } + + private func findSidebarView() -> UIView? { + var next = myProfileCell + while let cur = next { + if cur.superview?.superview === self.view { + return cur + } else { + next = cur.superview + } + } + return nil + } + + #if !os(visionOS) + override func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation { + guard !sidebar.isHidden, + myProfileCell != nil else { + return super.fastAccountSwitcherItemOrientation(fastAccountSwitcher) + } + return .iconsLeading + } + + override func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { + guard !sidebar.isHidden, + let myProfileCell else { + super.fastAccountSwitcherAddToViewHierarchy(fastAccountSwitcher) + return + } + + fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(fastAccountSwitcher.view) + + let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)! + NSLayoutConstraint.activate([ + currentAccount.centerYAnchor.constraint(equalTo: myProfileCell.centerYAnchor), + + fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: selectedTab!.viewController!.view.safeAreaLayoutGuide.leadingAnchor), + fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor), + fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + override func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { + guard !sidebar.isHidden, + myProfileCell != nil else { + return super.fastAccountSwitcher(fastAccountSwitcher, triggerZoneContains: point) + } + return true + } + #endif } @available(iOS 18.0, *) @@ -303,6 +371,24 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { vc.view.layoutIfNeeded() } } + + func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, itemFor request: UITabSidebarItem.Request) -> UITabSidebarItem { + let item = UITabSidebarItem(request: request) + if case .tab(let tab) = request.content, + UIDevice.current.userInterfaceIdiom != .mac, + tab.identifier == Tab.myProfile.rawValue { + let indicator = FastAccountSwitcherIndicatorView() + // need to explicitly set the frame to get it vertically centered + indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize) + item.accessories = [ + .customView(configuration: .init(customView: indicator, placement: .trailing())) + ] + item.contentConfiguration = MyProfileContentConfiguration(wrapped: item.contentConfiguration, view: $myProfileCell) { [unowned self] in + $0.addGestureRecognizer(self.fastAccountSwitcher.createSwitcherGesture()) + } + } + return item + } } @available(iOS 18.0, *) @@ -393,3 +479,26 @@ extension NewMainTabBarViewController: AccountSwitchableViewController { #endif } } + +private struct MyProfileContentConfiguration: UIContentConfiguration { + let wrapped: any UIContentConfiguration + @Box var view: UIView? + let configureView: (UIView) -> Void + + init(wrapped: any UIContentConfiguration, view: Box, configureView: @escaping (UIView) -> Void) { + self.wrapped = wrapped + self._view = view + self.configureView = configureView + } + + func makeContentView() -> any UIView & UIContentView { + let view = wrapped.makeContentView() + self.view = view + configureView(view) + return view + } + + func updated(for state: any UIConfigurationState) -> Self { + return .init(wrapped: wrapped.updated(for: state), view: $view, configureView: configureView) + } +} From 7c7af945e4dbcbec15488f6daf055b1ef07e0a9f Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 16:12:05 -0400 Subject: [PATCH 06/11] Show avatar in tab/side bar when using new API --- ...inSidebarMyProfileCollectionViewCell.swift | 14 +-- .../Main/NewMainTabBarViewController.swift | 103 ++++++++++++++++-- 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift b/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift index 8e9d66b034..48ed96df4f 100644 --- a/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift +++ b/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift @@ -11,14 +11,14 @@ import UserAccounts class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell { - private var verticalImageInset: CGFloat { + static var verticalImageInset: CGFloat { if UIDevice.current.userInterfaceIdiom == .mac { return (28 - avatarImageSize) / 2 } else { return (44 - avatarImageSize) / 2 } } - private var avatarImageSize: CGFloat { + static var avatarImageSize: CGFloat { if UIDevice.current.userInterfaceIdiom == .mac { return 20 } else { @@ -72,11 +72,11 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell { return } config.image = image - config.directionalLayoutMargins.top = self.verticalImageInset - config.directionalLayoutMargins.bottom = self.verticalImageInset - config.imageProperties.maximumSize = CGSize(width: self.avatarImageSize, height: self.avatarImageSize) + config.directionalLayoutMargins.top = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.directionalLayoutMargins.bottom = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.imageProperties.maximumSize = CGSize(width: MainSidebarMyProfileCollectionViewCell.avatarImageSize, height: MainSidebarMyProfileCollectionViewCell.avatarImageSize) config.imageProperties.reservedLayoutSize = CGSize(width: UIListContentConfiguration.ImageProperties.standardDimension, height: 0) - config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * self.avatarImageSize + config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize self.contentConfiguration = config } } @@ -86,7 +86,7 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell { guard var config = self.contentConfiguration as? UIListContentConfiguration else { return } - config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarImageSize + config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize self.contentConfiguration = config } diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 4726f44d0b..4b48d0cd31 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -9,6 +9,7 @@ import UIKit import Combine import Pachyderm +import TuskerPreferences @available(iOS 18.0, *) class NewMainTabBarViewController: BaseMainTabBarViewController { @@ -52,7 +53,7 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { bookmarksTab.preferredPlacement = .optional favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider) favoritesTab.preferredPlacement = .optional - myProfileTab = UITab(title: "My Profile", image: UIImage(systemName: "person"), identifier: Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) + myProfileTab = MyProfileTab(mastodonController: mastodonController, viewControllerProvider: viewControllerProvider) listsGroup = UITabGroup(title: "Lists", image: nil, identifier: Tab.lists.rawValue, children: []) { _ in // this closure is necessary to prevent UIKit from crashing (FB14860961) @@ -362,6 +363,13 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { } } +private var fastAccountSwitcherIndicator: UIView = { + let indicator = FastAccountSwitcherIndicatorView() + // need to explicitly set the frame to get it vertically centered + indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize) + return indicator +}() + @available(iOS 18.0, *) extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) { @@ -375,16 +383,22 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, itemFor request: UITabSidebarItem.Request) -> UITabSidebarItem { let item = UITabSidebarItem(request: request) if case .tab(let tab) = request.content, - UIDevice.current.userInterfaceIdiom != .mac, - tab.identifier == Tab.myProfile.rawValue { - let indicator = FastAccountSwitcherIndicatorView() - // need to explicitly set the frame to get it vertically centered - indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize) - item.accessories = [ - .customView(configuration: .init(customView: indicator, placement: .trailing())) - ] - item.contentConfiguration = MyProfileContentConfiguration(wrapped: item.contentConfiguration, view: $myProfileCell) { [unowned self] in - $0.addGestureRecognizer(self.fastAccountSwitcher.createSwitcherGesture()) + tab.identifier == Tab.myProfile.rawValue, + var config = item.contentConfiguration as? UIListContentConfiguration { + config.directionalLayoutMargins.top = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.directionalLayoutMargins.bottom = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.imageProperties.maximumSize = CGSize(width: MainSidebarMyProfileCollectionViewCell.avatarImageSize, height: MainSidebarMyProfileCollectionViewCell.avatarImageSize) + config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize + + if UIDevice.current.userInterfaceIdiom != .mac { + item.accessories = [ + .customView(configuration: .init(customView: fastAccountSwitcherIndicator, placement: .trailing())) + ] + item.contentConfiguration = MyProfileContentConfiguration(wrapped: config, view: $myProfileCell) { [unowned self] in + $0.addGestureRecognizer(self.fastAccountSwitcher.createSwitcherGesture()) + } + } else { + item.contentConfiguration = config } } return item @@ -502,3 +516,70 @@ private struct MyProfileContentConfiguration: UIContentConfiguration { return .init(wrapped: wrapped.updated(for: state), view: $view, configureView: configureView) } } + +@available(iOS 18.0, *) +private class MyProfileTab: UITab { + private let mastodonController: MastodonController + private var avatarStyle: AvatarStyle? + + init(mastodonController: MastodonController, viewControllerProvider: @escaping (UITab) -> UIViewController) { + self.mastodonController = mastodonController + + // try to add the avatar image synchronously if possible + var avatarImage: UIImage? + if !Preferences.shared.grayscaleImages, + let account = mastodonController.account, + let avatarURL = account.avatar, + let avatar = ImageCache.avatars.get(avatarURL) { + avatarImage = Self.renderAvatar(avatar.image) + self.avatarStyle = Preferences.shared.avatarStyle + } + + let image = avatarImage ?? UIImage(systemName: "person")! + super.init(title: "My Profile", image: image, identifier: NewMainTabBarViewController.Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) + + if avatarImage == nil { + Task { + await updateAvatar() + } + } + + NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) + } + + private func updateAvatar() async { + guard let account = try? await mastodonController.getOwnAccount(), + let avatarURL = account.avatar, + let image = await ImageCache.avatars.get(avatarURL).1 else { + return + } + + let maybeGrayscale = await ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) ?? image + let rendered = Self.renderAvatar(maybeGrayscale) + + self.avatarStyle = Preferences.shared.avatarStyle + self.image = rendered + } + + private static func renderAvatar(_ image: UIImage) -> UIImage { + let size = MainSidebarMyProfileCollectionViewCell.avatarImageSize + let radius = Preferences.shared.avatarStyle.cornerRadiusFraction * size + let rect = CGRect(x: 0, y: 0, width: size, height: size) + let renderer = UIGraphicsImageRenderer(bounds: rect) + let rendered = renderer.image { ctx in + UIBezierPath(roundedRect: rect, cornerRadius: radius).addClip() + image.draw(in: rect) + } + return rendered.withRenderingMode(.alwaysOriginal) + } + + @objc private func preferencesChanged() { + if avatarStyle != nil, + avatarStyle != Preferences.shared.avatarStyle { + Task { + await updateAvatar() + } + } + } + +} From ce10c7d6e28c43bdf40674a5efa77e112dc7f093 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 16:16:34 -0400 Subject: [PATCH 07/11] Implement adding list using new sidebar --- .../Main/MainSidebarViewController.swift | 5 ++-- .../Main/NewMainTabBarViewController.swift | 26 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Tusker/Screens/Main/MainSidebarViewController.swift b/Tusker/Screens/Main/MainSidebarViewController.swift index 6b0908bc37..d35363f521 100644 --- a/Tusker/Screens/Main/MainSidebarViewController.swift +++ b/Tusker/Screens/Main/MainSidebarViewController.swift @@ -269,8 +269,9 @@ class MainSidebarViewController: UIViewController { } private func showAddList() { - let service = CreateListService(mastodonController: mastodonController, present: { self.present($0, animated: true - ) }) { list in + let service = CreateListService(mastodonController: mastodonController, present: { + self.present($0, animated: true) + }) { list in let oldItem = self.selectedItem self.select(item: .list(list), animated: false) let list = ListTimelineViewController(for: list, mastodonController: self.mastodonController) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 4b48d0cd31..ebfbc6bd9a 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -12,7 +12,7 @@ import Pachyderm import TuskerPreferences @available(iOS 18.0, *) -class NewMainTabBarViewController: BaseMainTabBarViewController { +final class NewMainTabBarViewController: BaseMainTabBarViewController { private let composePlaceholder = UIViewController() @@ -61,8 +61,8 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { } listsGroup.preferredPlacement = .sidebarOnly listsGroup.sidebarActions = [ - UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { _ in - fatalError("TODO") + UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in + self.showAddList() }) ] reloadLists(mastodonController.lists) @@ -129,7 +129,7 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { if nav.viewControllers.count > 1 { switch nav.viewControllers[1] { case let listVC as ListTimelineViewController: - if let tab = listsGroup.tab(forIdentifier: "list:\(listVC.list.id)") { + if let tab = listsGroup.tab(forIdentifier: Self.listTabIdentifier(listVC.list)) { newTab = (tab, Array(nav.viewControllers[1...])) nav.viewControllers = [ nav.viewControllers[0], // leave the ExploreVC in place @@ -218,12 +218,16 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { private func reloadLists(_ lists: [List]) { listsGroup.children = lists.map { list in - UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: "list:\(list.id)") { [unowned self] _ in + UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: Self.listTabIdentifier(list)) { [unowned self] _ in return ListTimelineViewController(for: list, mastodonController: self.mastodonController) } } } + private static func listTabIdentifier(_ list: List) -> String { + "list:\(list.id)" + } + @objc func handleComposeKeyCommand() { compose(editing: nil) } @@ -232,6 +236,18 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { fastAccountSwitcher?.hide() } + private func showAddList() { + let service = CreateListService(mastodonController: mastodonController, present: { + self.present($0, animated: true) + }) { list in + let tab = self.listsGroup.tab(forIdentifier: Self.listTabIdentifier(list))! + let listVC = tab.viewController as! ListTimelineViewController + listVC.presentEditOnAppear = true + self.selectedTab = tab + } + service.run() + } + fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) { guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else { return From d321c317766aa78d0bcc16aa7d20f15a8d2c2d77 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 16:23:05 -0400 Subject: [PATCH 08/11] Implement more protocols for AdaptableNavigationController --- .../Utilities/AdaptableNavigationController.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tusker/Screens/Utilities/AdaptableNavigationController.swift b/Tusker/Screens/Utilities/AdaptableNavigationController.swift index 0a39fcabf1..145e75cd2d 100644 --- a/Tusker/Screens/Utilities/AdaptableNavigationController.swift +++ b/Tusker/Screens/Utilities/AdaptableNavigationController.swift @@ -134,3 +134,17 @@ extension AdaptableNavigationController: NavigationControllerProtocol { } } } + +@available(iOS 17.0, *) +extension AdaptableNavigationController: BackgroundableViewController { + func sceneDidEnterBackground() { + (topViewController as? BackgroundableViewController)?.sceneDidEnterBackground() + } +} + +@available(iOS 17.0, *) +extension AdaptableNavigationController: StatusBarTappableViewController { + func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { + (topViewController as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue + } +} From 59d43fd3f626e46154d9428ae2174784898bae34 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 16:38:24 -0400 Subject: [PATCH 09/11] Open in New Window context menu actions for new sidebar --- .../Main/NewMainTabBarViewController.swift | 88 +++++++++++++++++-- 1 file changed, 80 insertions(+), 8 deletions(-) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index ebfbc6bd9a..c0a7f02fe1 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -129,7 +129,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { if nav.viewControllers.count > 1 { switch nav.viewControllers[1] { case let listVC as ListTimelineViewController: - if let tab = listsGroup.tab(forIdentifier: Self.listTabIdentifier(listVC.list)) { + if let tab = listsGroup.tab(forIdentifier: ListTab.identifier(for: listVC.list)) { newTab = (tab, Array(nav.viewControllers[1...])) nav.viewControllers = [ nav.viewControllers[0], // leave the ExploreVC in place @@ -218,16 +218,12 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { private func reloadLists(_ lists: [List]) { listsGroup.children = lists.map { list in - UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: Self.listTabIdentifier(list)) { [unowned self] _ in + ListTab(list: list) { [unowned self] _ in return ListTimelineViewController(for: list, mastodonController: self.mastodonController) } } } - private static func listTabIdentifier(_ list: List) -> String { - "list:\(list.id)" - } - @objc func handleComposeKeyCommand() { compose(editing: nil) } @@ -240,7 +236,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { let service = CreateListService(mastodonController: mastodonController, present: { self.present($0, animated: true) }) { list in - let tab = self.listsGroup.tab(forIdentifier: Self.listTabIdentifier(list))! + let tab = self.listsGroup.tab(forIdentifier: ListTab.identifier(for: list))! let listVC = tab.viewController as! ListTimelineViewController listVC.presentEditOnAppear = true self.selectedTab = tab @@ -419,6 +415,69 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { } return item } + + func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, contextMenuConfigurationFor tab: UITab) -> UIContextMenuConfiguration? { + guard let id = mastodonController.accountInfo?.id else { + return nil + } + + let activity: NSUserActivity + + if let listTab = tab as? ListTab { + let timelineActivity = UserActivityManager.showTimelineActivity(timeline: .list(id: listTab.list.id), accountID: id) + if let timelineActivity { + activity = timelineActivity + } else { + return nil + } + } else if let tabID = Tab(rawValue: tab.identifier) { + switch tabID { + case .home: + return nil + case .notifications: + activity = UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode, accountID: id) + case .explore: + activity = UserActivityManager.searchActivity(query: nil, accountID: id) + case .bookmarks: + activity = UserActivityManager.bookmarksActivity(accountID: id) + case .favorites: + // TODO + return nil + case .myProfile: + // no 'Open in New Window' activity for my profile, because the context menu clashes with the fast account switcher + return nil + case .compose: + activity = UserActivityManager.newPostActivity(accountID: id) + case .lists: + return nil + } + } else { + return nil + } + + activity.displaysAuxiliaryScene = true + + return UIContextMenuConfiguration(actionProvider: { _ in + var actions: [UIAction] = [ + UIWindowScene.ActivationAction({ action in + return UIWindowScene.ActivationConfiguration(userActivity: activity) + }) + ] + + if let listTab = tab as? ListTab { + actions.append(UIAction(title: "Delete List", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { [unowned self] _ in + Task { + let service = DeleteListService(list: listTab.list, mastodonController: self.mastodonController) { + self.present($0, animated: true) + } + await service.run() + } + })) + } + + return UIMenu(children: actions) + }) + } } @available(iOS 18.0, *) @@ -597,5 +656,18 @@ private class MyProfileTab: UITab { } } } - +} + +@available(iOS 18.0, *) +private class ListTab: UITab { + let list: List + + init(list: List, viewControllerProvider: @escaping (UITab) -> UIViewController) { + self.list = list + super.init(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: Self.identifier(for: list), viewControllerProvider: viewControllerProvider) + } + + static func identifier(for list: List) -> String { + "list:\(list.id)" + } } From 0d9eed73dda16194525433ccbdf74b1fa08ddb40 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 16:58:16 -0400 Subject: [PATCH 10/11] Add saved/followed hashtags to new sidebar --- .../Main/NewMainTabBarViewController.swift | 123 +++++++++++++++--- 1 file changed, 103 insertions(+), 20 deletions(-) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index c0a7f02fe1..da54db0846 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -24,6 +24,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { private var favoritesTab: UITab! private var myProfileTab: UITab! private var listsGroup: UITabGroup! + private var hashtagsGroup: UITabGroup! private var cancellables = Set() @@ -67,6 +68,17 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { ] reloadLists(mastodonController.lists) + hashtagsGroup = UITabGroup(title: "Hashtags", image: nil, identifier: Tab.hashtags.rawValue, children: []) { _ in + return AdaptableNavigationController() + } + hashtagsGroup.preferredPlacement = .sidebarOnly + hashtagsGroup.sidebarActions = [ + UIAction(title: "Add Hashtag…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in + self.showAddSavedHashtag() + }) + ] + reloadHashtags() + if UIDevice.current.userInterfaceIdiom == .phone { self.tabs = [ homeTab, @@ -83,13 +95,19 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { let vcToUpdate = self.selectedTab!.parent?.viewController ?? self.selectedTab!.viewController! self.updateViewControllerSafeAreaInsets(vcToUpdate) } + + mastodonController.$lists + .sink { [unowned self] in self.reloadLists($0) } + .store(in: &cancellables) + + mastodonController.$followedHashtags + .map { _ in () } + .merge(with: NotificationCenter.default.publisher(for: .savedHashtagsChanged).map { _ in () }) + .sink { [unowned self] in self.reloadHashtags() } + .store(in: &cancellables) } setupFastAccountSwitcher() - - mastodonController.$lists - .sink { [unowned self] in self.reloadLists($0) } - .store(in: &cancellables) } private func updatePadTabs() { @@ -99,8 +117,9 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { isCompact = true var exploreNavStack: [UIViewController]? = nil - if selectedTab?.parent == listsGroup { - let nav = listsGroup.viewController as! any NavigationControllerProtocol + if let parent = selectedTab?.parent, + parent === listsGroup || parent === hashtagsGroup { + let nav = parent.viewController as! any NavigationControllerProtocol exploreNavStack = nav.viewControllers nav.viewControllers = [] } @@ -121,24 +140,33 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { } else { isCompact = false - var newTab: (UITab, [UIViewController])? = nil + var newTabAndNavigationStack: (UITab, [UIViewController])? = nil if wasCompact == true, selectedTab == exploreTab { let nav = exploreTab.viewController as! any NavigationControllerProtocol // skip over the ExploreViewController if nav.viewControllers.count > 1 { + var newTab: UITab? switch nav.viewControllers[1] { case let listVC as ListTimelineViewController: if let tab = listsGroup.tab(forIdentifier: ListTab.identifier(for: listVC.list)) { - newTab = (tab, Array(nav.viewControllers[1...])) - nav.viewControllers = [ - nav.viewControllers[0], // leave the ExploreVC in place - InlineTrendsViewController(mastodonController: mastodonController), // re-insert an InlineTrendsVC - ] + newTab = tab + } + case let hashtagVC as HashtagTimelineViewController: + if let tab = hashtagsGroup.tab(forIdentifier: HashtagTab.identifier(for: hashtagVC.hashtagName)) { + newTab = tab } default: break } + + if let newTab { + newTabAndNavigationStack = (newTab, Array(nav.viewControllers[1...])) + nav.viewControllers = [ + nav.viewControllers[0], // leave the ExploreVC in place + InlineTrendsViewController(mastodonController: mastodonController), // re-insert an InlineTrendsVC + ] + } } } @@ -151,9 +179,10 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { myProfileTab, composeTab, listsGroup, + hashtagsGroup, ] - if let (tab, navStack) = newTab { + if let (tab, navStack) = newTabAndNavigationStack { let nav = tab.parent!.viewController as! any NavigationControllerProtocol nav.viewControllers = navStack // Setting the tab now seems to be clobbered by the UITabBarController itself updating in response @@ -193,7 +222,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { root = FavoritesViewController(mastodonController: mastodonController) case .myProfile: root = MyProfileViewController(mastodonController: mastodonController) - case .lists: + case .lists, .hashtags: fatalError("unreachable") } return embedInNavigationController(root) @@ -217,11 +246,37 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { } private func reloadLists(_ lists: [List]) { - listsGroup.children = lists.map { list in - ListTab(list: list) { [unowned self] _ in - return ListTimelineViewController(for: list, mastodonController: self.mastodonController) - } + let viewControllerProvider = { [unowned self] (tab: UITab) in + let tab = tab as! ListTab + return ListTimelineViewController(for: tab.list, mastodonController: self.mastodonController) } + listsGroup.children = lists.map { list in + ListTab(list: list, viewControllerProvider: viewControllerProvider) + } + } + + private func reloadHashtags() { + let viewControllerProvider = { [unowned self] (tab: UITab) in + let tab = tab as! HashtagTab + return HashtagTimelineViewController(forNamed: tab.hashtagName, mastodonController: self.mastodonController) + } + var seenTags: Set = [] + var tabs: [UITab] = [] + let savedReq = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!) + let saved = (try? mastodonController.persistentContainer.viewContext.fetch(savedReq)) ?? [] + for hashtag in saved { + seenTags.insert(hashtag.name) + tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider)) + } + + let followedReq = FollowedHashtag.fetchRequest() + let followed = (try? mastodonController.persistentContainer.viewContext.fetch(followedReq)) ?? [] + for hashtag in followed where !seenTags.contains(hashtag.name) { + tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider)) + } + + tabs.sort(using: SemiCaseSensitiveComparator.keyPath(\.title)) + hashtagsGroup.children = tabs } @objc func handleComposeKeyCommand() { @@ -244,6 +299,12 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { service.run() } + private func showAddSavedHashtag() { + let addController = AddSavedHashtagViewController(mastodonController: mastodonController) + let nav = EnhancedNavigationViewController(rootViewController: addController) + present(nav, animated: true) + } + fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) { guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else { return @@ -317,6 +378,7 @@ extension NewMainTabBarViewController { case myProfile case lists + case hashtags } } @@ -356,7 +418,7 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { // get the new transition animation. // This would be much less complicated if the controller just used the individual VCs of items in a group. if let group = newTab.parent, - group.identifier == Tab.lists.rawValue, + group === listsGroup || group === hashtagsGroup, let nav = group.viewController as? any NavigationControllerProtocol { updateViewControllerSafeAreaInsets(nav) @@ -430,6 +492,13 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { } else { return nil } + } else if let hashtagTab = tab as? HashtagTab { + let timelineActivity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtagTab.hashtagName), accountID: id) + if let timelineActivity { + activity = timelineActivity + } else { + return nil + } } else if let tabID = Tab(rawValue: tab.identifier) { switch tabID { case .home: @@ -448,7 +517,7 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { return nil case .compose: activity = UserActivityManager.newPostActivity(accountID: id) - case .lists: + case .lists, .hashtags: return nil } } else { @@ -671,3 +740,17 @@ private class ListTab: UITab { "list:\(list.id)" } } + +@available(iOS 18.0, *) +private class HashtagTab: UITab { + let hashtagName: String + + init(hashtagName: String, viewControllerProvider: @escaping (UITab) -> UIViewController) { + self.hashtagName = hashtagName + super.init(title: hashtagName, image: UIImage(systemName: "number"), identifier: Self.identifier(for: hashtagName), viewControllerProvider: viewControllerProvider) + } + + static func identifier(for name: String) -> String { + "hashtag:\(name)" + } +} From 18172470772d3eb324f907632a0361f195a49e84 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 17:10:01 -0400 Subject: [PATCH 11/11] Add saved instances to new sidebar --- .../Main/MainSidebarViewController.swift | 2 +- .../Main/NewMainTabBarViewController.swift | 79 ++++++++++++++++++- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/Tusker/Screens/Main/MainSidebarViewController.swift b/Tusker/Screens/Main/MainSidebarViewController.swift index d35363f521..3391bbd5b4 100644 --- a/Tusker/Screens/Main/MainSidebarViewController.swift +++ b/Tusker/Screens/Main/MainSidebarViewController.swift @@ -371,7 +371,7 @@ extension MainSidebarViewController { case let .savedInstance(url): return url.host! case .addSavedInstance: - return "Find An Instance..." + return "Find an Instance..." } } diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index da54db0846..10124a4fa6 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -25,6 +25,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { private var myProfileTab: UITab! private var listsGroup: UITabGroup! private var hashtagsGroup: UITabGroup! + private var instancesGroup: UITabGroup! private var cancellables = Set() @@ -79,6 +80,17 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { ] reloadHashtags() + instancesGroup = UITabGroup(title: "Instance Timelines", image: nil, identifier: Tab.instances.rawValue, children: []) { _ in + return AdaptableNavigationController() + } + instancesGroup.preferredPlacement = .sidebarOnly + instancesGroup.sidebarActions = [ + UIAction(title: "Find an Instance…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in + self.showAddSavedInstance() + }) + ] + reloadSavedInstances() + if UIDevice.current.userInterfaceIdiom == .phone { self.tabs = [ homeTab, @@ -105,6 +117,8 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { .merge(with: NotificationCenter.default.publisher(for: .savedHashtagsChanged).map { _ in () }) .sink { [unowned self] in self.reloadHashtags() } .store(in: &cancellables) + + NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil) } setupFastAccountSwitcher() @@ -118,7 +132,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { var exploreNavStack: [UIViewController]? = nil if let parent = selectedTab?.parent, - parent === listsGroup || parent === hashtagsGroup { + parent === listsGroup || parent === hashtagsGroup || parent === instancesGroup { let nav = parent.viewController as! any NavigationControllerProtocol exploreNavStack = nav.viewControllers nav.viewControllers = [] @@ -180,6 +194,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { composeTab, listsGroup, hashtagsGroup, + instancesGroup, ] if let (tab, navStack) = newTabAndNavigationStack { @@ -222,7 +237,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { root = FavoritesViewController(mastodonController: mastodonController) case .myProfile: root = MyProfileViewController(mastodonController: mastodonController) - case .lists, .hashtags: + case .lists, .hashtags, .instances: fatalError("unreachable") } return embedInNavigationController(root) @@ -279,6 +294,19 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { hashtagsGroup.children = tabs } + @objc private func reloadSavedInstances() { + let viewControllerProvider = { [unowned self] (tab: UITab) in + let tab = tab as! InstanceTab + return InstanceTimelineViewController(for: tab.instance.url, parentMastodonController: self.mastodonController) + } + let req = SavedInstance.fetchRequest(account: mastodonController.accountInfo!) + req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)] + let instances = (try? mastodonController.persistentContainer.viewContext.fetch(req).uniques(by: \.url)) ?? [] + instancesGroup.children = instances.map { + InstanceTab(instance: $0, viewControllerProvider: viewControllerProvider) + } + } + @objc func handleComposeKeyCommand() { compose(editing: nil) } @@ -305,6 +333,13 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { present(nav, animated: true) } + private func showAddSavedInstance() { + let findController = FindInstanceViewController(parentMastodonController: mastodonController) + findController.instanceTimelineDelegate = self + let nav = EnhancedNavigationViewController(rootViewController: findController) + present(nav, animated: true) + } + fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) { guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else { return @@ -379,6 +414,7 @@ extension NewMainTabBarViewController { case lists case hashtags + case instances } } @@ -418,7 +454,7 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { // get the new transition animation. // This would be much less complicated if the controller just used the individual VCs of items in a group. if let group = newTab.parent, - group === listsGroup || group === hashtagsGroup, + group === listsGroup || group === hashtagsGroup || group === instancesGroup, let nav = group.viewController as? any NavigationControllerProtocol { updateViewControllerSafeAreaInsets(nav) @@ -499,6 +535,9 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { } else { return nil } + } else if tab is InstanceTab { + // don't currently have a scene type for this + return nil } else if let tabID = Tab(rawValue: tab.identifier) { switch tabID { case .home: @@ -517,7 +556,7 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { return nil case .compose: activity = UserActivityManager.newPostActivity(accountID: id) - case .lists, .hashtags: + case .lists, .hashtags, .instances: return nil } } else { @@ -638,6 +677,20 @@ extension NewMainTabBarViewController: AccountSwitchableViewController { } } +@available(iOS 18.0, *) +extension NewMainTabBarViewController: InstanceTimelineViewControllerDelegate { + func didSaveInstance(url: URL) { + dismiss(animated: true) { + let tab = self.instancesGroup.tab(forIdentifier: InstanceTab.identifier(for: url))! + self.selectedTab = tab + } + } + + func didUnsaveInstance(url: URL) { + dismiss(animated: true) + } +} + private struct MyProfileContentConfiguration: UIContentConfiguration { let wrapped: any UIContentConfiguration @Box var view: UIView? @@ -754,3 +807,21 @@ private class HashtagTab: UITab { "hashtag:\(name)" } } + +@available(iOS 18.0, *) +private class InstanceTab: UITab { + let instance: SavedInstance + + init(instance: SavedInstance, viewControllerProvider: @escaping (UITab) -> UIViewController) { + self.instance = instance + super.init(title: instance.url.host!, image: UIImage(systemName: "globe"), identifier: Self.identifier(for: instance), viewControllerProvider: viewControllerProvider) + } + + static func identifier(for instance: SavedInstance) -> String { + "instance:\(instance.url.host!)" + } + + static func identifier(for instanceURL: URL) -> String { + "instance:\(instanceURL.host!)" + } +}