feat: update for new iPad UI

This commit is contained in:
CMK 2021-10-28 19:17:41 +08:00
parent b2e8eb18a0
commit 19db0afa3e
34 changed files with 705 additions and 1083 deletions

View File

@ -187,6 +187,8 @@
DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; };
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; };
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; };
DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */; };
DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */; };
DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */; };
DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F42689B782007B274C /* ComposeTableView.swift */; };
DB040ED126538E3D00BEE9D8 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ED026538E3C00BEE9D8 /* Trie.swift */; };
@ -958,6 +960,8 @@
DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = "<group>"; };
DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = "<group>"; };
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListHeaderView.swift; sourceTree = "<group>"; };
DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSplitViewController.swift; sourceTree = "<group>"; };
DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentTableViewCell.swift; sourceTree = "<group>"; };
DB03F7F42689B782007B274C /* ComposeTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTableView.swift; sourceTree = "<group>"; };
DB040ED026538E3C00BEE9D8 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
@ -2125,6 +2129,7 @@
DBF156DE2701B17600EC00B7 /* SidebarAddAccountCollectionViewCell.swift */,
DB0EF72A26FDB1D200347686 /* SidebarListCollectionViewCell.swift */,
DB0EF72D26FDB24F00347686 /* SidebarListContentView.swift */,
DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */,
);
path = View;
sourceTree = "<group>";
@ -2587,6 +2592,7 @@
isa = PBXGroup;
children = (
DB852D1B26FB021500FC9D81 /* RootSplitViewController.swift */,
DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */,
DB852D1A26FAED0100FC9D81 /* Sidebar */,
DB8AF54E25C13703002E6C99 /* MainTab */,
);
@ -3873,6 +3879,7 @@
DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */,
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */,
DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */,
DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */,
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */,
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
@ -3935,6 +3942,7 @@
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
DB297B1B2679FAE200704C90 /* PlaceholderImageCacheService.swift in Sources */,
2D8FCA082637EABB00137F46 /* APIService+FollowRequest.swift in Sources */,
DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */,
2D152A8C25C295CC009AA50C /* StatusView.swift in Sources */,
DBBC24DE26A54BCB00398BB9 /* MastodonMetricFormatter.swift in Sources */,
DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */,

View File

@ -7,12 +7,12 @@
<key>AppShared.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>42</integer>
<integer>35</integer>
</dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>43</integer>
<integer>38</integer>
</dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict>
@ -97,7 +97,7 @@
<key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>41</integer>
<integer>36</integer>
</dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict>
@ -117,7 +117,7 @@
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>44</integer>
<integer>37</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -90,45 +90,45 @@ final public class SceneCoordinator {
// Delay in next run loop
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// Note:
// show (push) on phone or pad (compact)
// showDetail in .secondary in UISplitViewController on pad (expand)
let from: UIViewController? = {
if let splitViewController = self.splitViewController {
if splitViewController.mainTabBarController.topMost?.view.window != nil {
// compact
return splitViewController.mainTabBarController.topMost
} else {
// expand
return splitViewController.viewController(for: .supplementary)
}
} else {
return self.tabBarController.topMost
}
}()
// show notification related content
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return }
let notificationID = String(pushNotification.notificationID)
switch type {
case .follow:
let profileViewModel = RemoteProfileViewModel(context: appContext, notificationID: notificationID)
self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
case .followRequest:
// do nothing
break
case .mention, .reblog, .favourite, .poll, .status:
let threadViewModel = RemoteThreadViewModel(context: appContext, notificationID: notificationID)
self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
case ._other:
assertionFailure()
break
}
}
// DispatchQueue.main.async { [weak self] in
// guard let self = self else { return }
//
// // Note:
// // show (push) on phone or pad (compact)
// // showDetail in .secondary in UISplitViewController on pad (expand)
// let from: UIViewController? = {
// if let splitViewController = self.splitViewController {
// if splitViewController.mainTabBarController.topMost?.view.window != nil {
// // compact
// return splitViewController.mainTabBarController.topMost
// } else {
// // expand
// return splitViewController.viewController(for: .supplementary)
// }
// } else {
// return self.tabBarController.topMost
// }
// }()
//
// // show notification related content
// guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return }
// let notificationID = String(pushNotification.notificationID)
//
// switch type {
// case .follow:
// let profileViewModel = RemoteProfileViewModel(context: appContext, notificationID: notificationID)
// self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
// case .followRequest:
// // do nothing
// break
// case .mention, .reblog, .favourite, .poll, .status:
// let threadViewModel = RemoteThreadViewModel(context: appContext, notificationID: notificationID)
// self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
// case ._other:
// assertionFailure()
// break
// }
// } // end DispatchQueue.main.async
}
.store(in: &disposeBag)
}
@ -227,7 +227,7 @@ extension SceneCoordinator {
default:
let splitViewController = RootSplitViewController(context: appContext, coordinator: self)
self.splitViewController = splitViewController
self.tabBarController = splitViewController.mainTabBarController
self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
sceneDelegate.window?.rootViewController = splitViewController
}
}
@ -282,18 +282,19 @@ extension SceneCoordinator {
switch transition {
case .show:
if let splitViewController = splitViewController, !splitViewController.isCollapsed,
let supplementaryViewController = splitViewController.viewController(for: .supplementary) as? UINavigationController,
(supplementaryViewController === presentingViewController || supplementaryViewController.viewControllers.contains(presentingViewController)) ||
(presentingViewController is UserTimelineViewController && presentingViewController.view.isDescendant(of: supplementaryViewController.view))
{
fallthrough
} else {
if secondaryStackHashValues.contains(presentingViewController.hashValue) {
secondaryStackHashValues.insert(viewController.hashValue)
}
presentingViewController.show(viewController, sender: sender)
}
// if let splitViewController = splitViewController, !splitViewController.isCollapsed,
// let supplementaryViewController = splitViewController.viewController(for: .supplementary) as? UINavigationController,
// (supplementaryViewController === presentingViewController || supplementaryViewController.viewControllers.contains(presentingViewController)) ||
// (presentingViewController is UserTimelineViewController && presentingViewController.view.isDescendant(of: supplementaryViewController.view))
// {
// fallthrough
// } else {
// if secondaryStackHashValues.contains(presentingViewController.hashValue) {
// secondaryStackHashValues.insert(viewController.hashValue)
// }
// presentingViewController.show(viewController, sender: sender)
// }
presentingViewController.show(viewController, sender: sender)
case .showDetail:
secondaryStackHashValues.insert(viewController.hashValue)
let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController)

View File

@ -96,6 +96,9 @@ internal enum Asset {
internal static let usernameGray = ColorAsset(name: "Scene/Profile/Banner/username.gray")
}
}
internal enum Sidebar {
internal static let logo = ImageAsset(name: "Scene/Sidebar/logo")
}
internal enum Welcome {
internal enum Illustration {
internal static let backgroundCyan = ColorAsset(name: "Scene/Welcome/illustration/background.cyan")
@ -160,23 +163,6 @@ internal enum Asset {
internal static let tabBarItemInactiveIconColor = ColorAsset(name: "Theme/system/tab.bar.item.inactive.icon.color")
}
}
internal enum Deprecated {
internal enum Background {
internal static let danger = ColorAsset(name: "_Deprecated/Background/danger")
internal static let onboardingBackground = ColorAsset(name: "_Deprecated/Background/onboarding.background")
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "_Deprecated/Background/secondary.grouped.system.background")
internal static let secondarySystemBackground = ColorAsset(name: "_Deprecated/Background/secondary.system.background")
internal static let systemBackground = ColorAsset(name: "_Deprecated/Background/system.background")
internal static let systemElevatedBackground = ColorAsset(name: "_Deprecated/Background/system.elevated.background")
internal static let systemGroupedBackground = ColorAsset(name: "_Deprecated/Background/system.grouped.background")
internal static let tertiarySystemBackground = ColorAsset(name: "_Deprecated/Background/tertiary.system.background")
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "_Deprecated/Background/tertiary.system.grouped.background")
}
internal enum Compose {
internal static let background = ColorAsset(name: "_Deprecated/Compose/background")
internal static let toolbarBackground = ColorAsset(name: "_Deprecated/Compose/toolbar.background")
}
}
}
// swiftlint:enable identifier_name line_length nesting type_body_length type_name

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "logo.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}

