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

View File

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

View File

@ -90,45 +90,45 @@ final public class SceneCoordinator {
// Delay in next run loop // Delay in next run loop
DispatchQueue.main.async { [weak self] in // DispatchQueue.main.async { [weak self] in
guard let self = self else { return } // guard let self = self else { return }
//
// Note: // // Note:
// show (push) on phone or pad (compact) // // show (push) on phone or pad (compact)
// showDetail in .secondary in UISplitViewController on pad (expand) // // showDetail in .secondary in UISplitViewController on pad (expand)
let from: UIViewController? = { // let from: UIViewController? = {
if let splitViewController = self.splitViewController { // if let splitViewController = self.splitViewController {
if splitViewController.mainTabBarController.topMost?.view.window != nil { // if splitViewController.mainTabBarController.topMost?.view.window != nil {
// compact // // compact
return splitViewController.mainTabBarController.topMost // return splitViewController.mainTabBarController.topMost
} else { // } else {
// expand // // expand
return splitViewController.viewController(for: .supplementary) // return splitViewController.viewController(for: .supplementary)
} // }
} else { // } else {
return self.tabBarController.topMost // return self.tabBarController.topMost
} // }
}() // }()
//
// show notification related content // // show notification related content
guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return } // guard let type = Mastodon.Entity.Notification.NotificationType(rawValue: pushNotification.notificationType) else { return }
let notificationID = String(pushNotification.notificationID) // let notificationID = String(pushNotification.notificationID)
//
switch type { // switch type {
case .follow: // case .follow:
let profileViewModel = RemoteProfileViewModel(context: appContext, notificationID: notificationID) // let profileViewModel = RemoteProfileViewModel(context: appContext, notificationID: notificationID)
self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show) // self.present(scene: .profile(viewModel: profileViewModel), from: from, transition: .show)
case .followRequest: // case .followRequest:
// do nothing // // do nothing
break // break
case .mention, .reblog, .favourite, .poll, .status: // case .mention, .reblog, .favourite, .poll, .status:
let threadViewModel = RemoteThreadViewModel(context: appContext, notificationID: notificationID) // let threadViewModel = RemoteThreadViewModel(context: appContext, notificationID: notificationID)
self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show) // self.present(scene: .thread(viewModel: threadViewModel), from: from, transition: .show)
case ._other: // case ._other:
assertionFailure() // assertionFailure()
break // break
} // }
} // } // end DispatchQueue.main.async
} }
.store(in: &disposeBag) .store(in: &disposeBag)
} }
@ -227,7 +227,7 @@ extension SceneCoordinator {
default: default:
let splitViewController = RootSplitViewController(context: appContext, coordinator: self) let splitViewController = RootSplitViewController(context: appContext, coordinator: self)
self.splitViewController = splitViewController self.splitViewController = splitViewController
self.tabBarController = splitViewController.mainTabBarController self.tabBarController = splitViewController.contentSplitViewController.mainTabBarController
sceneDelegate.window?.rootViewController = splitViewController sceneDelegate.window?.rootViewController = splitViewController
} }
} }
@ -282,18 +282,19 @@ extension SceneCoordinator {
switch transition { switch transition {
case .show: case .show:
if let splitViewController = splitViewController, !splitViewController.isCollapsed, // if let splitViewController = splitViewController, !splitViewController.isCollapsed,
let supplementaryViewController = splitViewController.viewController(for: .supplementary) as? UINavigationController, // 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)) // (presentingViewController is UserTimelineViewController && presentingViewController.view.isDescendant(of: supplementaryViewController.view))
{ // {
fallthrough // fallthrough
} else { // } else {
if secondaryStackHashValues.contains(presentingViewController.hashValue) { // if secondaryStackHashValues.contains(presentingViewController.hashValue) {
secondaryStackHashValues.insert(viewController.hashValue) // secondaryStackHashValues.insert(viewController.hashValue)
} // }
presentingViewController.show(viewController, sender: sender) // presentingViewController.show(viewController, sender: sender)
} // }
presentingViewController.show(viewController, sender: sender)
case .showDetail: case .showDetail:
secondaryStackHashValues.insert(viewController.hashValue) secondaryStackHashValues.insert(viewController.hashValue)
let navigationController = AdaptiveStatusBarStyleNavigationController(rootViewController: viewController) 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 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 Welcome {
internal enum Illustration { internal enum Illustration {
internal static let backgroundCyan = ColorAsset(name: "Scene/Welcome/illustration/background.cyan") 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 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 // 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", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "46", "blue" : "0x2E",
"green" : "44", "green" : "0x2C",
"red" : "44" "red" : "0x2C"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

@ -23,9 +23,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.000", "alpha" : "1.000",
"blue" : "0.263", "blue" : "0x2E",
"green" : "0.208", "green" : "0x2C",
"red" : "0.192" "red" : "0x2C"
} }
}, },
"idiom" : "universal" "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 #endif
} }
.store(in: &disposeBag) .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 navigationItem.titleView = titleView
titleView.delegate = self titleView.delegate = self
@ -126,18 +144,6 @@ extension HomeTimelineViewController {
} }
.store(in: &disposeBag) .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 tableView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(HomeTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged) refreshControl.addTarget(self, action: #selector(HomeTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)

View File

@ -30,6 +30,8 @@ final class HomeTimelineViewModel: NSObject {
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
let lastAutomaticFetchTimestamp = CurrentValueSubject<Date?, Never>(nil) let lastAutomaticFetchTimestamp = CurrentValueSubject<Date?, Never>(nil)
let scrollPositionRecord = CurrentValueSubject<ScrollPositionRecord?, 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 contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
weak var tableView: UITableView? weak var tableView: UITableView?
@ -70,7 +72,6 @@ final class HomeTimelineViewModel: NSObject {
let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
var cellFrameCache = NSCache<NSNumber, NSValue>() var cellFrameCache = NSCache<NSNumber, NSValue>()
let displaySettingBarButtonItem = CurrentValueSubject<Bool, Never>(true)
init(context: AppContext) { init(context: AppContext) {
self.context = context 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) { func setup(in view: UIView) {
assert(delegate != nil, "need set delegate before use") assert(delegate != nil, "need set delegate before use")
guard !items.isEmpty else { return }
backgroundView.frame = view.bounds backgroundView.frame = view.bounds
backgroundView.translatesAutoresizingMaskIntoConstraints = false backgroundView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(backgroundView) view.addSubview(backgroundView)

View File

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

View File

@ -12,7 +12,6 @@ import CoreDataStack
protocol SidebarViewControllerDelegate: AnyObject { protocol SidebarViewControllerDelegate: AnyObject {
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab)
func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectSearchHistory searchHistoryViewModel: SidebarViewModel.SearchHistoryViewModel)
} }
final class SidebarViewController: UIViewController, NeedsDependency { final class SidebarViewController: UIViewController, NeedsDependency {
@ -21,28 +20,46 @@ final class SidebarViewController: UIViewController, NeedsDependency {
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
var observations = Set<NSKeyValueObservation>()
var viewModel: SidebarViewModel! var viewModel: SidebarViewModel!
weak var delegate: SidebarViewControllerDelegate? 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 { static func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in let layout = UICollectionViewCompositionalLayout() { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar) var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar)
configuration.backgroundColor = .clear configuration.backgroundColor = .clear
if sectionIndex == SidebarViewModel.Section.tab.rawValue { configuration.showsSeparators = false
// with indentation let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
configuration.headerMode = .none switch sectionIndex {
} else { case 0:
// remove indentation let header = NSCollectionLayoutBoundarySupplementaryItem(
configuration.headerMode = .firstItemInSection 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 configuration.showsSeparators = false
let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment) let section = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
return section return section
@ -50,12 +67,15 @@ final class SidebarViewController: UIViewController, NeedsDependency {
return layout return layout
} }
let collectionView: UICollectionView = { let secondaryCollectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: SidebarViewController.createLayout()) let layout = SidebarViewController.createSecondaryLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.isScrollEnabled = false
collectionView.alwaysBounceVertical = false
collectionView.backgroundColor = .clear collectionView.backgroundColor = .clear
return collectionView return collectionView
}() }()
var secondaryCollectionViewHeightLayoutConstraint: NSLayoutConstraint!
} }
extension SidebarViewController { extension SidebarViewController {
@ -63,23 +83,7 @@ extension SidebarViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
viewModel.context.authenticationService.activeMastodonAuthenticationBox navigationController?.setNavigationBarHidden(true, animated: false)
.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
setupBackground(theme: ThemeService.shared.currentTheme.value) setupBackground(theme: ThemeService.shared.currentTheme.value)
ThemeService.shared.currentTheme ThemeService.shared.currentTheme
@ -99,65 +103,101 @@ extension SidebarViewController {
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 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 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) { private func setupBackground(theme: Theme) {
let color: UIColor = theme.sidebarBackgroundColor 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 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 // MARK: - UICollectionViewDelegate
extension SidebarViewController: UICollectionViewDelegate { extension SidebarViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let diffableDataSource = viewModel.diffableDataSource else { return } switch collectionView {
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } case self.collectionView:
switch item { guard let diffableDataSource = viewModel.diffableDataSource else { return }
case .tab(let tab): guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
delegate?.sidebarViewController(self, didSelectTab: tab) switch item {
case .searchHistory(let viewModel): case .tab(let tab):
delegate?.sidebarViewController(self, didSelectSearchHistory: viewModel) delegate?.sidebarViewController(self, didSelectTab: tab)
case .header: case .setting:
break guard let setting = context.settingService.currentSetting.value else { return }
case .account(let viewModel): let settingsViewModel = SettingsViewModel(context: context, setting: setting)
assert(Thread.isMainThread) coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
let authentication = context.managedObjectContext.object(with: viewModel.authenticationObjectID) as! MastodonAuthentication case .compose:
context.authenticationService.activeMastodonUser(domain: authentication.domain, userID: authentication.userID) assertionFailure()
.receive(on: DispatchQueue.main) }
.sink { [weak self] result in case secondaryCollectionView:
guard let self = self else { return } guard let diffableDataSource = viewModel.secondaryDiffableDataSource else { return }
self.coordinator.setup() guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
} switch item {
.store(in: &disposeBag) case .compose:
case .addAccount: let composeViewModel = ComposeViewModel(context: context, composeKind: .post)
coordinator.present(scene: .welcome, from: self, transition: .modal(animated: true, completion: nil)) 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 // output
var diffableDataSource: UICollectionViewDiffableDataSource<Section, Item>? var diffableDataSource: UICollectionViewDiffableDataSource<Section, Item>?
var secondaryDiffableDataSource: UICollectionViewDiffableDataSource<Section, Item>?
let activeMastodonAuthenticationObjectID = CurrentValueSubject<NSManagedObjectID?, Never>(nil) let activeMastodonAuthenticationObjectID = CurrentValueSubject<NSManagedObjectID?, Never>(nil)
init(context: AppContext) { init(context: AppContext) {
@ -47,38 +49,22 @@ final class SidebarViewModel {
extension SidebarViewModel { extension SidebarViewModel {
enum Section: Int, Hashable, CaseIterable { enum Section: Int, Hashable, CaseIterable {
case tab case main
case account case secondary
} }
enum Item: Hashable { enum Item: Hashable {
case tab(MainTabBarController.Tab) case tab(MainTabBarController.Tab)
case searchHistory(SearchHistoryViewModel) case setting
case header(HeaderViewModel) case compose
case account(AccountViewModel)
case addAccount
} }
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 { extension SidebarViewModel {
func setupDiffableDataSource( func setupDiffableDataSource(
collectionView: UICollectionView collectionView: UICollectionView,
secondaryCollectionView: UICollectionView
) { ) {
let tabCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, MainTabBarController.Tab> { [weak self] cell, indexPath, item in let tabCellRegistration = UICollectionView.CellRegistration<SidebarListCollectionViewCell, MainTabBarController.Tab> { [weak self] cell, indexPath, item in
guard let self = self else { return } guard let self = self else { return }
@ -92,23 +78,10 @@ extension SidebarViewModel {
return nil 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( cell.item = SidebarListContentView.Item(
title: item.title,
image: item.sidebarImage, image: item.sidebarImage,
imageURL: imageURL, imageURL: imageURL
headline: headline,
subheadline: nil,
needsOutlineDisclosure: needsOutlineDisclosure
) )
cell.setNeedsUpdateConfiguration() 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 } guard let self = self else { return }
let managedObjectContext = self.searchHistoryFetchedResultController.fetchedResultsController.managedObjectContext cell.item = item
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.setNeedsUpdateConfiguration() cell.setNeedsUpdateConfiguration()
} }
let headerRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, HeaderViewModel> { (cell, indexPath, item) in // header
var content = UIListContentConfiguration.sidebarHeader() let headerRegistration = UICollectionView.SupplementaryRegistration<SidebarListHeaderView>(elementKind: UICollectionView.elementKindSectionHeader) { supplementaryView, elementKind, indexPath in
content.text = item.title // do nothing
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 = []
} }
let _diffableDataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in let _diffableDataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
switch item { switch item {
case .tab(let tab): case .tab(let tab):
return collectionView.dequeueConfiguredReusableCell(using: tabCellRegistration, for: indexPath, item: tab) return collectionView.dequeueConfiguredReusableCell(using: tabCellRegistration, for: indexPath, item: tab)
case .searchHistory(let viewModel): case .setting:
return collectionView.dequeueConfiguredReusableCell(using: searchHistoryCellRegistration, for: indexPath, item: viewModel) let item = SidebarListContentView.Item(
case .header(let viewModel): title: L10n.Common.Controls.Actions.settings,
return collectionView.dequeueConfiguredReusableCell(using: headerRegistration, for: indexPath, item: viewModel) image: UIImage(systemName: "gear")!,
case .account(let viewModel): imageURL: nil
return collectionView.dequeueConfiguredReusableCell(using: accountRegistration, for: indexPath, item: viewModel) )
case .addAccount: return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
return collectionView.dequeueConfiguredReusableCell(using: addAccountRegistration, for: indexPath, item: AddAccountViewModel()) 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 diffableDataSource = _diffableDataSource
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(Section.allCases) snapshot.appendSections([.main])
_diffableDataSource.apply(snapshot)
for section in Section.allCases { var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
switch section { let items: [Item] = [
case .tab: .tab(.home),
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>() .tab(.search),
let items: [Item] = [ .tab(.notification),
.tab(.home), .tab(.me),
.tab(.search), .setting,
.tab(.notification), ]
.tab(.me), sectionSnapshot.append(items, to: nil)
] _diffableDataSource.apply(sectionSnapshot, to: .main)
sectionSnapshot.append(items, to: nil)
_diffableDataSource.apply(sectionSnapshot, to: section)
case .account: // secondary
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>() let _secondaryDiffableDataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: secondaryCollectionView) { collectionView, indexPath, item in
let headerItem = Item.header(HeaderViewModel(title: "Accounts")) guard case .compose = item else {
sectionSnapshot.append([headerItem], to: nil) assertionFailure()
sectionSnapshot.append([], to: headerItem) return UICollectionViewCell()
sectionSnapshot.append([.addAccount], to: headerItem)
sectionSnapshot.expand([headerItem])
_diffableDataSource.apply(sectionSnapshot, to: section)
} }
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 var secondarySnapshot = NSDiffableDataSourceSnapshot<Section, Item>()
searchHistoryFetchedResultController.objectIDs secondarySnapshot.appendSections([.secondary])
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] objectIDs in
guard let self = self else { return }
guard let diffableDataSource = self.diffableDataSource else { return }
// update .search tab var secondarySectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
var sectionSnapshot = diffableDataSource.snapshot(for: .tab) let secondarySectionItems: [Item] = [
.compose,
// remove children ]
let searchHistorySnapshot = sectionSnapshot.snapshot(of: .tab(.search)) secondarySectionSnapshot.append(secondarySectionItems, to: nil)
sectionSnapshot.delete(searchHistorySnapshot.items) _secondaryDiffableDataSource.apply(secondarySectionSnapshot, to: .secondary)
// 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)
} }
} }

View File

@ -60,21 +60,6 @@ extension SidebarListCollectionViewCell {
newBackgroundConfiguration.backgroundColorTransformer = .init { $0.withAlphaComponent(0.8) } newBackgroundConfiguration.backgroundColorTransformer = .init { $0.withAlphaComponent(0.8) }
} }
backgroundConfiguration = newBackgroundConfiguration 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 logger = Logger(subsystem: "SidebarListContentView", category: "UI")
let imageView = UIImageView() let imageView = UIImageView()
let animationImageView = FLAnimatedImageView() // for animation image let avatarButton: CircleAvatarButton = {
let headlineLabel = MetaLabel(style: .sidebarHeadline(isSelected: false)) let button = CircleAvatarButton()
let subheadlineLabel = MetaLabel(style: .sidebarSubheadline(isSelected: false)) button.borderWidth = 2
let badgeButton = BadgeButton() return button
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
}() }()
private var currentConfiguration: ContentConfiguration! private var currentConfiguration: ContentConfiguration!
@ -53,93 +48,31 @@ final class SidebarListContentView: UIView, UIContentView {
extension SidebarListContentView { extension SidebarListContentView {
private func _init() { 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 imageView.translatesAutoresizingMaskIntoConstraints = false
imageViewContainer.addSubview(imageView) addSubview(imageView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: imageViewContainer.centerXAnchor), imageView.topAnchor.constraint(equalTo: topAnchor, constant: 16),
imageView.centerYAnchor.constraint(equalTo: imageViewContainer.centerYAnchor), imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.widthAnchor.constraint(equalTo: imageViewContainer.widthAnchor, multiplier: 1.0).priority(.required - 1), bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 16),
imageView.heightAnchor.constraint(equalTo: imageViewContainer.heightAnchor, multiplier: 1.0).priority(.required - 1), 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) avatarButton.translatesAutoresizingMaskIntoConstraints = false
addSubview(avatarButton)
let textContainer = UIStackView()
textContainer.axis = .vertical
textContainer.translatesAutoresizingMaskIntoConstraints = false
addSubview(textContainer)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
textContainer.topAnchor.constraint(equalTo: topAnchor, constant: 10), avatarButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
textContainer.leadingAnchor.constraint(equalTo: imageViewContainer.trailingAnchor, constant: 10), avatarButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
// textContainer.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), avatarButton.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1.0).priority(.required - 2),
bottomAnchor.constraint(equalTo: textContainer.bottomAnchor, constant: 12), avatarButton.heightAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1.0).priority(.required - 2),
]) ])
avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .vertical)
textContainer.addArrangedSubview(headlineLabel) avatarButton.setContentHuggingPriority(.defaultLow - 10, for: .horizontal)
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
imageView.contentMode = .scaleAspectFit imageView.contentMode = .scaleAspectFit
animationImageView.contentMode = .scaleAspectFit avatarButton.contentMode = .scaleAspectFit
imageView.tintColor = Asset.Colors.brandBlue.color imageView.tintColor = Asset.Colors.brandBlue.color
animationImageView.tintColor = Asset.Colors.brandBlue.color avatarButton.tintColor = Asset.Colors.brandBlue.color
badgeButton.setBadge(number: 0)
checkmarkImageView.isHidden = true
} }
private func apply(configuration: ContentConfiguration) { private func apply(configuration: ContentConfiguration) {
@ -152,31 +85,20 @@ extension SidebarListContentView {
// configure state // configure state
imageView.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color imageView.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color
animationImageView.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color avatarButton.tintColor = item.isSelected ? .white : Asset.Colors.brandBlue.color
headlineLabel.setup(style: .sidebarHeadline(isSelected: item.isSelected))
subheadlineLabel.setup(style: .sidebarSubheadline(isSelected: item.isSelected))
// configure model // configure model
imageView.isHidden = item.imageURL != nil imageView.isHidden = item.imageURL != nil
animationImageView.isHidden = item.imageURL == nil avatarButton.isHidden = item.imageURL == nil
imageView.image = item.image.withRenderingMode(.alwaysTemplate) imageView.image = item.image.withRenderingMode(.alwaysTemplate)
animationImageView.setImage( avatarButton.avatarImageView.setImage(
url: item.imageURL, 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 scaleToSize: nil
) )
animationImageView.layer.masksToBounds = true avatarButton.layer.masksToBounds = true
animationImageView.layer.cornerCurve = .continuous avatarButton.layer.cornerCurve = .continuous
animationImageView.layer.cornerRadius = 4 avatarButton.layer.cornerRadius = 4
headlineLabel.configure(content: item.headline)
if let subheadline = item.subheadline {
subheadlineLabel.configure(content: subheadline)
subheadlineLabel.isHidden = false
} else {
subheadlineLabel.isHidden = true
}
} }
} }
@ -186,27 +108,22 @@ extension SidebarListContentView {
var isSelected: Bool = false var isSelected: Bool = false
// model // model
let title: String
let image: UIImage let image: UIImage
let imageURL: URL? let imageURL: URL?
let headline: MetaContent
let subheadline: MetaContent?
let needsOutlineDisclosure: Bool
static func == (lhs: SidebarListContentView.Item, rhs: SidebarListContentView.Item) -> Bool { static func == (lhs: SidebarListContentView.Item, rhs: SidebarListContentView.Item) -> Bool {
return lhs.isSelected == rhs.isSelected return lhs.isSelected == rhs.isSelected
&& lhs.title == rhs.title
&& lhs.image == rhs.image && lhs.image == rhs.image
&& lhs.imageURL == rhs.imageURL && lhs.imageURL == rhs.imageURL
&& lhs.headline.string == rhs.headline.string
&& lhs.subheadline?.string == rhs.subheadline?.string
} }
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(isSelected) hasher.combine(isSelected)
hasher.combine(title)
hasher.combine(image) hasher.combine(image)
imageURL.flatMap { hasher.combine($0) } 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 { final class CircleAvatarButton: AvatarButton {
var borderColor: CGColor = UIColor.systemFill.cgColor
var borderWidth: CGFloat = 1.0
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
layer.masksToBounds = true layer.masksToBounds = true
layer.cornerRadius = frame.width * 0.5 layer.cornerRadius = frame.width * 0.5
layer.borderColor = UIColor.systemFill.cgColor layer.borderColor = borderColor
layer.borderWidth = 1 layer.borderWidth = borderWidth
} }
} }

View File

@ -23,7 +23,7 @@ struct SystemTheme: Theme {
let navigationBarBackgroundColor = Asset.Theme.System.navigationBarBackground.color 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 tabBarBackgroundColor = Asset.Theme.System.tabBarBackground.color
let tabBarItemSelectedIconColor = Asset.Colors.brandBlue.color let tabBarItemSelectedIconColor = Asset.Colors.brandBlue.color