From 19db0afa3eb5bb09f79772b0e4e2d86e0067afdd Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 28 Oct 2021 19:17:41 +0800 Subject: [PATCH] feat: update for new iPad UI --- Mastodon.xcodeproj/project.pbxproj | 8 + .../xcschemes/xcschememanagement.plist | 8 +- Mastodon/Coordinator/SceneCoordinator.swift | 105 ++++--- Mastodon/Generated/Assets.swift | 20 +- .../Sidebar}/Contents.json | 0 .../Scene/Sidebar/logo.imageset/Contents.json | 15 + .../Scene/Sidebar/logo.imageset/logo.pdf | 108 +++++++ .../Contents.json | 6 +- .../sidebar.background.colorset/Contents.json | 6 +- .../Background/danger.colorset/Contents.json | 20 -- .../Contents.json | 20 -- .../Contents.json | 38 --- .../Contents.json | 38 --- .../system.background.colorset/Contents.json | 38 --- .../Contents.json | 38 --- .../Contents.json | 38 --- .../Contents.json | 38 --- .../Contents.json | 38 --- .../_Deprecated/Compose/Contents.json | 9 - .../Compose/background.colorset/Contents.json | 38 --- .../toolbar.background.colorset/Contents.json | 38 --- .../Assets.xcassets/_Deprecated/Contents.json | 9 - .../HomeTimelineViewController.swift | 30 +- .../HomeTimeline/HomeTimelineViewModel.swift | 3 +- .../Root/ContentSplitViewController.swift | 92 ++++++ .../MainTab/MainTabBarController+Wizard.swift | 3 + .../Scene/Root/RootSplitViewController.swift | 274 +++++++--------- .../Root/Sidebar/SidebarViewController.swift | 200 +++++++----- .../Scene/Root/Sidebar/SidebarViewModel.swift | 295 +++++------------- .../View/SidebarListCollectionViewCell.swift | 15 - .../Sidebar/View/SidebarListContentView.swift | 149 ++------- .../Sidebar/View/SidebarListHeaderView.swift | 42 +++ .../View/Button/CircleAvatarButton.swift | 7 +- .../Service/ThemeService/SystemTheme.swift | 2 +- 34 files changed, 705 insertions(+), 1083 deletions(-) rename Mastodon/Resources/Assets.xcassets/{_Deprecated/Background => Scene/Sidebar}/Contents.json (100%) create mode 100644 Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/logo.pdf delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Background/danger.colorset/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Background/onboarding.background.colorset/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.grouped.system.background.colorset/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.system.background.colorset/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.background.colorset/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.elevated.background.colorset/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.grouped.background.colorset/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.background.colorset/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.grouped.background.colorset/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/background.colorset/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/toolbar.background.colorset/Contents.json delete mode 100644 Mastodon/Resources/Assets.xcassets/_Deprecated/Contents.json create mode 100644 Mastodon/Scene/Root/ContentSplitViewController.swift create mode 100644 Mastodon/Scene/Root/Sidebar/View/SidebarListHeaderView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 3c44f617b..1ab4b2559 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -187,6 +187,8 @@ DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; + DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */; }; + DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */; }; DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */; }; DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F42689B782007B274C /* ComposeTableView.swift */; }; DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; }; @@ -958,6 +960,8 @@ DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = ""; }; DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = ""; }; DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; + DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListHeaderView.swift; sourceTree = ""; }; + DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSplitViewController.swift; sourceTree = ""; }; DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentTableViewCell.swift; sourceTree = ""; }; DB03F7F42689B782007B274C /* ComposeTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTableView.swift; sourceTree = ""; }; DB040ED026538E3C00BEE9D8 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = ""; }; @@ -2125,6 +2129,7 @@ DBF156DE2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift */, DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */, DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */, + DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */, ); path = View; sourceTree = ""; @@ -2587,6 +2592,7 @@ isa = PBXGroup; children = ( DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */, + DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */, DB852D1A26FAED0100FC9D81 /* Sidebar */, DB8AF54E25C13703002E6C99 /* MainTab */, ); @@ -3873,6 +3879,7 @@ DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */, DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */, DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */, + DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */, DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */, DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */, 2D7631B325C159F700929FB9 /* Item.swift in Sources */, @@ -3935,6 +3942,7 @@ DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */, DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */, 2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */, + DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */, 2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */, DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */, DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 62b4c7f21..e6093a218 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 - 42 + 35 CoreDataStack.xcscheme_^#shared#^_ orderHint - 43 + 38 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -97,7 +97,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 41 + 36 MastodonIntents.xcscheme_^#shared#^_ @@ -117,7 +117,7 @@ ShareActionExtension.xcscheme_^#shared#^_ orderHint - 44 + 37 SuppressBuildableAutocreation diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 5df6f7e98..23c2a6f44 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -90,45 +90,45 @@ final public class SceneCoordinator { // Delay in next run loop - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // Note: - // show (push) on phone or pad (compact) - // showDetail in .secondary in UISplitViewController on pad (expand) - let from: UIViewController? = { - if let splitViewController = self.splitViewController { - if splitViewController.mainTabBarController.topMost?.view.window != nil { - // compact - return splitViewController.mainTabBarController.topMost - } else { - // expand - return splitViewController.viewController(for: .supplementary) - } - } else { - return self.tabBarController.topMost - } - }() - - // show notification related content - guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return } - let notificationID = String(pushNotification.notificationID) - - switch type { - case .follow: - let profileViewModel = RemoteProfileViewModel(context: appContext, notificationID: notificationID) - self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show) - case .followRequest: - // do nothing - break - case .mention, .reblog, .favourite, .poll, .status: - let threadViewModel = RemoteThreadViewModel(context: appContext, notificationID: notificationID) - self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show) - case ._other: - assertionFailure() - break - } - } +// DispatchQueue.main.async { [weak self] in +// guard let self = self else { return } +// +// // Note: +// // show (push) on phone or pad (compact) +// // showDetail in .secondary in UISplitViewController on pad (expand) +// let from: UIViewController? = { +// if let splitViewController = self.splitViewController { +// if splitViewController.mainTabBarController.topMost?.view.window != nil { +// // compact +// return splitViewController.mainTabBarController.topMost +// } else { +// // expand +// return splitViewController.viewController(for: .supplementary) +// } +// } else { +// return self.tabBarController.topMost +// } +// }() +// +// // show notification related content +// guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return } +// let notificationID = String(pushNotification.notificationID) +// +// switch type { +// case .follow: +// let profileViewModel = RemoteProfileViewModel(context: appContext, notificationID: notificationID) +// self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show) +// case .followRequest: +// // do nothing +// break +// case .mention, .reblog, .favourite, .poll, .status: +// let threadViewModel = RemoteThreadViewModel(context: appContext, notificationID: notificationID) +// self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show) +// case ._other: +// assertionFailure() +// break +// } +// } // end DispatchQueue.main.async } .store(in: &disposeBag) } @@ -227,7 +227,7 @@ extension SceneCoordinator { default: let splitViewController = RootSplitViewController(context: appContext, coordinator: self) self.splitViewController = splitViewController - self.tabBarController = splitViewController.mainTabBarController + self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController sceneDelegate.window?.rootViewController = splitViewController } } @@ -282,18 +282,19 @@ extension SceneCoordinator { switch transition { case .show: - if let splitViewController = splitViewController, !splitViewController.isCollapsed, - let supplementaryViewController = splitViewController.viewController(for: .supplementary) as? UINavigationController, - (supplementaryViewController === presentingViewController || supplementaryViewController.viewControllers.contains(presentingViewController)) || - (presentingViewController is UserTimelineViewController && presentingViewController.view.isDescendant(of: supplementaryViewController.view)) - { - fallthrough - } else { - if secondaryStackHashValues.contains(presentingViewController.hashValue) { - secondaryStackHashValues.insert(viewController.hashValue) - } - presentingViewController.show(viewController, sender: sender) - } +// if let splitViewController = splitViewController, !splitViewController.isCollapsed, +// let supplementaryViewController = splitViewController.viewController(for: .supplementary) as? UINavigationController, +// (supplementaryViewController === presentingViewController || supplementaryViewController.viewControllers.contains(presentingViewController)) || +// (presentingViewController is UserTimelineViewController && presentingViewController.view.isDescendant(of: supplementaryViewController.view)) +// { +// fallthrough +// } else { +// if secondaryStackHashValues.contains(presentingViewController.hashValue) { +// secondaryStackHashValues.insert(viewController.hashValue) +// } +// presentingViewController.show(viewController, sender: sender) +// } + presentingViewController.show(viewController, sender: sender) case .showDetail: secondaryStackHashValues.insert(viewController.hashValue) let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 96fe0fca8..906dd74e2 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -96,6 +96,9 @@ internal enum Asset { internal static let usernameGray = ColorAsset(name: "Scene/Profile/Banner/username.gray") } } + internal enum Sidebar { + internal static let logo = ImageAsset(name: "Scene/Sidebar/logo") + } internal enum Welcome { internal enum Illustration { internal static let backgroundCyan = ColorAsset(name: "Scene/Welcome/illustration/background.cyan") @@ -160,23 +163,6 @@ internal enum Asset { internal static let tabBarItemInactiveIconColor = ColorAsset(name: "Theme/system/tab.bar.item.inactive.icon.color") } } - internal enum Deprecated { - internal enum Background { - internal static let danger = ColorAsset(name: "_Deprecated/Background/danger") - internal static let onboardingBackground = ColorAsset(name: "_Deprecated/Background/onboarding.background") - internal static let secondaryGroupedSystemBackground = ColorAsset(name: "_Deprecated/Background/secondary.grouped.system.background") - internal static let secondarySystemBackground = ColorAsset(name: "_Deprecated/Background/secondary.system.background") - internal static let systemBackground = ColorAsset(name: "_Deprecated/Background/system.background") - internal static let systemElevatedBackground = ColorAsset(name: "_Deprecated/Background/system.elevated.background") - internal static let systemGroupedBackground = ColorAsset(name: "_Deprecated/Background/system.grouped.background") - internal static let tertiarySystemBackground = ColorAsset(name: "_Deprecated/Background/tertiary.system.background") - internal static let tertiarySystemGroupedBackground = ColorAsset(name: "_Deprecated/Background/tertiary.system.grouped.background") - } - internal enum Compose { - internal static let background = ColorAsset(name: "_Deprecated/Compose/background") - internal static let toolbarBackground = ColorAsset(name: "_Deprecated/Compose/toolbar.background") - } - } } // swiftlint:enable identifier_name line_length nesting type_body_length type_name diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/Contents.json similarity index 100% rename from Mastodon/Resources/Assets.xcassets/_Deprecated/Background/Contents.json rename to Mastodon/Resources/Assets.xcassets/Scene/Sidebar/Contents.json diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/Contents.json b/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/Contents.json new file mode 100644 index 000000000..4f547d09b --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "logo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/logo.pdf b/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/logo.pdf new file mode 100644 index 000000000..908727a57 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Scene/Sidebar/logo.imageset/logo.pdf @@ -0,0 +1,108 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 -0.103455 cm +0.168627 0.564706 0.850980 scn +27.796436 10.091343 m +33.035133 10.719734 37.596470 13.962151 38.169762 16.924883 c +39.073063 21.591980 38.998501 28.314186 38.998501 28.314186 c +38.998501 37.425270 33.056084 40.095867 33.056084 40.095867 c +30.059872 41.478233 24.914881 42.059555 19.569633 42.103455 c +19.438305 42.103455 l +14.093056 42.059555 8.951445 41.478233 5.955006 40.095867 c +5.955006 40.095867 0.012361 37.425270 0.012361 28.314186 c +0.012361 27.761837 0.009520 27.180878 0.006561 26.576080 c +-0.001656 24.896429 -0.010772 23.032921 0.037591 21.087820 c +0.253392 12.177679 1.663759 3.396290 9.864657 1.215820 c +13.645910 0.210445 16.892391 0.000000 19.507011 0.144371 c +24.248556 0.408443 26.910255 1.844212 26.910255 1.844212 c +26.753922 5.300014 l +26.753922 5.300014 23.365528 4.226753 19.560173 4.357544 c +15.789957 4.487431 11.809797 4.765984 11.200012 9.415886 c +11.143697 9.824329 11.115539 10.261055 11.115539 10.719732 c +11.115539 10.719732 14.816599 9.810978 19.507011 9.595104 c +22.375050 9.462955 25.064680 9.763912 27.796436 10.091343 c +h +31.989010 16.575367 m +31.989010 27.607372 l +31.989010 29.862061 31.417519 31.653776 30.269808 32.979347 c +29.085829 34.304916 27.535576 34.984444 25.611385 34.984444 c +23.384670 34.984444 21.698582 34.124794 20.583984 32.405266 c +19.500023 30.580288 l +18.416286 32.405266 l +17.301464 34.124794 15.615376 34.984444 13.388884 34.984444 c +11.464469 34.984444 9.914215 34.304916 8.730462 32.979347 c +7.582527 31.653776 7.011036 29.862061 7.011036 27.607372 c +7.011036 16.575367 l +11.361976 16.575367 l +11.361976 27.283108 l +11.361976 29.540287 12.307401 30.685961 14.198477 30.685961 c +16.289360 30.685961 17.337505 29.326900 17.337505 26.639557 c +17.337505 20.778585 l +21.662764 20.778585 l +21.662764 26.639557 l +21.662764 29.326900 22.710684 30.685961 24.801567 30.685961 c +26.692642 30.685961 27.638069 29.540287 27.638069 27.283108 c +27.638069 16.575367 l +31.989010 16.575367 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2035 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 39.000000 42.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Type /Catalog + /Pages 5 0 R + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002125 00000 n +0000002148 00000 n +0000002321 00000 n +0000002395 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2454 +%%EOF \ No newline at end of file diff --git a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json index b9a69ec7d..77d24b11d 100644 --- a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/secondary.system.background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "46", - "green" : "44", - "red" : "44" + "blue" : "0x2E", + "green" : "0x2C", + "red" : "0x2C" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/sidebar.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/sidebar.background.colorset/Contents.json index e30d6cabe..ee5b1c373 100644 --- a/Mastodon/Resources/Assets.xcassets/Theme/system/Background/sidebar.background.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/sidebar.background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.263", - "green" : "0.208", - "red" : "0.192" + "blue" : "0x2E", + "green" : "0x2C", + "red" : "0x2C" } }, "idiom" : "universal" diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/danger.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/danger.colorset/Contents.json deleted file mode 100644 index dabccc33e..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/danger.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.353", - "green" : "0.251", - "red" : "0.875" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/onboarding.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/onboarding.background.colorset/Contents.json deleted file mode 100644 index 0e4687fb4..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/onboarding.background.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.910", - "green" : "0.882", - "red" : "0.851" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.grouped.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.grouped.system.background.colorset/Contents.json deleted file mode 100644 index ef6c7f7b1..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.grouped.system.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.996", - "green" : "1.000", - "red" : "0.996" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.263", - "green" : "0.208", - "red" : "0.192" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.system.background.colorset/Contents.json deleted file mode 100644 index c915c8911..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/secondary.system.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.910", - "green" : "0.882", - "red" : "0.851" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.133", - "green" : "0.106", - "red" : "0.098" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.background.colorset/Contents.json deleted file mode 100644 index 4572c2409..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.996", - "green" : "1.000", - "red" : "0.996" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.216", - "green" : "0.173", - "red" : "0.157" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.elevated.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.elevated.background.colorset/Contents.json deleted file mode 100644 index 33b71ef90..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.elevated.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.216", - "green" : "0.173", - "red" : "0.157" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.grouped.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.grouped.background.colorset/Contents.json deleted file mode 100644 index c915c8911..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/system.grouped.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.910", - "green" : "0.882", - "red" : "0.851" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.133", - "green" : "0.106", - "red" : "0.098" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.background.colorset/Contents.json deleted file mode 100644 index 4572c2409..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.996", - "green" : "1.000", - "red" : "0.996" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.216", - "green" : "0.173", - "red" : "0.157" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.grouped.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.grouped.background.colorset/Contents.json deleted file mode 100644 index 98dd7bbde..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Background/tertiary.system.grouped.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.910", - "green" : "0.882", - "red" : "0.851" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.263", - "green" : "0.208", - "red" : "0.192" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/Contents.json deleted file mode 100644 index 6e965652d..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/background.colorset/Contents.json deleted file mode 100644 index 33b71ef90..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.216", - "green" : "0.173", - "red" : "0.157" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/toolbar.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/toolbar.background.colorset/Contents.json deleted file mode 100644 index da7b76069..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Compose/toolbar.background.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.871", - "green" : "0.847", - "red" : "0.839" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.920", - "blue" : "0.125", - "green" : "0.125", - "red" : "0.125" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Mastodon/Resources/Assets.xcassets/_Deprecated/Contents.json b/Mastodon/Resources/Assets.xcassets/_Deprecated/Contents.json deleted file mode 100644 index 6e965652d..000000000 --- a/Mastodon/Resources/Assets.xcassets/_Deprecated/Contents.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "provides-namespace" : true - } -} diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index dbd552d97..55bf544ca 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -114,6 +114,24 @@ extension HomeTimelineViewController { #endif } .store(in: &disposeBag) + #if DEBUG + // long press to trigger debug menu + settingBarButtonItem.menu = debugMenu + #else + settingBarButtonItem.target = self + settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) + #endif + + viewModel.displayComposeBarButtonItem + .receive(on: DispatchQueue.main) + .sink { [weak self] displayComposeBarButtonItem in + guard let self = self else { return } + self.navigationItem.rightBarButtonItem = displayComposeBarButtonItem ? self.composeBarButtonItem : nil + } + .store(in: &disposeBag) + composeBarButtonItem.target = self + composeBarButtonItem.action = #selector(HomeTimelineViewController.composeBarButtonItemPressed(_:)) + navigationItem.titleView = titleView titleView.delegate = self @@ -126,18 +144,6 @@ extension HomeTimelineViewController { } .store(in: &disposeBag) - #if DEBUG - // long press to trigger debug menu - settingBarButtonItem.menu = debugMenu - #else - settingBarButtonItem.target = self - settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:)) - #endif - - navigationItem.rightBarButtonItem = composeBarButtonItem - composeBarButtonItem.target = self - composeBarButtonItem.action = #selector(HomeTimelineViewController.composeBarButtonItemPressed(_:)) - tableView.refreshControl = refreshControl refreshControl.addTarget(self, action: #selector(HomeTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged) diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index a3fbcbd74..c4681b40b 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -30,6 +30,8 @@ final class HomeTimelineViewModel: NSObject { let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel let lastAutomaticFetchTimestamp = CurrentValueSubject(nil) let scrollPositionRecord = CurrentValueSubject(nil) + let displaySettingBarButtonItem = CurrentValueSubject(true) + let displayComposeBarButtonItem = CurrentValueSubject(true) weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate? weak var tableView: UITableView? @@ -70,7 +72,6 @@ final class HomeTimelineViewModel: NSObject { let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine var diffableDataSource: UITableViewDiffableDataSource? var cellFrameCache = NSCache() - let displaySettingBarButtonItem = CurrentValueSubject(true) init(context: AppContext) { self.context = context diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift new file mode 100644 index 000000000..42ce3afc3 --- /dev/null +++ b/Mastodon/Scene/Root/ContentSplitViewController.swift @@ -0,0 +1,92 @@ +// +// ContentSplitViewController.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-10-28. +// + +import os.log +import UIKit +import Combine +import CoreDataStack + +final class ContentSplitViewController: UIViewController, NeedsDependency { + + var disposeBag = Set() + + static let sidebarWidth: CGFloat = 89 + + 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 + }() + + @Published var currentSupplementaryTab: MainTabBarController.Tab = .home + private(set) lazy var mainTabBarController: MainTabBarController = { + let mainTabBarController = MainTabBarController(context: context, coordinator: coordinator) + return mainTabBarController + }() + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension ContentSplitViewController { + override func viewDidLoad() { + super.viewDidLoad() + + navigationController?.setNavigationBarHidden(true, animated: false) + + addChild(sidebarViewController) + sidebarViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(sidebarViewController.view) + sidebarViewController.didMove(toParent: self) + NSLayoutConstraint.activate([ + sidebarViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + sidebarViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + sidebarViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + sidebarViewController.view.widthAnchor.constraint(equalToConstant: ContentSplitViewController.sidebarWidth), + ]) + + addChild(mainTabBarController) + mainTabBarController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(mainTabBarController.view) + sidebarViewController.didMove(toParent: self) + NSLayoutConstraint.activate([ + mainTabBarController.view.topAnchor.constraint(equalTo: view.topAnchor), + mainTabBarController.view.leadingAnchor.constraint(equalTo: sidebarViewController.view.trailingAnchor), + mainTabBarController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + mainTabBarController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + $currentSupplementaryTab + .removeDuplicates() + .sink(receiveValue: { [weak self] tab in + guard let self = self else { return } + self.mainTabBarController.selectedIndex = tab.rawValue + self.mainTabBarController.currentTab.value = tab + }) + .store(in: &disposeBag) + } +} + +// MARK: - SidebarViewControllerDelegate +extension ContentSplitViewController: SidebarViewControllerDelegate { + + func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) { + guard let _ = MainTabBarController.Tab.allCases.firstIndex(of: tab) else { + assertionFailure() + return + } + currentSupplementaryTab = tab + } +} diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController+Wizard.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController+Wizard.swift index 8f3f2eea4..b69a6b786 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController+Wizard.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController+Wizard.swift @@ -70,6 +70,9 @@ extension MainTabBarController.Wizard { func setup(in view: UIView) { assert(delegate != nil, "need set delegate before use") + + guard !items.isEmpty else { return } + backgroundView.frame = view.bounds backgroundView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(backgroundView) diff --git a/Mastodon/Scene/Root/RootSplitViewController.swift b/Mastodon/Scene/Root/RootSplitViewController.swift index 15464c9db..4f818ea9d 100644 --- a/Mastodon/Scene/Root/RootSplitViewController.swift +++ b/Mastodon/Scene/Root/RootSplitViewController.swift @@ -14,57 +14,44 @@ final class RootSplitViewController: UISplitViewController, NeedsDependency { var disposeBag = Set() + static let sidebarWidth: CGFloat = 89 + 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 contentSplitViewController: ContentSplitViewController = { + let contentSplitViewController = ContentSplitViewController() + contentSplitViewController.context = context + contentSplitViewController.coordinator = coordinator + return contentSplitViewController }() - - var currentSupplementaryTab: MainTabBarController.Tab = .home - private(set) lazy var supplementaryViewControllers: [UIViewController] = { - let viewControllers = MainTabBarController.Tab.allCases.map { tab in - tab.viewController(context: context, coordinator: coordinator) - } - for viewController in viewControllers { - guard let navigationController = viewController as? UINavigationController else { - assertionFailure() - continue - } - if let homeViewController = navigationController.topViewController as? HomeTimelineViewController { - homeViewController.viewModel.displaySettingBarButtonItem.value = false - } - } - return viewControllers - }() - - 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) + super.init(style: .doubleColumn) + primaryEdge = .trailing primaryBackgroundStyle = .sidebar - preferredDisplayMode = .oneBesideSecondary + preferredDisplayMode = .twoBesideSecondary preferredSplitBehavior = .tile delegate = self + // disable edge swipe gesture + presentsWithGesture = false + if #available(iOS 14.5, *) { - displayModeButtonVisibility = .always + displayModeButtonVisibility = .never } else { // Fallback on earlier versions } - setViewController(sidebarViewController, for: .primary) - setViewController(supplementaryViewControllers[0], for: .supplementary) - setViewController(SecondaryPlaceholderViewController(), for: .secondary) - setViewController(mainTabBarController, for: .compact) + setViewController(UIViewController(), for: .primary) + setViewController(contentSplitViewController, for: .secondary) + + contentSplitViewController.sidebarViewController.view.layer.zPosition = 100 + contentSplitViewController.mainTabBarController.view.layer.zPosition = 90 + view.layer.zPosition = 80 } required init?(coder: NSCoder) { @@ -83,16 +70,11 @@ extension RootSplitViewController { super.viewDidLoad() updateBehavior(size: view.frame.size) - - mainTabBarController.currentTab + contentSplitViewController.$currentSupplementaryTab .receive(on: DispatchQueue.main) - .sink { [weak self] tab in + .sink { [weak self] _ in guard let self = self else { return } - guard tab != self.currentSupplementaryTab else { return } - guard let index = MainTabBarController.Tab.allCases.firstIndex(of: tab) else { return } - self.currentSupplementaryTab = tab - self.setViewController(self.supplementaryViewControllers[index], for: .supplementary) - + self.updateBehavior(size: self.view.frame.size) } .store(in: &disposeBag) } @@ -100,133 +82,111 @@ extension RootSplitViewController { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - updateBehavior(size: size) + coordinator.animate { [weak self] context in + guard let self = self else { return } + self.updateBehavior(size: size) + } completion: { context in + // do nothing + } } 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) { - - guard let index = MainTabBarController.Tab.allCases.firstIndex(of: tab) else { - assertionFailure() - return - } - currentSupplementaryTab = tab - setViewController(supplementaryViewControllers[index], for: .supplementary) - } - - func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectSearchHistory searchHistoryViewModel: SidebarViewModel.SearchHistoryViewModel) { - // self.sidebarViewController(sidebarViewController, didSelectTab: .search) - - let supplementaryViewController = viewController(for: .supplementary) - let managedObjectContext = context.managedObjectContext - managedObjectContext.perform { - let searchHistory = managedObjectContext.object(with: searchHistoryViewModel.searchHistoryObjectID) as! SearchHistory - if let account = searchHistory.account { - DispatchQueue.main.async { - let profileViewModel = CachedProfileViewModel(context: self.context, mastodonUser: account) - self.coordinator.present(scene: .profile(viewModel: profileViewModel), from: supplementaryViewController, transition: .show) - } - } else if let hashtag = searchHistory.hashtag { - DispatchQueue.main.async { - let hashtagTimelineViewModel = HashtagTimelineViewModel(context: self.context, hashtag: hashtag.name) - self.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: supplementaryViewController, transition: .show) - } + switch contentSplitViewController.currentSupplementaryTab { + case .search: + hide(.primary) + default: + if size.width > 960 { + show(.primary) } else { - assertionFailure() + hide(.primary) } } } - + } // MARK: - UISplitViewControllerDelegate extension RootSplitViewController: UISplitViewControllerDelegate { - // .regular to .compact - // move navigation stack from .supplementary & .secondary to .compact - func splitViewController( - _ svc: UISplitViewController, - topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column - ) -> UISplitViewController.Column { - switch proposedTopColumn { - case .compact: - guard let index = MainTabBarController.Tab.allCases.firstIndex(of: currentSupplementaryTab) else { - assertionFailure() - break - } - mainTabBarController.selectedIndex = index - mainTabBarController.currentTab.value = currentSupplementaryTab - - guard let navigationController = mainTabBarController.selectedViewController as? UINavigationController else { break } - navigationController.popToRootViewController(animated: false) - var viewControllers = navigationController.viewControllers // init navigation stack with topMost - - if let supplementaryNavigationController = viewController(for: .supplementary) as? UINavigationController { - // append supplementary - viewControllers.append(contentsOf: supplementaryNavigationController.popToRootViewController(animated: true) ?? []) - } - if let secondaryNavigationController = viewController(for: .secondary) as? UINavigationController { - // append secondary - viewControllers.append(contentsOf: secondaryNavigationController.popToRootViewController(animated: true) ?? []) - } - // set navigation stack - navigationController.setViewControllers(viewControllers, animated: false) - - default: - assertionFailure() - } - - return proposedTopColumn - } - - // .compact to .regular - // restore navigation stack to .supplementary & .secondary - func splitViewController( - _ svc: UISplitViewController, - displayModeForExpandingToProposedDisplayMode proposedDisplayMode: UISplitViewController.DisplayMode - ) -> UISplitViewController.DisplayMode { - let compactNavigationController = mainTabBarController.selectedViewController as? UINavigationController - - if let topMost = compactNavigationController?.topMost, - topMost is AccountListViewController { - topMost.dismiss(animated: false, completion: nil) - } - - let viewControllers = compactNavigationController?.popToRootViewController(animated: true) ?? [] - - var supplementaryViewControllers: [UIViewController] = [] - var secondaryViewControllers: [UIViewController] = [] - for viewController in viewControllers { - if coordinator.secondaryStackHashValues.contains(viewController.hashValue) { - secondaryViewControllers.append(viewController) - } else { - supplementaryViewControllers.append(viewController) - } - - } - if let supplementary = viewController(for: .supplementary) as? UINavigationController { - supplementary.setViewControllers(supplementary.viewControllers + supplementaryViewControllers, animated: false) - } - if let secondaryNavigationController = viewController(for: .secondary) as? UINavigationController { - secondaryNavigationController.setViewControllers(secondaryNavigationController.viewControllers + secondaryViewControllers, animated: false) - } - - return proposedDisplayMode - } +// // .regular to .compact +// // move navigation stack from .supplementary & .secondary to .compact +// func splitViewController( +// _ svc: UISplitViewController, +// topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column +// ) -> UISplitViewController.Column { +// switch proposedTopColumn { +// case .compact: +// guard let index = MainTabBarController.Tab.allCases.firstIndex(of: currentSupplementaryTab) else { +// assertionFailure() +// break +// } +// mainTabBarController.selectedIndex = index +// mainTabBarController.currentTab.value = currentSupplementaryTab +// +// guard let navigationController = mainTabBarController.selectedViewController as? UINavigationController else { break } +// navigationController.popToRootViewController(animated: false) +// var viewControllers = navigationController.viewControllers // init navigation stack with topMost +// +// if let supplementaryNavigationController = viewController(for: .supplementary) as? UINavigationController { +// // append supplementary +// viewControllers.append(contentsOf: supplementaryNavigationController.popToRootViewController(animated: true) ?? []) +// } +// if let secondaryNavigationController = viewController(for: .secondary) as? UINavigationController { +// // append secondary +// viewControllers.append(contentsOf: secondaryNavigationController.popToRootViewController(animated: true) ?? []) +// } +// // set navigation stack +// navigationController.setViewControllers(viewControllers, animated: false) +// +// default: +// assertionFailure() +// } +// +// return proposedTopColumn +// } +// +// // .compact to .regular +// // restore navigation stack to .supplementary & .secondary +// func splitViewController( +// _ svc: UISplitViewController, +// displayModeForExpandingToProposedDisplayMode proposedDisplayMode: UISplitViewController.DisplayMode +// ) -> UISplitViewController.DisplayMode { +// let compactNavigationController = mainTabBarController.selectedViewController as? UINavigationController +// +// if let topMost = compactNavigationController?.topMost, +// topMost is AccountListViewController { +// topMost.dismiss(animated: false, completion: nil) +// } +// +// let viewControllers = compactNavigationController?.popToRootViewController(animated: true) ?? [] +// +// var supplementaryViewControllers: [UIViewController] = [] +// var secondaryViewControllers: [UIViewController] = [] +// for viewController in viewControllers { +// if coordinator.secondaryStackHashValues.contains(viewController.hashValue) { +// secondaryViewControllers.append(viewController) +// } else { +// supplementaryViewControllers.append(viewController) +// } +// +// } +// if let supplementary = viewController(for: .supplementary) as? UINavigationController { +// supplementary.setViewControllers(supplementary.viewControllers + supplementaryViewControllers, animated: false) +// } +// if let secondaryNavigationController = viewController(for: .secondary) as? UINavigationController { +// secondaryNavigationController.setViewControllers(secondaryNavigationController.viewControllers + secondaryViewControllers, animated: false) +// } +// +// return proposedDisplayMode +// } } + +//extension UIView { +// func setNeedsLayoutForSubviews() { +// self.subviews.forEach({ +// $0.setNeedsLayout() +// $0.setNeedsLayoutForSubviews() +// }) +// } +//} diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 69d9f55c8..060091922 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -12,7 +12,6 @@ import CoreDataStack protocol SidebarViewControllerDelegate: AnyObject { func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) - func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectSearchHistory searchHistoryViewModel: SidebarViewModel.SearchHistoryViewModel) } final class SidebarViewController: UIViewController, NeedsDependency { @@ -21,28 +20,46 @@ final class SidebarViewController: UIViewController, NeedsDependency { weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var disposeBag = Set() + var observations = Set() var viewModel: SidebarViewModel! weak var delegate: SidebarViewControllerDelegate? - - let settingBarButtonItem: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem() - barButtonItem.tintColor = Asset.Colors.brandBlue.color - barButtonItem.image = UIImage(systemName: "gear")?.withRenderingMode(.alwaysTemplate) - return barButtonItem - }() static func createLayout() -> UICollectionViewLayout { let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar) configuration.backgroundColor = .clear - if sectionIndex == SidebarViewModel.Section.tab.rawValue { - // with indentation - configuration.headerMode = .none - } else { - // remove indentation - configuration.headerMode = .firstItemInSection + configuration.showsSeparators = false + let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) + switch sectionIndex { + case 0: + let header = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(100)), + elementKind: UICollectionView.elementKindSectionHeader, + alignment: .top + ) + section.boundarySupplementaryItems = [header] + default: + break } + return section + } + return layout + } + + let collectionView: UICollectionView = { + let layout = SidebarViewController.createLayout() + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.alwaysBounceVertical = false + collectionView.backgroundColor = .clear + return collectionView + }() + + static func createSecondaryLayout() -> UICollectionViewLayout { + let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in + var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar) + configuration.backgroundColor = .clear configuration.showsSeparators = false let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) return section @@ -50,12 +67,15 @@ final class SidebarViewController: UIViewController, NeedsDependency { return layout } - let collectionView: UICollectionView = { - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: SidebarViewController.createLayout()) + let secondaryCollectionView: UICollectionView = { + let layout = SidebarViewController.createSecondaryLayout() + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView.isScrollEnabled = false + collectionView.alwaysBounceVertical = false collectionView.backgroundColor = .clear return collectionView }() - + var secondaryCollectionViewHeightLayoutConstraint: NSLayoutConstraint! } extension SidebarViewController { @@ -63,23 +83,7 @@ extension SidebarViewController { override func viewDidLoad() { super.viewDidLoad() - viewModel.context.authenticationService.activeMastodonAuthenticationBox - .receive(on: DispatchQueue.main) - .sink { [weak self] activeMastodonAuthenticationBox in - guard let self = self else { return } - let domain = activeMastodonAuthenticationBox?.domain - self.navigationItem.backBarButtonItem = { - let barButtonItem = UIBarButtonItem() - barButtonItem.image = UIImage(systemName: "sidebar.leading") - return barButtonItem - }() - self.navigationItem.title = domain - } - .store(in: &disposeBag) - navigationItem.rightBarButtonItem = settingBarButtonItem - settingBarButtonItem.target = self - settingBarButtonItem.action = #selector(SidebarViewController.settingBarButtonItemPressed(_:)) - navigationController?.navigationBar.prefersLargeTitles = true + navigationController?.setNavigationBarHidden(true, animated: false) setupBackground(theme: ThemeService.shared.currentTheme.value) ThemeService.shared.currentTheme @@ -99,65 +103,101 @@ extension SidebarViewController { collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) + secondaryCollectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(secondaryCollectionView) + secondaryCollectionViewHeightLayoutConstraint = secondaryCollectionView.heightAnchor.constraint(equalToConstant: 44).priority(.required - 1) + NSLayoutConstraint.activate([ + secondaryCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + secondaryCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + view.bottomAnchor.constraint(equalTo: secondaryCollectionView.bottomAnchor), + secondaryCollectionViewHeightLayoutConstraint, + ]) + collectionView.delegate = self - viewModel.setupDiffableDataSource(collectionView: collectionView) + secondaryCollectionView.delegate = self + viewModel.setupDiffableDataSource( + collectionView: collectionView, + secondaryCollectionView: secondaryCollectionView + ) + + secondaryCollectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] secondaryCollectionView, _ in + guard let self = self else { return } + let height = secondaryCollectionView.contentSize.height + self.secondaryCollectionViewHeightLayoutConstraint.constant = height + self.collectionView.contentInset.bottom = height + } + .store(in: &observations) } private func setupBackground(theme: Theme) { let color: UIColor = theme.sidebarBackgroundColor - let barAppearance = UINavigationBarAppearance() - barAppearance.configureWithOpaqueBackground() - barAppearance.backgroundColor = color - barAppearance.shadowColor = .clear - barAppearance.shadowImage = UIImage() // remove separator line - navigationItem.standardAppearance = barAppearance - navigationItem.compactAppearance = barAppearance - navigationItem.scrollEdgeAppearance = barAppearance - if #available(iOS 15.0, *) { - navigationItem.compactScrollEdgeAppearance = barAppearance - } else { - // Fallback on earlier versions - } - view.backgroundColor = color - collectionView.backgroundColor = color + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate { context in + self.collectionView.collectionViewLayout.invalidateLayout() +// // do nothing + } completion: { [weak self] context in +// guard let self = self else { return } + } + } } -extension SidebarViewController { - @objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard let setting = context.settingService.currentSetting.value else { return } - let settingsViewModel = SettingsViewModel(context: context, setting: setting) - coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) - } -} - // 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 .searchHistory(let viewModel): - delegate?.sidebarViewController(self, didSelectSearchHistory: viewModel) - case .header: - break - 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)) + switch collectionView { + case self.collectionView: + 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 .setting: + guard let setting = context.settingService.currentSetting.value else { return } + let settingsViewModel = SettingsViewModel(context: context, setting: setting) + coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil)) + case .compose: + assertionFailure() + } + case secondaryCollectionView: + guard let diffableDataSource = viewModel.secondaryDiffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + switch item { + case .compose: + let composeViewModel = ComposeViewModel(context: context, composeKind: .post) + coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) + default: + assertionFailure() + } + default: + assertionFailure() } +// switch item { +// case .tab(let tab): +// delegate?.sidebarViewController(self, didSelectTab: tab) +// case .searchHistory(let viewModel): +// delegate?.sidebarViewController(self, didSelectSearchHistory: viewModel) +// case .header: +// break +// 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)) +// } } } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index d7ec5b717..6dd46d08c 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -22,6 +22,8 @@ final class SidebarViewModel { // output var diffableDataSource: UICollectionViewDiffableDataSource? + var secondaryDiffableDataSource: UICollectionViewDiffableDataSource? + let activeMastodonAuthenticationObjectID = CurrentValueSubject(nil) init(context: AppContext) { @@ -47,38 +49,22 @@ final class SidebarViewModel { extension SidebarViewModel { enum Section: Int, Hashable, CaseIterable { - case tab - case account + case main + case secondary } enum Item: Hashable { case tab(MainTabBarController.Tab) - case searchHistory(SearchHistoryViewModel) - case header(HeaderViewModel) - case account(AccountViewModel) - case addAccount + case setting + case compose } - struct SearchHistoryViewModel: Hashable { - let searchHistoryObjectID: NSManagedObjectID - } - - struct HeaderViewModel: Hashable { - let title: String - } - - struct AccountViewModel: Hashable { - let authenticationObjectID: NSManagedObjectID - } - - struct AddAccountViewModel: Hashable { - let id = UUID() - } } extension SidebarViewModel { func setupDiffableDataSource( - collectionView: UICollectionView + collectionView: UICollectionView, + secondaryCollectionView: UICollectionView ) { let tabCellRegistration = UICollectionView.CellRegistration { [weak self] cell, indexPath, item in guard let self = self else { return } @@ -92,23 +78,10 @@ extension SidebarViewModel { return nil } }() - let headline: MetaContent = { - switch item { - case .me: - return PlaintextMetaContent(string: item.title) - // TODO: - // return PlaintextMetaContent(string: "Myself") - default: - return PlaintextMetaContent(string: item.title) - } - }() - let needsOutlineDisclosure = item == .search cell.item = SidebarListContentView.Item( + title: item.title, image: item.sidebarImage, - imageURL: imageURL, - headline: headline, - subheadline: nil, - needsOutlineDisclosure: needsOutlineDisclosure + imageURL: imageURL ) cell.setNeedsUpdateConfiguration() @@ -135,209 +108,91 @@ extension SidebarViewModel { } } - let searchHistoryCellRegistration = UICollectionView.CellRegistration { [weak self] cell, indexPath, item in + let cellRegistration = UICollectionView.CellRegistration { [weak self] cell, indexPath, item in guard let self = self else { return } - let managedObjectContext = self.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext - - guard let searchHistory = try? managedObjectContext.existingObject(with: item.searchHistoryObjectID) as? SearchHistory else { return } - - if let account = searchHistory.account { - let headline: MetaContent = { - do { - let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojiMeta) - return try MastodonMetaContent.convert(document: content) - } catch { - return PlaintextMetaContent(string: account.displayNameWithFallback) - } - }() - cell.item = SidebarListContentView.Item( - image: .placeholder(color: .systemFill), - imageURL: account.avatarImageURL(), - headline: headline, - subheadline: PlaintextMetaContent(string: "@" + account.acctWithDomain), - needsOutlineDisclosure: false - ) - } else if let hashtag = searchHistory.hashtag { - let image = UIImage(systemName: "number.square.fill")!.withRenderingMode(.alwaysTemplate) - let headline = PlaintextMetaContent(string: "#" + hashtag.name) - cell.item = SidebarListContentView.Item( - image: image, - imageURL: nil, - headline: headline, - subheadline: nil, - needsOutlineDisclosure: false - ) - } else { - assertionFailure() - } - + cell.item = item cell.setNeedsUpdateConfiguration() } - let headerRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in - var content = UIListContentConfiguration.sidebarHeader() - content.text = item.title - cell.contentConfiguration = content - cell.accessories = [.outlineDisclosure()] - } - - let accountRegistration = UICollectionView.CellRegistration { [weak self] (cell, indexPath, item) in - guard let self = self else { return } - - // accounts maybe already sign-out - // check isDeleted before using - guard let authentication = try? AppContext.shared.managedObjectContext.existingObject(with: item.authenticationObjectID) as? MastodonAuthentication, - !authentication.isDeleted else { - return - } - let user = authentication.user - let imageURL = user.avatarImageURL() - let headline: MetaContent = { - do { - let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta) - return try MastodonMetaContent.convert(document: content) - } catch { - return PlaintextMetaContent(string: user.displayNameWithFallback) - } - }() - cell.item = SidebarListContentView.Item( - image: .placeholder(color: .systemFill), - imageURL: imageURL, - headline: headline, - subheadline: PlaintextMetaContent(string: "@" + user.acctWithDomain), - needsOutlineDisclosure: false - ) - cell.setNeedsUpdateConfiguration() - - // FIXME: use notification, not timer - let accessToken = authentication.userAccessToken - AppContext.shared.timestampUpdatePublisher - .map { _ in UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) } - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak cell] count in - guard let cell = cell else { return } - cell._contentView?.badgeButton.setBadge(number: count) - } - .store(in: &cell.disposeBag) - - let authenticationObjectID = item.authenticationObjectID - self.activeMastodonAuthenticationObjectID - .receive(on: DispatchQueue.main) - .sink { [weak cell] objectID in - guard let cell = cell else { return } - cell._contentView?.checkmarkImageView.isHidden = authenticationObjectID != objectID - } - .store(in: &cell.disposeBag) - } - - let addAccountRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in - var content = UIListContentConfiguration.sidebarCell() - content.text = L10n.Scene.AccountList.addAccount - content.image = UIImage(systemName: "plus.square.fill")! - - cell.contentConfiguration = content - cell.accessories = [] + // header + let headerRegistration = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in + // do nothing } let _diffableDataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in switch item { case .tab(let tab): return collectionView.dequeueConfiguredReusableCell(using: tabCellRegistration, for: indexPath, item: tab) - case .searchHistory(let viewModel): - return collectionView.dequeueConfiguredReusableCell(using: searchHistoryCellRegistration, for: indexPath, item: viewModel) - 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()) + case .setting: + let item = SidebarListContentView.Item( + title: L10n.Common.Controls.Actions.settings, + image: UIImage(systemName: "gear")!, + imageURL: nil + ) + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) + case .compose: + let item = SidebarListContentView.Item( + title: "Compose", // FIXME: + image: UIImage(systemName: "square.and.pencil")!, + imageURL: nil + ) + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) + } + } + _diffableDataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in + switch elementKind { + case UICollectionView.elementKindSectionHeader: + return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath) + default: + assertionFailure() + return UICollectionReusableView() } } diffableDataSource = _diffableDataSource var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections(Section.allCases) - _diffableDataSource.apply(snapshot) + snapshot.appendSections([.main]) - 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) + var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() + let items: [Item] = [ + .tab(.home), + .tab(.search), + .tab(.notification), + .tab(.me), + .setting, + ] + sectionSnapshot.append(items, to: nil) + _diffableDataSource.apply(sectionSnapshot, to: .main) + + + // secondary + let _secondaryDiffableDataSource = UICollectionViewDiffableDataSource(collectionView: secondaryCollectionView) { collectionView, indexPath, item in + guard case .compose = item else { + assertionFailure() + return UICollectionViewCell() } + + let item = SidebarListContentView.Item( + title: "Compose", // FIXME: + image: UIImage(systemName: "square.and.pencil")!, + imageURL: nil + ) + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) } +// _secondaryDiffableDataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in +// return nil +// } + secondaryDiffableDataSource = _secondaryDiffableDataSource - // update .search tab - searchHistoryFetchedResultController.objectIDs - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] objectIDs in - guard let self = self else { return } - guard let diffableDataSource = self.diffableDataSource else { return } + var secondarySnapshot = NSDiffableDataSourceSnapshot() + secondarySnapshot.appendSections([.secondary]) - // update .search tab - var sectionSnapshot = diffableDataSource.snapshot(for: .tab) - - // remove children - let searchHistorySnapshot = sectionSnapshot.snapshot(of: .tab(.search)) - sectionSnapshot.delete(searchHistorySnapshot.items) - - // append children - let managedObjectContext = self.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext - let items: [Item] = objectIDs.compactMap { objectID -> Item? in - guard let searchHistory = try? managedObjectContext.existingObject(with: objectID) as? SearchHistory else { return nil } - guard searchHistory.account != nil || searchHistory.hashtag != nil else { return nil } - let viewModel = SearchHistoryViewModel(searchHistoryObjectID: objectID) - return Item.searchHistory(viewModel) - } - sectionSnapshot.append(Array(items.prefix(5)), to: .tab(.search)) - sectionSnapshot.expand([.tab(.search)]) - - // apply snapshot - diffableDataSource.apply(sectionSnapshot, to: .tab, animatingDifferences: false) - } - .store(in: &disposeBag) - - // update .me tab and .account section - context.authenticationService.mastodonAuthentications - .receive(on: DispatchQueue.main) - .sink { [weak self] authentications in - guard let self = self else { return } - guard let diffableDataSource = self.diffableDataSource else { return } - // tab - var snapshot = diffableDataSource.snapshot() - snapshot.reloadItems([.tab(.me)]) - diffableDataSource.apply(snapshot) - - // account - var accountSectionSnapshot = NSDiffableDataSourceSectionSnapshot() - let headerItem = Item.header(HeaderViewModel(title: "Accounts")) - accountSectionSnapshot.append([headerItem], to: nil) - let accountItems = authentications.map { authentication in - Item.account(AccountViewModel(authenticationObjectID: authentication.objectID)) - } - accountSectionSnapshot.append(accountItems, to: headerItem) - accountSectionSnapshot.append([.addAccount], to: headerItem) - accountSectionSnapshot.expand([headerItem]) - diffableDataSource.apply(accountSectionSnapshot, to: .account) - } - .store(in: &disposeBag) + var secondarySectionSnapshot = NSDiffableDataSourceSectionSnapshot() + let secondarySectionItems: [Item] = [ + .compose, + ] + secondarySectionSnapshot.append(secondarySectionItems, to: nil) + _secondaryDiffableDataSource.apply(secondarySectionSnapshot, to: .secondary) } } diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift index 1bb76f59e..dc71eadfb 100644 --- a/Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift @@ -60,21 +60,6 @@ extension SidebarListCollectionViewCell { newBackgroundConfiguration.backgroundColorTransformer = .init { $0.withAlphaComponent(0.8) } } - backgroundConfiguration = newBackgroundConfiguration - - let needsOutlineDisclosure = item?.needsOutlineDisclosure ?? false - if !needsOutlineDisclosure { - accessories = [] - } else { - let tintColor: UIColor = state.isHighlighted || state.isSelected ? .white : Asset.Colors.brandBlue.color - accessories = [ - UICellAccessory.outlineDisclosure( - displayed: .always, - options: UICellAccessory.OutlineDisclosureOptions(tintColor: tintColor), - actionHandler: nil - ) - ] - } } } diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift index 62b188325..dedd47b88 100644 --- a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift @@ -15,15 +15,10 @@ final class SidebarListContentView: UIView, UIContentView { let logger = Logger(subsystem: "SidebarListContentView", category: "UI") let imageView = UIImageView() - let animationImageView = FLAnimatedImageView() // for animation image - let headlineLabel = MetaLabel(style: .sidebarHeadline(isSelected: false)) - let subheadlineLabel = MetaLabel(style: .sidebarSubheadline(isSelected: false)) - let badgeButton = BadgeButton() - let checkmarkImageView: UIImageView = { - let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .semibold)) - let imageView = UIImageView(image: image) - imageView.tintColor = .label - return imageView + let avatarButton: CircleAvatarButton = { + let button = CircleAvatarButton() + button.borderWidth = 2 + return button }() private var currentConfiguration: ContentConfiguration! @@ -53,93 +48,31 @@ final class SidebarListContentView: UIView, UIContentView { extension SidebarListContentView { private func _init() { - let imageViewContainer = UIView() - imageViewContainer.translatesAutoresizingMaskIntoConstraints = false - addSubview(imageViewContainer) - NSLayoutConstraint.activate([ - imageViewContainer.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), - imageViewContainer.centerYAnchor.constraint(equalTo: centerYAnchor), - ]) - imageViewContainer.setContentHuggingPriority(.defaultLow, for: .horizontal) - imageViewContainer.setContentHuggingPriority(.defaultLow, for: .vertical) - - animationImageView.translatesAutoresizingMaskIntoConstraints = false - imageViewContainer.addSubview(animationImageView) - NSLayoutConstraint.activate([ - animationImageView.centerXAnchor.constraint(equalTo: imageViewContainer.centerXAnchor), - animationImageView.centerYAnchor.constraint(equalTo: imageViewContainer.centerYAnchor), - animationImageView.widthAnchor.constraint(equalTo: imageViewContainer.widthAnchor, multiplier: 1.0).priority(.required - 1), - animationImageView.heightAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1), - ]) - animationImageView.setContentHuggingPriority(.defaultLow - 10, for: .vertical) - animationImageView.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageViewContainer.addSubview(imageView) + addSubview(imageView) NSLayoutConstraint.activate([ - imageView.centerXAnchor.constraint(equalTo: imageViewContainer.centerXAnchor), - imageView.centerYAnchor.constraint(equalTo: imageViewContainer.centerYAnchor), - imageView.widthAnchor.constraint(equalTo: imageViewContainer.widthAnchor, multiplier: 1.0).priority(.required - 1), - imageView.heightAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1), + imageView.topAnchor.constraint(equalTo: topAnchor, constant: 16), + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 16), + imageView.widthAnchor.constraint(equalToConstant: 40).priority(.required - 1), + imageView.heightAnchor.constraint(equalToConstant: 40).priority(.required - 1), ]) - imageView.setContentHuggingPriority(.defaultLow - 10, for: .vertical) - imageView.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) - - let textContainer = UIStackView() - textContainer.axis = .vertical - textContainer.translatesAutoresizingMaskIntoConstraints = false - addSubview(textContainer) + + avatarButton.translatesAutoresizingMaskIntoConstraints = false + addSubview(avatarButton) NSLayoutConstraint.activate([ - textContainer.topAnchor.constraint(equalTo: topAnchor, constant: 10), - textContainer.leadingAnchor.constraint(equalTo: imageViewContainer.trailingAnchor, constant: 10), - // textContainer.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), - bottomAnchor.constraint(equalTo: textContainer.bottomAnchor, constant: 12), + avatarButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + avatarButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), + avatarButton.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0).priority(.required - 2), + avatarButton.heightAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1.0).priority(.required - 2), ]) - - textContainer.addArrangedSubview(headlineLabel) - textContainer.addArrangedSubview(subheadlineLabel) - headlineLabel.setContentHuggingPriority(.required - 9, for: .vertical) - headlineLabel.setContentCompressionResistancePriority(.required - 9, for: .vertical) - subheadlineLabel.setContentHuggingPriority(.required - 10, for: .vertical) - subheadlineLabel.setContentCompressionResistancePriority(.required - 10, for: .vertical) - - badgeButton.translatesAutoresizingMaskIntoConstraints = false - addSubview(badgeButton) - NSLayoutConstraint.activate([ - badgeButton.leadingAnchor.constraint(equalTo: textContainer.trailingAnchor, constant: 4), - badgeButton.centerYAnchor.constraint(equalTo: centerYAnchor), - badgeButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 16).priority(.required - 1), - badgeButton.widthAnchor.constraint(equalTo: badgeButton.heightAnchor, multiplier: 1.0).priority(.required - 1), - ]) - badgeButton.setContentHuggingPriority(.required - 10, for: .horizontal) - badgeButton.setContentCompressionResistancePriority(.required - 10, for: .horizontal) - - NSLayoutConstraint.activate([ - imageViewContainer.heightAnchor.constraint(equalTo: headlineLabel.heightAnchor, multiplier: 1.0).priority(.required - 1), - imageViewContainer.widthAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1), - ]) - - checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false - addSubview(checkmarkImageView) - NSLayoutConstraint.activate([ - checkmarkImageView.centerYAnchor.constraint(equalTo: centerYAnchor), - checkmarkImageView.leadingAnchor.constraint(equalTo: badgeButton.trailingAnchor, constant: 16), - checkmarkImageView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), - ]) - checkmarkImageView.setContentHuggingPriority(.required - 9, for: .horizontal) - checkmarkImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal) - - animationImageView.isUserInteractionEnabled = false - headlineLabel.isUserInteractionEnabled = false - subheadlineLabel.isUserInteractionEnabled = false - + avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .vertical) + avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) + imageView.contentMode = .scaleAspectFit - animationImageView.contentMode = .scaleAspectFit + avatarButton.contentMode = .scaleAspectFit imageView.tintColor = Asset.Colors.brandBlue.color - animationImageView.tintColor = Asset.Colors.brandBlue.color - - badgeButton.setBadge(number: 0) - checkmarkImageView.isHidden = true + avatarButton.tintColor = Asset.Colors.brandBlue.color } private func apply(configuration: ContentConfiguration) { @@ -152,31 +85,20 @@ extension SidebarListContentView { // configure state imageView.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color - animationImageView.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color - headlineLabel.setup(style: .sidebarHeadline(isSelected: item.isSelected)) - subheadlineLabel.setup(style: .sidebarSubheadline(isSelected: item.isSelected)) + avatarButton.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color // configure model imageView.isHidden = item.imageURL != nil - animationImageView.isHidden = item.imageURL == nil + avatarButton.isHidden = item.imageURL == nil imageView.image = item.image.withRenderingMode(.alwaysTemplate) - animationImageView.setImage( + avatarButton.avatarImageView.setImage( url: item.imageURL, - placeholder: animationImageView.image ?? .placeholder(color: .systemFill), // reuse to avoid blink + placeholder: avatarButton.avatarImageView.image ?? .placeholder(color: .systemFill), // reuse to avoid blink scaleToSize: nil ) - animationImageView.layer.masksToBounds = true - animationImageView.layer.cornerCurve = .continuous - animationImageView.layer.cornerRadius = 4 - - headlineLabel.configure(content: item.headline) - - if let subheadline = item.subheadline { - subheadlineLabel.configure(content: subheadline) - subheadlineLabel.isHidden = false - } else { - subheadlineLabel.isHidden = true - } + avatarButton.layer.masksToBounds = true + avatarButton.layer.cornerCurve = .continuous + avatarButton.layer.cornerRadius = 4 } } @@ -186,27 +108,22 @@ extension SidebarListContentView { var isSelected: Bool = false // model + let title: String let image: UIImage let imageURL: URL? - let headline: MetaContent - let subheadline: MetaContent? - - let needsOutlineDisclosure: Bool - + static func == (lhs: SidebarListContentView.Item, rhs: SidebarListContentView.Item) -> Bool { return lhs.isSelected == rhs.isSelected + && lhs.title == rhs.title && lhs.image == rhs.image && lhs.imageURL == rhs.imageURL - && lhs.headline.string == rhs.headline.string - && lhs.subheadline?.string == rhs.subheadline?.string } func hash(into hasher: inout Hasher) { hasher.combine(isSelected) + hasher.combine(title) hasher.combine(image) imageURL.flatMap { hasher.combine($0) } - hasher.combine(headline.string) - subheadline.flatMap { hasher.combine($0.string) } } } diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListHeaderView.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListHeaderView.swift new file mode 100644 index 000000000..2056c5dcd --- /dev/null +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListHeaderView.swift @@ -0,0 +1,42 @@ +// +// SidebarListHeaderView.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-10-28. +// + +import UIKit + +final class SidebarListHeaderView: UICollectionReusableView { + + let imageView: UIImageView = { + let imageView = UIImageView() + imageView.image = Asset.Scene.Sidebar.logo.image + return imageView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension SidebarListHeaderView { + private func _init() { + imageView.translatesAutoresizingMaskIntoConstraints = false + addSubview(imageView) + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: topAnchor, constant: 8), + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 16), + imageView.widthAnchor.constraint(equalToConstant: 44).priority(.required - 1), + imageView.heightAnchor.constraint(equalToConstant: 44).priority(.required - 1), + ]) + } +} diff --git a/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift b/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift index 40272d290..0bc2aeefd 100644 --- a/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift +++ b/Mastodon/Scene/Share/View/Button/CircleAvatarButton.swift @@ -9,12 +9,15 @@ import UIKit final class CircleAvatarButton: AvatarButton { + var borderColor: CGColor = UIColor.systemFill.cgColor + var borderWidth: CGFloat = 1.0 + override func layoutSubviews() { super.layoutSubviews() layer.masksToBounds = true layer.cornerRadius = frame.width * 0.5 - layer.borderColor = UIColor.systemFill.cgColor - layer.borderWidth = 1 + layer.borderColor = borderColor + layer.borderWidth = borderWidth } } diff --git a/Mastodon/Service/ThemeService/SystemTheme.swift b/Mastodon/Service/ThemeService/SystemTheme.swift index 2e3b290db..1ea923979 100644 --- a/Mastodon/Service/ThemeService/SystemTheme.swift +++ b/Mastodon/Service/ThemeService/SystemTheme.swift @@ -23,7 +23,7 @@ struct SystemTheme: Theme { let navigationBarBackgroundColor = Asset.Theme.System.navigationBarBackground.color - let sidebarBackgroundColor = Asset.Theme.Mastodon.sidebarBackground.color + let sidebarBackgroundColor = Asset.Theme.System.sidebarBackground.color let tabBarBackgroundColor = Asset.Theme.System.tabBarBackground.color let tabBarItemSelectedIconColor = Asset.Colors.brandBlue.color