Compare commits

...

20 Commits

Author SHA1 Message Date
Shadowfacts 3a21983b98 Merge branch 'tabbarnav' into develop 2024-08-21 17:53:08 -04:00
Shadowfacts 1817247077 Add saved instances to new sidebar 2024-08-21 17:10:01 -04:00
Shadowfacts 0d9eed73dd Add saved/followed hashtags to new sidebar 2024-08-21 16:58:16 -04:00
Shadowfacts 59d43fd3f6 Open in New Window context menu actions for new sidebar 2024-08-21 16:50:30 -04:00
Shadowfacts d321c31776 Implement more protocols for AdaptableNavigationController 2024-08-21 16:36:13 -04:00
Shadowfacts ce10c7d6e2 Implement adding list using new sidebar 2024-08-21 16:19:51 -04:00
Shadowfacts 37b9673b12 Fix list timeline no content view being added repetedly on refresh 2024-08-21 16:17:57 -04:00
Shadowfacts 7c7af945e4 Show avatar in tab/side bar when using new API 2024-08-21 16:12:05 -04:00
Shadowfacts cb32c66a59 Support fast account switching with new sidebar 2024-08-21 14:48:47 -04:00
Shadowfacts 4249ab30ca Fix crash when hashtag search results include duplicate 2024-08-21 14:10:59 -04:00
Shadowfacts 67e9c1245e Size class switching fixes for new tab/side bar 2024-08-21 12:17:26 -04:00
Shadowfacts 3d9a1086b6 Remove dead code 2024-08-20 12:31:29 -04:00
Shadowfacts fda0c18794 Fix insets with new sidebar 2024-08-20 12:31:06 -04:00
Shadowfacts dffa5d8f75 Lists in new sidebar 2024-08-20 11:55:19 -04:00
Shadowfacts 9891b601a8 Initial tab bar/sidebar implementation 2024-08-19 19:10:31 -04:00
Shadowfacts a8f6aa6ed7 Use new UITabBarController API on iOS 18 2024-08-19 13:29:48 -04:00
Shadowfacts 348dcc558c Fix profile page switching on iOS 18 2024-08-19 11:34:17 -04:00
Shadowfacts 703f6f695b Update Sentry and swift-url 2024-08-19 11:33:07 -04:00
Shadowfacts fdbfe49a7c Improve tab switching animation in non-pure-black dark mode on iOS 18 2024-08-19 11:32:29 -04:00
Shadowfacts 3f0dd599b3 Fix compiling with Xcode 16 2024-08-19 11:31:10 -04:00
44 changed files with 1505 additions and 643 deletions

View File

@ -33,11 +33,11 @@ public enum DuckAttemptAction {
extension UIViewController { extension UIViewController {
@available(iOS 16.0, *) @available(iOS 16.0, *)
public func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool = false) -> Bool { public func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool = false, completion: (() -> Void)? = nil) -> Bool {
var cur: UIViewController? = self var cur: UIViewController? = self
while let vc = cur { while let vc = cur {
if let container = vc as? DuckableContainerViewController { if let container = vc as? DuckableContainerViewController {
container.presentDuckable(viewController, animated: animated, isDucked: isDucked, completion: nil) container._presentDuckable(viewController, animated: animated, isDucked: isDucked, completion: completion)
return true return true
} else { } else {
cur = vc.parent cur = vc.parent

View File

@ -58,7 +58,7 @@ public class DuckableContainerViewController: UIViewController {
]) ])
} }
func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool, completion: (() -> Void)?) { func _presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
guard case .idle = state else { guard case .idle = state else {
if animated, if animated,
case .ducked(_, placeholder: let placeholder) = state { case .ducked(_, placeholder: let placeholder) = state {

View File

@ -16,7 +16,7 @@ let package = Package(
], ],
dependencies: [ dependencies: [
// Dependencies declare other packages that this package depends on. // Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/karwa/swift-url.git", branch: "main"), .package(url: "https://github.com/karwa/swift-url.git", exact: "0.4.2"),
], ],
targets: [ targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets are the basic building blocks of a package. A target can define a module or a test suite.

View File

@ -129,6 +129,10 @@
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; }; D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; };
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; }; D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; };
D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; }; D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; };
D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */; };
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */; };
D64A50BC2C74F8F4009D7193 /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */; };
D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */; };
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; }; D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; }; D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; }; D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
@ -215,15 +219,12 @@
D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; }; D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; };
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */; }; D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */; };
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; }; D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; };
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */; };
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; }; D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; }; D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; }; D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; };
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; }; D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; };
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; }; D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; };
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; }; D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; };
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */; };
D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */; }; D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */; };
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */; }; D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */; };
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; }; D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; };
@ -560,6 +561,10 @@
D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationCollectionViewCell.swift; sourceTree = "<group>"; }; D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedNotificationCollectionViewCell.swift; sourceTree = "<group>"; }; D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = "<group>"; }; D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMainTabBarViewController.swift; sourceTree = "<group>"; };
D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMainTabBarViewController.swift; sourceTree = "<group>"; };
D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInstanceViewController.swift; sourceTree = "<group>"; };
D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptableNavigationController.swift; sourceTree = "<group>"; };
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; }; D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; }; D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; }; D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
@ -649,15 +654,12 @@
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; }; D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; };
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.swift; sourceTree = "<group>"; }; D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.swift; sourceTree = "<group>"; };
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; }; D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeaturedProfileCollectionViewCell.xib; sourceTree = "<group>"; };
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; }; D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; }; D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = "<group>"; }; D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = "<group>"; };
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; }; D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; }; D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; };
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = "<group>"; }; D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = "<group>"; };
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FindInstanceViewController.swift; path = Tusker/Screens/FindInstanceViewController.swift; sourceTree = SOURCE_ROOT; };
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidescreenNavigationPrefsView.swift; sourceTree = "<group>"; }; D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidescreenNavigationPrefsView.swift; sourceTree = "<group>"; };
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowSceneDelegate+Close.swift"; sourceTree = "<group>"; }; D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowSceneDelegate+Close.swift"; sourceTree = "<group>"; };
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = "<group>"; }; D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = "<group>"; };
@ -977,7 +979,7 @@
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */, D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */,
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */, D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */,
D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */, D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */,
D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */, D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */,
D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */, D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */,
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */, D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */,
D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */, D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */,
@ -987,8 +989,6 @@
D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */, D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */,
D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */, D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */,
D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.swift */, D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.swift */,
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */,
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */,
D6C3F4FA299035650009FCFF /* TrendsViewController.swift */, D6C3F4FA299035650009FCFF /* TrendsViewController.swift */,
D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */, D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */,
D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */, D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */,
@ -1124,7 +1124,9 @@
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */, D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */,
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */, D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */,
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */, D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */,
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */, 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */,
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */, D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */, D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */,
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */, D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */,
@ -1556,6 +1558,7 @@
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */, D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */, D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
D61F759129365C6C00C0B37F /* CollectionViewController.swift */, D61F759129365C6C00C0B37F /* CollectionViewController.swift */,
D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */,
); );
path = Utilities; path = Utilities;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2014,7 +2017,6 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */, D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */,
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */,
D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */, D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */,
D691296E2BA75ADF005C58ED /* PrivacyInfo.xcprivacy in Resources */, D691296E2BA75ADF005C58ED /* PrivacyInfo.xcprivacy in Resources */,
D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */, D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */,
@ -2141,6 +2143,7 @@
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */, D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */, D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */, D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */,
D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */,
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */, D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */,
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */, D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */,
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */, D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
@ -2193,6 +2196,7 @@
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */, D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */,
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */, D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */, D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */,
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */, D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */,
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */, D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */, D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
@ -2233,6 +2237,7 @@
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */, D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */,
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */, D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */,
D64A50BC2C74F8F4009D7193 /* FindInstanceViewController.swift in Sources */,
D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */, D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */,
D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */, D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */,
D698F4672BD079800054DB14 /* AnnouncementsHostingController.swift in Sources */, D698F4672BD079800054DB14 /* AnnouncementsHostingController.swift in Sources */,
@ -2252,7 +2257,6 @@
D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */, D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */,
D6187BED2BFA840B00B3A281 /* FollowRequestNotificationViewController.swift in Sources */, D6187BED2BFA840B00B3A281 /* FollowRequestNotificationViewController.swift in Sources */,
D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */, D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */,
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */,
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */, D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */,
D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */, D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */,
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */, D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */,
@ -2272,7 +2276,6 @@
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */, D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */,
D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */, D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */,
D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */, D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */,
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */,
D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */, D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */,
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */, D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */,
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */, D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */,
@ -2341,6 +2344,7 @@
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */, D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */,
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */, D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */,
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */, D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */,
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */, D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
@ -3268,7 +3272,7 @@
repositoryURL = "https://github.com/getsentry/sentry-cocoa.git"; repositoryURL = "https://github.com/getsentry/sentry-cocoa.git";
requirement = { requirement = {
kind = upToNextMinorVersion; kind = upToNextMinorVersion;
minimumVersion = 8.21.0; minimumVersion = 8.33.0;
}; };
}; };
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */ = { D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */ = {
@ -3283,8 +3287,8 @@
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/karwa/swift-url"; repositoryURL = "https://github.com/karwa/swift-url";
requirement = { requirement = {
branch = main; kind = exactVersion;
kind = branch; version = 0.4.2;
}; };
}; };
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */

View File

@ -110,6 +110,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// we don't care about events like battery, keyboard show/hide // we don't care about events like battery, keyboard show/hide
options.enableAutoBreadcrumbTracking = false options.enableAutoBreadcrumbTracking = false
options.enableUserInteractionTracing = false options.enableUserInteractionTracing = false
options.profilesSampleRate = nil
options.tracesSampleRate = nil
options.beforeSend = { event in options.beforeSend = { event in
// just no, why would anyone need this information // just no, why would anyone need this information

View File

@ -9,10 +9,14 @@
import Foundation import Foundation
@propertyWrapper @propertyWrapper
class Box<Value> { final class Box<Value> {
var wrappedValue: Value var wrappedValue: Value
init(wrappedValue: Value) { init(wrappedValue: Value) {
self.wrappedValue = wrappedValue self.wrappedValue = wrappedValue
} }
var projectedValue: Box<Value> {
self
}
} }

View File

@ -63,7 +63,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
let draft = mastodonController.createDraft() let draft = mastodonController.createDraft()
let text = components.queryItems?.first(where: { $0.name == "text" })?.value let text = components.queryItems?.first(where: { $0.name == "text" })?.value
draft.text = text ?? "" draft.text = text ?? ""
rootViewController.compose(editing: draft, animated: true, isDucked: false) rootViewController.compose(editing: draft, animated: true, isDucked: false, completion: nil)
} }
} else { } else {
// Assume anything else is a search query // Assume anything else is a search query
@ -266,15 +266,24 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
mastodonController.initialize() mastodonController.initialize()
#if os(visionOS) #if os(visionOS)
return MainTabBarViewController(mastodonController: mastodonController) if #available(visionOS 2.0, *) {
return NewMainTabBarViewController(mastodonController: mastodonController)
} else {
return MainTabBarViewController(mastodonController: mastodonController)
}
#else #else
let split = MainSplitViewController(mastodonController: mastodonController) let mainVC: UIViewController & AccountSwitchableViewController
if #available(iOS 18.0, *) {
mainVC = NewMainTabBarViewController(mastodonController: mastodonController)
} else {
mainVC = MainSplitViewController(mastodonController: mastodonController)
}
if UIDevice.current.userInterfaceIdiom == .phone, if UIDevice.current.userInterfaceIdiom == .phone,
#available(iOS 16.0, *) { #available(iOS 16.0, *) {
// TODO: maybe the duckable container should be outside the account switching container // TODO: maybe the duckable container should be outside the account switching container
return DuckableContainerViewController(child: split) return DuckableContainerViewController(child: mainVC)
} else { } else {
return split return mainVC
} }
#endif #endif
} }