View File

@ -0,0 +1,108 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.103455 cm
0.168627 0.564706 0.850980 scn
27.796436 10.091343 m
33.035133 10.719734 37.596470 13.962151 38.169762 16.924883 c
39.073063 21.591980 38.998501 28.314186 38.998501 28.314186 c
38.998501 37.425270 33.056084 40.095867 33.056084 40.095867 c
30.059872 41.478233 24.914881 42.059555 19.569633 42.103455 c
19.438305 42.103455 l
14.093056 42.059555 8.951445 41.478233 5.955006 40.095867 c
5.955006 40.095867 0.012361 37.425270 0.012361 28.314186 c
0.012361 27.761837 0.009520 27.180878 0.006561 26.576080 c
-0.001656 24.896429 -0.010772 23.032921 0.037591 21.087820 c
0.253392 12.177679 1.663759 3.396290 9.864657 1.215820 c
13.645910 0.210445 16.892391 0.000000 19.507011 0.144371 c
24.248556 0.408443 26.910255 1.844212 26.910255 1.844212 c
26.753922 5.300014 l
26.753922 5.300014 23.365528 4.226753 19.560173 4.357544 c
15.789957 4.487431 11.809797 4.765984 11.200012 9.415886 c
11.143697 9.824329 11.115539 10.261055 11.115539 10.719732 c
11.115539 10.719732 14.816599 9.810978 19.507011 9.595104 c
22.375050 9.462955 25.064680 9.763912 27.796436 10.091343 c
h
31.989010 16.575367 m
31.989010 27.607372 l
31.989010 29.862061 31.417519 31.653776 30.269808 32.979347 c
29.085829 34.304916 27.535576 34.984444 25.611385 34.984444 c
23.384670 34.984444 21.698582 34.124794 20.583984 32.405266 c
19.500023 30.580288 l
18.416286 32.405266 l
17.301464 34.124794 15.615376 34.984444 13.388884 34.984444 c
11.464469 34.984444 9.914215 34.304916 8.730462 32.979347 c
7.582527 31.653776 7.011036 29.862061 7.011036 27.607372 c
7.011036 16.575367 l
11.361976 16.575367 l
11.361976 27.283108 l
11.361976 29.540287 12.307401 30.685961 14.198477 30.685961 c
16.289360 30.685961 17.337505 29.326900 17.337505 26.639557 c
17.337505 20.778585 l
21.662764 20.778585 l
21.662764 26.639557 l
21.662764 29.326900 22.710684 30.685961 24.801567 30.685961 c
26.692642 30.685961 27.638069 29.540287 27.638069 27.283108 c
27.638069 16.575367 l
31.989010 16.575367 l
h
f*
n
Q
endstream
endobj
3 0 obj
2035
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 39.000000 42.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Type /Catalog
/Pages 5 0 R
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002125 00000 n
0000002148 00000 n
0000002321 00000 n
0000002395 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2454
%%EOF

View File

@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "46",
"green" : "44",
"red" : "44"
"blue" : "0x2E",
"green" : "0x2C",
"red" : "0x2C"
}
},
"idiom" : "universal"

View File

@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.263",
"green" : "0.208",
"red" : "0.192"
"blue" : "0x2E",
"green" : "0x2C",
"red" : "0x2C"
}
},
"idiom" : "universal"

View File

@ -1,20 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.353",
"green" : "0.251",
"red" : "0.875"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,20 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.910",
"green" : "0.882",
"red" : "0.851"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.996",
"green" : "1.000",
"red" : "0.996"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.263",
"green" : "0.208",
"red" : "0.192"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.910",
"green" : "0.882",
"red" : "0.851"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.133",
"green" : "0.106",
"red" : "0.098"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.996",
"green" : "1.000",
"red" : "0.996"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.216",
"green" : "0.173",
"red" : "0.157"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.216",
"green" : "0.173",
"red" : "0.157"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.910",
"green" : "0.882",
"red" : "0.851"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.133",
"green" : "0.106",
"red" : "0.098"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.996",
"green" : "1.000",
"red" : "0.996"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.216",
"green" : "0.173",
"red" : "0.157"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.910",
"green" : "0.882",
"red" : "0.851"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.263",
"green" : "0.208",
"red" : "0.192"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,9 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.216",
"green" : "0.173",
"red" : "0.157"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.871",
"green" : "0.847",
"red" : "0.839"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "0.920",
"blue" : "0.125",
"green" : "0.125",
"red" : "0.125"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,9 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -114,6 +114,24 @@ extension HomeTimelineViewController {
#endif
}
.store(in: &disposeBag)
#if DEBUG
// long press to trigger debug menu
settingBarButtonItem.menu = debugMenu
#else
settingBarButtonItem.target = self
settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:))
#endif
viewModel.displayComposeBarButtonItem
.receive(on: DispatchQueue.main)
.sink { [weak self] displayComposeBarButtonItem in
guard let self = self else { return }
self.navigationItem.rightBarButtonItem = displayComposeBarButtonItem ? self.composeBarButtonItem : nil
}
.store(in: &disposeBag)
composeBarButtonItem.target = self
composeBarButtonItem.action = #selector(HomeTimelineViewController.composeBarButtonItemPressed(_:))
navigationItem.titleView = titleView
titleView.delegate = self
@ -126,18 +144,6 @@ extension HomeTimelineViewController {
}
.store(in: &disposeBag)
#if DEBUG
// long press to trigger debug menu
settingBarButtonItem.menu = debugMenu
#else
settingBarButtonItem.target = self
settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:))
#endif
navigationItem.rightBarButtonItem = composeBarButtonItem
composeBarButtonItem.target = self
composeBarButtonItem.action = #selector(HomeTimelineViewController.composeBarButtonItemPressed(_:))
tableView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(HomeTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)

View File

