From d8de3c4f651fbb399276fecf727f8f5349c09e89 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 24 Sep 2021 19:58:50 +0800 Subject: [PATCH] feat: update sidebar UI --- Mastodon.xcodeproj/project.pbxproj | 16 ++ .../xcschemes/xcschememanagement.plist | 8 +- .../xcshareddata/swiftpm/Package.resolved | 9 + Mastodon/Coordinator/SceneCoordinator.swift | 22 +- Mastodon/Extension/MetaLabel.swift | 30 ++- Mastodon/Generated/Assets.swift | 2 + Mastodon/Info.plist | 2 + .../sidebar.background.colorset/Contents.json | 38 ++++ .../sidebar.background.colorset/Contents.json | 38 ++++ .../Scene/Compose/ComposeViewController.swift | 8 +- .../HomeTimelineViewController.swift | 8 +- .../HomeTimeline/HomeTimelineViewModel.swift | 2 +- .../Root/MainTab/MainTabBarController.swift | 9 + .../Scene/Root/RootSplitViewController.swift | 12 +- .../Root/Sidebar/SidebarViewController.swift | 57 ++++- .../Scene/Root/Sidebar/SidebarViewModel.swift | 83 +++++-- .../View/SidebarListCollectionViewCell.swift | 48 ++++ .../Sidebar/View/SidebarListContentView.swift | 215 ++++++++++++++++++ .../Service/ThemeService/MastodonTheme.swift | 2 + .../Service/ThemeService/SystemTheme.swift | 2 + Mastodon/Service/ThemeService/Theme.swift | 2 + 21 files changed, 557 insertions(+), 56 deletions(-) create mode 100644 Mastodon/Resources/Assets.xcassets/Theme/Mastodon/Background/sidebar.background.colorset/Contents.json create mode 100644 Mastodon/Resources/Assets.xcassets/Theme/system/Background/sidebar.background.colorset/Contents.json create mode 100644 Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift create mode 100644 Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4fc1963cb..a63d76b89 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -199,6 +199,8 @@ DB0C947226A7D2D70088FB11 /* AvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C947126A7D2D70088FB11 /* AvatarButton.swift */; }; DB0C947726A7FE840088FB11 /* NotificationAvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */; }; DB0E91EA26A9675100BD2ACC /* MetaLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0E91E926A9675100BD2ACC /* MetaLabel.swift */; }; + DB0EF72B26FDB1D200347686 /* SidebarListCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */; }; + DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */; }; DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; }; DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; }; DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; }; @@ -954,6 +956,8 @@ DB0C947126A7D2D70088FB11 /* AvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarButton.swift; sourceTree = ""; }; DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAvatarButton.swift; sourceTree = ""; }; DB0E91E926A9675100BD2ACC /* MetaLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaLabel.swift; sourceTree = ""; }; + DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListCollectionViewCell.swift; sourceTree = ""; }; + DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListContentView.swift; sourceTree = ""; }; DB0F814D264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerLoaderTableViewCell.swift; sourceTree = ""; }; @@ -2082,6 +2086,15 @@ path = Button; sourceTree = ""; }; + DB0EF72C26FDB1D600347686 /* View */ = { + isa = PBXGroup; + children = ( + DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */, + DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */, + ); + path = View; + sourceTree = ""; + }; DB1D187125EF5BBD003F1F23 /* TableView */ = { isa = PBXGroup; children = ( @@ -2526,6 +2539,7 @@ DB852D1A26FAED0100FC9D81 /* Sidebar */ = { isa = PBXGroup; children = ( + DB0EF72C26FDB1D600347686 /* View */, DB852D1826FAEB6B00FC9D81 /* SidebarViewController.swift */, DB852D1E26FB037800FC9D81 /* SidebarViewModel.swift */, ); @@ -4136,6 +4150,7 @@ DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */, DBCBCC0D2680B908000F5B51 /* HomeTimelinePreference.swift in Sources */, DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */, + DB0EF72E26FDB24F00347686 /* SidebarListContentView.swift in Sources */, DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */, DB98338725C945ED00AD9700 /* Strings.swift in Sources */, 2D7867192625B77500211898 /* NotificationItem.swift in Sources */, @@ -4151,6 +4166,7 @@ 2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */, 5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */, 0F20223926146553000C64BF /* Array.swift in Sources */, + DB0EF72B26FDB1D200347686 /* SidebarListCollectionViewCell.swift in Sources */, 5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */, DB0C946B26A700AB0088FB11 /* MastodonUser+Property.swift in Sources */, DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 648f74156..91145420e 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 - 59 + 36 CoreDataStack.xcscheme_^#shared#^_ orderHint - 62 + 35 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -97,7 +97,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 60 + 37 MastodonIntents.xcscheme_^#shared#^_ @@ -117,7 +117,7 @@ ShareActionExtension.xcscheme_^#shared#^_ orderHint - 61 + 38 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index e5bae6f1f..44043f885 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -216,6 +216,15 @@ "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 f5e0928d7..4978c0a7e 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -113,16 +113,17 @@ extension SceneCoordinator { extension SceneCoordinator { -// func setup() { -// let viewController = MainTabBarController(context: appContext, coordinator: self) -// sceneDelegate.window?.rootViewController = viewController -// tabBarController = viewController -// } - func setup() { - let splitViewController = RootSplitViewController(context: appContext, coordinator: self) - self.splitViewController = splitViewController - sceneDelegate.window?.rootViewController = splitViewController + switch UIDevice.current.userInterfaceIdiom { + case .phone: + let viewController = MainTabBarController(context: appContext, coordinator: self) + sceneDelegate.window?.rootViewController = viewController + tabBarController = viewController + default: + let splitViewController = RootSplitViewController(context: appContext, coordinator: self) + self.splitViewController = splitViewController + sceneDelegate.window?.rootViewController = splitViewController + } } func setupOnboardingIfNeeds(animated: Bool) { @@ -177,7 +178,8 @@ extension SceneCoordinator { case .show: if let splitViewController = splitViewController, !splitViewController.isCollapsed, let supplementaryViewController = splitViewController.viewController(for: .supplementary) as? UINavigationController, - (supplementaryViewController === presentingViewController || supplementaryViewController.viewControllers.contains(presentingViewController)) + (supplementaryViewController === presentingViewController || supplementaryViewController.viewControllers.contains(presentingViewController)) || + (presentingViewController is UserTimelineViewController && presentingViewController.view.isDescendant(of: supplementaryViewController.view)) { fallthrough } else { diff --git a/Mastodon/Extension/MetaLabel.swift b/Mastodon/Extension/MetaLabel.swift index a9696892a..04b214d82 100644 --- a/Mastodon/Extension/MetaLabel.swift +++ b/Mastodon/Extension/MetaLabel.swift @@ -22,6 +22,8 @@ extension MetaLabel { case autoCompletion case accountListName case accountListUsername + case sidebarHeadline(isSelected: Bool) + case sidebarSubheadline(isSelected: Bool) } convenience init(style: Style) { @@ -32,41 +34,45 @@ extension MetaLabel { textContainer.lineBreakMode = .byTruncatingTail textContainer.lineFragmentPadding = 0 + setup(style: style) + } + + func setup(style: Style) { let font: UIFont let textColor: UIColor - + switch style { case .statusHeader: font = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: .systemFont(ofSize: 13, weight: .medium), maximumPointSize: 17) textColor = Asset.Colors.Label.secondary.color - + case .statusName: font = .systemFont(ofSize: 17, weight: .semibold) textColor = Asset.Colors.Label.primary.color - + case .notificationTitle: font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20) textColor = Asset.Colors.Label.secondary.color - + case .profileFieldName: font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20) textColor = Asset.Colors.Label.primary.color - + case .profileFieldValue: font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20) textColor = Asset.Colors.Label.primary.color textAlignment = .right - + case .titleView: font = .systemFont(ofSize: 17, weight: .semibold) textColor = Asset.Colors.Label.primary.color textAlignment = .center paragraphStyle.alignment = .center - + case .recommendAccountName: font = .systemFont(ofSize: 18, weight: .semibold) textColor = .white - + case .settingTableFooter: font = .preferredFont(forTextStyle: .footnote) textColor = Asset.Colors.Label.secondary.color @@ -82,8 +88,14 @@ extension MetaLabel { case .accountListUsername: font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular), maximumPointSize: 20) textColor = Asset.Colors.Label.secondary.color + case .sidebarHeadline(let isSelected): + font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 22, weight: .regular), maximumPointSize: 20) + textColor = isSelected ? .white : Asset.Colors.Label.primary.color + case .sidebarSubheadline(let isSelected): + font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 13, weight: .regular), maximumPointSize: 18) + textColor = isSelected ? .white : Asset.Colors.Label.secondary.color } - + self.font = font self.textColor = textColor diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index 36edb7bcd..96fe0fca8 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -126,6 +126,7 @@ internal enum Asset { internal static let profileFieldCollectionViewBackground = ColorAsset(name: "Theme/Mastodon/profile.field.collection.view.background") internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Theme/Mastodon/secondary.grouped.system.background") internal static let secondarySystemBackground = ColorAsset(name: "Theme/Mastodon/secondary.system.background") + internal static let sidebarBackground = ColorAsset(name: "Theme/Mastodon/sidebar.background") internal static let systemBackground = ColorAsset(name: "Theme/Mastodon/system.background") internal static let systemElevatedBackground = ColorAsset(name: "Theme/Mastodon/system.elevated.background") internal static let systemGroupedBackground = ColorAsset(name: "Theme/Mastodon/system.grouped.background") @@ -145,6 +146,7 @@ internal enum Asset { internal static let profileFieldCollectionViewBackground = ColorAsset(name: "Theme/system/profile.field.collection.view.background") internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Theme/system/secondary.grouped.system.background") internal static let secondarySystemBackground = ColorAsset(name: "Theme/system/secondary.system.background") + internal static let sidebarBackground = ColorAsset(name: "Theme/system/sidebar.background") internal static let systemBackground = ColorAsset(name: "Theme/system/system.background") internal static let systemElevatedBackground = ColorAsset(name: "Theme/system/system.elevated.background") internal static let systemGroupedBackground = ColorAsset(name: "Theme/system/system.grouped.background") diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist index a04553608..73d3654f9 100644 --- a/Mastodon/Info.plist +++ b/Mastodon/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDuration + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/Background/sidebar.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/Background/sidebar.background.colorset/Contents.json new file mode 100644 index 000000000..c24074078 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Theme/Mastodon/Background/sidebar.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF1", + "green" : "0xF1", + "red" : "0xF1" + } + }, + "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/Theme/system/Background/sidebar.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/sidebar.background.colorset/Contents.json new file mode 100644 index 000000000..e30d6cabe --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Theme/system/Background/sidebar.background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.945", + "green" : "0.945", + "red" : "0.945" + } + }, + "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/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 275c8e456..9be0e4f7d 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -924,8 +924,12 @@ extension ComposeViewController: UICollectionViewDelegate { extension ComposeViewController: UIAdaptivePresentationControllerDelegate { func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { - return .overFullScreen - //return traitCollection.userInterfaceIdiom == .pad ? .formSheet : .automatic + switch traitCollection.horizontalSizeClass { + case .compact: + return .overFullScreen + default: + return .pageSheet + } } func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index c47eb3633..154fad528 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -95,7 +95,13 @@ extension HomeTimelineViewController { self.view.backgroundColor = theme.secondarySystemBackgroundColor } .store(in: &disposeBag) -// navigationItem.leftBarButtonItem = settingBarButtonItem + viewModel.displaySettingBarButtonItem + .receive(on: DispatchQueue.main) + .sink { [weak self] displaySettingBarButtonItem in + guard let self = self else { return } + self.navigationItem.leftBarButtonItem = displaySettingBarButtonItem ? self.settingBarButtonItem : nil + } + .store(in: &disposeBag) navigationItem.titleView = titleView titleView.delegate = self diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 0bf1e1041..cf0b69b9d 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -68,7 +68,7 @@ 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/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 546988be4..1681b6171 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -63,6 +63,15 @@ class MainTabBarController: UITabBarController { } } + var sidebarImage: UIImage { + switch self { + case .home: return UIImage(systemName: "house")! + case .search: return UIImage(systemName: "magnifyingglass")! + case .notification: return UIImage(systemName: "bell")! + case .me: return UIImage(systemName: "person.fill")! + } + } + func viewController(context: AppContext, coordinator: SceneCoordinator) -> UIViewController { let viewController: UIViewController switch self { diff --git a/Mastodon/Scene/Root/RootSplitViewController.swift b/Mastodon/Scene/Root/RootSplitViewController.swift index 7437b2597..441cc992e 100644 --- a/Mastodon/Scene/Root/RootSplitViewController.swift +++ b/Mastodon/Scene/Root/RootSplitViewController.swift @@ -27,9 +27,19 @@ final class RootSplitViewController: UISplitViewController, NeedsDependency { var currentSupplementaryTab: MainTabBarController.Tab = .home private(set) lazy var supplementaryViewControllers: [UIViewController] = { - return MainTabBarController.Tab.allCases.map { tab in + 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) diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 4fe34c89a..d93fcd807 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -5,6 +5,7 @@ // Created by Cirno MainasuK on 2021-9-22. // +import os.log import UIKit import Combine import CoreDataStack @@ -22,10 +23,17 @@ final class SidebarViewController: UIViewController, NeedsDependency { 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: .plain) + var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar) configuration.showsSeparators = false let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) return section @@ -46,14 +54,27 @@ extension SidebarViewController { override func viewDidLoad() { super.viewDidLoad() - navigationItem.title = "Title" + 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.title = domain + } + .store(in: &disposeBag) + navigationItem.rightBarButtonItem = settingBarButtonItem + settingBarButtonItem.target = self + settingBarButtonItem.action = #selector(SidebarViewController.settingBarButtonItemPressed(_:)) navigationController?.navigationBar.prefersLargeTitles = true - let barAppearance = UINavigationBarAppearance() - barAppearance.configureWithTransparentBackground() - navigationItem.standardAppearance = barAppearance - navigationItem.compactAppearance = barAppearance - navigationItem.scrollEdgeAppearance = barAppearance + setupBackground(theme: ThemeService.shared.currentTheme.value) + ThemeService.shared.currentTheme + .receive(on: DispatchQueue.main) + .sink { [weak self] theme in + guard let self = self else { return } + self.setupBackground(theme: theme) + } + .store(in: &disposeBag) collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) @@ -68,6 +89,28 @@ extension SidebarViewController { viewModel.setupDiffableDataSource(collectionView: collectionView) } + private func setupBackground(theme: Theme) { + let barAppearance = UINavigationBarAppearance() + barAppearance.configureWithOpaqueBackground() + barAppearance.backgroundColor = theme.sidebarBackgroundColor + barAppearance.shadowColor = .clear + barAppearance.shadowImage = UIImage() // remove separator line + navigationItem.standardAppearance = barAppearance + navigationItem.compactAppearance = barAppearance + navigationItem.scrollEdgeAppearance = barAppearance + + view.backgroundColor = theme.sidebarBackgroundColor + } + +} + +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 diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift index f65bb1d6f..9de5c1bb2 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewModel.swift @@ -9,6 +9,8 @@ import UIKit import Combine import CoreData import CoreDataStack +import Meta +import MastodonMeta final class SidebarViewModel { @@ -57,28 +59,61 @@ 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 tabCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in + let imageURL: URL? = { + switch item { + case .me: + let authentication = self.context.authenticationService.activeMastodonAuthentication.value + return authentication?.user.avatarImageURL() + default: + 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) + } + }() + cell.item = SidebarListContentView.Item( + image: item.sidebarImage, + imageURL: imageURL, + headline: headline, + subheadline: nil + ) + cell.setNeedsUpdateConfiguration() } let headerRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in - var content = cell.defaultContentConfiguration() + var content = UIListContentConfiguration.sidebarHeader() content.text = item.title cell.contentConfiguration = content cell.accessories = [.outlineDisclosure()] } - let accountRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in - var content = cell.defaultContentConfiguration() + let accountRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in 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 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) + ) + cell.setNeedsUpdateConfiguration() } let addAccountRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in @@ -92,7 +127,7 @@ extension SidebarViewModel { diffableDataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in switch item { case .tab(let tab): - return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: tab) + return collectionView.dequeueConfiguredReusableCell(using: tabCellRegistration, for: indexPath, item: tab) case .header(let viewModel): return collectionView.dequeueConfiguredReusableCell(using: headerRegistration, for: indexPath, item: viewModel) case .account(let viewModel): @@ -133,16 +168,22 @@ extension SidebarViewModel { .receive(on: DispatchQueue.main) .sink { [weak self] authentications in guard let self = self else { return } - var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() + // tab + var snapshot = self.diffableDataSource.snapshot() + snapshot.reloadItems([.tab(.me)]) + self.diffableDataSource.apply(snapshot) + + // account + var accountSectionSnapshot = NSDiffableDataSourceSectionSnapshot() let headerItem = Item.header(HeaderViewModel(title: "Accounts")) - sectionSnapshot.append([headerItem], to: nil) - let items = authentications.map { authentication in + accountSectionSnapshot.append([headerItem], to: nil) + let accountItems = 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) + accountSectionSnapshot.append(accountItems, to: headerItem) + accountSectionSnapshot.append([.addAccount], to: headerItem) + accountSectionSnapshot.expand([headerItem]) + self.diffableDataSource.apply(accountSectionSnapshot, to: .account) } .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift new file mode 100644 index 000000000..0c9a95fef --- /dev/null +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListCollectionViewCell.swift @@ -0,0 +1,48 @@ +// +// SidebarListTableViewCell.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-24. +// + +import UIKit + +final class SidebarListCollectionViewCell: UICollectionViewListCell { + + var item: SidebarListContentView.Item? + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension SidebarListCollectionViewCell { + private func _init() { + + } + + override func updateConfiguration(using state: UICellConfigurationState) { + var newConfiguration = SidebarListContentView.ContentConfiguration().updated(for: state) + newConfiguration.item = item + contentConfiguration = newConfiguration + + var newBackgroundConfiguration = UIBackgroundConfiguration.listSidebarCell().updated(for: state) + // Customize the background color to use the tint color when the cell is highlighted or selected. + if state.isSelected || state.isHighlighted { + newBackgroundConfiguration.backgroundColor = Asset.Colors.brandBlue.color + } + if state.isHighlighted { + newBackgroundConfiguration.backgroundColorTransformer = .init { $0.withAlphaComponent(0.8) } + } + + + backgroundConfiguration = newBackgroundConfiguration + } +} diff --git a/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift new file mode 100644 index 000000000..6d665eb81 --- /dev/null +++ b/Mastodon/Scene/Root/Sidebar/View/SidebarListContentView.swift @@ -0,0 +1,215 @@ +// +// SidebarListContentView.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-24. +// + +import os.log +import UIKit +import MetaTextKit +import FLAnimatedImage + +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)) + + private var currentConfiguration: ContentConfiguration! + var configuration: UIContentConfiguration { + get { + currentConfiguration + } + set { + guard let newConfiguration = newValue as? ContentConfiguration else { return } + apply(configuration: newConfiguration) + } + } + + init(configuration: ContentConfiguration) { + super.init(frame: .zero) + + _init() + apply(configuration: configuration) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +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) + 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.setContentHuggingPriority(.defaultLow - 10, for: .vertical) + imageView.setContentHuggingPriority(.defaultLow - 10, for: .horizontal) + + let textContainer = UIStackView() + textContainer.axis = .vertical + textContainer.translatesAutoresizingMaskIntoConstraints = false + addSubview(textContainer) + 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), + ]) + + 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) + + 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), + ]) + + animationImageView.isUserInteractionEnabled = false + headlineLabel.isUserInteractionEnabled = false + subheadlineLabel.isUserInteractionEnabled = false + + imageView.contentMode = .scaleAspectFit + animationImageView.contentMode = .scaleAspectFit + imageView.tintColor = Asset.Colors.brandBlue.color + animationImageView.tintColor = Asset.Colors.brandBlue.color + } + + private func apply(configuration: ContentConfiguration) { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + guard currentConfiguration != configuration else { return } + currentConfiguration = configuration + + guard let item = configuration.item else { return } + + // 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)) + + // configure model + imageView.isHidden = item.imageURL != nil + animationImageView.isHidden = item.imageURL == nil + imageView.image = item.image.withRenderingMode(.alwaysTemplate) + animationImageView.setImage( + url: item.imageURL, + placeholder: animationImageView.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 + } + } +} + +extension SidebarListContentView { + struct Item: Hashable { + // state + var isSelected: Bool = false + + // model + let image: UIImage + let imageURL: URL? + let headline: MetaContent + let subheadline: MetaContent? + + static func == (lhs: SidebarListContentView.Item, rhs: SidebarListContentView.Item) -> Bool { + return lhs.isSelected == rhs.isSelected + && 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(image) + imageURL.flatMap { hasher.combine($0) } + hasher.combine(headline.string) + subheadline.flatMap { hasher.combine($0.string) } + } + } + + struct ContentConfiguration: UIContentConfiguration, Hashable { + let logger = Logger(subsystem: "SidebarListContentView.ContentConfiguration", category: "ContentConfiguration") + + var item: Item? + + func makeContentView() -> UIView & UIContentView { + SidebarListContentView(configuration: self) + } + + func updated(for state: UIConfigurationState) -> ContentConfiguration { + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + + var updatedConfiguration = self + + if let state = state as? UICellConfigurationState { + updatedConfiguration.item?.isSelected = state.isHighlighted || state.isSelected + } else { + assertionFailure() + updatedConfiguration.item?.isSelected = false + } + + return updatedConfiguration + } + + static func == ( + lhs: ContentConfiguration, + rhs: ContentConfiguration + ) -> Bool { + return lhs.item == rhs.item + } + + func hash(into hasher: inout Hasher) { + hasher.combine(item) + } + } +} diff --git a/Mastodon/Service/ThemeService/MastodonTheme.swift b/Mastodon/Service/ThemeService/MastodonTheme.swift index dbf2324cd..85b0d42db 100644 --- a/Mastodon/Service/ThemeService/MastodonTheme.swift +++ b/Mastodon/Service/ThemeService/MastodonTheme.swift @@ -22,6 +22,8 @@ struct MastodonTheme: Theme { let tertiarySystemGroupedBackgroundColor = Asset.Theme.Mastodon.tertiarySystemGroupedBackground.color let navigationBarBackgroundColor = Asset.Theme.Mastodon.navigationBarBackground.color + + let sidebarBackgroundColor = Asset.Theme.Mastodon.sidebarBackground.color let tabBarBackgroundColor = Asset.Theme.Mastodon.tabBarBackground.color let tabBarItemSelectedIconColor = Asset.Colors.brandBlue.color diff --git a/Mastodon/Service/ThemeService/SystemTheme.swift b/Mastodon/Service/ThemeService/SystemTheme.swift index 3a8ddd2d8..2e3b290db 100644 --- a/Mastodon/Service/ThemeService/SystemTheme.swift +++ b/Mastodon/Service/ThemeService/SystemTheme.swift @@ -23,6 +23,8 @@ struct SystemTheme: Theme { let navigationBarBackgroundColor = Asset.Theme.System.navigationBarBackground.color + let sidebarBackgroundColor = Asset.Theme.Mastodon.sidebarBackground.color + let tabBarBackgroundColor = Asset.Theme.System.tabBarBackground.color let tabBarItemSelectedIconColor = Asset.Colors.brandBlue.color let tabBarItemFocusedIconColor = Asset.Theme.System.tabBarItemInactiveIconColor.color diff --git a/Mastodon/Service/ThemeService/Theme.swift b/Mastodon/Service/ThemeService/Theme.swift index 4074e0904..1a3b3c5d1 100644 --- a/Mastodon/Service/ThemeService/Theme.swift +++ b/Mastodon/Service/ThemeService/Theme.swift @@ -22,6 +22,8 @@ public protocol Theme { var tertiarySystemGroupedBackgroundColor: UIColor { get } var navigationBarBackgroundColor: UIColor { get } + + var sidebarBackgroundColor: UIColor { get } var tabBarBackgroundColor: UIColor { get } var tabBarItemSelectedIconColor: UIColor { get }