View File

@ -17,9 +17,7 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
let mastodonController: MastodonController let mastodonController: MastodonController
let mode: AccountFollowsViewController.Mode let mode: AccountFollowsViewController.Mode
var collectionView: UICollectionView! { private(set) var collectionView: UICollectionView!
view as? UICollectionView
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var state: State = .unloaded private var state: State = .unloaded
@ -40,7 +38,11 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadView() { override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .appGroupedBackground
var config = UICollectionLayoutListConfiguration(appearance: .plain) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appBackground config.backgroundColor = .appBackground
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
@ -65,10 +67,19 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll
section.readableContentInset(in: environment) section.readableContentInset(in: environment)
return section return section
} }
view = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
dataSource = createDataSource() dataSource = createDataSource()
} }

View File

@ -14,9 +14,7 @@ class AccountListViewController: UIViewController, CollectionViewController {
private let mastodonController: MastodonController private let mastodonController: MastodonController
private let accountIDs: [String] private let accountIDs: [String]
var collectionView: UICollectionView! { private(set) var collectionView: UICollectionView!
view as? UICollectionView
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
init(accountIDs: [String], mastodonController: MastodonController) { init(accountIDs: [String], mastodonController: MastodonController) {
@ -30,7 +28,11 @@ class AccountListViewController: UIViewController, CollectionViewController {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadView() { override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .appGroupedBackground
var config = UICollectionLayoutListConfiguration(appearance: .grouped) var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.backgroundColor = .appGroupedBackground config.backgroundColor = .appGroupedBackground
config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
@ -40,11 +42,25 @@ class AccountListViewController: UIViewController, CollectionViewController {
section.readableContentInset(in: environment) section.readableContentInset(in: environment)
return section return section
} }
view = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
dataSource = createDataSource() dataSource = createDataSource()
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts])
snapshot.appendItems(accountIDs)
dataSource.apply(snapshot, animatingDifferences: false)
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
@ -56,16 +72,7 @@ class AccountListViewController: UIViewController, CollectionViewController {
return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: itemIdentifier) return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: itemIdentifier)
} }
} }
override func viewDidLoad() {
super.viewDidLoad()
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts])
snapshot.appendItems(accountIDs)
dataSource.apply(snapshot, animatingDifferences: false)
}
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)

View File

@ -19,9 +19,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
var statusIDToScrollToOnLoad: String var statusIDToScrollToOnLoad: String
var showStatusesAutomatically = false var showStatusesAutomatically = false
var collectionView: UICollectionView! { private(set) var collectionView: UICollectionView!
view as? UICollectionView
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
init(for mainStatusID: String, state: CollapseState, conversationViewController: ConversationViewController) { init(for mainStatusID: String, state: CollapseState, conversationViewController: ConversationViewController) {
@ -38,7 +36,9 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadView() { override func viewDidLoad() {
super.viewDidLoad()
var config = UICollectionLayoutListConfiguration(appearance: .plain) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appSecondaryBackground config.backgroundColor = .appSecondaryBackground
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
@ -66,13 +66,19 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont
return section return section
} }
viewRespectsSystemMinimumLayoutMargins = false viewRespectsSystemMinimumLayoutMargins = false
view = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
// something about the autoresizing mask breaks resizing the vc
view.translatesAutoresizingMaskIntoConstraints = false
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
collectionView.refreshControl = UIRefreshControl() collectionView.refreshControl = UIRefreshControl()
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)

View File

@ -48,12 +48,18 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
configuration.headerMode = .supplementary configuration.headerMode = .supplementary
let layout = UICollectionViewCompositionalLayout.list(using: configuration) let layout = UICollectionViewCompositionalLayout.list(using: configuration)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView) view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
dataSource = createDataSource() dataSource = createDataSource()
applyInitialSnapshot() applyInitialSnapshot()

View File

@ -1,152 +0,0 @@
//
// FeaturedProfileCollectionViewCell.swift
// Tusker
//
// Created by Shadowfacts on 2/6/21.
// Copyright © 2021 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
class FeaturedProfileCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var clippingView: UIView!
@IBOutlet weak var headerImageView: UIImageView!
@IBOutlet weak var avatarContainerView: UIView!
@IBOutlet weak var avatarImageView: UIImageView!
@IBOutlet weak var displayNameLabel: AccountDisplayNameLabel!
@IBOutlet weak var noteTextView: StatusContentTextView!
var account: Account?
private var accountImagesTask: Task<Void, Never>?
deinit {
accountImagesTask?.cancel()
}
override func awakeFromNib() {
super.awakeFromNib()
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
avatarContainerView.layer.cornerCurve = .continuous
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
avatarImageView.layer.cornerCurve = .continuous
displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
displayNameLabel.adjustsFontForContentSizeCategory = true
noteTextView.adjustsFontForContentSizeCategory = true
noteTextView.textContainer.lineBreakMode = .byTruncatingTail
noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
backgroundColor = .clear
clippingView.backgroundColor = .appBackground
clippingView.layer.cornerRadius = 5
clippingView.layer.cornerCurve = .continuous
clippingView.layer.borderWidth = 1
clippingView.layer.masksToBounds = true
layer.shadowOpacity = 0.2
layer.shadowRadius = 8
layer.shadowOffset = .zero
layer.masksToBounds = false
updateLayerColors()
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
func updateUI(account: Account) {
self.account = account
displayNameLabel.updateForAccountDisplayName(account: account)
noteTextView.setBodyTextFromHTML(account.note)
noteTextView.setEmojis(account.emojis, identifier: account.id)
avatarImageView.image = nil
headerImageView.image = nil
accountImagesTask?.cancel()
accountImagesTask = Task {
await updateImages(account: account)
}
}
private nonisolated func updateImages(account: Account) async {
await withTaskGroup(of: Void.self) { group in
group.addTask {
guard let avatar = account.avatar,
let image = await ImageCache.avatars.get(avatar).1 else {
return
}
await MainActor.run {
self.avatarImageView.image = image
}
}
group.addTask {
guard let header = account.header,
let image = await ImageCache.headers.get(header).1 else {
return
}
await MainActor.run {
self.headerImageView.image = image
}
}
await group.waitForAll()
}
}
private func updateLayerColors() {
if traitCollection.userInterfaceStyle == .dark {
clippingView.layer.borderColor = UIColor.darkGray.withAlphaComponent(0.5).cgColor
layer.shadowColor = UIColor.darkGray.cgColor
} else {
clippingView.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
layer.shadowColor = UIColor.black.cgColor
}
}
// Unneeded on visionOS because there is no light/dark mode
#if !os(visionOS)
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
updateLayerColors()
}
#endif
override func layoutSubviews() {
super.layoutSubviews()
layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: 5, cornerHeight: 5, transform: nil)
}
@objc private func preferencesChanged() {
avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView)
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView)
if let account = account {
displayNameLabel.updateForAccountDisplayName(account: account)
}
}
// MARK: Accessibility
override var isAccessibilityElement: Bool {
get { true }
set {}
}
override var accessibilityAttributedLabel: NSAttributedString? {
get {
guard let account else {
return nil
}
let s = NSMutableAttributedString(string: "\(account.displayNameWithoutCustomEmoji), ")
s.append(noteTextView.attributedText)
return s
}
set {}
}
}

View File

@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="FeaturedProfileCollectionViewCell" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
<autoresizingMask key="autoresizingMask"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YkJ-rV-f3C">
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="bo4-Sd-caI">
<rect key="frame" x="0.0" y="0.0" width="400" height="66"/>
<color key="backgroundColor" systemColor="systemGray5Color"/>
<constraints>
<constraint firstAttribute="height" constant="66" id="9Aa-Up-chJ"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RQe-uE-TEv">
<rect key="frame" x="8" y="34" width="64" height="64"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4wd-wq-Sh2">
<rect key="frame" x="2" y="2" width="60" height="60"/>
<constraints>
<constraint firstAttribute="width" constant="60" id="Xyl-Ry-J3r"/>
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="height" multiplier="1:1" id="YEc-fT-FRB"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="width" secondItem="RQe-uE-TEv" secondAttribute="height" multiplier="1:1" id="4vR-IF-yS8"/>
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="width" constant="4" id="52Q-zq-k28"/>
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerY" secondItem="RQe-uE-TEv" secondAttribute="centerY" id="Ped-H7-QtP"/>
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerX" secondItem="RQe-uE-TEv" secondAttribute="centerX" id="bRk-uJ-JGg"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="voW-Is-1b2" customClass="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="76" y="72" width="316" height="24"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bvj-F0-ggC" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="8" y="102" width="384" height="98"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" systemColor="labelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="bvj-F0-ggC" secondAttribute="trailing" constant="8" id="1sd-Df-jR1"/>
<constraint firstItem="bvj-F0-ggC" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" constant="8" id="35h-Wh-fvk"/>
<constraint firstItem="voW-Is-1b2" firstAttribute="top" relation="greaterThanOrEqual" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="39l-yo-g8V"/>
<constraint firstItem="bo4-Sd-caI" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" id="3lQ-uN-93N"/>
<constraint firstAttribute="trailing" secondItem="voW-Is-1b2" secondAttribute="trailing" constant="8" id="Ckp-Bq-lB5"/>
<constraint firstItem="bo4-Sd-caI" firstAttribute="top" secondItem="YkJ-rV-f3C" secondAttribute="top" id="DWh-S5-PLQ"/>
<constraint firstAttribute="bottom" secondItem="bvj-F0-ggC" secondAttribute="bottom" id="MH3-7E-THx"/>
<constraint firstItem="RQe-uE-TEv" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" constant="8" id="Tzo-aN-Bxq"/>
<constraint firstItem="voW-Is-1b2" firstAttribute="bottom" secondItem="4wd-wq-Sh2" secondAttribute="bottom" id="Wk6-u2-azz"/>
<constraint firstItem="RQe-uE-TEv" firstAttribute="centerY" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="bon-bj-qnk"/>
<constraint firstItem="bvj-F0-ggC" firstAttribute="top" secondItem="RQe-uE-TEv" secondAttribute="bottom" constant="4" id="dyg-LN-BDn"/>
<constraint firstItem="voW-Is-1b2" firstAttribute="leading" secondItem="RQe-uE-TEv" secondAttribute="trailing" constant="4" id="shC-67-vC2"/>
<constraint firstAttribute="trailing" secondItem="bo4-Sd-caI" secondAttribute="trailing" id="wZn-gO-zue"/>
</constraints>
</view>
</subviews>
</view>
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="YkJ-rV-f3C" secondAttribute="trailing" id="Dy3-h1-zfM"/>
<constraint firstItem="YkJ-rV-f3C" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="Gld-3x-oE0"/>
<constraint firstItem="YkJ-rV-f3C" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="NIV-n8-4Rl"/>
<constraint firstAttribute="bottom" secondItem="YkJ-rV-f3C" secondAttribute="bottom" id="zNw-2z-Hlx"/>
</constraints>
<connections>
<outlet property="avatarContainerView" destination="RQe-uE-TEv" id="tBI-fT-26P"/>
<outlet property="avatarImageView" destination="4wd-wq-Sh2" id="rba-cv-8fb"/>
<outlet property="clippingView" destination="YkJ-rV-f3C" id="hLI-4z-yIc"/>
<outlet property="displayNameLabel" destination="voW-Is-1b2" id="XVS-4d-PKx"/>
<outlet property="headerImageView" destination="bo4-Sd-caI" id="YkL-Wi-BXb"/>
<outlet property="noteTextView" destination="bvj-F0-ggC" id="Bbm-ai-bu1"/>
</connections>
<point key="canvasLocation" x="535" y="428"/>
</collectionViewCell>
</objects>
<resources>
<systemColor name="labelColor">
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemGray5Color">
<color red="0.89803921568627454" green="0.89803921568627454" blue="0.91764705882352937" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@ -46,8 +46,8 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
collectionView.allowsFocus = true collectionView.allowsFocus = true
view.addSubview(collectionView) view.addSubview(collectionView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]) ])

