Refactor tab/MainTabBarController to add viewcontrollers as properties

This is a WIP-step for account-stuff. Also: iPhone only, iPad should come next
This commit is contained in:
Nathan Mattes 2024-01-11 23:36:13 +01:00
parent cea6129229
commit 2c653320fb
8 changed files with 180 additions and 155 deletions

View File

@ -166,6 +166,7 @@
D8B5E4F22A4EBCF90008970C /* NotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */; };
D8B5E4F42A4ED0240008970C /* NotificationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8B5E4F32A4ED0240008970C /* NotificationSettingsViewModel.swift */; };
D8BE30B32A179E26006B8270 /* SuggestionAccountTableViewFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */; };
D8CF45832B50893900C84D70 /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CF45822B50893900C84D70 /* Tab.swift */; };
D8D688F62AB869CB000F651A /* SearchResultsProfileTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */; };
D8D688F92AB8B970000F651A /* SearchResultOverviewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */; };
D8E5C346296DAB84007E76A7 /* DataSourceFacade+Status+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */; };
@ -833,6 +834,7 @@
D8B5E4F12A4EBCF90008970C /* NotificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettings.swift; sourceTree = "<group>"; };
D8B5E4F32A4ED0240008970C /* NotificationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewModel.swift; sourceTree = "<group>"; };
D8BE30B22A179E26006B8270 /* SuggestionAccountTableViewFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewFooter.swift; sourceTree = "<group>"; };
D8CF45822B50893900C84D70 /* Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = "<group>"; };
D8D688F52AB869CB000F651A /* SearchResultsProfileTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsProfileTableViewCell.swift; sourceTree = "<group>"; };
D8D688F82AB8B970000F651A /* SearchResultOverviewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultOverviewCoordinator.swift; sourceTree = "<group>"; };
D8E5C345296DAB84007E76A7 /* DataSourceFacade+Status+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status+History.swift"; sourceTree = "<group>"; };
@ -2557,6 +2559,7 @@
DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */,
DB852D1A26FAED0100FC9D81 /* Sidebar */,
DB8AF54E25C13703002E6C99 /* MainTab */,
D8CF45822B50893900C84D70 /* Tab.swift */,
);
path = Root;
sourceTree = "<group>";
@ -3970,6 +3973,7 @@
2A3F6FE5292F6E44002E6DA7 /* FollowedTagsTableViewCell.swift in Sources */,
C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */,
DB023D2A27A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift in Sources */,
D8CF45832B50893900C84D70 /* Tab.swift in Sources */,
D8363B1629469CE200A74079 /* OnboardingNextView.swift in Sources */,
DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */,
DB0FCB962797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift in Sources */,

View File