@ -30,6 +30,8 @@ final class HomeTimelineViewModel: NSObject {
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
let lastAutomaticFetchTimestamp = CurrentValueSubject<Date?, Never>(nil)
let scrollPositionRecord = CurrentValueSubject<ScrollPositionRecord?, Never>(nil)
let displaySettingBarButtonItem = CurrentValueSubject<Bool, Never>(true)
let displayComposeBarButtonItem = CurrentValueSubject<Bool, Never>(true)
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
weak var tableView: UITableView?
@ -70,7 +72,6 @@ final class HomeTimelineViewModel: NSObject {
let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
var cellFrameCache = NSCache<NSNumber, NSValue>()
let displaySettingBarButtonItem = CurrentValueSubject<Bool, Never>(true)
init(context: AppContext) {
self.context = context

View File

@ -0,0 +1,92 @@
//
// ContentSplitViewController.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-10-28.
//
import os.log
import UIKit
import Combine
import CoreDataStack
final class ContentSplitViewController: UIViewController, NeedsDependency {
var disposeBag = Set<AnyCancellable>()
static let sidebarWidth: CGFloat = 89
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
private(set) lazy var sidebarViewController: SidebarViewController = {
let sidebarViewController = SidebarViewController()
sidebarViewController.context = context
sidebarViewController.coordinator = coordinator
sidebarViewController.viewModel = SidebarViewModel(context: context)
sidebarViewController.delegate = self
return sidebarViewController
}()
@Published var currentSupplementaryTab: MainTabBarController.Tab = .home
private(set) lazy var mainTabBarController: MainTabBarController = {
let mainTabBarController = MainTabBarController(context: context, coordinator: coordinator)
return mainTabBarController
}()
deinit {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
}
}
extension ContentSplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.setNavigationBarHidden(true, animated: false)
addChild(sidebarViewController)
sidebarViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(sidebarViewController.view)
sidebarViewController.didMove(toParent: self)
NSLayoutConstraint.activate([
sidebarViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
sidebarViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
sidebarViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
sidebarViewController.view.widthAnchor.constraint(equalToConstant: ContentSplitViewController.sidebarWidth),
])
addChild(mainTabBarController)
mainTabBarController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(mainTabBarController.view)
sidebarViewController.didMove(toParent: self)
NSLayoutConstraint.activate([
mainTabBarController.view.topAnchor.constraint(equalTo: view.topAnchor),
mainTabBarController.view.leadingAnchor.constraint(equalTo: sidebarViewController.view.trailingAnchor),
mainTabBarController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
mainTabBarController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
$currentSupplementaryTab
.removeDuplicates()
.sink(receiveValue: { [weak self] tab in
guard let self = self else { return }
self.mainTabBarController.selectedIndex = tab.rawValue
self.mainTabBarController.currentTab.value = tab
})
.store(in: &disposeBag)
}
}
// MARK: - SidebarViewControllerDelegate
extension ContentSplitViewController: SidebarViewControllerDelegate {
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) {
guard let _ = MainTabBarController.Tab.allCases.firstIndex(of: tab) else {
assertionFailure()
return
}
currentSupplementaryTab = tab
}
}

View File

@ -70,6 +70,9 @@ extension MainTabBarController.Wizard {
func setup(in view: UIView) {
assert(delegate != nil, "need set delegate before use")
guard !items.isEmpty else { return }
backgroundView.frame = view.bounds
backgroundView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(backgroundView)

View File

@ -14,57 +14,44 @@ final class RootSplitViewController: UISplitViewController, NeedsDependency {
var disposeBag = Set<AnyCancellable>()
static let sidebarWidth: CGFloat = 89
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
private(set) lazy var sidebarViewController: SidebarViewController = {
let sidebarViewController = SidebarViewController()
sidebarViewController.context = context
sidebarViewController.coordinator = coordinator
sidebarViewController.viewModel = SidebarViewModel(context: context)
sidebarViewController.delegate = self
return sidebarViewController
private(set) lazy var contentSplitViewController: ContentSplitViewController = {
let contentSplitViewController = ContentSplitViewController()
contentSplitViewController.context = context
contentSplitViewController.coordinator = coordinator
return contentSplitViewController
}()
var currentSupplementaryTab: MainTabBarController.Tab = .home
private(set) lazy var supplementaryViewControllers: [UIViewController] = {
let viewControllers = MainTabBarController.Tab.allCases.map { tab in
tab.viewController(context: context, coordinator: coordinator)
}
for viewController in viewControllers {
guard let navigationController = viewController as? UINavigationController else {
assertionFailure()
continue
}
if let homeViewController = navigationController.topViewController as? HomeTimelineViewController {
homeViewController.viewModel.displaySettingBarButtonItem.value = false
}
}
return viewControllers
}()
private(set) lazy var mainTabBarController = MainTabBarController(context: context, coordinator: coordinator)
init(context: AppContext, coordinator: SceneCoordinator) {
self.context = context
self.coordinator = coordinator
super.init(style: .tripleColumn)
super.init(style: .doubleColumn)
primaryEdge = .trailing
primaryBackgroundStyle = .sidebar
preferredDisplayMode = .oneBesideSecondary
preferredDisplayMode = .twoBesideSecondary
preferredSplitBehavior = .tile
delegate = self
// disable edge swipe gesture
presentsWithGesture = false
if #available(iOS 14.5, *) {
displayModeButtonVisibility = .always
displayModeButtonVisibility = .never
} else {
// Fallback on earlier versions
}
setViewController(sidebarViewController, for: .primary)
setViewController(supplementaryViewControllers[0], for: .supplementary)
setViewController(SecondaryPlaceholderViewController(), for: .secondary)
setViewController(mainTabBarController, for: .compact)
setViewController(UIViewController(), for: .primary)
setViewController(contentSplitViewController, for: .secondary)
contentSplitViewController.sidebarViewController.view.layer.zPosition = 100
contentSplitViewController.mainTabBarController.view.layer.zPosition = 90
view.layer.zPosition = 80
}
required init?(coder: NSCoder) {
@ -83,16 +70,11 @@ extension RootSplitViewController {
super.viewDidLoad()
updateBehavior(size: view.frame.size)
mainTabBarController.currentTab
contentSplitViewController.$currentSupplementaryTab
.receive(on: DispatchQueue.main)
.sink { [weak self] tab in
.sink { [weak self] _ in
guard let self = self else { return }
guard tab != self.currentSupplementaryTab else { return }
guard let index = MainTabBarController.Tab.allCases.firstIndex(of: tab) else { return }
self.currentSupplementaryTab = tab
self.setViewController(self.supplementaryViewControllers[index], for: .supplementary)
self.updateBehavior(size: self.view.frame.size)
}
.store(in: &disposeBag)
}
@ -100,133 +82,111 @@ extension RootSplitViewController {
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
updateBehavior(size: size)
coordinator.animate { [weak self] context in
guard let self = self else { return }
self.updateBehavior(size: size)
} completion: { context in
// do nothing
}
}
private func updateBehavior(size: CGSize) {
// fix secondary too small on iPad mini issue
if size.width > 960 {
preferredDisplayMode = .oneBesideSecondary
preferredSplitBehavior = .tile
} else {
preferredDisplayMode = .oneBesideSecondary
preferredSplitBehavior = .displace
}
}
}
// MARK: - SidebarViewControllerDelegate
extension RootSplitViewController: SidebarViewControllerDelegate {
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) {
guard let index = MainTabBarController.Tab.allCases.firstIndex(of: tab) else {
assertionFailure()
return
}
currentSupplementaryTab = tab
setViewController(supplementaryViewControllers[index], for: .supplementary)
}
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectSearchHistory searchHistoryViewModel: SidebarViewModel.SearchHistoryViewModel) {
// self.sidebarViewController(sidebarViewController, didSelectTab: .search)
let supplementaryViewController = viewController(for: .supplementary)
let managedObjectContext = context.managedObjectContext
managedObjectContext.perform {
let searchHistory = managedObjectContext.object(with: searchHistoryViewModel.searchHistoryObjectID) as! SearchHistory
if let account = searchHistory.account {
DispatchQueue.main.async {
let profileViewModel = CachedProfileViewModel(context: self.context, mastodonUser: account)
self.coordinator.present(scene: .profile(viewModel: profileViewModel), from: supplementaryViewController, transition: .show)
}
} else if let hashtag = searchHistory.hashtag {
DispatchQueue.main.async {
let hashtagTimelineViewModel = HashtagTimelineViewModel(context: self.context, hashtag: hashtag.name)
self.coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: supplementaryViewController, transition: .show)
}
switch contentSplitViewController.currentSupplementaryTab {
case .search:
hide(.primary)
default:
if size.width > 960 {
show(.primary)
} else {
assertionFailure()
hide(.primary)
}
}
}
}
// MARK: - UISplitViewControllerDelegate
extension RootSplitViewController: UISplitViewControllerDelegate {
// .regular to .compact
// move navigation stack from .supplementary & .secondary to .compact
func splitViewController(
_ svc: UISplitViewController,
topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column
) -> UISplitViewController.Column {
switch proposedTopColumn {
case .compact:
guard let index = MainTabBarController.Tab.allCases.firstIndex(of: currentSupplementaryTab) else {
assertionFailure()
break
}
mainTabBarController.selectedIndex = index
mainTabBarController.currentTab.value = currentSupplementaryTab
guard let navigationController = mainTabBarController.selectedViewController as? UINavigationController else { break }
navigationController.popToRootViewController(animated: false)
var viewControllers = navigationController.viewControllers // init navigation stack with topMost
if let supplementaryNavigationController = viewController(for: .supplementary) as? UINavigationController {
// append supplementary
viewControllers.append(contentsOf: supplementaryNavigationController.popToRootViewController(animated: true) ?? [])
}
if let secondaryNavigationController = viewController(for: .secondary) as? UINavigationController {
// append secondary
viewControllers.append(contentsOf: secondaryNavigationController.popToRootViewController(animated: true) ?? [])
}
// set navigation stack
navigationController.setViewControllers(viewControllers, animated: false)
default:
assertionFailure()
}
return proposedTopColumn
}
// .compact to .regular
// restore navigation stack to .supplementary & .secondary
func splitViewController(
_ svc: UISplitViewController,
displayModeForExpandingToProposedDisplayMode proposedDisplayMode: UISplitViewController.DisplayMode
) -> UISplitViewController.DisplayMode {
let compactNavigationController = mainTabBarController.selectedViewController as? UINavigationController
if let topMost = compactNavigationController?.topMost,
topMost is AccountListViewController {
topMost.dismiss(animated: false, completion: nil)
}
let viewControllers = compactNavigationController?.popToRootViewController(animated: true) ?? []
var supplementaryViewControllers: [UIViewController] = []
var secondaryViewControllers: [UIViewController] = []
for viewController in viewControllers {
if coordinator.secondaryStackHashValues.contains(viewController.hashValue) {
secondaryViewControllers.append(viewController)
} else {
supplementaryViewControllers.append(viewController)
}
}
if let supplementary = viewController(for: .supplementary) as? UINavigationController {
supplementary.setViewControllers(supplementary.viewControllers + supplementaryViewControllers, animated: false)
}
if let secondaryNavigationController = viewController(for: .secondary) as? UINavigationController {
secondaryNavigationController.setViewControllers(secondaryNavigationController.viewControllers + secondaryViewControllers, animated: false)
}
return proposedDisplayMode
}
// // .regular to .compact
// // move navigation stack from .supplementary & .secondary to .compact
// func splitViewController(
// _ svc: UISplitViewController,
// topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column
// ) -> UISplitViewController.Column {
// switch proposedTopColumn {
// case .compact:
// guard let index = MainTabBarController.Tab.allCases.firstIndex(of: currentSupplementaryTab) else {
// assertionFailure()
// break
// }
// mainTabBarController.selectedIndex = index
// mainTabBarController.currentTab.value = currentSupplementaryTab
//
// guard let navigationController = mainTabBarController.selectedViewController as? UINavigationController else { break }
// navigationController.popToRootViewController(animated: false)
// var viewControllers = navigationController.viewControllers // init navigation stack with topMost
//
// if let supplementaryNavigationController = viewController(for: .supplementary) as? UINavigationController {
// // append supplementary
// viewControllers.append(contentsOf: supplementaryNavigationController.popToRootViewController(animated: true) ?? [])
// }
// if let secondaryNavigationController = viewController(for: .secondary) as? UINavigationController {
// // append secondary
// viewControllers.append(contentsOf: secondaryNavigationController.popToRootViewController(animated: true) ?? [])
// }
// // set navigation stack
// navigationController.setViewControllers(viewControllers, animated: false)
//
// default:
// assertionFailure()
// }
//
// return proposedTopColumn
// }
//
// // .compact to .regular
// // restore navigation stack to .supplementary & .secondary
// func splitViewController(
// _ svc: UISplitViewController,
// displayModeForExpandingToProposedDisplayMode proposedDisplayMode: UISplitViewController.DisplayMode
// ) -> UISplitViewController.DisplayMode {
// let compactNavigationController = mainTabBarController.selectedViewController as? UINavigationController
//
// if let topMost = compactNavigationController?.topMost,
// topMost is AccountListViewController {
// topMost.dismiss(animated: false, completion: nil)
// }
//
// let viewControllers = compactNavigationController?.popToRootViewController(animated: true) ?? []
//
// var supplementaryViewControllers: [UIViewController] = []
// var secondaryViewControllers: [UIViewController] = []
// for viewController in viewControllers {
// if coordinator.secondaryStackHashValues.contains(viewController.hashValue) {
// secondaryViewControllers.append(viewController)
// } else {
// supplementaryViewControllers.append(viewController)
// }
//
// }
// if let supplementary = viewController(for: .supplementary) as? UINavigationController {
// supplementary.setViewControllers(supplementary.viewControllers + supplementaryViewControllers, animated: false)
// }
// if let secondaryNavigationController = viewController(for: .secondary) as? UINavigationController {
// secondaryNavigationController.setViewControllers(secondaryNavigationController.viewControllers + secondaryViewControllers, animated: false)
// }
//
// return proposedDisplayMode
// }
}
//extension UIView {
// func setNeedsLayoutForSubviews() {
// self.subviews.forEach({
// $0.setNeedsLayout()
// $0.setNeedsLayoutForSubviews()
// })
// }
//}

View File

@ -12,7 +12,6 @@ import CoreDataStack
protocol SidebarViewControllerDelegate: AnyObject {
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab)
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectSearchHistory searchHistoryViewModel: SidebarViewModel.SearchHistoryViewModel)
}
final class SidebarViewController: UIViewController, NeedsDependency {
@ -21,28 +20,46 @@ final class SidebarViewController: UIViewController, NeedsDependency {
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>()
var observations = Set<NSKeyValueObservation>()
var viewModel: SidebarViewModel!
weak var delegate: SidebarViewControllerDelegate?
let settingBarButtonItem: UIBarButtonItem = {
let barButtonItem = UIBarButtonItem()
barButtonItem.tintColor = Asset.Colors.brandBlue.color
barButtonItem.image = UIImage(systemName: "gear")?.withRenderingMode(.alwaysTemplate)
return barButtonItem
}()
static func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar)
configuration.backgroundColor = .clear
if sectionIndex == SidebarViewModel.Section.tab.rawValue {
// with indentation
configuration.headerMode = .none
} else {
// remove indentation
configuration.headerMode = .firstItemInSection
configuration.showsSeparators = false
let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
switch sectionIndex {
case 0:
let header = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(100)),
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top
)
section.boundarySupplementaryItems = [header]
default:
break
}
return section
}
return layout
}
let collectionView: UICollectionView = {
let layout = SidebarViewController.createLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.alwaysBounceVertical = false
collectionView.backgroundColor = .clear
return collectionView
}()
static func createSecondaryLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar)
configuration.backgroundColor = .clear
configuration.showsSeparators = false
let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
return section
@ -50,12 +67,15 @@ final class SidebarViewController: UIViewController, NeedsDependency {
return layout
}
let collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: SidebarViewController.createLayout())
let secondaryCollectionView: UICollectionView = {
let layout = SidebarViewController.createSecondaryLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.isScrollEnabled = false
collectionView.alwaysBounceVertical = false
collectionView.backgroundColor = .clear
return collectionView
}()
var secondaryCollectionViewHeightLayoutConstraint: NSLayoutConstraint!
}
extension SidebarViewController {
@ -63,23 +83,7 @@ extension SidebarViewController {
override func viewDidLoad() {
super.viewDidLoad()
viewModel.context.authenticationService.activeMastodonAuthenticationBox
.receive(on: DispatchQueue.main)
.sink { [weak self] activeMastodonAuthenticationBox in
guard let self = self else { return }
let domain = activeMastodonAuthenticationBox?.domain
self.navigationItem.backBarButtonItem = {
let barButtonItem = UIBarButtonItem()
barButtonItem.image = UIImage(systemName: "sidebar.leading")
return barButtonItem
}()
self.navigationItem.title = domain
}
.store(in: &disposeBag)
navigationItem.rightBarButtonItem = settingBarButtonItem
settingBarButtonItem.target = self
settingBarButtonItem.action = #selector(SidebarViewController.settingBarButtonItemPressed(_:))
navigationController?.navigationBar.prefersLargeTitles = true
navigationController?.setNavigationBarHidden(true, animated: false)
setupBackground(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme
@ -99,65 +103,101 @@ extension SidebarViewController {
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
secondaryCollectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(secondaryCollectionView)
secondaryCollectionViewHeightLayoutConstraint = secondaryCollectionView.heightAnchor.constraint(equalToConstant: 44).priority(.required - 1)
NSLayoutConstraint.activate([
secondaryCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
secondaryCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
view.bottomAnchor.constraint(equalTo: secondaryCollectionView.bottomAnchor),
secondaryCollectionViewHeightLayoutConstraint,
])
collectionView.delegate = self
viewModel.setupDiffableDataSource(collectionView: collectionView)
secondaryCollectionView.delegate = self
viewModel.setupDiffableDataSource(
collectionView: collectionView,
secondaryCollectionView: secondaryCollectionView
)
secondaryCollectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] secondaryCollectionView, _ in
guard let self = self else { return }
let height = secondaryCollectionView.contentSize.height
self.secondaryCollectionViewHeightLayoutConstraint.constant = height
self.collectionView.contentInset.bottom = height
}
.store(in: &observations)
}
private func setupBackground(theme: Theme) {
let color: UIColor = theme.sidebarBackgroundColor
let barAppearance = UINavigationBarAppearance()
barAppearance.configureWithOpaqueBackground()
barAppearance.backgroundColor = color
barAppearance.shadowColor = .clear
barAppearance.shadowImage = UIImage() // remove separator line
navigationItem.standardAppearance = barAppearance
navigationItem.compactAppearance = barAppearance
navigationItem.scrollEdgeAppearance = barAppearance
if #available(iOS 15.0, *) {
navigationItem.compactScrollEdgeAppearance = barAppearance
} else {
// Fallback on earlier versions
}
view.backgroundColor = color
collectionView.backgroundColor = color
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { context in
self.collectionView.collectionViewLayout.invalidateLayout()
// // do nothing
} completion: { [weak self] context in
// guard let self = self else { return }
}
}
}
extension SidebarViewController {
@objc private func settingBarButtonItemPressed(_ sender: UIBarButtonItem) {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
guard let setting = context.settingService.currentSetting.value else { return }
let settingsViewModel = SettingsViewModel(context: context, setting: setting)
coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
}
}
// MARK: - UICollectionViewDelegate
extension SidebarViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .tab(let tab):
delegate?.sidebarViewController(self, didSelectTab: tab)
case .searchHistory(let viewModel):
delegate?.sidebarViewController(self, didSelectSearchHistory: viewModel)
case .header:
break
case .account(let viewModel):
assert(Thread.isMainThread)
let authentication = context.managedObjectContext.object(with: viewModel.authenticationObjectID) as! MastodonAuthentication
context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID)
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
guard let self = self else { return }
self.coordinator.setup()
}
.store(in: &disposeBag)
case .addAccount:
coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil))
switch collectionView {
case self.collectionView:
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .tab(let tab):
delegate?.sidebarViewController(self, didSelectTab: tab)
case .setting:
guard let setting = context.settingService.currentSetting.value else { return }
let settingsViewModel = SettingsViewModel(context: context, setting: setting)
coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
case .compose:
assertionFailure()
}
case secondaryCollectionView:
guard let diffableDataSource = viewModel.secondaryDiffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .compose:
let composeViewModel = ComposeViewModel(context: context, composeKind: .post)
coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil))
default:
assertionFailure()
}
default:
assertionFailure()
}
// switch item {
// case .tab(let tab):
// delegate?.sidebarViewController(self, didSelectTab: tab)
// case .searchHistory(let viewModel):
// delegate?.sidebarViewController(self, didSelectSearchHistory: viewModel)
// case .header:
// break
// case .account(let viewModel):
// assert(Thread.isMainThread)
// let authentication = context.managedObjectContext.object(with: viewModel.authenticationObjectID) as! MastodonAuthentication
// context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID)
// .receive(on: DispatchQueue.main)
// .sink { [weak self] result in
// guard let self = self else { return }
// self.coordinator.setup()
// }
// .store(in: &disposeBag)
// case .addAccount:
// coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil))
// }
}
}