View File

@ -53,12 +53,18 @@ class TrendingHashtagsViewController: UIViewController, CollectionViewController
} }
let layout = UICollectionViewCompositionalLayout.list(using: config) let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView) view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
cell.indicator.startAnimating() cell.indicator.startAnimating()
} }

View File

@ -40,6 +40,8 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
title = NSLocalizedString("Trending Links", comment: "trending links screen title") title = NSLocalizedString("Trending Links", comment: "trending links screen title")
view.backgroundColor = .appGroupedBackground
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
switch dataSource.sectionIdentifier(for: sectionIndex) { switch dataSource.sectionIdentifier(for: sectionIndex) {
case nil: case nil:
@ -80,8 +82,8 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
collectionView.allowsFocus = true collectionView.allowsFocus = true
view.addSubview(collectionView) view.addSubview(collectionView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]) ])

View File

@ -14,9 +14,7 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
private let mastodonController: MastodonController private let mastodonController: MastodonController
let filterer: Filterer let filterer: Filterer
var collectionView: UICollectionView! { private(set) var collectionView: UICollectionView!
view as? UICollectionView
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var loaded = false private var loaded = false
@ -34,7 +32,9 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadView() { override func viewDidLoad() {
super.viewDidLoad()
var config = UICollectionLayoutListConfiguration(appearance: .plain) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
(collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
@ -62,12 +62,22 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
section.readableContentInset(in: environment) section.readableContentInset(in: environment)
return section return section
} }
view = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
dataSource = createDataSource() dataSource = createDataSource()
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController)
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
@ -96,12 +106,6 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
} }
} }
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController)
}
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)

View File

@ -44,6 +44,8 @@ class TrendsViewController: UIViewController, CollectionViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .appGroupedBackground
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex] let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
@ -114,13 +116,19 @@ class TrendsViewController: UIViewController, CollectionViewController {
} }
} }
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.backgroundColor = .appGroupedBackground collectionView.backgroundColor = .appGroupedBackground
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView) view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
dataSource = createDataSource() dataSource = createDataSource()
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)

View File

@ -11,6 +11,7 @@ import UserAccounts
@MainActor @MainActor
protocol FastAccountSwitcherViewControllerDelegate: AnyObject { protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
/// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached. /// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached.
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
@ -31,7 +32,7 @@ class FastAccountSwitcherViewController: UIViewController {
#endif #endif
private var touchBeganFeedbackWorkItem: DispatchWorkItem? private var touchBeganFeedbackWorkItem: DispatchWorkItem?
var itemOrientation: ItemOrientation = .iconsTrailing private var itemOrientation: ItemOrientation = .iconsTrailing
init() { init() {
super.init(nibName: "FastAccountSwitcherViewController", bundle: .main) super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
@ -60,6 +61,9 @@ class FastAccountSwitcherViewController: UIViewController {
} }
func show() { func show() {
if let delegate {
itemOrientation = delegate.fastAccountSwitcherItemOrientation(self)
}
createAccountViews() createAccountViews()
// add after creating account views so that the presenter can align based on them // add after creating account views so that the presenter can align based on them
delegate?.fastAccountSwitcherAddToViewHierarchy(self) delegate?.fastAccountSwitcherAddToViewHierarchy(self)

View File

@ -56,6 +56,10 @@ class ListTimelineViewController: TimelineViewController {
} }
private func createNoContentView() { private func createNoContentView() {
guard noContentView == nil else {
return
}
let title = UILabel() let title = UILabel()
title.textColor = .secondaryLabel title.textColor = .secondaryLabel
title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)! title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
@ -133,6 +137,9 @@ class ListTimelineViewController: TimelineViewController {
override func handleReplaceAllItems(_ timelineItems: [String]) async { override func handleReplaceAllItems(_ timelineItems: [String]) async {
if timelineItems.isEmpty { if timelineItems.isEmpty {
createNoContentView() createNoContentView()
} else {
noContentView?.removeFromSuperview()
noContentView = nil
} }
await super.handleReplaceAllItems(timelineItems) await super.handleReplaceAllItems(timelineItems)
} }

View File

@ -19,9 +19,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
private let predicateTitle: String private let predicateTitle: String
private let request: (RequestRange) -> Request<[TryDecode<Status>]> private let request: (RequestRange) -> Request<[TryDecode<Status>]>
var collectionView: UICollectionView! { private(set) var collectionView: UICollectionView!
view as? UICollectionView
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var state = State.unloaded private var state = State.unloaded
@ -43,7 +41,9 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadView() { override func viewDidLoad() {
super.viewDidLoad()
var config = UICollectionLayoutListConfiguration(appearance: .plain) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appBackground config.backgroundColor = .appBackground
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
@ -71,12 +71,30 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
section.readableContentInset(in: environment) section.readableContentInset(in: environment)
return section return section
} }
view = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
dataSource = createDataSource() dataSource = createDataSource()
#if !targetEnvironment(macCatalyst)
collectionView.refreshControl = UIRefreshControl()
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
#endif
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh \(predicateTitle)"))
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController)
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext)
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
@ -97,20 +115,6 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
} }
} }
override func viewDidLoad() {
super.viewDidLoad()
#if !targetEnvironment(macCatalyst)
collectionView.refreshControl = UIRefreshControl()
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
#endif
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh \(predicateTitle)"))
NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController)
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext)
}
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)

View File

@ -39,7 +39,16 @@ class AccountSwitchingContainerViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
embedChild(root) addChild(root)
root.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(root.view)
NSLayoutConstraint.activate([
root.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
root.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
root.view.topAnchor.constraint(equalTo: view.topAnchor),
root.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
root.didMove(toParent: self)
} }
override func didReceiveMemoryWarning() { override func didReceiveMemoryWarning() {
@ -147,9 +156,9 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
return root.stateRestorationActivity() return root.stateRestorationActivity()
} }
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) { func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
loadViewIfNeeded() loadViewIfNeeded()
root.compose(editing: draft, animated: animated, isDucked: isDucked) root.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion)
} }
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
@ -157,11 +166,6 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
root.select(route: route, animated: animated, completion: completion) root.select(route: route, animated: animated, completion: completion)
} }
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
loadViewIfNeeded()
return root.getTabController(tab: tab)
}
func getNavigationDelegate() -> TuskerNavigationDelegate? { func getNavigationDelegate() -> TuskerNavigationDelegate? {
loadViewIfNeeded() loadViewIfNeeded()
return root.getNavigationDelegate() return root.getNavigationDelegate()

View File

@ -0,0 +1,195 @@
//
// BaseMainTabBarViewController.swift
// Tusker
//
// Created by Shadowfacts on 8/19/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UIKit
class BaseMainTabBarViewController: UITabBarController, FastAccountSwitcherViewControllerDelegate {
let mastodonController: MastodonController
#if !os(visionOS)
private(set) var fastAccountSwitcher: FastAccountSwitcherViewController!
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
#endif
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func show(_ vc: UIViewController, sender: Any?) {
if let nav = selectedViewController as? UINavigationController {
nav.pushViewController(vc, animated: true)
} else {
present(vc, animated: true)
}
}
// Fast account switcher is not supported on visionOS
#if !os(visionOS)
func setupFastAccountSwitcher() {
fastAccountSwitcher = FastAccountSwitcherViewController()
fastAccountSwitcher.delegate = self
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture())
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped))
tapRecognizer.cancelsTouchesInView = false
tabBar.addGestureRecognizer(tapRecognizer)
if findMyProfileTabBarButton() != nil {
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(fastSwitcherIndicator)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// i hate that we have to do this so often :S
// but doing it only in viewWillAppear makes it not appear initially
// doing it in viewWillAppear inside a DispatchQueue.main.async works initially but then it disappears when long-pressed
repositionFastSwitcherIndicator()
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
repositionFastSwitcherIndicator()
}
private func repositionFastSwitcherIndicator() {
guard let myProfileButton = findMyProfileTabBarButton(),
myProfileButton.window != nil,
let fastSwitcherIndicator else {
fastSwitcherIndicator?.isHidden = true
return
}
fastSwitcherIndicator.isHidden = false
NSLayoutConstraint.deactivate(fastSwitcherConstraints)
let isPortrait = view.bounds.width < view.bounds.height
if traitCollection.horizontalSizeClass == .compact && isPortrait {
fastSwitcherConstraints = [
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor, constant: -4),
// tab bar button image width is 30
fastSwitcherIndicator.leftAnchor.constraint(equalTo: myProfileButton.centerXAnchor, constant: 15 + 2),
]
} else {
fastSwitcherConstraints = [
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor),
fastSwitcherIndicator.trailingAnchor.constraint(equalTo: myProfileButton.trailingAnchor),
]
}
NSLayoutConstraint.activate(fastSwitcherConstraints)
}
private func findMyProfileTabBarButton() -> UIView? {
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") }
let tabCount: Int?
if #available(iOS 18.0, *) {
tabCount = viewControllers?.count ?? tabs.count
} else {
tabCount = viewControllers?.count
}
// sanity check that there is 1 button per VC
guard tabBarButtons.count == tabCount,
let myProfileButton = tabBarButtons.last else {
return nil
}
return myProfileButton
}
@objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) {
fastAccountSwitcher.hide()
}
#endif // !os(visionOS)
// MARK: FastAccountSwitcherViewControllerDelegate
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
return .iconsTrailing
}
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
#if !os(visionOS)
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(fastAccountSwitcher.view)
NSLayoutConstraint.activate([
fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor),
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
// The safe area insets don't automatically propagate for some reason, so do it ourselves.
fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
#endif // !os(visionOS)
}
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
#if !os(visionOS)
guard let myProfileButton = findMyProfileTabBarButton() else {
return false
}
let locationInButton = myProfileButton.convert(point, from: tabBar)
return myProfileButton.bounds.contains(locationInButton)
#else
return false
#endif // !os(visionOS)
}
}
extension BaseMainTabBarViewController: TuskerNavigationDelegate {
var apiController: MastodonController! { mastodonController }
}
extension BaseMainTabBarViewController: StateRestorableViewController {
func stateRestorationActivity() -> NSUserActivity? {
var activity: NSUserActivity?
if let presentedNav = presentedViewController as? UINavigationController,
let compose = presentedNav.viewControllers.first as? ComposeHostingController {
let draft = compose.controller.draft
activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID)
} else if let vc = (selectedViewController as? any NavigationControllerProtocol)?.topViewController as? StateRestorableViewController {
activity = vc.stateRestorationActivity()
}
if activity == nil {
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find StateRestorableViewController")
}
return activity
}
}
extension BaseMainTabBarViewController: BackgroundableViewController {
func sceneDidEnterBackground() {
if let selectedVC = selectedViewController as? BackgroundableViewController {
selectedVC.sceneDidEnterBackground()
}
}
}
extension BaseMainTabBarViewController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
guard presentedViewController == nil else {
return .stop
}
guard let vc = selectedViewController as? StatusBarTappableViewController else {
return .continue
}
return vc.handleStatusBarTapped(xPosition: xPosition)
}
}

