feat: update sidebar UI

This commit is contained in:
CMK 2021-09-24 19:58:50 +08:00
parent 98bec294f6
commit d8de3c4f65
21 changed files with 557 additions and 56 deletions

View File

@ -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 */,

View File

@ -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>

View File

@ -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"
}
}
]
},

View File

@ -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 {

View File

@ -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

View File

@ -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")

View File

@ -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>

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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 }