feat: update sidebar UI
This commit is contained in:
parent
98bec294f6
commit
d8de3c4f65
|
@ -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 = "<group>"; };
|
||||
DB0C947626A7FE840088FB11 /* NotificationAvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAvatarButton.swift; sourceTree = "<group>"; };
|
||||
DB0E91E926A9675100BD2ACC /* MetaLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaLabel.swift; sourceTree = "<group>"; };
|
||||
DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListContentView.swift; sourceTree = "<group>"; };
|
||||
DB0F814D264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
|
@ -2082,6 +2086,15 @@
|
|||
path = Button;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB0EF72C26FDB1D600347686 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */,
|
||||
DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
<key>AppShared.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>59</integer>
|
||||
<integer>36</integer>
|
||||
</dict>
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>62</integer>
|
||||
<integer>35</integer>
|
||||
</dict>
|
||||
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -97,7 +97,7 @@
|
|||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>60</integer>
|
||||
<integer>37</integer>
|
||||
</dict>
|
||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -117,7 +117,7 @@
|
|||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>61</integer>
|
||||
<integer>38</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,6 +34,10 @@ extension MetaLabel {
|
|||
textContainer.lineBreakMode = .byTruncatingTail
|
||||
textContainer.lineFragmentPadding = 0
|
||||
|
||||
setup(style: style)
|
||||
}
|
||||
|
||||
func setup(style: Style) {
|
||||
let font: UIFont
|
||||
let textColor: UIColor
|
||||
|
||||
|
@ -82,6 +88,12 @@ 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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDuration</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ final class HomeTimelineViewModel: NSObject {
|
|||
let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine
|
||||
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
|
||||
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||
|
||||
let displaySettingBarButtonItem = CurrentValueSubject<Bool, Never>(true)
|
||||
|
||||
init(context: AppContext) {
|
||||
self.context = context
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
// Created by Cirno MainasuK on 2021-9-22.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
|
@ -23,9 +24,16 @@ final class SidebarViewController: UIViewController, NeedsDependency {
|
|||
|
||||
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
|
||||
|
|
|
@ -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<UICollectionViewListCell, MainTabBarController.Tab> { (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<SidebarListCollectionViewCell, MainTabBarController.Tab> { (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<UICollectionViewListCell, HeaderViewModel> { (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<UICollectionViewListCell, AccountViewModel> { (cell, indexPath, item) in
|
||||
var content = cell.defaultContentConfiguration()
|
||||
let accountRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, AccountViewModel> { (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<UICollectionViewListCell, AddAccountViewModel> { (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<Item>()
|
||||
// tab
|
||||
var snapshot = self.diffableDataSource.snapshot()
|
||||
snapshot.reloadItems([.tab(.me)])
|
||||
self.diffableDataSource.apply(snapshot)
|
||||
|
||||
// account
|
||||
var accountSectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ struct MastodonTheme: Theme {
|
|||
|
||||
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
|
||||
let tabBarItemFocusedIconColor = Asset.Theme.Mastodon.tabBarItemInactiveIconColor.color
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -23,6 +23,8 @@ public protocol Theme {
|
|||
|
||||
var navigationBarBackgroundColor: UIColor { get }
|
||||
|
||||
var sidebarBackgroundColor: UIColor { get }
|
||||
|
||||
var tabBarBackgroundColor: UIColor { get }
|
||||
var tabBarItemSelectedIconColor: UIColor { get }
|
||||
var tabBarItemFocusedIconColor: UIColor { get }
|
||||
|
|
Loading…
Reference in New Issue