View File

@ -23,8 +23,8 @@ extension DuckableContainerViewController: AccountSwitchableViewController {
return activity return activity
} }
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) { func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
(child as? TuskerRootViewController)?.compose(editing: draft, animated: animated, isDucked: isDucked) (child as? TuskerRootViewController)?.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion)
} }
func getNavigationDelegate() -> TuskerNavigationDelegate? { func getNavigationDelegate() -> TuskerNavigationDelegate? {
@ -39,10 +39,6 @@ extension DuckableContainerViewController: AccountSwitchableViewController {
(child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion) (child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion)
} }
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
return (child as? TuskerRootViewController)?.getTabController(tab: tab)
}
func performSearch(query: String) { func performSearch(query: String) {
(child as? TuskerRootViewController)?.performSearch(query: query) (child as? TuskerRootViewController)?.performSearch(query: query)
} }

View File

@ -11,14 +11,14 @@ import UserAccounts
class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell { class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
private var verticalImageInset: CGFloat { static var verticalImageInset: CGFloat {
if UIDevice.current.userInterfaceIdiom == .mac { if UIDevice.current.userInterfaceIdiom == .mac {
return (28 - avatarImageSize) / 2 return (28 - avatarImageSize) / 2
} else { } else {
return (44 - avatarImageSize) / 2 return (44 - avatarImageSize) / 2
} }
} }
private var avatarImageSize: CGFloat { static var avatarImageSize: CGFloat {
if UIDevice.current.userInterfaceIdiom == .mac { if UIDevice.current.userInterfaceIdiom == .mac {
return 20 return 20
} else { } else {
@ -72,11 +72,11 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
return return
} }
config.image = image config.image = image
config.directionalLayoutMargins.top = self.verticalImageInset config.directionalLayoutMargins.top = MainSidebarMyProfileCollectionViewCell.verticalImageInset
config.directionalLayoutMargins.bottom = self.verticalImageInset config.directionalLayoutMargins.bottom = MainSidebarMyProfileCollectionViewCell.verticalImageInset
config.imageProperties.maximumSize = CGSize(width: self.avatarImageSize, height: self.avatarImageSize) config.imageProperties.maximumSize = CGSize(width: MainSidebarMyProfileCollectionViewCell.avatarImageSize, height: MainSidebarMyProfileCollectionViewCell.avatarImageSize)
config.imageProperties.reservedLayoutSize = CGSize(width: UIListContentConfiguration.ImageProperties.standardDimension, height: 0) config.imageProperties.reservedLayoutSize = CGSize(width: UIListContentConfiguration.ImageProperties.standardDimension, height: 0)
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * self.avatarImageSize config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize
self.contentConfiguration = config self.contentConfiguration = config
} }
} }
@ -86,7 +86,7 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
guard var config = self.contentConfiguration as? UIListContentConfiguration else { guard var config = self.contentConfiguration as? UIListContentConfiguration else {
return return
} }
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarImageSize config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize
self.contentConfiguration = config self.contentConfiguration = config
} }

View File