View File

@ -22,6 +22,8 @@ final class SidebarViewModel {
// output
var diffableDataSource: UICollectionViewDiffableDataSource<Section, Item>?
var secondaryDiffableDataSource: UICollectionViewDiffableDataSource<Section, Item>?
let activeMastodonAuthenticationObjectID = CurrentValueSubject<NSManagedObjectID?, Never>(nil)
init(context: AppContext) {
@ -47,38 +49,22 @@ final class SidebarViewModel {
extension SidebarViewModel {
enum Section: Int, Hashable, CaseIterable {
case tab
case account
case main
case secondary
}
enum Item: Hashable {
case tab(MainTabBarController.Tab)
case searchHistory(SearchHistoryViewModel)
case header(HeaderViewModel)
case account(AccountViewModel)
case addAccount
case setting
case compose
}
struct SearchHistoryViewModel: Hashable {
let searchHistoryObjectID: NSManagedObjectID
}
struct HeaderViewModel: Hashable {
let title: String
}
struct AccountViewModel: Hashable {
let authenticationObjectID: NSManagedObjectID
}
struct AddAccountViewModel: Hashable {
let id = UUID()
}
}
extension SidebarViewModel {
func setupDiffableDataSource(
collectionView: UICollectionView
collectionView: UICollectionView,
secondaryCollectionView: UICollectionView
) {
let tabCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, MainTabBarController.Tab> { [weak self] cell, indexPath, item in
guard let self = self else { return }
@ -92,23 +78,10 @@ extension SidebarViewModel {
return nil
}
}()
let headline: MetaContent = {
switch item {
case .me:
return PlaintextMetaContent(string: item.title)
// TODO:
// return PlaintextMetaContent(string: "Myself")
default:
return PlaintextMetaContent(string: item.title)
}
}()
let needsOutlineDisclosure = item == .search
cell.item = SidebarListContentView.Item(
title: item.title,
image: item.sidebarImage,
imageURL: imageURL,
headline: headline,
subheadline: nil,
needsOutlineDisclosure: needsOutlineDisclosure
imageURL: imageURL
)
cell.setNeedsUpdateConfiguration()
@ -135,209 +108,91 @@ extension SidebarViewModel {
}
}
let searchHistoryCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, SearchHistoryViewModel> { [weak self] cell, indexPath, item in
let cellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, SidebarListContentView.Item> { [weak self] cell, indexPath, item in
guard let self = self else { return }
let managedObjectContext = self.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext
guard let searchHistory = try? managedObjectContext.existingObject(with: item.searchHistoryObjectID) as? SearchHistory else { return }
if let account = searchHistory.account {
let headline: MetaContent = {
do {
let content = MastodonContent(content: account.displayNameWithFallback, emojis: account.emojiMeta)
return try MastodonMetaContent.convert(document: content)
} catch {
return PlaintextMetaContent(string: account.displayNameWithFallback)
}
}()
cell.item = SidebarListContentView.Item(
image: .placeholder(color: .systemFill),
imageURL: account.avatarImageURL(),
headline: headline,
subheadline: PlaintextMetaContent(string: "@" + account.acctWithDomain),
needsOutlineDisclosure: false
)
} else if let hashtag = searchHistory.hashtag {
let image = UIImage(systemName: "number.square.fill")!.withRenderingMode(.alwaysTemplate)
let headline = PlaintextMetaContent(string: "#" + hashtag.name)
cell.item = SidebarListContentView.Item(
image: image,
imageURL: nil,
headline: headline,
subheadline: nil,
needsOutlineDisclosure: false
)
} else {
assertionFailure()
}
cell.item = item
cell.setNeedsUpdateConfiguration()
}
let headerRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, HeaderViewModel> { (cell, indexPath, item) in
var content = UIListContentConfiguration.sidebarHeader()
content.text = item.title
cell.contentConfiguration = content
cell.accessories = [.outlineDisclosure()]
}
let accountRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, AccountViewModel> { [weak self] (cell, indexPath, item) in
guard let self = self else { return }
// accounts maybe already sign-out
// check isDeleted before using
guard let authentication = try? AppContext.shared.managedObjectContext.existingObject(with: item.authenticationObjectID) as? MastodonAuthentication,
!authentication.isDeleted else {
return
}
let user = authentication.user
let imageURL = user.avatarImageURL()
let headline: MetaContent = {
do {
let content = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojiMeta)
return try MastodonMetaContent.convert(document: content)
} catch {
return PlaintextMetaContent(string: user.displayNameWithFallback)
}
}()
cell.item = SidebarListContentView.Item(
image: .placeholder(color: .systemFill),
imageURL: imageURL,
headline: headline,
subheadline: PlaintextMetaContent(string: "@" + user.acctWithDomain),
needsOutlineDisclosure: false
)
cell.setNeedsUpdateConfiguration()
// FIXME: use notification, not timer
let accessToken = authentication.userAccessToken
AppContext.shared.timestampUpdatePublisher
.map { _ in UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) }
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak cell] count in
guard let cell = cell else { return }
cell._contentView?.badgeButton.setBadge(number: count)
}
.store(in: &cell.disposeBag)
let authenticationObjectID = item.authenticationObjectID
self.activeMastodonAuthenticationObjectID
.receive(on: DispatchQueue.main)
.sink { [weak cell] objectID in
guard let cell = cell else { return }
cell._contentView?.checkmarkImageView.isHidden = authenticationObjectID != objectID
}
.store(in: &cell.disposeBag)
}
let addAccountRegistration = UICollectionView.CellRegistration<SidebarAddAccountCollectionViewCell, AddAccountViewModel> { (cell, indexPath, item) in
var content = UIListContentConfiguration.sidebarCell()
content.text = L10n.Scene.AccountList.addAccount
content.image = UIImage(systemName: "plus.square.fill")!
cell.contentConfiguration = content
cell.accessories = []
// header
let headerRegistration = UICollectionView.SupplementaryRegistration<SidebarListHeaderView>(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
// do nothing
}
let _diffableDataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
switch item {
case .tab(let tab):
return collectionView.dequeueConfiguredReusableCell(using: tabCellRegistration, for: indexPath, item: tab)
case .searchHistory(let viewModel):
return collectionView.dequeueConfiguredReusableCell(using: searchHistoryCellRegistration, for: indexPath, item: viewModel)
case .header(let viewModel):
return collectionView.dequeueConfiguredReusableCell(using: headerRegistration, for: indexPath, item: viewModel)
case .account(let viewModel):
return collectionView.dequeueConfiguredReusableCell(using: accountRegistration, for: indexPath, item: viewModel)
case .addAccount:
return collectionView.dequeueConfiguredReusableCell(using: addAccountRegistration, for: indexPath, item: AddAccountViewModel())
case .setting:
let item = SidebarListContentView.Item(
title: L10n.Common.Controls.Actions.settings,
image: UIImage(systemName: "gear")!,
imageURL: nil
)
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
case .compose:
let item = SidebarListContentView.Item(
title: "Compose", // FIXME:
image: UIImage(systemName: "square.and.pencil")!,
imageURL: nil
)
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}
}
_diffableDataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
switch elementKind {
case UICollectionView.elementKindSectionHeader:
return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
default:
assertionFailure()
return UICollectionReusableView()
}
}
diffableDataSource = _diffableDataSource
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(Section.allCases)
_diffableDataSource.apply(snapshot)
snapshot.appendSections([.main])
for section in Section.allCases {
switch section {
case .tab:
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
let items: [Item] = [
.tab(.home),
.tab(.search),
.tab(.notification),
.tab(.me),
]
sectionSnapshot.append(items, to: nil)
_diffableDataSource.apply(sectionSnapshot, to: section)
case .account:
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
let headerItem = Item.header(HeaderViewModel(title: "Accounts"))
sectionSnapshot.append([headerItem], to: nil)
sectionSnapshot.append([], to: headerItem)
sectionSnapshot.append([.addAccount], to: headerItem)
sectionSnapshot.expand([headerItem])
_diffableDataSource.apply(sectionSnapshot, to: section)
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
let items: [Item] = [
.tab(.home),
.tab(.search),
.tab(.notification),
.tab(.me),
.setting,
]
sectionSnapshot.append(items, to: nil)
_diffableDataSource.apply(sectionSnapshot, to: .main)
// secondary
let _secondaryDiffableDataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: secondaryCollectionView) { collectionView, indexPath, item in
guard case .compose = item else {
assertionFailure()
return UICollectionViewCell()
}
let item = SidebarListContentView.Item(
title: "Compose", // FIXME:
image: UIImage(systemName: "square.and.pencil")!,
imageURL: nil
)
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}
// _secondaryDiffableDataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
// return nil
// }
secondaryDiffableDataSource = _secondaryDiffableDataSource
// update .search tab
searchHistoryFetchedResultController.objectIDs
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] objectIDs in
guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return }
var secondarySnapshot = NSDiffableDataSourceSnapshot<Section, Item>()
secondarySnapshot.appendSections([.secondary])
// update .search tab
var sectionSnapshot = diffableDataSource.snapshot(for: .tab)
// remove children
let searchHistorySnapshot = sectionSnapshot.snapshot(of: .tab(.search))
sectionSnapshot.delete(searchHistorySnapshot.items)
// append children
let managedObjectContext = self.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext
let items: [Item] = objectIDs.compactMap { objectID -> Item? in
guard let searchHistory = try? managedObjectContext.existingObject(with: objectID) as? SearchHistory else { return nil }
guard searchHistory.account != nil || searchHistory.hashtag != nil else { return nil }
let viewModel = SearchHistoryViewModel(searchHistoryObjectID: objectID)
return Item.searchHistory(viewModel)
}
sectionSnapshot.append(Array(items.prefix(5)), to: .tab(.search))
sectionSnapshot.expand([.tab(.search)])
// apply snapshot
diffableDataSource.apply(sectionSnapshot, to: .tab, animatingDifferences: false)
}
.store(in: &disposeBag)
// update .me tab and .account section
context.authenticationService.mastodonAuthentications
.receive(on: DispatchQueue.main)
.sink { [weak self] authentications in
guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return }
// tab
var snapshot = diffableDataSource.snapshot()
snapshot.reloadItems([.tab(.me)])
diffableDataSource.apply(snapshot)
// account
var accountSectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
let headerItem = Item.header(HeaderViewModel(title: "Accounts"))
accountSectionSnapshot.append([headerItem], to: nil)
let accountItems = authentications.map { authentication in
Item.account(AccountViewModel(authenticationObjectID: authentication.objectID))
}
accountSectionSnapshot.append(accountItems, to: headerItem)
accountSectionSnapshot.append([.addAccount], to: headerItem)
accountSectionSnapshot.expand([headerItem])
diffableDataSource.apply(accountSectionSnapshot, to: .account)
}
.store(in: &disposeBag)
var secondarySectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
let secondarySectionItems: [Item] = [
.compose,
]
secondarySectionSnapshot.append(secondarySectionItems, to: nil)
_secondaryDiffableDataSource.apply(secondarySectionSnapshot, to: .secondary)
}
}