@ -387,7 +387,7 @@ extension SceneCoordinator {
return viewController
}
func switchToTabBar(tab: MainTabBarController.Tab) {
func switchToTabBar(tab: Tab) {
splitViewController?.contentSplitViewController.currentSupplementaryTab = tab
splitViewController?.compactMainTabBarViewController.selectedIndex = tab.rawValue

View File

@ -11,8 +11,8 @@ import CoreDataStack
import MastodonCore
protocol ContentSplitViewControllerDelegate: AnyObject {
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab)
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: MainTabBarController.Tab)
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: Tab)
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: Tab)
}
final class ContentSplitViewController: UIViewController, NeedsDependency {
@ -37,7 +37,7 @@ final class ContentSplitViewController: UIViewController, NeedsDependency {
return sidebarViewController
}()
@Published var currentSupplementaryTab: MainTabBarController.Tab = .home
@Published var currentSupplementaryTab: Tab = .home
private(set) lazy var mainTabBarController: MainTabBarController = {
let mainTabBarController = MainTabBarController(context: context, coordinator: coordinator, authContext: authContext)
if let homeTimelineViewController = mainTabBarController.viewController(of: HomeTimelineViewController.self) {
@ -102,7 +102,7 @@ extension ContentSplitViewController {
// MARK: - SidebarViewControllerDelegate
extension ContentSplitViewController: SidebarViewControllerDelegate {
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) {
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: Tab) {
delegate?.contentSplitViewController(self, sidebarViewController: sidebarViewController, didSelectTab: tab)
}

View File

@ -34,102 +34,13 @@ class MainTabBarController: UITabBarController {
)
@Published var currentTab: Tab = .home
enum Tab: Int, CaseIterable {
case home
case search
case compose
case notifications
case me
var tag: Int {
return rawValue
}
var title: String {
switch self {
case .home: return L10n.Common.Controls.Tabs.home
case .search: return L10n.Common.Controls.Tabs.searchAndExplore
case .compose: return L10n.Common.Controls.Actions.compose
case .notifications: return L10n.Common.Controls.Tabs.notifications
case .me: return L10n.Common.Controls.Tabs.profile
}
}
let homeTimelineViewController: HomeTimelineViewController
let searchViewController: SearchViewController
let composeViewController: UIViewController // placeholder
let notificationViewController: NotificationViewController
let meProfileViewController: ProfileViewController
var inputLabels: [String]? {
switch self {
case .home, .compose, .notifications, .me:
return nil
case .search:
return [
L10n.Common.Controls.Tabs.A11Y.search,
L10n.Common.Controls.Tabs.A11Y.explore,
L10n.Common.Controls.Tabs.searchAndExplore
]
}
}
var image: UIImage {
switch self {
case .home: return UIImage(systemName: "house")!
case .search: return UIImage(systemName: "magnifyingglass")!
case .compose: return UIImage(systemName: "square.and.pencil")!
case .notifications: return UIImage(systemName: "bell")!
case .me: return UIImage(systemName: "person")!
}
}
var selectedImage: UIImage {
return image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal)
}
var largeImage: UIImage {
return image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80))
}
@MainActor
func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) -> UIViewController {
guard let authContext else { return UITableViewController() }
let viewController: UIViewController
switch self {
case .home:
let _viewController = HomeTimelineViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = HomeTimelineViewModel(context: context, authContext: authContext)
viewController = _viewController
case .search:
let _viewController = SearchViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = .init(context: context, authContext: authContext)
viewController = _viewController
case .compose:
viewController = UIViewController()
case .notifications:
let _viewController = NotificationViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = .init(context: context, authContext: authContext)
viewController = _viewController
case .me:
#warning("What happens if there's no me at the beginning? I guess we _do_ need another migration?")
guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return UIViewController() }
let _viewController = ProfileViewController()
_viewController.context = context
_viewController.coordinator = coordinator
_viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me, relationship: nil, me: me)
viewController = _viewController
}
viewController.title = self.title
return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
}
}
var _viewControllers: [UIViewController] = []
private(set) var isReadyForWizardAvatarButton = false
// output
@ -146,15 +57,43 @@ class MainTabBarController: UITabBarController {
self.context = context
self.coordinator = coordinator
self.authContext = authContext
homeTimelineViewController = HomeTimelineViewController()
homeTimelineViewController.configureTabBarItem(with: .home)
homeTimelineViewController.context = context
homeTimelineViewController.coordinator = coordinator
searchViewController = SearchViewController()
searchViewController.configureTabBarItem(with: .search)
searchViewController.context = context
searchViewController.coordinator = coordinator
composeViewController = UIViewController()
composeViewController.configureTabBarItem(with: .compose)
notificationViewController = NotificationViewController()
notificationViewController.configureTabBarItem(with: .notifications)
notificationViewController.context = context
notificationViewController.coordinator = coordinator
meProfileViewController = ProfileViewController()
meProfileViewController.context = context
meProfileViewController.coordinator = coordinator
meProfileViewController.configureTabBarItem(with: .me)
if let authContext {
notificationViewController.viewModel = NotificationViewModel(context: context, authContext: authContext)
homeTimelineViewController.viewModel = HomeTimelineViewModel(context: context, authContext: authContext)
searchViewController.viewModel = SearchViewModel(context: context, authContext: authContext)
}
super.init(nibName: nil, bundle: nil)
viewControllers = [homeTimelineViewController, searchViewController, composeViewController, notificationViewController, meProfileViewController].map { AdaptiveStatusBarStyleNavigationController(rootViewController: $0) }
tabBar.addInteraction(largeContentViewerInteraction)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
extension MainTabBarController {
@ -171,26 +110,9 @@ extension MainTabBarController {
view.backgroundColor = .systemBackground
// seealso: `ThemeService.apply(theme:)`
let tabs = Tab.allCases
var viewControllers = [UIViewController]()
for tab in tabs {
let viewController = tab.viewController(context: context, authContext: authContext, coordinator: coordinator)
viewController.tabBarItem.tag = tab.tag
viewController.tabBarItem.title = tab.title // needs for acessiblity large content label
viewController.tabBarItem.image = tab.image.imageWithoutBaseline()
viewController.tabBarItem.largeContentSizeImage = tab.largeImage.imageWithoutBaseline()
viewController.tabBarItem.accessibilityLabel = tab.title
viewController.tabBarItem.accessibilityUserInputLabels = tab.inputLabels
viewController.tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
viewControllers.append(viewController)
}
_viewControllers = viewControllers
setViewControllers(viewControllers, animated: false)
selectedIndex = 0
// hacky workaround for FB11986255 (Setting accessibilityUserInputLabels on a UITabBarItem has no effect)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
if let searchItem = self.tabBar.subviews.first(where: { $0.accessibilityLabel == Tab.search.title }) {
@ -201,7 +123,7 @@ extension MainTabBarController {
context.apiService.error
.receive(on: DispatchQueue.main)
.sink { [weak self] error in
guard let self = self, let coordinator = self.coordinator else { return }
guard let self, let coordinator = self.coordinator else { return }
switch error {
case .implicit:
break
@ -228,15 +150,14 @@ extension MainTabBarController {
)
.receive(on: DispatchQueue.main)
.sink { [weak self] authentication, currentTab in
guard let self = self else { return }
guard let notificationViewController = self.notificationViewController else { return }
guard let self else { return }
let authentication = self.authContext?.mastodonAuthenticationBox.userAuthorization
let hasUnreadPushNotification: Bool = authentication.flatMap { authentication in
let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.accessToken)
return count > 0
} ?? false
let image: UIImage
if hasUnreadPushNotification {
let imageConfiguration = UIImage.SymbolConfiguration(paletteColors: [.red, SystemTheme.tabBarItemNormalIconColor])
@ -244,17 +165,18 @@ extension MainTabBarController {
} else {
image = Tab.notifications.image
}
notificationViewController.tabBarItem.image = image.imageWithoutBaseline()
notificationViewController.navigationController?.tabBarItem.image = image.imageWithoutBaseline()
}
.store(in: &disposeBag)
layoutAvatarButton()
$avatarURL
.receive(on: DispatchQueue.main)
.sink { [weak self] avatarURL in
guard let self = self else { return }
guard let self else { return }
self.avatarButton.avatarImageView.setImage(
url: avatarURL,
placeholder: .placeholder(color: .systemFill),
@ -262,7 +184,7 @@ extension MainTabBarController {
)
}
.store(in: &disposeBag)
NotificationCenter.default.publisher(for: .userFetched)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
@ -300,11 +222,11 @@ extension MainTabBarController {
$currentTab
.receive(on: DispatchQueue.main)
.sink { [weak self] tab in
guard let self = self else { return }
guard let self else { return }
self.updateAvatarButtonAppearance()
}
.store(in: &disposeBag)
updateTabBarDisplay()
}
@ -358,7 +280,7 @@ extension MainTabBarController {
case .search:
assert(Thread.isMainThread)
// double tapping search tab opens the search bar without additional taps
searchViewController?.searchBar.becomeFirstResponder()
searchViewController.searchBar.becomeFirstResponder()
default:
break
}
@ -460,18 +382,6 @@ extension MainTabBarController {
}
}
extension MainTabBarController {
var notificationViewController: NotificationViewController? {
return viewController(of: NotificationViewController.self)
}
var searchViewController: SearchViewController? {
return viewController(of: SearchViewController.self)
}
}
// MARK: - UITabBarControllerDelegate
extension MainTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {

View File

@ -127,8 +127,8 @@ extension RootSplitViewController {
// MARK: - ContentSplitViewControllerDelegate
extension RootSplitViewController: ContentSplitViewControllerDelegate {
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) {
guard let _ = MainTabBarController.Tab.allCases.firstIndex(of: tab) else {
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didSelectTab tab: Tab) {
guard let _ = Tab.allCases.firstIndex(of: tab) else {
assertionFailure()
return
}
@ -158,8 +158,8 @@ extension RootSplitViewController: ContentSplitViewControllerDelegate {
}
}
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: MainTabBarController.Tab) {
guard let _ = MainTabBarController.Tab.allCases.firstIndex(of: tab) else {
func contentSplitViewController(_ contentSplitViewController: ContentSplitViewController, sidebarViewController: SidebarViewController, didDoubleTapTab tab: Tab) {
guard let _ = Tab.allCases.firstIndex(of: tab) else {
assertionFailure()
return
}
@ -170,7 +170,7 @@ extension RootSplitViewController: ContentSplitViewControllerDelegate {
guard !isPrimaryDisplay else {
return
}
contentSplitViewController.mainTabBarController.searchViewController?.searchBar.becomeFirstResponder()
contentSplitViewController.mainTabBarController.searchViewController.searchBar.becomeFirstResponder()
default:
break
}

View File

@ -12,7 +12,7 @@ import MastodonCore
import MastodonUI
protocol SidebarViewControllerDelegate: AnyObject {
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab)
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: Tab)
func sidebarViewController(_ sidebarViewController: SidebarViewController, didLongPressItem item: SidebarViewModel.Item, sourceView: UIView)
func sidebarViewController(_ sidebarViewController: SidebarViewController, didDoubleTapItem item: SidebarViewModel.Item, sourceView: UIView)
}

View File

@ -23,7 +23,7 @@ final class SidebarViewModel {
let authContext: AuthContext?
@Published private var isSidebarDataSourceReady = false
@Published private var isAvatarButtonDataReady = false
@Published var currentTab: MainTabBarController.Tab = .home
@Published var currentTab: Tab = .home
// output
var diffableDataSource: UICollectionViewDiffableDataSource<Section, Item>?
@ -57,7 +57,7 @@ extension SidebarViewModel {
}
enum Item: Hashable {
case tab(MainTabBarController.Tab)
case tab(Tab)
case setting
case compose
}
@ -69,7 +69,7 @@ extension SidebarViewModel {
collectionView: UICollectionView,
secondaryCollectionView: UICollectionView
) {
let tabCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, MainTabBarController.Tab> { [weak self] cell, indexPath, item in
let tabCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, Tab> { [weak self] cell, indexPath, item in
guard let self else { return }
let imageURL: URL?
@ -125,7 +125,7 @@ extension SidebarViewModel {
let imageConfiguration = UIImage.SymbolConfiguration(paletteColors: [.red, SystemTheme.tabBarItemNormalIconColor])
image = UIImage(systemName: "bell.badge", withConfiguration: imageConfiguration)!
} else {
image = MainTabBarController.Tab.notifications.image
image = Tab.notifications.image
}
cell.item?.image = image
cell.item?.activeImage = image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal)

View File

@ -0,0 +1,111 @@
// Copyright © 2024 Mastodon gGmbH. All rights reserved.
import UIKit
import MastodonLocalization
import MastodonAsset
enum Tab: Int, CaseIterable {
case home
case search
case compose
case notifications
case me
var tag: Int {
return rawValue
}
var title: String {
switch self {
case .home: return L10n.Common.Controls.Tabs.home
case .search: return L10n.Common.Controls.Tabs.searchAndExplore
case .compose: return L10n.Common.Controls.Actions.compose
case .notifications: return L10n.Common.Controls.Tabs.notifications
case .me: return L10n.Common.Controls.Tabs.profile
}
}
var inputLabels: [String]? {
switch self {
case .home, .compose, .notifications, .me:
return nil
case .search:
return [
L10n.Common.Controls.Tabs.A11Y.search,
L10n.Common.Controls.Tabs.A11Y.explore,
L10n.Common.Controls.Tabs.searchAndExplore
]
}
}
var image: UIImage {
switch self {
case .home: return UIImage(systemName: "house")!
case .search: return UIImage(systemName: "magnifyingglass")!
case .compose: return UIImage(systemName: "square.and.pencil")!
case .notifications: return UIImage(systemName: "bell")!
case .me: return UIImage(systemName: "person")!
}
}
var selectedImage: UIImage {
return image.withTintColor(Asset.Colors.Brand.blurple.color, renderingMode: .alwaysOriginal)
}
var largeImage: UIImage {
return image.withRenderingMode(.alwaysTemplate).resized(size: CGSize(width: 80, height: 80))
}
// @MainActor
// func viewController(context: AppContext, authContext: AuthContext?, coordinator: SceneCoordinator) -> UIViewController {
// guard let authContext else { return UITableViewController() }
//
// let viewController: UIViewController
// switch self {
// case .home:
// let _viewController = HomeTimelineViewController()
// _viewController.context = context
// _viewController.coordinator = coordinator
// _viewController.viewModel = HomeTimelineViewModel(context: context, authContext: authContext)
// viewController = _viewController
// case .search:
// let _viewController = SearchViewController()
// _viewController.context = context
// _viewController.coordinator = coordinator
// _viewController.viewModel = SearchViewModel(context: context, authContext: authContext)
// viewController = _viewController
// case .compose:
// viewController = UIViewController()
// case .notifications:
// let _viewController = NotificationViewController()
// _viewController.context = context
// _viewController.coordinator = coordinator
// _viewController.viewModel = NotificationViewModel(context: context, authContext: authContext)
// viewController = _viewController
// case .me:
// #warning("What happens if there's no me at the beginning? I guess we _do_ need another migration?")
// guard let me = authContext.mastodonAuthenticationBox.authentication.account() else { return UIViewController() }
//
// let _viewController = ProfileViewController()
// _viewController.context = context
// _viewController.coordinator = coordinator
// _viewController.viewModel = ProfileViewModel(context: context, authContext: authContext, account: me, relationship: nil, me: me)
// viewController = _viewController
// }
// viewController.title = self.title
// return AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)
// }
}
extension UIViewController {
func configureTabBarItem(with tab: Tab) {
title = tab.title
tabBarItem.tag = tab.tag
tabBarItem.title = tab.title // needs for acessiblity large content label
tabBarItem.image = tab.image.imageWithoutBaseline()
tabBarItem.largeContentSizeImage = tab.largeImage.imageWithoutBaseline()
tabBarItem.accessibilityLabel = tab.title
tabBarItem.accessibilityUserInputLabels = tab.inputLabels
tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
}
}