@ -269,8 +269,9 @@ class MainSidebarViewController: UIViewController {
} }
private func showAddList() { private func showAddList() {
let service = CreateListService(mastodonController: mastodonController, present: { self.present($0, animated: true let service = CreateListService(mastodonController: mastodonController, present: {
) }) { list in self.present($0, animated: true)
}) { list in
let oldItem = self.selectedItem let oldItem = self.selectedItem
self.select(item: .list(list), animated: false) self.select(item: .list(list), animated: false)
let list = ListTimelineViewController(for: list, mastodonController: self.mastodonController) let list = ListTimelineViewController(for: list, mastodonController: self.mastodonController)
@ -370,7 +371,7 @@ extension MainSidebarViewController {
case let .savedInstance(url): case let .savedInstance(url):
return url.host! return url.host!
case .addSavedInstance: case .addSavedInstance:
return "Find An Instance..." return "Find an Instance..."
} }
} }

View File

@ -10,6 +10,7 @@ import UIKit
import Combine import Combine
import TuskerPreferences import TuskerPreferences
@available(iOS, obsoleted: 18.0)
class MainSplitViewController: UISplitViewController { class MainSplitViewController: UISplitViewController {
private let mastodonController: MastodonController private let mastodonController: MastodonController
@ -92,7 +93,6 @@ class MainSplitViewController: UISplitViewController {
if UIDevice.current.userInterfaceIdiom != .mac { if UIDevice.current.userInterfaceIdiom != .mac {
let switcher = FastAccountSwitcherViewController() let switcher = FastAccountSwitcherViewController()
fastAccountSwitcher = switcher fastAccountSwitcher = switcher
switcher.itemOrientation = .iconsLeading
switcher.view.translatesAutoresizingMaskIntoConstraints = false switcher.view.translatesAutoresizingMaskIntoConstraints = false
switcher.delegate = self switcher.delegate = self
// accessing .view unconditionally loads the view, which we don't want to happen // accessing .view unconditionally loads the view, which we don't want to happen
@ -447,10 +447,10 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
} }
// Transfer the selected tab from the tab bar VC to the sidebar // Transfer the selected tab from the tab bar VC to the sidebar
switch tabBarViewController.selectedTab { switch tabBarViewController.currentTab {
case .timelines, .notifications, .myProfile: case .timelines, .notifications, .myProfile:
// These tabs map 1 <-> 1 with sidebar items // These tabs map 1 <-> 1 with sidebar items
let item = MainSidebarViewController.Item.tab(tabBarViewController.selectedTab) let item = MainSidebarViewController.Item.tab(tabBarViewController.currentTab)
sidebar.select(item: item, animated: false) sidebar.select(item: item, animated: false)
doSelect(item: item) doSelect(item: item)
@ -578,20 +578,6 @@ extension MainSplitViewController: TuskerRootViewController {
completion?() completion?()
} }
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? {
if traitCollection.horizontalSizeClass == .compact {
return tabBarViewController?.getTabController(tab: tab)
} else {
if tab == .compose {
return nil
} else if case .tab(tab) = sidebar.selectedItem {
return secondaryNavController
} else {
return nil
}
}
}
func getNavigationDelegate() -> TuskerNavigationDelegate? { func getNavigationDelegate() -> TuskerNavigationDelegate? {
if traitCollection.horizontalSizeClass == .compact { if traitCollection.horizontalSizeClass == .compact {
return tabBarViewController.getNavigationDelegate() return tabBarViewController.getNavigationDelegate()
@ -677,6 +663,10 @@ extension MainSplitViewController: BackgroundableViewController {
} }
extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate { extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
return .iconsLeading
}
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
view.addSubview(fastAccountSwitcher.view) view.addSubview(fastAccountSwitcher.view)
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)! let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
@ -690,6 +680,7 @@ extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]) ])
} }
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
guard !isCollapsed, guard !isCollapsed,
let cell = sidebar.myProfileCell() else { let cell = sidebar.myProfileCell() else {

View File

@ -9,19 +9,12 @@
import UIKit import UIKit
import ComposeUI import ComposeUI
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { @available(iOS, obsoleted: 18.0)
class MainTabBarViewController: BaseMainTabBarViewController {
private let mastodonController: MastodonController
private var composePlaceholder: UIViewController! private var composePlaceholder: UIViewController!
#if !os(visionOS) var currentTab: Tab {
private var fastAccountSwitcher: FastAccountSwitcherViewController!
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
#endif
var selectedTab: Tab {
return Tab(rawValue: selectedIndex)! return Tab(rawValue: selectedIndex)!
} }
@ -33,16 +26,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
} }
} }
init(mastodonController: MastodonController) {
self.mastodonController = mastodonController
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -62,44 +45,13 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
embedInNavigationController(Tab.myProfile.createViewController(mastodonController)), embedInNavigationController(Tab.myProfile.createViewController(mastodonController)),
] ]
#if !os(visionOS) setupFastAccountSwitcher()
fastAccountSwitcher = FastAccountSwitcherViewController()
fastAccountSwitcher.delegate = self
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture())
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped))
tapRecognizer.cancelsTouchesInView = false
tabBar.addGestureRecognizer(tapRecognizer)
if findMyProfileTabBarButton() != nil {
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(fastSwitcherIndicator)
}
#endif
tabBar.isSpringLoaded = true tabBar.isSpringLoaded = true
}
// Fast account switcher is not supported on visionOS
#if !os(visionOS)
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// i hate that we have to do this so often :S view.backgroundColor = .appBackground
// but doing it only in viewWillAppear makes it not appear initially
// doing it in viewWillAppear inside a DispatchQueue.main.async works initially but then it disappears when long-pressed
repositionFastSwitcherIndicator()
} }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
repositionFastSwitcherIndicator()
}
#endif
func select(tab: Tab, dismissPresented: Bool) { func select(tab: Tab, dismissPresented: Bool) {
if tab == .compose { if tab == .compose {
compose(editing: nil) compose(editing: nil)
@ -117,53 +69,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
} }
} }
override func show(_ vc: UIViewController, sender: Any?) {
if let nav = selectedViewController as? UINavigationController {
nav.pushViewController(vc, animated: true)
} else {
present(vc, animated: true)
}
}
#if !os(visionOS)
private func repositionFastSwitcherIndicator() {
guard let myProfileButton = findMyProfileTabBarButton() else {
return
}
NSLayoutConstraint.deactivate(fastSwitcherConstraints)
let isPortrait = view.bounds.width < view.bounds.height
if traitCollection.horizontalSizeClass == .compact && isPortrait {
fastSwitcherConstraints = [
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor, constant: -4),
// tab bar button image width is 30
fastSwitcherIndicator.leftAnchor.constraint(equalTo: myProfileButton.centerXAnchor, constant: 15 + 2),
]
} else {
fastSwitcherConstraints = [
fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor),
fastSwitcherIndicator.trailingAnchor.constraint(equalTo: myProfileButton.trailingAnchor),
]
}
NSLayoutConstraint.activate(fastSwitcherConstraints)
}
#endif
private func findMyProfileTabBarButton() -> UIView? {
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") }
// sanity check that there is 1 button per VC
guard tabBarButtons.count == viewControllers!.count,
let myProfileButton = tabBarButtons.last else {
return nil
}
return myProfileButton
}
#if !os(visionOS)
@objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) {
fastAccountSwitcher.hide()
}
#endif
@objc func handleComposeKeyCommand() { @objc func handleComposeKeyCommand() {
compose(editing: nil) compose(editing: nil)
} }
@ -177,22 +82,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
} }
} }
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewController == composePlaceholder {
compose(editing: nil)
return false
}
if selectedIndex != NSNotFound,
viewController == viewControllers![selectedIndex],
let nav = viewController as? UINavigationController,
nav.viewControllers.count == 1,
let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController {
scrollableVC.tabBarScrollToTop()
return false
}
return true
}
func setViewController(_ viewController: UIViewController, forTab tab: Tab) { func setViewController(_ viewController: UIViewController, forTab tab: Tab) {
viewControllers![tab.rawValue] = viewController viewControllers![tab.rawValue] = viewController
} }
@ -227,7 +116,7 @@ extension MainTabBarViewController {
} }
} }
func getTabController(tab: Tab) -> UIViewController? { private func getTabController(tab: Tab) -> UIViewController? {
if tab == .compose { if tab == .compose {
return nil return nil
} else { } else {
@ -238,53 +127,21 @@ extension MainTabBarViewController {
} }
} }
#if !os(visionOS) extension MainTabBarViewController: UITabBarControllerDelegate {
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { if viewController == composePlaceholder {
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false compose(editing: nil)
view.addSubview(fastAccountSwitcher.view)
NSLayoutConstraint.activate([
fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor),
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
// The safe area insets don't automatically propagate for some reason, so do it ourselves.
fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
}
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
guard let myProfileButton = findMyProfileTabBarButton() else {
return false return false
} }
let locationInButton = myProfileButton.convert(point, from: tabBar) if selectedIndex != NSNotFound,
return myProfileButton.bounds.contains(locationInButton) viewController == viewControllers![selectedIndex],
} let nav = viewController as? UINavigationController,
} nav.viewControllers.count == 1,
#endif let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController {
scrollableVC.tabBarScrollToTop()
extension MainTabBarViewController: TuskerNavigationDelegate { return false
var apiController: MastodonController! { mastodonController }
}
extension MainTabBarViewController: StateRestorableViewController {
func stateRestorationActivity() -> NSUserActivity? {
var activity: NSUserActivity?
if let presentedNav = presentedViewController as? UINavigationController,
let compose = presentedNav.viewControllers.first as? ComposeHostingController {
let draft = compose.controller.draft
activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID)
} else if let vc = (selectedViewController as! UINavigationController).topViewController as? StateRestorableViewController {
activity = vc.stateRestorationActivity()
} }
if activity == nil { return true
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find StateRestorableViewController")
}
return activity
} }
} }
@ -348,24 +205,6 @@ extension MainTabBarViewController: TuskerRootViewController {
present(vc, animated: true, completion: completion) present(vc, animated: true, completion: completion)
return vc return vc
} }
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
guard presentedViewController == nil else {
return .stop
}
guard let vc = viewController(for: selectedTab) as? StatusBarTappableViewController else {
return .continue
}
return vc.handleStatusBarTapped(xPosition: xPosition)
}
}
extension MainTabBarViewController: BackgroundableViewController {
func sceneDidEnterBackground() {
if let selectedVC = selectedViewController as? BackgroundableViewController {
selectedVC.sceneDidEnterBackground()
}
}
} }
extension MainTabBarViewController: AccountSwitchableViewController { extension MainTabBarViewController: AccountSwitchableViewController {

View File

@ -0,0 +1,827 @@
//
// NewMainTabBarViewController.swift
// Tusker
//
// Created by Shadowfacts on 8/19/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UIKit
import Combine
import Pachyderm
import TuskerPreferences
@available(iOS 18.0, *)
final class NewMainTabBarViewController: BaseMainTabBarViewController {
private let composePlaceholder = UIViewController()
private var homeTab: UITab!
private var notificationsTab: UITab!
private var composeTab: UITab!
private var exploreTab: UITab!
private var bookmarksTab: UITab!
private var favoritesTab: UITab!
private var myProfileTab: UITab!
private var listsGroup: UITabGroup!
private var hashtagsGroup: UITabGroup!
private var instancesGroup: UITabGroup!
private var cancellables = Set<AnyCancellable>()
private var navigationStacks = [String: [UIViewController]]()
private var isCompact: Bool?
@Box fileprivate var myProfileCell: UIView?
private var sidebarTapRecognizer: UITapGestureRecognizer?
override func viewDidLoad() {
super.viewDidLoad()
mode = .tabSidebar
delegate = self
sidebar.delegate = self
tabBar.isSpringLoaded = true
view.backgroundColor = .appBackground
let viewControllerProvider = { [unowned self] (tab: UITab) -> UIViewController in
self.makeViewController(for: tab)
}
homeTab = UITab(title: "Home", image: UIImage(systemName: "house"), identifier: Tab.home.rawValue, viewControllerProvider: viewControllerProvider)
notificationsTab = UITab(title: "Notifications", image: UIImage(systemName: "bell"), identifier: Tab.notifications.rawValue, viewControllerProvider: viewControllerProvider)
composeTab = UITab(title: "Compose", image: UIImage(systemName: "pencil"), identifier: Tab.compose.rawValue, viewControllerProvider: viewControllerProvider)
exploreTab = UITab(title: "Explore", image: UIImage(systemName: "magnifyingglass"), identifier: Tab.explore.rawValue, viewControllerProvider: viewControllerProvider)
bookmarksTab = UITab(title: "Bookmarks", image: UIImage(systemName: "bookmark"), identifier: Tab.bookmarks.rawValue, viewControllerProvider: viewControllerProvider)
bookmarksTab.preferredPlacement = .optional
favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider)
favoritesTab.preferredPlacement = .optional
myProfileTab = MyProfileTab(mastodonController: mastodonController, viewControllerProvider: viewControllerProvider)
listsGroup = UITabGroup(title: "Lists", image: nil, identifier: Tab.lists.rawValue, children: []) { _ in
// this closure is necessary to prevent UIKit from crashing (FB14860961)
return AdaptableNavigationController()
}
listsGroup.preferredPlacement = .sidebarOnly
listsGroup.sidebarActions = [
UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in
self.showAddList()
})
]
reloadLists(mastodonController.lists)
hashtagsGroup = UITabGroup(title: "Hashtags", image: nil, identifier: Tab.hashtags.rawValue, children: []) { _ in
return AdaptableNavigationController()
}
hashtagsGroup.preferredPlacement = .sidebarOnly
hashtagsGroup.sidebarActions = [
UIAction(title: "Add Hashtag…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in
self.showAddSavedHashtag()
})
]
reloadHashtags()
instancesGroup = UITabGroup(title: "Instance Timelines", image: nil, identifier: Tab.instances.rawValue, children: []) { _ in
return AdaptableNavigationController()
}
instancesGroup.preferredPlacement = .sidebarOnly
instancesGroup.sidebarActions = [
UIAction(title: "Find an Instance…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in
self.showAddSavedInstance()
})
]
reloadSavedInstances()
if UIDevice.current.userInterfaceIdiom == .phone {
self.tabs = [
homeTab,
notificationsTab,
composeTab,
exploreTab,
myProfileTab,
]
} else {
self.updatePadTabs()
registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: NewMainTabBarViewController, previousTraitCollection) in
self.updatePadTabs()
let vcToUpdate = self.selectedTab!.parent?.viewController ?? self.selectedTab!.viewController!
self.updateViewControllerSafeAreaInsets(vcToUpdate)
}
mastodonController.$lists
.sink { [unowned self] in self.reloadLists($0) }
.store(in: &cancellables)
mastodonController.$followedHashtags
.map { _ in () }
.merge(with: NotificationCenter.default.publisher(for: .savedHashtagsChanged).map { _ in () })
.sink { [unowned self] in self.reloadHashtags() }
.store(in: &cancellables)
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
}
setupFastAccountSwitcher()
}
private func updatePadTabs() {
let wasCompact = isCompact
if self.traitCollection.horizontalSizeClass == .compact {
isCompact = true
var exploreNavStack: [UIViewController]? = nil
if let parent = selectedTab?.parent,
parent === listsGroup || parent === hashtagsGroup || parent === instancesGroup {
let nav = parent.viewController as! any NavigationControllerProtocol
exploreNavStack = nav.viewControllers
nav.viewControllers = []
}
self.tabs = [
homeTab,
notificationsTab,
composeTab,
exploreTab,
myProfileTab,
]
if let exploreNavStack {
selectedTab = exploreTab
let nav = exploreTab.viewController as! any NavigationControllerProtocol
nav.viewControllers = exploreNavStack
}
} else {
isCompact = false
var newTabAndNavigationStack: (UITab, [UIViewController])? = nil
if wasCompact == true,
selectedTab == exploreTab {
let nav = exploreTab.viewController as! any NavigationControllerProtocol
// skip over the ExploreViewController
if nav.viewControllers.count > 1 {
var newTab: UITab?
switch nav.viewControllers[1] {
case let listVC as ListTimelineViewController:
if let tab = listsGroup.tab(forIdentifier: ListTab.identifier(for: listVC.list)) {
newTab = tab
}
case let hashtagVC as HashtagTimelineViewController:
if let tab = hashtagsGroup.tab(forIdentifier: HashtagTab.identifier(for: hashtagVC.hashtagName)) {
newTab = tab
}
default:
break
}
if let newTab {
newTabAndNavigationStack = (newTab, Array(nav.viewControllers[1...]))
nav.viewControllers = [
nav.viewControllers[0], // leave the ExploreVC in place
InlineTrendsViewController(mastodonController: mastodonController), // re-insert an InlineTrendsVC
]
}
}
}
self.tabs = [
homeTab,
notificationsTab,
exploreTab,
bookmarksTab,
favoritesTab,
myProfileTab,
composeTab,
listsGroup,
hashtagsGroup,
instancesGroup,
]
if let (tab, navStack) = newTabAndNavigationStack {
let nav = tab.parent!.viewController as! any NavigationControllerProtocol
nav.viewControllers = navStack
// Setting the tab now seems to be clobbered by the UITabBarController itself updating in response
// to the size class change. So wait until it finishes to do so.
DispatchQueue.main.async {
self.selectedTab = tab
}
}
}
}
private func makeViewController(for tab: UITab) -> UIViewController {
guard let tab = Tab(rawValue: tab.identifier) else {
fatalError("unreachable")
}
let root: UIViewController
switch tab {
case .home:
root = TimelinesPageViewController(mastodonController: mastodonController)
case .notifications:
root = NotificationsPageViewController(mastodonController: mastodonController)
case .compose:
return composePlaceholder
case .explore:
if UIDevice.current.userInterfaceIdiom == .phone {
root = ExploreViewController(mastodonController: mastodonController)
} else {
let nav = AdaptableNavigationController(viewControllersToPrependInCompact: [
ExploreViewController(mastodonController: mastodonController)
])
nav.viewControllers = [InlineTrendsViewController(mastodonController: mastodonController)]
return nav
}
case .bookmarks:
root = BookmarksViewController(mastodonController: mastodonController)
case .favorites:
root = FavoritesViewController(mastodonController: mastodonController)
case .myProfile:
root = MyProfileViewController(mastodonController: mastodonController)
case .lists, .hashtags, .instances:
fatalError("unreachable")
}
return embedInNavigationController(root)
}
private func embedInNavigationController(_ vc: UIViewController) -> UIViewController {
let nav = AdaptableNavigationController()
nav.viewControllers = [vc]
return nav
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if sidebarTapRecognizer == nil,
let sidebarView = findSidebarView() {
sidebarTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped))
sidebarTapRecognizer!.cancelsTouchesInView = false
sidebarView.addGestureRecognizer(sidebarTapRecognizer!)
}
}
private func reloadLists(_ lists: [List]) {
let viewControllerProvider = { [unowned self] (tab: UITab) in
let tab = tab as! ListTab
return ListTimelineViewController(for: tab.list, mastodonController: self.mastodonController)
}
listsGroup.children = lists.map { list in
ListTab(list: list, viewControllerProvider: viewControllerProvider)
}
}
private func reloadHashtags() {
let viewControllerProvider = { [unowned self] (tab: UITab) in
let tab = tab as! HashtagTab
return HashtagTimelineViewController(forNamed: tab.hashtagName, mastodonController: self.mastodonController)
}
var seenTags: Set<String> = []
var tabs: [UITab] = []
let savedReq = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
let saved = (try? mastodonController.persistentContainer.viewContext.fetch(savedReq)) ?? []
for hashtag in saved {
seenTags.insert(hashtag.name)
tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider))
}
let followedReq = FollowedHashtag.fetchRequest()
let followed = (try? mastodonController.persistentContainer.viewContext.fetch(followedReq)) ?? []
for hashtag in followed where !seenTags.contains(hashtag.name) {
tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider))
}
tabs.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
hashtagsGroup.children = tabs
}
@objc private func reloadSavedInstances() {
let viewControllerProvider = { [unowned self] (tab: UITab) in
let tab = tab as! InstanceTab
return InstanceTimelineViewController(for: tab.instance.url, parentMastodonController: self.mastodonController)
}
let req = SavedInstance.fetchRequest(account: mastodonController.accountInfo!)
req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)]
let instances = (try? mastodonController.persistentContainer.viewContext.fetch(req).uniques(by: \.url)) ?? []
instancesGroup.children = instances.map {
InstanceTab(instance: $0, viewControllerProvider: viewControllerProvider)
}
}
@objc func handleComposeKeyCommand() {
compose(editing: nil)
}
@objc private func sidebarTapped() {
fastAccountSwitcher?.hide()
}
private func showAddList() {
let service = CreateListService(mastodonController: mastodonController, present: {
self.present($0, animated: true)
}) { list in
let tab = self.listsGroup.tab(forIdentifier: ListTab.identifier(for: list))!
let listVC = tab.viewController as! ListTimelineViewController
listVC.presentEditOnAppear = true
self.selectedTab = tab
}
service.run()
}
private func showAddSavedHashtag() {
let addController = AddSavedHashtagViewController(mastodonController: mastodonController)
let nav = EnhancedNavigationViewController(rootViewController: addController)
present(nav, animated: true)
}
private func showAddSavedInstance() {
let findController = FindInstanceViewController(parentMastodonController: mastodonController)
findController.instanceTimelineDelegate = self
let nav = EnhancedNavigationViewController(rootViewController: findController)
present(nav, animated: true)
}
fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) {
guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else {
return
}
// When in sidebar mode, for multi column mode, don't leave an inset for the floating tab bar, because it leaves a massive gap.
// The floating tab bar seems to always be 88pt tall, regardless of, e.g., Dynamic Type size.
vc.additionalSafeAreaInsets = UIEdgeInsets(top: sidebar.isHidden ? 0 : -88, left: 0, bottom: 0, right: 0)
}
private func findSidebarView() -> UIView? {
var next = myProfileCell
while let cur = next {
if cur.superview?.superview === self.view {
return cur
} else {
next = cur.superview
}
}
return nil
}
#if !os(visionOS)
override func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation {
guard !sidebar.isHidden,
myProfileCell != nil else {
return super.fastAccountSwitcherItemOrientation(fastAccountSwitcher)
}
return .iconsLeading
}
override func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
guard !sidebar.isHidden,
let myProfileCell else {
super.fastAccountSwitcherAddToViewHierarchy(fastAccountSwitcher)
return
}
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(fastAccountSwitcher.view)
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
NSLayoutConstraint.activate([
currentAccount.centerYAnchor.constraint(equalTo: myProfileCell.centerYAnchor),
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: selectedTab!.viewController!.view.safeAreaLayoutGuide.leadingAnchor),
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
override func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
guard !sidebar.isHidden,
myProfileCell != nil else {
return super.fastAccountSwitcher(fastAccountSwitcher, triggerZoneContains: point)
}
return true
}
#endif
}
@available(iOS 18.0, *)
extension NewMainTabBarViewController {
enum Tab: String, Hashable, CaseIterable {
case home
case notifications
case compose
case explore
case bookmarks
case favorites
case myProfile
case lists
case hashtags
case instances
}
}
@available(iOS 18.0, *)
extension NewMainTabBarViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
if tab.identifier == Tab.compose.rawValue {
let currentTab = selectedTab
// returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254)
// but we need it to change to _something_ so that we can change back to the current tab
self.selectedTab = tab
self.selectedTab = currentTab
compose(editing: nil)
return false
} else if let selectedTab,
selectedTab == tab,
let nav = selectedViewController as? any NavigationControllerProtocol,
nav.viewControllers.count == 1,
let scrollableVC = nav.viewControllers[0] as? TabBarScrollableViewController {
scrollableVC.tabBarScrollToTop()
return false
} else {
return true
}
}
func tabBarController(_ tabBarController: UITabBarController, didSelectTab newTab: UITab, previousTab: UITab?) {
self.updateViewControllerSafeAreaInsets(newTab.viewController!)
// All tabs in a tab group deliberately share the same view controller, so we have to do this ourselves.
// I think this is pretty unfortunate API design--half the time, the tab bar controller takes care of
// this, but the rest of the time it's up to you.
// The managingNavigationController API would theoretically solve this, but split-screen/multi-column
// nav can't straightforwardly be implemented as UINavigationController subclasses.
// Unfortunately this, in turn, means that when switching between tabs in the same group, we don't
// get the new transition animation.
// This would be much less complicated if the controller just used the individual VCs of items in a group.
if let group = newTab.parent,
group === listsGroup || group === hashtagsGroup || group === instancesGroup,
let nav = group.viewController as? any NavigationControllerProtocol {
updateViewControllerSafeAreaInsets(nav)
if let previousTab {
navigationStacks[previousTab.identifier] = nav.viewControllers
}
if let existing = navigationStacks[newTab.identifier] {
nav.viewControllers = existing
} else if let newVC = newTab.viewController {
nav.viewControllers = [newVC]
} else {
fatalError("unreachable")
}
}
}
}
private var fastAccountSwitcherIndicator: UIView = {
let indicator = FastAccountSwitcherIndicatorView()
// need to explicitly set the frame to get it vertically centered
indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize)
return indicator
}()
@available(iOS 18.0, *)
extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate {
func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) {
let vc = selectedTab!.parent?.viewController ?? selectedTab!.viewController!
animator.addAnimations {
self.updateViewControllerSafeAreaInsets(vc)
vc.view.layoutIfNeeded()
}
}
func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, itemFor request: UITabSidebarItem.Request) -> UITabSidebarItem {
let item = UITabSidebarItem(request: request)
if case .tab(let tab) = request.content,
tab.identifier == Tab.myProfile.rawValue,
var config = item.contentConfiguration as? UIListContentConfiguration {
config.directionalLayoutMargins.top = MainSidebarMyProfileCollectionViewCell.verticalImageInset
config.directionalLayoutMargins.bottom = MainSidebarMyProfileCollectionViewCell.verticalImageInset
config.imageProperties.maximumSize = CGSize(width: MainSidebarMyProfileCollectionViewCell.avatarImageSize, height: MainSidebarMyProfileCollectionViewCell.avatarImageSize)
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize
if UIDevice.current.userInterfaceIdiom != .mac {
item.accessories = [
.customView(configuration: .init(customView: fastAccountSwitcherIndicator, placement: .trailing()))
]
item.contentConfiguration = MyProfileContentConfiguration(wrapped: config, view: $myProfileCell) { [unowned self] in
$0.addGestureRecognizer(self.fastAccountSwitcher.createSwitcherGesture())
}
} else {
item.contentConfiguration = config
}
}
return item
}
func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, contextMenuConfigurationFor tab: UITab) -> UIContextMenuConfiguration? {
guard let id = mastodonController.accountInfo?.id else {
return nil
}
let activity: NSUserActivity
if let listTab = tab as? ListTab {
let timelineActivity = UserActivityManager.showTimelineActivity(timeline: .list(id: listTab.list.id), accountID: id)
if let timelineActivity {
activity = timelineActivity
} else {
return nil
}
} else if let hashtagTab = tab as? HashtagTab {
let timelineActivity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtagTab.hashtagName), accountID: id)
if let timelineActivity {
activity = timelineActivity
} else {
return nil
}
} else if tab is InstanceTab {
// don't currently have a scene type for this
return nil
} else if let tabID = Tab(rawValue: tab.identifier) {
switch tabID {
case .home:
return nil
case .notifications:
activity = UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode, accountID: id)
case .explore:
activity = UserActivityManager.searchActivity(query: nil, accountID: id)
case .bookmarks:
activity = UserActivityManager.bookmarksActivity(accountID: id)
case .favorites:
// TODO
return nil
case .myProfile:
// no 'Open in New Window' activity for my profile, because the context menu clashes with the fast account switcher
return nil
case .compose:
activity = UserActivityManager.newPostActivity(accountID: id)
case .lists, .hashtags, .instances:
return nil
}
} else {
return nil
}
activity.displaysAuxiliaryScene = true
return UIContextMenuConfiguration(actionProvider: { _ in
var actions: [UIAction] = [
UIWindowScene.ActivationAction({ action in
return UIWindowScene.ActivationConfiguration(userActivity: activity)
})
]
if let listTab = tab as? ListTab {
actions.append(UIAction(title: "Delete List", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { [unowned self] _ in
Task {
let service = DeleteListService(list: listTab.list, mastodonController: self.mastodonController) {
self.present($0, animated: true)
}
await service.run()
}
}))
}
return UIMenu(children: actions)
})
}
}
@available(iOS 18.0, *)
extension NewMainTabBarViewController: TuskerRootViewController {
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
func doSelect() {
switch route {
case .timelines:
selectedTab = tab(forIdentifier: Tab.home.rawValue)
case .notifications:
selectedTab = tab(forIdentifier: Tab.notifications.rawValue)
case .myProfile:
selectedTab = tab(forIdentifier: Tab.myProfile.rawValue)
case .explore:
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
case .bookmarks:
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
let nav = getNavigationController()
nav.popToRootViewController(animated: animated)
nav.pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated)
case .list(let id):
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
if let list = mastodonController.getCachedList(id: id) {
let nav = getNavigationController()
nav.popToRootViewController(animated: animated)
nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated)
}
}
completion?()
}
if presentedViewController != nil {
dismiss(animated: animated) {
doSelect()
}
} else {
doSelect()
}
}
func getNavigationDelegate() -> (any TuskerNavigationDelegate)? {
return self
}
func getNavigationController() -> any NavigationControllerProtocol {
return selectedViewController as! any NavigationControllerProtocol
}
func performSearch(query: String) {
selectedTab = tab(forIdentifier: Tab.explore.rawValue)
guard let exploreNavController = selectedViewController as? any NavigationControllerProtocol,
let exploreController = exploreNavController.viewControllers.first as? ExploreViewController else {
return
}
exploreNavController.popToRootViewController(animated: false)
// setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time
if exploreController.isViewLoaded {
exploreController.searchController.isActive = true
} else {
exploreController.searchControllerStatusOnAppearance = true
// we still need to load the view so that we can setup the search query
exploreController.loadViewIfNeeded()
}
exploreController.searchController.searchBar.text = query
exploreController.resultsController.performSearch(query: query)
}
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? {
let vc = PreferencesNavigationController(mastodonController: mastodonController)
present(vc, animated: true, completion: completion)
return vc
}
}
@available(iOS 18.0, *)
extension NewMainTabBarViewController: AccountSwitchableViewController {
var isFastAccountSwitcherActive: Bool {
#if os(visionOS)
return false
#else
if let fastAccountSwitcher {
return !fastAccountSwitcher.view.isHidden
} else {
return false
}
#endif
}
}
@available(iOS 18.0, *)
extension NewMainTabBarViewController: InstanceTimelineViewControllerDelegate {
func didSaveInstance(url: URL) {
dismiss(animated: true) {
let tab = self.instancesGroup.tab(forIdentifier: InstanceTab.identifier(for: url))!
self.selectedTab = tab
}
}
func didUnsaveInstance(url: URL) {
dismiss(animated: true)
}
}
private struct MyProfileContentConfiguration: UIContentConfiguration {
let wrapped: any UIContentConfiguration
@Box var view: UIView?
let configureView: (UIView) -> Void
init(wrapped: any UIContentConfiguration, view: Box<UIView?>, configureView: @escaping (UIView) -> Void) {
self.wrapped = wrapped
self._view = view
self.configureView = configureView
}
func makeContentView() -> any UIView & UIContentView {
let view = wrapped.makeContentView()
self.view = view
configureView(view)
return view
}
func updated(for state: any UIConfigurationState) -> Self {
return .init(wrapped: wrapped.updated(for: state), view: $view, configureView: configureView)
}
}
@available(iOS 18.0, *)
private class MyProfileTab: UITab {
private let mastodonController: MastodonController
private var avatarStyle: AvatarStyle?
init(mastodonController: MastodonController, viewControllerProvider: @escaping (UITab) -> UIViewController) {
self.mastodonController = mastodonController
// try to add the avatar image synchronously if possible
var avatarImage: UIImage?
if !Preferences.shared.grayscaleImages,
let account = mastodonController.account,
let avatarURL = account.avatar,
let avatar = ImageCache.avatars.get(avatarURL) {
avatarImage = Self.renderAvatar(avatar.image)
self.avatarStyle = Preferences.shared.avatarStyle
}
let image = avatarImage ?? UIImage(systemName: "person")!
super.init(title: "My Profile", image: image, identifier: NewMainTabBarViewController.Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider)
if avatarImage == nil {
Task {
await updateAvatar()
}
}
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
private func updateAvatar() async {
guard let account = try? await mastodonController.getOwnAccount(),
let avatarURL = account.avatar,
let image = await ImageCache.avatars.get(avatarURL).1 else {
return
}
let maybeGrayscale = await ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) ?? image
let rendered = Self.renderAvatar(maybeGrayscale)
self.avatarStyle = Preferences.shared.avatarStyle
self.image = rendered
}
private static func renderAvatar(_ image: UIImage) -> UIImage {
let size = MainSidebarMyProfileCollectionViewCell.avatarImageSize
let radius = Preferences.shared.avatarStyle.cornerRadiusFraction * size
let rect = CGRect(x: 0, y: 0, width: size, height: size)
let renderer = UIGraphicsImageRenderer(bounds: rect)
let rendered = renderer.image { ctx in
UIBezierPath(roundedRect: rect, cornerRadius: radius).addClip()
image.draw(in: rect)
}
return rendered.withRenderingMode(.alwaysOriginal)
}
@objc private func preferencesChanged() {
if avatarStyle != nil,
avatarStyle != Preferences.shared.avatarStyle {
Task {
await updateAvatar()
}
}
}
}
@available(iOS 18.0, *)
private class ListTab: UITab {
let list: List
init(list: List, viewControllerProvider: @escaping (UITab) -> UIViewController) {
self.list = list
super.init(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: Self.identifier(for: list), viewControllerProvider: viewControllerProvider)
}
static func identifier(for list: List) -> String {
"list:\(list.id)"
}
}
@available(iOS 18.0, *)
private class HashtagTab: UITab {
let hashtagName: String
init(hashtagName: String, viewControllerProvider: @escaping (UITab) -> UIViewController) {
self.hashtagName = hashtagName
super.init(title: hashtagName, image: UIImage(systemName: "number"), identifier: Self.identifier(for: hashtagName), viewControllerProvider: viewControllerProvider)
}
static func identifier(for name: String) -> String {
"hashtag:\(name)"
}
}
@available(iOS 18.0, *)
private class InstanceTab: UITab {
let instance: SavedInstance
init(instance: SavedInstance, viewControllerProvider: @escaping (UITab) -> UIViewController) {
self.instance = instance
super.init(title: instance.url.host!, image: UIImage(systemName: "globe"), identifier: Self.identifier(for: instance), viewControllerProvider: viewControllerProvider)
}
static func identifier(for instance: SavedInstance) -> String {
"instance:\(instance.url.host!)"
}
static func identifier(for instanceURL: URL) -> String {
"instance:\(instanceURL.host!)"
}
}