View File

@ -60,21 +60,6 @@ extension SidebarListCollectionViewCell {
newBackgroundConfiguration.backgroundColorTransformer = .init { $0.withAlphaComponent(0.8) }
}
backgroundConfiguration = newBackgroundConfiguration
let needsOutlineDisclosure = item?.needsOutlineDisclosure ?? false
if !needsOutlineDisclosure {
accessories = []
} else {
let tintColor: UIColor = state.isHighlighted || state.isSelected ? .white : Asset.Colors.brandBlue.color
accessories = [
UICellAccessory.outlineDisclosure(
displayed: .always,
options: UICellAccessory.OutlineDisclosureOptions(tintColor: tintColor),
actionHandler: nil
)
]
}
}
}

View File

@ -15,15 +15,10 @@ final class SidebarListContentView: UIView, UIContentView {
let logger = Logger(subsystem: "SidebarListContentView", category: "UI")
let imageView = UIImageView()
let animationImageView = FLAnimatedImageView() // for animation image
let headlineLabel = MetaLabel(style: .sidebarHeadline(isSelected: false))
let subheadlineLabel = MetaLabel(style: .sidebarSubheadline(isSelected: false))
let badgeButton = BadgeButton()
let checkmarkImageView: UIImageView = {
let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .semibold))
let imageView = UIImageView(image: image)
imageView.tintColor = .label
return imageView
let avatarButton: CircleAvatarButton = {
let button = CircleAvatarButton()
button.borderWidth = 2
return button
}()
private var currentConfiguration: ContentConfiguration!
@ -53,93 +48,31 @@ final class SidebarListContentView: UIView, UIContentView {
extension SidebarListContentView {
private func _init() {
let imageViewContainer = UIView()
imageViewContainer.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageViewContainer)
NSLayoutConstraint.activate([
imageViewContainer.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
imageViewContainer.centerYAnchor.constraint(equalTo: centerYAnchor),
])
imageViewContainer.setContentHuggingPriority(.defaultLow, for: .horizontal)
imageViewContainer.setContentHuggingPriority(.defaultLow, for: .vertical)
animationImageView.translatesAutoresizingMaskIntoConstraints = false
imageViewContainer.addSubview(animationImageView)
NSLayoutConstraint.activate([
animationImageView.centerXAnchor.constraint(equalTo: imageViewContainer.centerXAnchor),
animationImageView.centerYAnchor.constraint(equalTo: imageViewContainer.centerYAnchor),
animationImageView.widthAnchor.constraint(equalTo: imageViewContainer.widthAnchor, multiplier: 1.0).priority(.required - 1),
animationImageView.heightAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1),
])
animationImageView.setContentHuggingPriority(.defaultLow - 10, for: .vertical)
animationImageView.setContentHuggingPriority(.defaultLow - 10, for: .horizontal)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageViewContainer.addSubview(imageView)
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: imageViewContainer.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: imageViewContainer.centerYAnchor),
imageView.widthAnchor.constraint(equalTo: imageViewContainer.widthAnchor, multiplier: 1.0).priority(.required - 1),
imageView.heightAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1),
imageView.topAnchor.constraint(equalTo: topAnchor, constant: 16),
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 16),
imageView.widthAnchor.constraint(equalToConstant: 40).priority(.required - 1),
imageView.heightAnchor.constraint(equalToConstant: 40).priority(.required - 1),
])
imageView.setContentHuggingPriority(.defaultLow - 10, for: .vertical)
imageView.setContentHuggingPriority(.defaultLow - 10, for: .horizontal)
let textContainer = UIStackView()
textContainer.axis = .vertical
textContainer.translatesAutoresizingMaskIntoConstraints = false
addSubview(textContainer)
avatarButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(avatarButton)
NSLayoutConstraint.activate([
textContainer.topAnchor.constraint(equalTo: topAnchor, constant: 10),
textContainer.leadingAnchor.constraint(equalTo: imageViewContainer.trailingAnchor, constant: 10),
// textContainer.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
bottomAnchor.constraint(equalTo: textContainer.bottomAnchor, constant: 12),
avatarButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
avatarButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
avatarButton.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0).priority(.required - 2),
avatarButton.heightAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1.0).priority(.required - 2),
])
textContainer.addArrangedSubview(headlineLabel)
textContainer.addArrangedSubview(subheadlineLabel)
headlineLabel.setContentHuggingPriority(.required - 9, for: .vertical)
headlineLabel.setContentCompressionResistancePriority(.required - 9, for: .vertical)
subheadlineLabel.setContentHuggingPriority(.required - 10, for: .vertical)
subheadlineLabel.setContentCompressionResistancePriority(.required - 10, for: .vertical)
badgeButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(badgeButton)
NSLayoutConstraint.activate([
badgeButton.leadingAnchor.constraint(equalTo: textContainer.trailingAnchor, constant: 4),
badgeButton.centerYAnchor.constraint(equalTo: centerYAnchor),
badgeButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 16).priority(.required - 1),
badgeButton.widthAnchor.constraint(equalTo: badgeButton.heightAnchor, multiplier: 1.0).priority(.required - 1),
])
badgeButton.setContentHuggingPriority(.required - 10, for: .horizontal)
badgeButton.setContentCompressionResistancePriority(.required - 10, for: .horizontal)
NSLayoutConstraint.activate([
imageViewContainer.heightAnchor.constraint(equalTo: headlineLabel.heightAnchor, multiplier: 1.0).priority(.required - 1),
imageViewContainer.widthAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1),
])
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(checkmarkImageView)
NSLayoutConstraint.activate([
checkmarkImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
checkmarkImageView.leadingAnchor.constraint(equalTo: badgeButton.trailingAnchor, constant: 16),
checkmarkImageView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
])
checkmarkImageView.setContentHuggingPriority(.required - 9, for: .horizontal)
checkmarkImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal)
animationImageView.isUserInteractionEnabled = false
headlineLabel.isUserInteractionEnabled = false
subheadlineLabel.isUserInteractionEnabled = false
avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .vertical)
avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .horizontal)
imageView.contentMode = .scaleAspectFit
animationImageView.contentMode = .scaleAspectFit
avatarButton.contentMode = .scaleAspectFit
imageView.tintColor = Asset.Colors.brandBlue.color
animationImageView.tintColor = Asset.Colors.brandBlue.color
badgeButton.setBadge(number: 0)
checkmarkImageView.isHidden = true
avatarButton.tintColor = Asset.Colors.brandBlue.color
}
private func apply(configuration: ContentConfiguration) {
@ -152,31 +85,20 @@ extension SidebarListContentView {
// configure state
imageView.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color
animationImageView.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color
headlineLabel.setup(style: .sidebarHeadline(isSelected: item.isSelected))
subheadlineLabel.setup(style: .sidebarSubheadline(isSelected: item.isSelected))
avatarButton.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color
// configure model
imageView.isHidden = item.imageURL != nil
animationImageView.isHidden = item.imageURL == nil
avatarButton.isHidden = item.imageURL == nil
imageView.image = item.image.withRenderingMode(.alwaysTemplate)
animationImageView.setImage(
avatarButton.avatarImageView.setImage(
url: item.imageURL,
placeholder: animationImageView.image ?? .placeholder(color: .systemFill), // reuse to avoid blink
placeholder: avatarButton.avatarImageView.image ?? .placeholder(color: .systemFill), // reuse to avoid blink
scaleToSize: nil
)
animationImageView.layer.masksToBounds = true
animationImageView.layer.cornerCurve = .continuous
animationImageView.layer.cornerRadius = 4
headlineLabel.configure(content: item.headline)
if let subheadline = item.subheadline {
subheadlineLabel.configure(content: subheadline)
subheadlineLabel.isHidden = false
} else {
subheadlineLabel.isHidden = true
}
avatarButton.layer.masksToBounds = true
avatarButton.layer.cornerCurve = .continuous
avatarButton.layer.cornerRadius = 4
}
}
@ -186,27 +108,22 @@ extension SidebarListContentView {
var isSelected: Bool = false
// model
let title: String
let image: UIImage
let imageURL: URL?
let headline: MetaContent
let subheadline: MetaContent?
let needsOutlineDisclosure: Bool
static func == (lhs: SidebarListContentView.Item, rhs: SidebarListContentView.Item) -> Bool {
return lhs.isSelected == rhs.isSelected
&& lhs.title == rhs.title
&& lhs.image == rhs.image
&& lhs.imageURL == rhs.imageURL
&& lhs.headline.string == rhs.headline.string
&& lhs.subheadline?.string == rhs.subheadline?.string
}
func hash(into hasher: inout Hasher) {
hasher.combine(isSelected)
hasher.combine(title)
hasher.combine(image)
imageURL.flatMap { hasher.combine($0) }
hasher.combine(headline.string)
subheadline.flatMap { hasher.combine($0.string) }
}
}

View File

@ -0,0 +1,42 @@
//
// SidebarListHeaderView.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-10-28.
//
import UIKit
final class SidebarListHeaderView: UICollectionReusableView {
let imageView: UIImageView = {
let imageView = UIImageView()
imageView.image = Asset.Scene.Sidebar.logo.image
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
_init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
_init()
}
}
extension SidebarListHeaderView {
private func _init() {
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 16),
imageView.widthAnchor.constraint(equalToConstant: 44).priority(.required - 1),
imageView.heightAnchor.constraint(equalToConstant: 44).priority(.required - 1),
])
}
}

View File

@ -9,12 +9,15 @@ import UIKit
final class CircleAvatarButton: AvatarButton {
var borderColor: CGColor = UIColor.systemFill.cgColor
var borderWidth: CGFloat = 1.0
override func layoutSubviews() {
super.layoutSubviews()
layer.masksToBounds = true
layer.cornerRadius = frame.width * 0.5
layer.borderColor = UIColor.systemFill.cgColor
layer.borderWidth = 1
layer.borderColor = borderColor
layer.borderWidth = borderWidth
}
}

View File

@ -23,7 +23,7 @@ struct SystemTheme: Theme {
let navigationBarBackgroundColor = Asset.Theme.System.navigationBarBackground.color
let sidebarBackgroundColor = Asset.Theme.Mastodon.sidebarBackground.color
let sidebarBackgroundColor = Asset.Theme.System.sidebarBackground.color
let tabBarBackgroundColor = Asset.Theme.System.tabBarBackground.color
let tabBarItemSelectedIconColor = Asset.Colors.brandBlue.color