From 5b3e85b68ce6f831b78047d222761ddff2c647a8 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 22 Sep 2021 19:08:09 +0800 Subject: [PATCH] feat: add Root scene for iPad --- Mastodon.xcodeproj/project.pbxproj | 60 +++++-- .../xcschemes/xcschememanagement.plist | 18 +-- .../xcshareddata/swiftpm/Package.resolved | 21 +-- Mastodon/Coordinator/SceneCoordinator.swift | 11 +- .../StatusProvider/StatusProviderFacade.swift | 4 +- .../HomeTimelineViewController.swift | 2 +- .../Scene/MainTab/MainTabBarController.swift | 24 ++- .../Scene/Root/RootSplitViewController.swift | 101 ++++++++++++ .../Scene/Sidebar/SidebarViewController.swift | 98 ++++++++++++ Mastodon/Scene/Sidebar/SidebarViewModel.swift | 150 ++++++++++++++++++ Podfile.lock | 6 +- 11 files changed, 445 insertions(+), 50 deletions(-) create mode 100644 Mastodon/Scene/Root/RootSplitViewController.swift create mode 100644 Mastodon/Scene/Sidebar/SidebarViewController.swift create mode 100644 Mastodon/Scene/Sidebar/SidebarViewModel.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 45bd07248..c32f52550 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -359,6 +359,9 @@ DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */; }; DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */; }; DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */; }; + DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */; }; + DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */; }; + DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */; }; DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */; }; DB87D44B2609C11900D12C0D /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D44A2609C11900D12C0D /* PollOptionView.swift */; }; DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */; }; @@ -1138,6 +1141,9 @@ DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewModel.swift; sourceTree = ""; }; DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentContainerView.swift; sourceTree = ""; }; + DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewController.swift; sourceTree = ""; }; + DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = ""; }; + DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarViewModel.swift; sourceTree = ""; }; DB87D4442609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionCollectionViewCell.swift; sourceTree = ""; }; DB87D44A2609C11900D12C0D /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = ""; }; DB87D4502609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollOptionAppendEntryCollectionViewCell.swift; sourceTree = ""; }; @@ -2517,6 +2523,23 @@ path = CollectionViewCell; sourceTree = ""; }; + DB852D1A26FAED0100FC9D81 /* Sidebar */ = { + isa = PBXGroup; + children = ( + DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */, + DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */, + ); + path = Sidebar; + sourceTree = ""; + }; + DB852D1D26FB021900FC9D81 /* Root */ = { + isa = PBXGroup; + children = ( + DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */, + ); + path = Root; + sourceTree = ""; + }; DB87D45C2609DE6600D12C0D /* TextField */ = { isa = PBXGroup; children = ( @@ -2628,6 +2651,8 @@ children = ( 2D7631A425C1532200929FB9 /* Share */, DB6180E426391A500018D199 /* Transition */, + DB852D1D26FB021900FC9D81 /* Root */, + DB852D1A26FAED0100FC9D81 /* Sidebar */, DB8AF54E25C13703002E6C99 /* MainTab */, DB01409B25C40BB600F9F3CF /* Onboarding */, DB9F58ED26EF435800E7BBE9 /* Account */, @@ -3979,6 +4004,7 @@ DBAEDE5C267A058D00D25FF5 /* BlurhashImageCacheService.swift in Sources */, 2D38F1DF25CD46A400561493 /* HomeTimelineViewController+Provider.swift in Sources */, DB1D843026566512000346B3 /* KeyboardPreference.swift in Sources */, + DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */, 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, 2D9DB96B263A91D1007C1D71 /* APIService+DomainBlock.swift in Sources */, DBBF1DC92652538500E5B703 /* AutoCompleteSection.swift in Sources */, @@ -4010,6 +4036,7 @@ DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */, DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */, DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */, + DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */, DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */, 2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */, DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */, @@ -4047,6 +4074,7 @@ DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */, DBCBCC092680B01B000F5B51 /* AsyncHomeTimelineViewModel+LoadMiddleState.swift in Sources */, 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */, + DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */, DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */, DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */, 0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */, @@ -4711,7 +4739,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -4738,7 +4766,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -5003,7 +5031,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -5028,7 +5056,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = "ASDK - Debug"; @@ -5053,7 +5081,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = "ASDK - Release"; @@ -5078,7 +5106,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -5103,7 +5131,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -5128,7 +5156,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = "ASDK - Debug"; @@ -5153,7 +5181,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = "ASDK - Release"; @@ -5178,7 +5206,7 @@ SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "APP_EXTENSION $(inherited)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -5269,7 +5297,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = "ASDK - Release"; @@ -5383,7 +5411,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = "ASDK - Release"; @@ -5505,7 +5533,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = "ASDK - Debug"; @@ -5619,7 +5647,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = "ASDK - Debug"; @@ -5674,7 +5702,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -5698,7 +5726,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index ebb300d54..ee1add271 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ AppShared.xcscheme_^#shared#^_ orderHint - 36 + 39 CoreDataStack.xcscheme_^#shared#^_ orderHint - 38 + 36 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -22,7 +22,7 @@ Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 14 + 13 Mastodon - Release.xcscheme_^#shared#^_ @@ -32,17 +32,17 @@ Mastodon - ar.xcscheme_^#shared#^_ orderHint - 11 + 10 Mastodon - ca.xcscheme_^#shared#^_ orderHint - 18 + 16 Mastodon - de.xcscheme_^#shared#^_ orderHint - 12 + 11 Mastodon - en.xcscheme_^#shared#^_ @@ -67,12 +67,12 @@ Mastodon - jp.xcscheme_^#shared#^_ orderHint - 16 + 14 Mastodon - nl.xcscheme_^#shared#^_ orderHint - 13 + 12 Mastodon - ru.xcscheme_^#shared#^_ @@ -87,7 +87,7 @@ Mastodon - zh_Hans.xcscheme_^#shared#^_ orderHint - 17 + 15 Mastodon.xcscheme_^#shared#^_ diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index c6a659bd5..e5bae6f1f 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/Alamofire/Alamofire.git", "state": { "branch": null, - "revision": "f96b619bcb2383b43d898402283924b80e2c4bae", - "version": "5.4.3" + "revision": "d120af1e8638c7da36c8481fd61a66c0c08dc4fc", + "version": "5.4.4" } }, { @@ -105,8 +105,8 @@ "repositoryURL": "https://github.com/kean/Nuke.git", "state": { "branch": null, - "revision": "3bd3a1765bdf62d561d4c2e10e1c4fc7a010f44e", - "version": "10.3.2" + "revision": "0db18dd34998cca18e9a28bcee136f84518007a0", + "version": "10.4.1" } }, { @@ -159,8 +159,8 @@ "repositoryURL": "https://github.com/apple/swift-nio.git", "state": { "branch": null, - "revision": "8da5c5a4e6c5084c296b9f39dc54f00be146e0fa", - "version": "1.14.2" + "revision": "546610d52b19be3e19935e0880bb06b9c03f5cef", + "version": "1.14.4" } }, { @@ -216,15 +216,6 @@ "revision": "dad97167bf1be16aeecd109130900995dd01c515", "version": "2.6.0" } - }, - { - "package": "UITextView+Placeholder", - "repositoryURL": "https://github.com/MainasuK/UITextView-Placeholder", - "state": { - "branch": null, - "revision": "20f513ded04a040cdf5467f0891849b1763ede3b", - "version": "1.4.1" - } } ] }, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index f604ef546..4445eac2b 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -111,10 +111,15 @@ extension SceneCoordinator { extension SceneCoordinator { +// func setup() { +// let viewController = MainTabBarController(context: appContext, coordinator: self) +// sceneDelegate.window?.rootViewController = viewController +// tabBarController = viewController +// } + func setup() { - let viewController = MainTabBarController(context: appContext, coordinator: self) - sceneDelegate.window?.rootViewController = viewController - tabBarController = viewController + let splitViewController = RootSplitViewController(context: appContext, coordinator: self) + sceneDelegate.window?.rootViewController = splitViewController } func setupOnboardingIfNeeds(animated: Bool) { diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index d11870ed2..02a564d82 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -111,10 +111,10 @@ extension StatusProviderFacade { if provider.navigationController == nil { let from = provider.presentingViewController ?? provider provider.dismiss(animated: true) { - provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show) + provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .showDetail) } } else { - provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: provider, transition: .show) + provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: provider, transition: .showDetail) } } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index a08f162a8..c47eb3633 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -95,7 +95,7 @@ extension HomeTimelineViewController { self.view.backgroundColor = theme.secondarySystemBackgroundColor } .store(in: &disposeBag) - navigationItem.leftBarButtonItem = settingBarButtonItem +// navigationItem.leftBarButtonItem = settingBarButtonItem navigationItem.titleView = titleView titleView.delegate = self diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index d6d40a5d0..b184ff25f 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -97,6 +97,8 @@ class MainTabBarController: UITabBarController { } } + var _viewControllers: [UIViewController] = [] + init(context: AppContext, coordinator: SceneCoordinator) { self.context = context self.coordinator = coordinator @@ -140,6 +142,7 @@ extension MainTabBarController { viewController.tabBarItem.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0) return viewController } + _viewControllers = viewControllers setViewControllers(viewControllers, animated: false) selectedIndex = 0 @@ -252,7 +255,9 @@ extension MainTabBarController { let tabBarLongPressGestureRecognizer = UILongPressGestureRecognizer() tabBarLongPressGestureRecognizer.addTarget(self, action: #selector(MainTabBarController.tabBarLongPressGestureRecognizerHandler(_:))) tabBar.addGestureRecognizer(tabBarLongPressGestureRecognizer) - + + updateTabBarDisplay() + #if DEBUG // selectedIndex = 1 #endif @@ -263,9 +268,26 @@ extension MainTabBarController { wizard.consume() } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + updateTabBarDisplay() + } } +extension MainTabBarController { + private func updateTabBarDisplay() { + switch traitCollection.horizontalSizeClass { + case .compact: + tabBar.isHidden = false + default: + tabBar.isHidden = true + } + } +} + extension MainTabBarController { @objc private func tabBarLongPressGestureRecognizerHandler(_ sender: UILongPressGestureRecognizer) { guard sender.state == .began else { return } diff --git a/Mastodon/Scene/Root/RootSplitViewController.swift b/Mastodon/Scene/Root/RootSplitViewController.swift new file mode 100644 index 000000000..640219152 --- /dev/null +++ b/Mastodon/Scene/Root/RootSplitViewController.swift @@ -0,0 +1,101 @@ +// +// RootSplitViewController.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-22. +// + +import os.log +import UIKit + +final class RootSplitViewController: UISplitViewController, NeedsDependency { + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + private(set) lazy var sidebarViewController: SidebarViewController = { + let sidebarViewController = SidebarViewController() + sidebarViewController.context = context + sidebarViewController.coordinator = coordinator + sidebarViewController.viewModel = SidebarViewModel(context: context) + sidebarViewController.delegate = self + return sidebarViewController + }() + + private(set) lazy var mainTabBarController = MainTabBarController(context: context, coordinator: coordinator) + + init(context: AppContext, coordinator: SceneCoordinator) { + self.context = context + self.coordinator = coordinator + super.init(style: .tripleColumn) + + primaryBackgroundStyle = .sidebar + preferredDisplayMode = .oneBesideSecondary + preferredSplitBehavior = .tile + + if #available(iOS 14.5, *) { + displayModeButtonVisibility = .always + } else { + // Fallback on earlier versions + } + + setViewController(sidebarViewController, for: .primary) + setViewController(mainTabBarController.viewControllers!.first, for: .supplementary) + setViewController(UIViewController(), for: .secondary) + setViewController(mainTabBarController, for: .compact) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension RootSplitViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + updateBehavior(size: view.frame.size) + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + updateBehavior(size: size) + } + + private func updateBehavior(size: CGSize) { + // fix secondary too small on iPad mini issue + if size.width > 960 { + preferredDisplayMode = .oneBesideSecondary + preferredSplitBehavior = .tile + } else { + preferredDisplayMode = .oneBesideSecondary + preferredSplitBehavior = .displace + } + } + +} + +// MARK: - SidebarViewControllerDelegate +extension RootSplitViewController: SidebarViewControllerDelegate { + func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) { + + // FIXME: remove hard code + switch tab { + case .home: + setViewController(mainTabBarController._viewControllers[0], for: .supplementary) + case .search: + setViewController(mainTabBarController._viewControllers[1], for: .supplementary) + case .notification: + setViewController(mainTabBarController._viewControllers[2], for: .supplementary) + case .me: + setViewController(mainTabBarController._viewControllers[3], for: .supplementary) + } + } +} diff --git a/Mastodon/Scene/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Sidebar/SidebarViewController.swift new file mode 100644 index 000000000..4fe34c89a --- /dev/null +++ b/Mastodon/Scene/Sidebar/SidebarViewController.swift @@ -0,0 +1,98 @@ +// +// SidebarViewController.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-22. +// + +import UIKit +import Combine +import CoreDataStack + +protocol SidebarViewControllerDelegate: AnyObject { + func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) +} + +final class SidebarViewController: UIViewController, NeedsDependency { + + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + + var disposeBag = Set() + var viewModel: SidebarViewModel! + + weak var delegate: SidebarViewControllerDelegate? + + static func createLayout() -> UICollectionViewLayout { + let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in + var configuration = UICollectionLayoutListConfiguration(appearance: .plain) + configuration.showsSeparators = false + let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) + return section + } + return layout + } + + let collectionView: UICollectionView = { + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: SidebarViewController.createLayout()) + collectionView.backgroundColor = .clear + return collectionView + }() + +} + +extension SidebarViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.title = "Title" + navigationController?.navigationBar.prefersLargeTitles = true + + let barAppearance = UINavigationBarAppearance() + barAppearance.configureWithTransparentBackground() + navigationItem.standardAppearance = barAppearance + navigationItem.compactAppearance = barAppearance + navigationItem.scrollEdgeAppearance = barAppearance + + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + collectionView.delegate = self + viewModel.setupDiffableDataSource(collectionView: collectionView) + } + +} + +// MARK: - UICollectionViewDelegate +extension SidebarViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + switch item { + case .tab(let tab): + delegate?.sidebarViewController(self, didSelectTab: tab) + case .account(let viewModel): + assert(Thread.isMainThread) + let authentication = context.managedObjectContext.object(with: viewModel.authenticationObjectID) as! MastodonAuthentication + context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + guard let self = self else { return } + self.coordinator.setup() + } + .store(in: &disposeBag) + case .addAccount: + coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) + default: + // TODO: + break + } + } +} diff --git a/Mastodon/Scene/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Sidebar/SidebarViewModel.swift new file mode 100644 index 000000000..f65bb1d6f --- /dev/null +++ b/Mastodon/Scene/Sidebar/SidebarViewModel.swift @@ -0,0 +1,150 @@ +// +// SidebarViewModel.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-22. +// + +import UIKit +import Combine +import CoreData +import CoreDataStack + +final class SidebarViewModel { + + var disposeBag = Set() + + // input + let context: AppContext + + // output + var diffableDataSource: UICollectionViewDiffableDataSource! + + + init(context: AppContext) { + self.context = context + } + +} + +extension SidebarViewModel { + enum Section: Hashable, CaseIterable { + case tab + case account + } + + enum Item: Hashable { + case tab(MainTabBarController.Tab) + case header(HeaderViewModel) + case account(AccountViewModel) + case addAccount + } + + struct HeaderViewModel: Hashable { + let title: String + } + + struct AccountViewModel: Hashable { + let authenticationObjectID: NSManagedObjectID + } + + struct AddAccountViewModel: Hashable { + let id = UUID() + } +} + +extension SidebarViewModel { + func setupDiffableDataSource( + collectionView: UICollectionView + ) { + let cellRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in + var content = cell.defaultContentConfiguration() + content.text = item.title + content.image = item.image + cell.contentConfiguration = content + cell.accessories = [] + } + + let headerRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in + var content = cell.defaultContentConfiguration() + content.text = item.title + cell.contentConfiguration = content + cell.accessories = [.outlineDisclosure()] + } + + let accountRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in + var content = cell.defaultContentConfiguration() + let authentication = AppContext.shared.managedObjectContext.object(with: item.authenticationObjectID) as! MastodonAuthentication + content.text = authentication.user.acctWithDomain + content.image = nil + cell.contentConfiguration = content + cell.accessories = [] + } + + let addAccountRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in + var content = cell.defaultContentConfiguration() + content.text = L10n.Scene.AccountList.addAccount + content.image = nil + cell.contentConfiguration = content + cell.accessories = [] + } + + diffableDataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in + switch item { + case .tab(let tab): + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: tab) + case .header(let viewModel): + return collectionView.dequeueConfiguredReusableCell(using: headerRegistration, for: indexPath, item: viewModel) + case .account(let viewModel): + return collectionView.dequeueConfiguredReusableCell(using: accountRegistration, for: indexPath, item: viewModel) + case .addAccount: + return collectionView.dequeueConfiguredReusableCell(using: addAccountRegistration, for: indexPath, item: AddAccountViewModel()) + } + } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections(Section.allCases) + diffableDataSource.apply(snapshot) + + for section in Section.allCases { + switch section { + case .tab: + var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() + let items: [Item] = [ + .tab(.home), + .tab(.search), + .tab(.notification), + .tab(.me), + ] + sectionSnapshot.append(items, to: nil) + diffableDataSource.apply(sectionSnapshot, to: section) + case .account: + var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() + let headerItem = Item.header(HeaderViewModel(title: "Accounts")) + sectionSnapshot.append([headerItem], to: nil) + sectionSnapshot.append([], to: headerItem) + sectionSnapshot.append([.addAccount], to: headerItem) + sectionSnapshot.expand([headerItem]) + diffableDataSource.apply(sectionSnapshot, to: section) + } + } + + context.authenticationService.mastodonAuthentications + .receive(on: DispatchQueue.main) + .sink { [weak self] authentications in + guard let self = self else { return } + var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() + let headerItem = Item.header(HeaderViewModel(title: "Accounts")) + sectionSnapshot.append([headerItem], to: nil) + let items = authentications.map { authentication in + Item.account(AccountViewModel(authenticationObjectID: authentication.objectID)) + } + sectionSnapshot.append(items, to: headerItem) + sectionSnapshot.append([.addAccount], to: headerItem) + sectionSnapshot.expand([headerItem]) + self.diffableDataSource.apply(sectionSnapshot, to: .account) + } + .store(in: &disposeBag) + } + +} diff --git a/Podfile.lock b/Podfile.lock index 7c64e59b9..3541289d0 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,7 +1,7 @@ PODS: - DateToolsSwift (5.0.0) - FLEX (4.4.1) - - Kanna (5.2.4) + - Kanna (5.2.7) - Keys (1.0.1) - PINCache (3.0.3): - PINCache/Arc-exception-safe (= 3.0.3) @@ -69,7 +69,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab - Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f + Kanna: 01cfbddc127f5ff0963692f285fcbc8a9d62d234 Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086 PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20 @@ -80,4 +80,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 4db0bdf969729c5758bd923e33d9e097cb892086 -COCOAPODS: 1.11.0 +COCOAPODS: 1.11.2