View File

@ -11,9 +11,8 @@ import ComposeUI
@MainActor @MainActor
protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController { protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController {
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?)
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?)
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
func getNavigationDelegate() -> TuskerNavigationDelegate? func getNavigationDelegate() -> TuskerNavigationDelegate?
func getNavigationController() -> NavigationControllerProtocol func getNavigationController() -> NavigationControllerProtocol
func performSearch(query: String) func performSearch(query: String)

View File

@ -108,8 +108,8 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView) view.addSubview(collectionView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]) ])

View File

@ -38,8 +38,10 @@ class ProfileHeaderCollectionViewCell: UICollectionViewCell {
header.translatesAutoresizingMaskIntoConstraints = false header.translatesAutoresizingMaskIntoConstraints = false
contentView.embedSubview(header) contentView.embedSubview(header)
self.state = .view(header) self.state = .view(header)
case .view(_): case .view(let existing):
fatalError("profile header collection view cell already has view") if existing !== header {
fatalError("profile header collection view cell already has view")
}
} }
} }

View File

@ -17,7 +17,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
let filterer: Filterer let filterer: Filterer
private(set) var accountID: String! private(set) var accountID: String!
let kind: Kind let kind: Kind
var initialHeaderMode: HeaderMode? var headerViewMode: HeaderMode?
weak var profileHeaderDelegate: ProfileHeaderViewDelegate? weak var profileHeaderDelegate: ProfileHeaderViewDelegate?
private(set) var controller: TimelineLikeController<TimelineItem>! private(set) var controller: TimelineLikeController<TimelineItem>!
@ -26,11 +26,11 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
private var older: RequestRange? private var older: RequestRange?
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
var collectionView: UICollectionView! { private(set) var collectionView: UICollectionView!
view as? UICollectionView
}
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private(set) var headerCell: ProfileHeaderCollectionViewCell? var headerCell: ProfileHeaderCollectionViewCell? {
collectionView.cellForItem(at: IndexPath(item: 0, section: 0)) as? ProfileHeaderCollectionViewCell
}
var reconfigureVisibleItemsOnEndDecelerating: Bool = false var reconfigureVisibleItemsOnEndDecelerating: Bool = false
@ -54,7 +54,9 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadView() { override func viewDidLoad() {
super.viewDidLoad()
var config = UICollectionLayoutListConfiguration(appearance: .plain) var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.backgroundColor = .appBackground config.backgroundColor = .appBackground
config.leadingSwipeActionsConfigurationProvider = { [unowned self] in config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
@ -101,10 +103,18 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
return section return section
} }
} }
view = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
registerTimelineLikeCells() registerTimelineLikeCells()
dataSource = createDataSource() dataSource = createDataSource()
@ -113,10 +123,6 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
collectionView.refreshControl = UIRefreshControl() collectionView.refreshControl = UIRefreshControl()
collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
#endif #endif
}
override func viewDidLoad() {
super.viewDidLoad()
mastodonController.persistentContainer.accountSubject mastodonController.persistentContainer.accountSubject
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
@ -173,29 +179,29 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
switch itemIdentifier { switch itemIdentifier {
case .header(let id): case .header(let id):
if let headerCell = self.headerCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "headerCell", for: indexPath) as! ProfileHeaderCollectionViewCell
headerCell.view?.updateUI(for: id) switch self.headerViewMode {
return headerCell case nil:
} else { fatalError("missing headerViewMode")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "headerCell", for: indexPath) as! ProfileHeaderCollectionViewCell case .createViewIfNeeded:
switch self.initialHeaderMode { if let view = cell.view {
case nil: view.updateUI(for: id)
fatalError("missing initialHeaderMode") self.headerViewMode = .useExistingView(view)
case .createView: } else {
let view = ProfileHeaderView.create() let view = ProfileHeaderView.create()
view.delegate = self.profileHeaderDelegate view.delegate = self.profileHeaderDelegate
view.updateUI(for: id) view.updateUI(for: id)
view.pagesSegmentedControl.setSelectedOption(self.owner!.currentPage, animated: false) view.pagesSegmentedControl.setSelectedOption(self.owner!.currentPage, animated: false)
cell.addHeader(view) cell.addHeader(view)
case .useExistingView(let view): self.headerViewMode = .useExistingView(view)
view.updateUI(for: id)
cell.addHeader(view)
case .placeholder(height: let height):
_ = cell.addConstraint(height: height)
} }
self.headerCell = cell case .useExistingView(let view):
return cell view.updateUI(for: id)
cell.addHeader(view)
case .placeholder(height: let height):
_ = cell.addConstraint(height: height)
} }
return cell
case .status(id: let id, collapseState: let collapseState, filterState: let filterState, pinned: let pinned): case .status(id: let id, collapseState: let collapseState, filterState: let filterState, pinned: let pinned):
let (result, precomputedContent) = filterResult(state: filterState, statusID: id) let (result, precomputedContent) = filterResult(state: filterState, statusID: id)
switch result { switch result {
@ -411,7 +417,9 @@ extension ProfileStatusesViewController {
case statuses, withReplies, onlyMedia case statuses, withReplies, onlyMedia
} }
enum HeaderMode { enum HeaderMode {
case createView, useExistingView(ProfileHeaderView), placeholder(height: CGFloat) case createViewIfNeeded
case useExistingView(ProfileHeaderView)
case placeholder(height: CGFloat)
} }
} }

View File

@ -178,7 +178,7 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
guard let currentIndex else { guard let currentIndex else {
assert(!animated) assert(!animated)
// if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary // if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary
new.initialHeaderMode = .createView new.headerViewMode = .createViewIfNeeded
new.view.translatesAutoresizingMaskIntoConstraints = false new.view.translatesAutoresizingMaskIntoConstraints = false
addChild(new) addChild(new)
view.addSubview(new.view) view.addSubview(new.view)
@ -213,11 +213,14 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
// old header cell must have the header view // old header cell must have the header view
let headerView = oldHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)! let headerView = oldHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)!
// Set the outgoing VC's header view mode to placeholder, so that it does steal the header view back
// in case it updates the cell in the background.
old.headerViewMode = .placeholder(height: oldHeaderCell.bounds.height)
if let newHeaderCell = new.headerCell { if let newHeaderCell = new.headerCell {
_ = newHeaderCell.addConstraint(height: oldHeaderCell.bounds.height) _ = newHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)
} else { } else {
new.initialHeaderMode = .placeholder(height: oldHeaderCell.bounds.height) new.headerViewMode = .placeholder(height: oldHeaderCell.bounds.height)
} }
// disable user interaction during animation, to avoid any potential weird race conditions // disable user interaction during animation, to avoid any potential weird race conditions
@ -285,7 +288,7 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
if let newHeaderCell = new.headerCell { if let newHeaderCell = new.headerCell {
newHeaderCell.addHeader(headerView) newHeaderCell.addHeader(headerView)
} else { } else {
new.initialHeaderMode = .useExistingView(headerView) new.headerViewMode = .useExistingView(headerView)
} }
self.state = .idle self.state = .idle

View File

@ -36,7 +36,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
weak var delegate: SearchResultsViewControllerDelegate? weak var delegate: SearchResultsViewControllerDelegate?
var tokenHandler: ((String, SearchOperatorType) -> Void)? var tokenHandler: ((String, SearchOperatorType) -> Void)?
var collectionView: UICollectionView! { view as? UICollectionView } private(set) var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
/// Types of results to search for. /// Types of results to search for.
@ -62,7 +62,9 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadView() { override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
let sectionIdentifier = self.dataSource.sectionIdentifier(for: sectionIndex)! let sectionIdentifier = self.dataSource.sectionIdentifier(for: sectionIndex)!
switch sectionIdentifier { switch sectionIdentifier {
@ -102,7 +104,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
return .list(using: config, layoutEnvironment: environment) return .list(using: config, layoutEnvironment: environment)
} }
} }
view = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
@ -110,12 +112,16 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
#if !os(visionOS) #if !os(visionOS)
collectionView.keyboardDismissMode = .interactive collectionView.keyboardDismissMode = .interactive
#endif #endif
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
dataSource = createDataSource() dataSource = createDataSource()
}
override func viewDidLoad() {
super.viewDidLoad()
searchCancellable = searchSubject searchCancellable = searchSubject
.debounce(for: .seconds(1), scheduler: RunLoop.main) .debounce(for: .seconds(1), scheduler: RunLoop.main)
@ -295,7 +301,10 @@ class SearchResultsViewController: UIViewController, CollectionViewController {
} }
if !results.hashtags.isEmpty && resultTypes.contains(.hashtags) { if !results.hashtags.isEmpty && resultTypes.contains(.hashtags) {
snapshot.appendSections([.hashtags]) snapshot.appendSections([.hashtags])
snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .hashtags) // mastodon sometimes includes duplicate hashtags with the same name but different urls
// (e.g., containing %C3%B8 vs o)
let uniqueHashtags = results.hashtags.uniques(by: \.name)
snapshot.appendItems(uniqueHashtags.map { .hashtag($0) }, toSection: .hashtags)
} }
if !results.statuses.isEmpty && resultTypes.contains(.statuses) { if !results.statuses.isEmpty && resultTypes.contains(.statuses) {
snapshot.appendSections([.statuses]) snapshot.appendSections([.statuses])

View File

@ -19,9 +19,7 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
private var needsInaccurateCountWarning = false private var needsInaccurateCountWarning = false
var collectionView: UICollectionView! { private(set) var collectionView: UICollectionView!
view as? UICollectionView
}
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var state: State = .unloaded private var state: State = .unloaded
@ -45,7 +43,11 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func loadView() { override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .appGroupedBackground
var accountsConfig = UICollectionLayoutListConfiguration(appearance: .grouped) var accountsConfig = UICollectionLayoutListConfiguration(appearance: .grouped)
accountsConfig.backgroundColor = .appGroupedBackground accountsConfig.backgroundColor = .appGroupedBackground
accountsConfig.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in accountsConfig.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
@ -85,10 +87,19 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect
section.readableContentInset(in: environment) section.readableContentInset(in: environment)
return section return section
} }
view = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self collectionView.dragDelegate = self
collectionView.allowsFocus = true collectionView.allowsFocus = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
dataSource = createDataSource() dataSource = createDataSource()
} }

View File

@ -35,6 +35,8 @@ class StatusEditHistoryViewController: UIViewController, CollectionViewControlle
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .appGroupedBackground
var config = UICollectionLayoutListConfiguration(appearance: .grouped) var config = UICollectionLayoutListConfiguration(appearance: .grouped)
config.backgroundColor = .appGroupedBackground config.backgroundColor = .appGroupedBackground
config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
@ -62,8 +64,8 @@ class StatusEditHistoryViewController: UIViewController, CollectionViewControlle
collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView) view.addSubview(collectionView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]) ])

View File

@ -123,8 +123,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView) view.addSubview(collectionView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
]) ])

View File

@ -0,0 +1,150 @@
//
// AdaptableNavigationController.swift
// Tusker
//
// Created by Shadowfacts on 8/20/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UIKit
import Combine
@available(iOS 17.0, *)
class AdaptableNavigationController: UIViewController {
private let viewControllersToPrependInCompact: [UIViewController]
private var initialViewControllers: [UIViewController] = []
private lazy var regular = makeRegularNavigationController()
private lazy var compact = makeCompactNavigationController()
private var _current: (any NavigationControllerProtocol)?
var current: any NavigationControllerProtocol {
traitCollection.horizontalSizeClass == .regular ? regular : compact
}
init(viewControllersToPrependInCompact: [UIViewController] = []) {
self.viewControllersToPrependInCompact = viewControllersToPrependInCompact
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
updateNavigationController()
registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: AdaptableNavigationController, previousTraitCollection) in
self.updateNavigationController()
}
}
private func updateNavigationController() {
let isTransferring: Bool
var stack: [UIViewController]
if let _current {
_current.removeViewAndController()
stack = _current.viewControllers
isTransferring = true
} else {
stack = initialViewControllers
initialViewControllers = []
isTransferring = false
}
if traitCollection.horizontalSizeClass == .regular {
if isTransferring {
stack.removeFirst(viewControllersToPrependInCompact.count)
}
} else {
stack.insert(contentsOf: viewControllersToPrependInCompact, at: 0)
}
_current = current
current.viewControllers = stack
addChild(current)
current.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(current.view)
NSLayoutConstraint.activate([
current.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
current.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
current.view.topAnchor.constraint(equalTo: view.topAnchor),
current.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
current.didMove(toParent: self)
}
private func makeRegularNavigationController() -> any NavigationControllerProtocol {
// TODO: need to figure out how to update the navigation controller if the pref changes
switch Preferences.shared.widescreenNavigationMode {
case .stack:
return EnhancedNavigationViewController()
case .splitScreen:
return SplitNavigationController()
case .multiColumn:
return MultiColumnNavigationController()
}
}
private func makeCompactNavigationController() -> any NavigationControllerProtocol {
EnhancedNavigationViewController()
}
}
@available(iOS 17.0, *)
extension AdaptableNavigationController: NavigationControllerProtocol {
var viewControllers: [UIViewController] {
get {
_current?.viewControllers ?? initialViewControllers
}
set {
if let _current {
_current.viewControllers = newValue
} else {
initialViewControllers = newValue
}
}
}
var topViewController: UIViewController? {
if let _current {
return _current.topViewController
} else {
return initialViewControllers.last
}
}
func popToRootViewController(animated: Bool) -> [UIViewController]? {
if let _current {
return _current.popToRootViewController(animated: animated)
} else {
defer { initialViewControllers = [] }
return initialViewControllers
}
}
func pushViewController(_ vc: UIViewController, animated: Bool) {
if let _current {
_current.pushViewController(vc, animated: animated)
} else {
initialViewControllers.append(vc)
}
}
}
@available(iOS 17.0, *)
extension AdaptableNavigationController: BackgroundableViewController {
func sceneDidEnterBackground() {
(topViewController as? BackgroundableViewController)?.sceneDidEnterBackground()
}
}
@available(iOS 17.0, *)
extension AdaptableNavigationController: StatusBarTappableViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
(topViewController as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue
}
}

View File

@ -155,7 +155,7 @@ class MultiColumnNavigationController: UIViewController {
if columnFrame.maxX < scrollView.bounds.width - scrollView.adjustedTrailingContentInset { if columnFrame.maxX < scrollView.bounds.width - scrollView.adjustedTrailingContentInset {
offset = -scrollView.adjustedLeadingContentInset offset = -scrollView.adjustedLeadingContentInset
} else { } else {
offset = columnFrame.minX + scrollView.adjustedLeadingContentInset - (scrollView.bounds.width - columnFrame.width) offset = scrollView.contentSize.width - scrollView.bounds.width + scrollView.adjustedTrailingContentInset
} }
scrollView.setContentOffset(CGPoint(x: offset, y: -scrollView.adjustedContentInset.top), animated: animated) scrollView.setContentOffset(CGPoint(x: offset, y: -scrollView.adjustedContentInset.top), animated: animated)
} }
@ -185,6 +185,11 @@ class MultiColumnNavigationController: UIViewController {
} }
animator.startAnimation() animator.startAnimation()
} }
// blergh, overriding private method on UIViewController
@objc func _shouldOverlayTabBar() -> Bool {
false
}
} }
extension MultiColumnNavigationController: NavigationControllerProtocol { extension MultiColumnNavigationController: NavigationControllerProtocol {

View File

@ -48,7 +48,7 @@ enum AppShortcutItem: String, CaseIterable {
case .showNotifications: case .showNotifications:
root.select(route: .notifications, animated: false, completion: nil) root.select(route: .notifications, animated: false, completion: nil)
case .composePost: case .composePost:
root.compose(editing: nil, animated: false, isDucked: false) root.compose(editing: nil, animated: false, isDucked: false, completion: nil)
} }
} }
} }

View File

@ -109,10 +109,10 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
func compose(editing draft: Draft) { func compose(editing draft: Draft) {
if #available(iOS 16.0, *), if #available(iOS 16.0, *),
UIDevice.current.userInterfaceIdiom == .phone { UIDevice.current.userInterfaceIdiom == .phone {
self.root.compose(editing: draft, animated: false, isDucked: true) self.root.compose(editing: draft, animated: false, isDucked: true, completion: nil)
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
self.root.compose(editing: draft, animated: true, isDucked: false) self.root.compose(editing: draft, animated: true, isDucked: false, completion: nil)
} }
} }
state = .presented state = .presented
@ -123,7 +123,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
#if !os(visionOS) #if !os(visionOS)
if #available(iOS 16.0, *), if #available(iOS 16.0, *),
let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) { let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) {
self.root.compose(editing: duckedDraft, animated: false, isDucked: true) self.root.compose(editing: duckedDraft, animated: false, isDucked: true, completion: nil)
} }
#endif #endif
} }

View File

@ -96,7 +96,7 @@ extension TuskerNavigationDelegate {
show(ConversationViewController(for: statusID, state: state, mastodonController: apiController), sender: self) show(ConversationViewController(for: statusID, state: state, mastodonController: apiController), sender: self)
} }
func compose(editing draft: Draft?, animated: Bool = true, isDucked: Bool = false) { func compose(editing draft: Draft?, animated: Bool = true, isDucked: Bool = false, completion: (() -> Void)? = nil) {
let draft = draft ?? apiController.createDraft() let draft = draft ?? apiController.createDraft()
let visionIdiom = UIUserInterfaceIdiom(rawValue: 6) // .vision is not available pre-iOS 17 :S let visionIdiom = UIUserInterfaceIdiom(rawValue: 6) // .vision is not available pre-iOS 17 :S
if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) { if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) {
@ -108,16 +108,17 @@ extension TuskerNavigationDelegate {
options.preferredPresentationStyle = .prominent options.preferredPresentationStyle = .prominent
#endif #endif
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil) UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil)
completion?()
} else { } else {
let compose = ComposeHostingController(draft: draft, mastodonController: apiController) let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
#if os(visionOS) #if os(visionOS)
fatalError("unreachable") fatalError("unreachable")
#else #else
if #available(iOS 16.0, *), if #available(iOS 16.0, *),
presentDuckable(compose, animated: animated, isDucked: isDucked) { presentDuckable(compose, animated: animated, isDucked: isDucked, completion: completion) {
return return
} else { } else {
present(compose, animated: animated) present(compose, animated: animated, completion: completion)
} }
#endif #endif
} }