Compare commits
No commits in common. "3a3b7aaee4bf4c658935cc2e2c09f71af4702655" and "04deb08bcf980e4aa131249a7a78d6d14d494a63" have entirely different histories.
3a3b7aaee4
...
04deb08bcf
|
@ -171,7 +171,6 @@ public class Preferences: Codable, ObservableObject {
|
||||||
@Published public var showLinkPreviews = true
|
@Published public var showLinkPreviews = true
|
||||||
@Published public var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog]
|
@Published public var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog]
|
||||||
@Published public var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
|
@Published public var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
|
||||||
@Published public var widescreenNavigationMode = WidescreenNavigationMode.multiColumn
|
|
||||||
|
|
||||||
// MARK: Composing
|
// MARK: Composing
|
||||||
@Published public var defaultPostVisibility = Visibility.public
|
@Published public var defaultPostVisibility = Visibility.public
|
||||||
|
@ -225,10 +224,6 @@ public class Preferences: Codable, ObservableObject {
|
||||||
@Published public var hasShownLocalTimelineDescription = false
|
@Published public var hasShownLocalTimelineDescription = false
|
||||||
@Published public var hasShownFederatedTimelineDescription = false
|
@Published public var hasShownFederatedTimelineDescription = false
|
||||||
|
|
||||||
public func hasFeatureFlag(_ flag: FeatureFlag) -> Bool {
|
|
||||||
enabledFeatureFlags.contains(flag)
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case theme
|
case theme
|
||||||
case pureBlackDarkMode
|
case pureBlackDarkMode
|
||||||
|
@ -430,15 +425,6 @@ extension Preferences {
|
||||||
|
|
||||||
extension Preferences {
|
extension Preferences {
|
||||||
public enum FeatureFlag: String, Codable {
|
public enum FeatureFlag: String, Codable {
|
||||||
case iPadMultiColumn = "ipad-multi-column"
|
case test
|
||||||
case iPadBrowserNavigation = "ipad-browser-navigation"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Preferences {
|
|
||||||
public enum WidescreenNavigationMode: String, Codable {
|
|
||||||
case stack
|
|
||||||
case splitScreen
|
|
||||||
case multiColumn
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,7 +92,6 @@
|
||||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; };
|
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; };
|
||||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
|
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
|
||||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
|
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
|
||||||
D62D67C52A97D8CD00167EE2 /* MultiColumnNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D67C42A97D8CD00167EE2 /* MultiColumnNavigationController.swift */; };
|
|
||||||
D62E9985279CA23900C26176 /* URLSession+Development.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9984279CA23900C26176 /* URLSession+Development.swift */; };
|
D62E9985279CA23900C26176 /* URLSession+Development.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9984279CA23900C26176 /* URLSession+Development.swift */; };
|
||||||
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */; };
|
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */; };
|
||||||
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
|
||||||
|
@ -206,7 +205,6 @@
|
||||||
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 */; };
|
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.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 */; };
|
||||||
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; };
|
D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; };
|
||||||
|
@ -490,7 +488,6 @@
|
||||||
D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
|
D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
|
||||||
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
|
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
|
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
|
||||||
D62D67C42A97D8CD00167EE2 /* MultiColumnNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiColumnNavigationController.swift; sourceTree = "<group>"; };
|
|
||||||
D62E9984279CA23900C26176 /* URLSession+Development.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Development.swift"; sourceTree = "<group>"; };
|
D62E9984279CA23900C26176 /* URLSession+Development.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Development.swift"; sourceTree = "<group>"; };
|
||||||
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMetaIndicatorsView.swift; sourceTree = "<group>"; };
|
D62E9986279D094F00C26176 /* StatusMetaIndicatorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMetaIndicatorsView.swift; sourceTree = "<group>"; };
|
||||||
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
|
||||||
|
@ -606,7 +603,6 @@
|
||||||
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; };
|
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>"; };
|
|
||||||
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>"; };
|
||||||
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = "<group>"; };
|
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1113,7 +1109,6 @@
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */,
|
||||||
04586B4222B301470021BD04 /* AppearancePrefsView.swift */,
|
04586B4222B301470021BD04 /* AppearancePrefsView.swift */,
|
||||||
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
|
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */,
|
||||||
D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */,
|
|
||||||
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
|
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */,
|
||||||
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */,
|
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */,
|
||||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */,
|
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */,
|
||||||
|
@ -1414,7 +1409,6 @@
|
||||||
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
D625E4812588262A0074BB2B /* DraggableTableViewCell.swift */,
|
||||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||||
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */,
|
D6E77D0E286F773900D8B732 /* SplitNavigationController.swift */,
|
||||||
D62D67C42A97D8CD00167EE2 /* MultiColumnNavigationController.swift */,
|
|
||||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
||||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
||||||
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
D6C693FB2162FE6F007D6A6D /* LoadingViewController.swift */,
|
||||||
|
@ -2166,7 +2160,6 @@
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
||||||
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */,
|
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */,
|
||||||
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
|
D65B4B542971F71D00DABDFB /* EditedReport.swift in Sources */,
|
||||||
D62D67C52A97D8CD00167EE2 /* MultiColumnNavigationController.swift in Sources */,
|
|
||||||
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
|
D6A6C11525B62E9700298D0F /* CacheExpiry.swift in Sources */,
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
||||||
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
|
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
|
||||||
|
@ -2222,7 +2215,6 @@
|
||||||
D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */,
|
D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */,
|
||||||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
||||||
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */,
|
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */,
|
||||||
D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */,
|
|
||||||
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */,
|
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */,
|
||||||
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
||||||
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
|
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
|
||||||
|
|
|
@ -81,30 +81,13 @@ extension Color {
|
||||||
static let appFill = Color(uiColor: .appFill)
|
static let appFill = Color(uiColor: .appFill)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS, obsoleted: 17.0)
|
|
||||||
private let traitsKey: String = ["Traits", "Defined", "client", "_"].reversed().joined()
|
private let traitsKey: String = ["Traits", "Defined", "client", "_"].reversed().joined()
|
||||||
@available(iOS, obsoleted: 17.0)
|
|
||||||
private let key = "tusker_usePureBlackDarkMode"
|
private let key = "tusker_usePureBlackDarkMode"
|
||||||
|
|
||||||
@available(iOS 17.0, *)
|
|
||||||
private struct PureBlackDarkModeTrait: UITraitDefinition {
|
|
||||||
static let defaultValue = true
|
|
||||||
static let affectsColorAppearance = true
|
|
||||||
}
|
|
||||||
|
|
||||||
extension UITraitCollection {
|
extension UITraitCollection {
|
||||||
var pureBlackDarkMode: Bool {
|
var pureBlackDarkMode: Bool {
|
||||||
if #available(iOS 17.0, *) {
|
|
||||||
return self[PureBlackDarkModeTrait.self]
|
|
||||||
} else {
|
|
||||||
return obsoletePureBlackDarkMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS, obsoleted: 17.0)
|
|
||||||
var obsoletePureBlackDarkMode: Bool {
|
|
||||||
get {
|
get {
|
||||||
// default to true to match OS behavior
|
// default to true to mach OS behavior
|
||||||
(value(forKey: traitsKey) as? [String: Any])?[key] as? Bool ?? true
|
(value(forKey: traitsKey) as? [String: Any])?[key] as? Bool ?? true
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
|
@ -115,19 +98,7 @@ extension UITraitCollection {
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(pureBlackDarkMode: Bool) {
|
convenience init(pureBlackDarkMode: Bool) {
|
||||||
if #available(iOS 17.0, *) {
|
self.init()
|
||||||
self.init(PureBlackDarkModeTrait.self, value: pureBlackDarkMode)
|
self.pureBlackDarkMode = pureBlackDarkMode
|
||||||
} else {
|
|
||||||
self.init()
|
|
||||||
self.obsoletePureBlackDarkMode = pureBlackDarkMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@available(iOS 17.0, *)
|
|
||||||
extension UIMutableTraits {
|
|
||||||
var pureBlackDarkMode: Bool {
|
|
||||||
get { self[PureBlackDarkModeTrait.self] }
|
|
||||||
set { self[PureBlackDarkModeTrait.self] = newValue }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,18 +32,14 @@ extension TuskerSceneDelegate {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
window.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||||
window.tintColor = Preferences.shared.accentColor.color
|
window.tintColor = Preferences.shared.accentColor.color
|
||||||
if #available(iOS 17.0, *) {
|
let exception = catchNSException {
|
||||||
window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode
|
let key = ["Controller", "Presentation", "root", "_"].reversed().joined()
|
||||||
} else {
|
if let rootPresentationController = window.value(forKey: key) as? UIPresentationController {
|
||||||
let exception = catchNSException {
|
rootPresentationController.overrideTraitCollection = UITraitCollection(pureBlackDarkMode: Preferences.shared.pureBlackDarkMode)
|
||||||
let key = ["Controller", "Presentation", "root", "_"].reversed().joined()
|
|
||||||
if let rootPresentationController = window.value(forKey: key) as? UIPresentationController {
|
|
||||||
rootPresentationController.overrideTraitCollection = UITraitCollection(pureBlackDarkMode: Preferences.shared.pureBlackDarkMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let exception {
|
|
||||||
SentrySDK.capture(exception: exception)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let exception {
|
||||||
|
SentrySDK.capture(exception: exception)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
|
||||||
|
|
||||||
class MainSplitViewController: UISplitViewController {
|
class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
|
@ -21,13 +20,10 @@ class MainSplitViewController: UISplitViewController {
|
||||||
|
|
||||||
private var tabBarViewController: MainTabBarViewController!
|
private var tabBarViewController: MainTabBarViewController!
|
||||||
|
|
||||||
private var navigationMode: Preferences.WidescreenNavigationMode!
|
private var secondaryNavController: SplitNavigationController! {
|
||||||
private var secondaryNavController: NavigationControllerProtocol! {
|
viewController(for: .secondary) as? SplitNavigationController
|
||||||
viewController(for: .secondary) as? NavigationControllerProtocol
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
private var sidebarVisibile: Bool {
|
private var sidebarVisibile: Bool {
|
||||||
get {
|
get {
|
||||||
(UserDefaults.standard.object(forKey: "MainSplitViewControllerSidebarVisible") as? Bool) ?? true
|
(UserDefaults.standard.object(forKey: "MainSplitViewControllerSidebarVisible") as? Bool) ?? true
|
||||||
|
@ -63,19 +59,9 @@ class MainSplitViewController: UISplitViewController {
|
||||||
} else {
|
} else {
|
||||||
hide(.primary)
|
hide(.primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
let nav: UIViewController
|
let splitNav = SplitNavigationController()
|
||||||
navigationMode = Preferences.shared.widescreenNavigationMode
|
setViewController(splitNav, for: .secondary)
|
||||||
switch navigationMode! {
|
|
||||||
case .stack:
|
|
||||||
nav = EnhancedNavigationViewController()
|
|
||||||
case .splitScreen:
|
|
||||||
nav = SplitNavigationController()
|
|
||||||
case .multiColumn:
|
|
||||||
nav = MultiColumnNavigationController()
|
|
||||||
}
|
|
||||||
setViewController(nav, for: .secondary)
|
|
||||||
|
|
||||||
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
// don't unnecesarily construct a content VC unless the we're in actually split mode
|
||||||
// when we change from compact -> split for the first time, the VC will be transferred anyways
|
// when we change from compact -> split for the first time, the VC will be transferred anyways
|
||||||
if traitCollection.horizontalSizeClass != .compact {
|
if traitCollection.horizontalSizeClass != .compact {
|
||||||
|
@ -105,37 +91,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
addKeyCommand(MenuController.composeCommand)
|
addKeyCommand(MenuController.composeCommand)
|
||||||
|
|
||||||
MenuController.sidebarItemKeyCommands.forEach(addKeyCommand(_:))
|
MenuController.sidebarItemKeyCommands.forEach(addKeyCommand(_:))
|
||||||
|
|
||||||
Preferences.shared.$widescreenNavigationMode
|
|
||||||
.sink { [unowned self] in
|
|
||||||
self.updateNavigationMode($0)
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateNavigationMode(_ mode: Preferences.WidescreenNavigationMode) {
|
|
||||||
guard mode != navigationMode else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let viewControllers = secondaryNavController.viewControllers
|
|
||||||
secondaryNavController.viewControllers = []
|
|
||||||
// Setting viewControllers = [] doesn't remove the VC views from their superviews immediately,
|
|
||||||
// so do that ourselves so we can re-parent the VCs to the new nav controller.
|
|
||||||
for viewController in viewControllers {
|
|
||||||
viewController.viewIfLoaded?.removeFromSuperview()
|
|
||||||
}
|
|
||||||
|
|
||||||
let newNav: NavigationControllerProtocol
|
|
||||||
switch mode {
|
|
||||||
case .stack:
|
|
||||||
newNav = EnhancedNavigationViewController()
|
|
||||||
case .splitScreen:
|
|
||||||
newNav = SplitNavigationController()
|
|
||||||
case .multiColumn:
|
|
||||||
newNav = MultiColumnNavigationController()
|
|
||||||
}
|
|
||||||
newNav.viewControllers = viewControllers
|
|
||||||
self.setViewController(newNav, for: .secondary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func select(item: MainSidebarViewController.Item) {
|
func select(item: MainSidebarViewController.Item) {
|
||||||
|
@ -211,7 +166,6 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
var itemNavStack: [UIViewController]
|
var itemNavStack: [UIViewController]
|
||||||
if item == sidebar.selectedItem {
|
if item == sidebar.selectedItem {
|
||||||
itemNavStack = secondaryNavController.viewControllers
|
itemNavStack = secondaryNavController.viewControllers
|
||||||
secondaryNavController.viewControllers = []
|
|
||||||
} else {
|
} else {
|
||||||
itemNavStack = navigationStacks[item] ?? []
|
itemNavStack = navigationStacks[item] ?? []
|
||||||
navigationStacks.removeValue(forKey: item)
|
navigationStacks.removeValue(forKey: item)
|
||||||
|
@ -619,9 +573,8 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
return tabBarViewController.handleStatusBarTapped(xPosition: xPosition)
|
return tabBarViewController.handleStatusBarTapped(xPosition: xPosition)
|
||||||
} else {
|
} else {
|
||||||
let pointInSecondary = secondaryNavController.view.convert(CGPoint(x: xPosition, y: 0), from: view)
|
let pointInSecondary = secondaryNavController.view.convert(CGPoint(x: xPosition, y: 0), from: view)
|
||||||
if secondaryNavController.view.bounds.contains(pointInSecondary),
|
if secondaryNavController.view.bounds.contains(pointInSecondary) {
|
||||||
let statusBarTappable = secondaryNavController as? StatusBarTappableViewController {
|
return secondaryNavController.handleStatusBarTapped(xPosition: pointInSecondary.x)
|
||||||
return statusBarTappable.handleStatusBarTapped(xPosition: pointInSecondary.x)
|
|
||||||
} else {
|
} else {
|
||||||
return .continue
|
return .continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,6 +180,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
return vc
|
return vc
|
||||||
} else {
|
} else {
|
||||||
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
let nav = EnhancedNavigationViewController(rootViewController: vc)
|
||||||
|
// nav.useBrowserStyleNavigation = true
|
||||||
return nav
|
return nav
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,10 +83,8 @@ enum TuskerRoute {
|
||||||
// case myProfile
|
// case myProfile
|
||||||
//}
|
//}
|
||||||
//
|
//
|
||||||
protocol NavigationControllerProtocol: UIViewController {
|
protocol NavigationControllerProtocol {
|
||||||
var viewControllers: [UIViewController] { get set }
|
|
||||||
var topViewController: UIViewController? { get }
|
var topViewController: UIViewController? { get }
|
||||||
@discardableResult
|
|
||||||
func popToRootViewController(animated: Bool) -> [UIViewController]?
|
func popToRootViewController(animated: Bool) -> [UIViewController]?
|
||||||
func pushViewController(_ vc: UIViewController, animated: Bool)
|
func pushViewController(_ vc: UIViewController, animated: Bool)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ struct AdvancedPrefsView : View {
|
||||||
isShowingFeatureFlagAlert = true
|
isShowingFeatureFlagAlert = true
|
||||||
}
|
}
|
||||||
.alert("Enable Feature Flag", isPresented: $isShowingFeatureFlagAlert) {
|
.alert("Enable Feature Flag", isPresented: $isShowingFeatureFlagAlert) {
|
||||||
TextField("Flag Name", text: $featureFlagName)
|
TextField("Name", text: $featureFlagName)
|
||||||
.textInputAutocapitalization(.never)
|
.textInputAutocapitalization(.never)
|
||||||
.autocorrectionDisabled()
|
.autocorrectionDisabled()
|
||||||
|
|
||||||
|
@ -45,8 +45,6 @@ struct AdvancedPrefsView : View {
|
||||||
preferences.enabledFeatureFlags.insert(flag)
|
preferences.enabledFeatureFlags.insert(flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} message: {
|
|
||||||
Text("Warning: Feature flags are intended for development and debugging use only. They are experimental and subject to change at any time.")
|
|
||||||
}
|
}
|
||||||
.navigationBarTitle(Text("Advanced"))
|
.navigationBarTitle(Text("Advanced"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,6 @@ struct AppearancePrefsView : View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
themeSection
|
themeSection
|
||||||
interfaceSection
|
|
||||||
accountsSection
|
accountsSection
|
||||||
postsSection
|
postsSection
|
||||||
}
|
}
|
||||||
|
@ -88,15 +87,6 @@ struct AppearancePrefsView : View {
|
||||||
.appGroupedListRowBackground()
|
.appGroupedListRowBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var interfaceSection: some View {
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
|
||||||
Section(header: Text("Interface")) {
|
|
||||||
WidescreenNavigationPrefsView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var accountsSection: some View {
|
private var accountsSection: some View {
|
||||||
Section(header: Text("Accounts")) {
|
Section(header: Text("Accounts")) {
|
||||||
Toggle(isOn: useCircularAvatars) {
|
Toggle(isOn: useCircularAvatars) {
|
||||||
|
|
|
@ -1,464 +0,0 @@
|
||||||
//
|
|
||||||
// WidescreenNavigationPrefsView.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 9/2/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
struct WidescreenNavigationPrefsView: View {
|
|
||||||
@ObservedObject private var preferences = Preferences.shared
|
|
||||||
@State private var startAnimation = PassthroughSubject<Void, Never>()
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
OptionView<StackNavigationPreview>(
|
|
||||||
value: .stack,
|
|
||||||
selection: $preferences.widescreenNavigationMode,
|
|
||||||
startAnimation: startAnimation
|
|
||||||
) {
|
|
||||||
Text("Stack")
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(minLength: 32)
|
|
||||||
|
|
||||||
OptionView<SplitNavigationPreview>(
|
|
||||||
value: .splitScreen,
|
|
||||||
selection: $preferences.widescreenNavigationMode,
|
|
||||||
startAnimation: startAnimation
|
|
||||||
) {
|
|
||||||
Text("Split Screen")
|
|
||||||
}
|
|
||||||
|
|
||||||
if preferences.hasFeatureFlag(.iPadMultiColumn) {
|
|
||||||
Spacer(minLength: 32)
|
|
||||||
|
|
||||||
OptionView<MultiColumnNavigationPreview>(
|
|
||||||
value: .multiColumn,
|
|
||||||
selection: $preferences.widescreenNavigationMode,
|
|
||||||
startAnimation: startAnimation
|
|
||||||
) {
|
|
||||||
Text("Multi-Column")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.frame(height: 100)
|
|
||||||
.onAppear {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
|
|
||||||
startAnimation.send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct OptionView<Content: NavigationModePreview>: View {
|
|
||||||
let value: Preferences.WidescreenNavigationMode
|
|
||||||
@Binding var selection: Preferences.WidescreenNavigationMode
|
|
||||||
let startAnimation: PassthroughSubject<Void, Never>
|
|
||||||
@ViewBuilder let label: Text
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
|
||||||
|
|
||||||
private var selected: Bool {
|
|
||||||
selection == value
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Button(action: self.selectValue) {
|
|
||||||
VStack {
|
|
||||||
preview
|
|
||||||
|
|
||||||
label
|
|
||||||
.foregroundStyle(selected ? Color.white : .primary)
|
|
||||||
.background(selected ? AnyShapeStyle(.tint) : AnyShapeStyle(.clear), in: WideCapsule())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var preview: some View {
|
|
||||||
NavigationModeRepresentable<Content>(startAnimation: startAnimation)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12.5, style: .continuous))
|
|
||||||
.overlay {
|
|
||||||
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
|
|
||||||
.stroke(.gray, lineWidth: 3)
|
|
||||||
}
|
|
||||||
.aspectRatio(4/3, contentMode: .fit)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func selectValue() {
|
|
||||||
selection = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct WideCapsule: Shape {
|
|
||||||
func path(in rect: CGRect) -> Path {
|
|
||||||
Capsule().path(in: rect.insetBy(dx: -6, dy: -2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private protocol NavigationModePreview: UIView {
|
|
||||||
init(startAnimation: PassthroughSubject<Void, Never>)
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct NavigationModeRepresentable<UIViewType: NavigationModePreview>: UIViewRepresentable {
|
|
||||||
let startAnimation: PassthroughSubject<Void, Never>
|
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIViewType {
|
|
||||||
UIViewType(startAnimation: startAnimation)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateUIView(_ uiView: UIViewType, context: Context) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private let timingParams = UISpringTimingParameters(mass: 1, stiffness: 70, damping: 16, initialVelocity: .zero)
|
|
||||||
|
|
||||||
private final class StackNavigationPreview: UIView, NavigationModePreview {
|
|
||||||
private let cellStack = UIStackView(arrangedSubviews: [CellView(), CellView(), CellView()])
|
|
||||||
private let destinationView = UIView()
|
|
||||||
private var cancellable: AnyCancellable?
|
|
||||||
|
|
||||||
init(startAnimation: PassthroughSubject<Void, Never>) {
|
|
||||||
super.init(frame: .zero)
|
|
||||||
|
|
||||||
backgroundColor = .appBackground
|
|
||||||
layer.masksToBounds = true
|
|
||||||
|
|
||||||
cellStack.axis = .vertical
|
|
||||||
cellStack.spacing = 0
|
|
||||||
cellStack.distribution = .fillEqually
|
|
||||||
cellStack.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(cellStack)
|
|
||||||
cellStack.arrangedSubviews[1].backgroundColor = .clear
|
|
||||||
|
|
||||||
destinationView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
destinationView.backgroundColor = .tintColor
|
|
||||||
destinationView.isHidden = true
|
|
||||||
addSubview(destinationView)
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
cellStack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
cellStack.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
cellStack.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
|
||||||
cellStack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
||||||
|
|
||||||
destinationView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
destinationView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
destinationView.topAnchor.constraint(equalTo: topAnchor),
|
|
||||||
destinationView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
cancellable = startAnimation.sink { [unowned self] _ in
|
|
||||||
self.startAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startAnimation() {
|
|
||||||
destinationView.transform = CGAffineTransform(translationX: destinationView.bounds.width, y: 0)
|
|
||||||
destinationView.isHidden = false
|
|
||||||
|
|
||||||
let animator = UIViewPropertyAnimator(duration: 1, timingParameters: timingParams)
|
|
||||||
animator.addAnimations {
|
|
||||||
self.cellStack.arrangedSubviews[1].backgroundColor = .tertiaryLabel
|
|
||||||
self.cellStack.transform = CGAffineTransform(translationX: -0.3 * self.cellStack.bounds.width, y: 0)
|
|
||||||
self.destinationView.transform = .identity
|
|
||||||
}
|
|
||||||
animator.addCompletion { [weak self] _ in
|
|
||||||
self?.reverseAnimation()
|
|
||||||
}
|
|
||||||
animator.startAnimation(afterDelay: 0.6)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func reverseAnimation() {
|
|
||||||
let animator = UIViewPropertyAnimator(duration: 1, timingParameters: timingParams)
|
|
||||||
animator.addAnimations {
|
|
||||||
self.cellStack.arrangedSubviews[1].backgroundColor = .clear
|
|
||||||
self.cellStack.transform = .identity
|
|
||||||
self.destinationView.transform = CGAffineTransform(translationX: self.destinationView.bounds.width, y: 0)
|
|
||||||
}
|
|
||||||
animator.addCompletion { [weak self] _ in
|
|
||||||
self?.startAnimation()
|
|
||||||
}
|
|
||||||
animator.startAnimation(afterDelay: 0.5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class SplitNavigationPreview: UIView, NavigationModePreview {
|
|
||||||
private let cellStack = UIStackView(arrangedSubviews: [CellView(), CellView(), CellView()])
|
|
||||||
private let destinationView = UIView()
|
|
||||||
private var cellStackTrailingConstraint: NSLayoutConstraint!
|
|
||||||
private var cancellable: AnyCancellable?
|
|
||||||
|
|
||||||
init(startAnimation: PassthroughSubject<Void, Never>) {
|
|
||||||
super.init(frame: .zero)
|
|
||||||
|
|
||||||
backgroundColor = .appBackground
|
|
||||||
layer.masksToBounds = true
|
|
||||||
|
|
||||||
cellStack.axis = .vertical
|
|
||||||
cellStack.spacing = 0
|
|
||||||
cellStack.distribution = .fillEqually
|
|
||||||
cellStack.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(cellStack)
|
|
||||||
cellStack.arrangedSubviews[1].backgroundColor = .clear
|
|
||||||
|
|
||||||
destinationView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
destinationView.backgroundColor = .tintColor
|
|
||||||
destinationView.isHidden = true
|
|
||||||
addSubview(destinationView)
|
|
||||||
|
|
||||||
cellStackTrailingConstraint = cellStack.trailingAnchor.constraint(equalTo: trailingAnchor)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
cellStack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
cellStackTrailingConstraint,
|
|
||||||
cellStack.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
|
||||||
cellStack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
||||||
|
|
||||||
destinationView.leadingAnchor.constraint(equalTo: centerXAnchor),
|
|
||||||
destinationView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
destinationView.topAnchor.constraint(equalTo: topAnchor),
|
|
||||||
destinationView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
cancellable = startAnimation.sink { [unowned self] _ in
|
|
||||||
self.startAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startAnimation() {
|
|
||||||
destinationView.transform = CGAffineTransform(translationX: destinationView.bounds.width, y: 0)
|
|
||||||
destinationView.isHidden = false
|
|
||||||
|
|
||||||
cellStackTrailingConstraint.isActive = false
|
|
||||||
cellStackTrailingConstraint = cellStack.trailingAnchor.constraint(equalTo: centerXAnchor)
|
|
||||||
cellStackTrailingConstraint.isActive = true
|
|
||||||
|
|
||||||
let animator = UIViewPropertyAnimator(duration: 1, timingParameters: timingParams)
|
|
||||||
animator.addAnimations {
|
|
||||||
self.layoutIfNeeded()
|
|
||||||
self.destinationView.transform = .identity
|
|
||||||
}
|
|
||||||
animator.addCompletion { [weak self] _ in
|
|
||||||
self?.reverseAnimation()
|
|
||||||
}
|
|
||||||
animator.startAnimation(afterDelay: 0.6)
|
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.1, delay: 0.5, options: .curveEaseIn) {
|
|
||||||
self.cellStack.arrangedSubviews[1].backgroundColor = .tertiaryLabel
|
|
||||||
} completion: { _ in
|
|
||||||
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
|
|
||||||
self.cellStack.arrangedSubviews[1].backgroundColor = .clear
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func reverseAnimation() {
|
|
||||||
cellStackTrailingConstraint.isActive = false
|
|
||||||
cellStackTrailingConstraint = cellStack.trailingAnchor.constraint(equalTo: trailingAnchor)
|
|
||||||
cellStackTrailingConstraint.isActive = true
|
|
||||||
|
|
||||||
let animator = UIViewPropertyAnimator(duration: 1, timingParameters: timingParams)
|
|
||||||
animator.addAnimations {
|
|
||||||
self.layoutIfNeeded()
|
|
||||||
self.destinationView.transform = CGAffineTransform(translationX: self.destinationView.bounds.width, y: 0)
|
|
||||||
}
|
|
||||||
animator.addCompletion { [weak self] _ in
|
|
||||||
self?.startAnimation()
|
|
||||||
}
|
|
||||||
animator.startAnimation(afterDelay: 0.5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class MultiColumnNavigationPreview: UIView, NavigationModePreview {
|
|
||||||
private static let columnSpacing: CGFloat = 5
|
|
||||||
|
|
||||||
private let cellStack1 = UIStackView(arrangedSubviews: [CellView(), CellView(), CellView()])
|
|
||||||
private let cellStack2 = UIStackView(arrangedSubviews: [CellView(), CellView(), CellView()])
|
|
||||||
private let destinationView = UIView()
|
|
||||||
private var cancellable: AnyCancellable?
|
|
||||||
|
|
||||||
private var startedAnimation = false
|
|
||||||
|
|
||||||
init(startAnimation: PassthroughSubject<Void, Never>) {
|
|
||||||
super.init(frame: .zero)
|
|
||||||
|
|
||||||
backgroundColor = .appSecondaryBackground
|
|
||||||
layer.masksToBounds = true
|
|
||||||
|
|
||||||
cellStack1.axis = .vertical
|
|
||||||
cellStack1.spacing = 0
|
|
||||||
cellStack1.distribution = .fillEqually
|
|
||||||
cellStack1.backgroundColor = .appBackground
|
|
||||||
cellStack1.layer.cornerRadius = 6
|
|
||||||
cellStack1.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(cellStack1)
|
|
||||||
|
|
||||||
cellStack2.axis = .vertical
|
|
||||||
cellStack2.spacing = 0
|
|
||||||
cellStack2.distribution = .fillEqually
|
|
||||||
cellStack2.backgroundColor = .appBackground
|
|
||||||
cellStack2.layer.cornerRadius = 6
|
|
||||||
cellStack2.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(cellStack2)
|
|
||||||
cellStack2.arrangedSubviews[1].backgroundColor = .clear
|
|
||||||
|
|
||||||
destinationView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
destinationView.backgroundColor = .tintColor
|
|
||||||
destinationView.layer.cornerRadius = 6
|
|
||||||
destinationView.layer.opacity = 0
|
|
||||||
addSubview(destinationView)
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
cellStack1.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Self.columnSpacing),
|
|
||||||
cellStack1.widthAnchor.constraint(equalToConstant: 50),
|
|
||||||
cellStack1.topAnchor.constraint(equalTo: topAnchor, constant: Self.columnSpacing),
|
|
||||||
cellStack1.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Self.columnSpacing),
|
|
||||||
|
|
||||||
cellStack2.leadingAnchor.constraint(equalTo: cellStack1.trailingAnchor, constant: Self.columnSpacing),
|
|
||||||
cellStack2.widthAnchor.constraint(equalToConstant: 50),
|
|
||||||
cellStack2.topAnchor.constraint(equalTo: topAnchor, constant: Self.columnSpacing),
|
|
||||||
cellStack2.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Self.columnSpacing),
|
|
||||||
|
|
||||||
destinationView.leadingAnchor.constraint(equalTo: cellStack2.trailingAnchor, constant: Self.columnSpacing),
|
|
||||||
destinationView.widthAnchor.constraint(equalToConstant: 50),
|
|
||||||
destinationView.topAnchor.constraint(equalTo: topAnchor, constant: Self.columnSpacing),
|
|
||||||
destinationView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Self.columnSpacing),
|
|
||||||
])
|
|
||||||
|
|
||||||
cancellable = startAnimation.sink { [unowned self] _ in
|
|
||||||
self.startedAnimation = true
|
|
||||||
self.startAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
if !startedAnimation {
|
|
||||||
setUnscrolledTransform()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startAnimation() {
|
|
||||||
let totalWidth = 50 * 3 + Self.columnSpacing * 4
|
|
||||||
let offset = bounds.width - totalWidth
|
|
||||||
let transform = CGAffineTransform(translationX: offset, y: 0)
|
|
||||||
|
|
||||||
let animator = UIViewPropertyAnimator(duration: 1, timingParameters: timingParams)
|
|
||||||
animator.addAnimations {
|
|
||||||
self.cellStack1.transform = transform
|
|
||||||
self.cellStack2.transform = transform
|
|
||||||
self.destinationView.transform = transform
|
|
||||||
self.destinationView.layer.opacity = 1
|
|
||||||
}
|
|
||||||
animator.addCompletion { [weak self] _ in
|
|
||||||
self?.reverseAnimation()
|
|
||||||
}
|
|
||||||
animator.startAnimation(afterDelay: 0.6)
|
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.1, delay: 0.5, options: .curveEaseIn) {
|
|
||||||
self.cellStack2.arrangedSubviews[1].backgroundColor = .tertiaryLabel
|
|
||||||
} completion: { _ in
|
|
||||||
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
|
|
||||||
self.cellStack2.arrangedSubviews[1].backgroundColor = .clear
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func reverseAnimation() {
|
|
||||||
let animator = UIViewPropertyAnimator(duration: 1, timingParameters: timingParams)
|
|
||||||
animator.addAnimations {
|
|
||||||
self.setUnscrolledTransform()
|
|
||||||
self.destinationView.layer.opacity = 0
|
|
||||||
}
|
|
||||||
animator.addCompletion { [weak self] _ in
|
|
||||||
self?.startAnimation()
|
|
||||||
}
|
|
||||||
animator.startAnimation(afterDelay: 0.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setUnscrolledTransform() {
|
|
||||||
let totalWidth = 50 * 2 + Self.columnSpacing * 3
|
|
||||||
let offset = bounds.width - totalWidth
|
|
||||||
let transform = CGAffineTransform(translationX: offset, y: 0)
|
|
||||||
|
|
||||||
cellStack1.transform = transform
|
|
||||||
cellStack2.transform = transform
|
|
||||||
destinationView.transform = transform
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CellView: UIView {
|
|
||||||
init() {
|
|
||||||
super.init(frame: .zero)
|
|
||||||
|
|
||||||
let avatarView = UIView()
|
|
||||||
avatarView.backgroundColor = .tertiaryLabel
|
|
||||||
avatarView.layer.cornerRadius = 1
|
|
||||||
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(avatarView)
|
|
||||||
|
|
||||||
let line1 = UIView()
|
|
||||||
line1.backgroundColor = .tertiaryLabel
|
|
||||||
line1.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(line1)
|
|
||||||
|
|
||||||
let line2 = UIView()
|
|
||||||
line2.backgroundColor = .tertiaryLabel
|
|
||||||
line2.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(line2)
|
|
||||||
|
|
||||||
let line3 = UIView()
|
|
||||||
line3.backgroundColor = .tertiaryLabel
|
|
||||||
line3.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(line3)
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
avatarView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
|
||||||
avatarView.topAnchor.constraint(equalTo: topAnchor, constant: 4),
|
|
||||||
avatarView.widthAnchor.constraint(equalToConstant: 10),
|
|
||||||
avatarView.heightAnchor.constraint(equalToConstant: 10),
|
|
||||||
|
|
||||||
line1.topAnchor.constraint(equalTo: avatarView.topAnchor),
|
|
||||||
line1.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 4),
|
|
||||||
line1.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
|
||||||
line1.heightAnchor.constraint(equalToConstant: 4),
|
|
||||||
|
|
||||||
line2.topAnchor.constraint(equalTo: line1.bottomAnchor, constant: 2),
|
|
||||||
line2.leadingAnchor.constraint(equalTo: line1.leadingAnchor),
|
|
||||||
line2.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24),
|
|
||||||
line2.heightAnchor.constraint(equalToConstant: 4),
|
|
||||||
|
|
||||||
line3.topAnchor.constraint(equalTo: line2.bottomAnchor, constant: 2),
|
|
||||||
line3.leadingAnchor.constraint(equalTo: line1.leadingAnchor),
|
|
||||||
line3.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
|
|
||||||
line3.heightAnchor.constraint(equalToConstant: 4),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
WidescreenNavigationPrefsView()
|
|
||||||
}
|
|
|
@ -76,14 +76,8 @@ private struct ScrollBackgroundModifier: ViewModifier {
|
||||||
// otherwise the pureBlackDarkMode isn't propagated, for some reason?
|
// otherwise the pureBlackDarkMode isn't propagated, for some reason?
|
||||||
// even though it is for ReportSelectRulesView??
|
// even though it is for ReportSelectRulesView??
|
||||||
let traits: UITraitCollection = {
|
let traits: UITraitCollection = {
|
||||||
var t = UITraitCollection(userInterfaceStyle: colorScheme == .dark ? .dark : .light)
|
let t = UITraitCollection(userInterfaceStyle: colorScheme == .dark ? .dark : .light)
|
||||||
if #available(iOS 17.0, *) {
|
t.pureBlackDarkMode = true
|
||||||
t = t.modifyingTraits({ mutableTraits in
|
|
||||||
mutableTraits.pureBlackDarkMode = true
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
t.obsoletePureBlackDarkMode = true
|
|
||||||
}
|
|
||||||
return t
|
return t
|
||||||
}()
|
}()
|
||||||
Color(uiColor: .appGroupedBackground.resolvedColor(with: traits))
|
Color(uiColor: .appGroupedBackground.resolvedColor(with: traits))
|
||||||
|
|
|
@ -110,9 +110,9 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
// just setting layout.configuration.contentInsetsReference doesn't work with UICollectionViewCompositionalLayout.list
|
// just setting layout.configuration.contentInsetsReference doesn't work with UICollectionViewCompositionalLayout.list
|
||||||
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
|
||||||
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
|
||||||
// if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
// section.contentInsetsReference = .readableContent
|
section.contentInsetsReference = .readableContent
|
||||||
// }
|
}
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
|
|
||||||
class EnhancedNavigationViewController: UINavigationController {
|
class EnhancedNavigationViewController: UINavigationController {
|
||||||
|
|
||||||
let useBrowserStyleNavigation = Preferences.shared.hasFeatureFlag(.iPadBrowserNavigation)
|
var useBrowserStyleNavigation = false
|
||||||
|
|
||||||
var poppedViewControllers = [UIViewController]()
|
var poppedViewControllers = [UIViewController]()
|
||||||
var skipResetPoppedOnNextPush = false
|
var skipResetPoppedOnNextPush = false
|
||||||
|
@ -20,8 +20,7 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
override var viewControllers: [UIViewController] {
|
override var viewControllers: [UIViewController] {
|
||||||
didSet {
|
didSet {
|
||||||
poppedViewControllers = []
|
poppedViewControllers = []
|
||||||
if #available(iOS 16.0, *),
|
if #available(iOS 16.0, *) {
|
||||||
useBrowserStyleNavigation {
|
|
||||||
// TODO: this for loop might not be necessary
|
// TODO: this for loop might not be necessary
|
||||||
for vc in viewControllers {
|
for vc in viewControllers {
|
||||||
configureNavItem(vc.navigationItem)
|
configureNavItem(vc.navigationItem)
|
||||||
|
@ -37,7 +36,6 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
self.interactivePushTransition = InteractivePushTransition(navigationController: self)
|
self.interactivePushTransition = InteractivePushTransition(navigationController: self)
|
||||||
|
|
||||||
if #available(iOS 16.0, *),
|
if #available(iOS 16.0, *),
|
||||||
useBrowserStyleNavigation,
|
|
||||||
let topViewController {
|
let topViewController {
|
||||||
configureNavItem(topViewController.navigationItem)
|
configureNavItem(topViewController.navigationItem)
|
||||||
updateTopNavItemState()
|
updateTopNavItemState()
|
||||||
|
@ -126,7 +124,7 @@ class EnhancedNavigationViewController: UINavigationController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// match the system behavior when popping multiple by animated-ly pushing the final destination one,
|
// match the system behavior when popping multiple by animated-ly pushing the final destination one,
|
||||||
// and then inserting the intermediary ones before it, as if they'd all been pushed together
|
// and then intersiting the intermediary ones before it, as if they'd all been pushed together
|
||||||
performAfterAnimating(block: {
|
performAfterAnimating(block: {
|
||||||
pushViewController(target, animated: true)
|
pushViewController(target, animated: true)
|
||||||
}, after: {
|
}, after: {
|
||||||
|
|
|
@ -1,329 +0,0 @@
|
||||||
//
|
|
||||||
// MultiColumnNavigationController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 8/24/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class MultiColumnNavigationController: UIViewController {
|
|
||||||
|
|
||||||
private var isManuallyUpdating = false
|
|
||||||
var viewControllers: [UIViewController] = [] {
|
|
||||||
didSet {
|
|
||||||
guard isViewLoaded,
|
|
||||||
!isManuallyUpdating else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateViews()
|
|
||||||
scrollToEnd(animated: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var scrollView = UIScrollView()
|
|
||||||
private var stackView = UIStackView()
|
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
view.backgroundColor = .appSecondaryBackground
|
|
||||||
|
|
||||||
scrollView.contentInsetAdjustmentBehavior = .always
|
|
||||||
scrollView.contentInset = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
|
|
||||||
scrollView.alwaysBounceHorizontal = true
|
|
||||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
stackView.axis = .horizontal
|
|
||||||
stackView.spacing = 8
|
|
||||||
stackView.alignment = .fill
|
|
||||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
scrollView.addSubview(stackView)
|
|
||||||
|
|
||||||
view.addSubview(scrollView)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
||||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
||||||
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
||||||
|
|
||||||
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
|
||||||
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
|
||||||
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
|
||||||
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
|
|
||||||
stackView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
updateViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateViews() {
|
|
||||||
var i = 0
|
|
||||||
while i < viewControllers.count {
|
|
||||||
let needsCloseButton = i > 0
|
|
||||||
if i <= stackView.arrangedSubviews.count - 1 {
|
|
||||||
let existing = stackView.arrangedSubviews[i] as! ColumnView
|
|
||||||
existing.setContent(viewControllers[i], needsCloseButton: needsCloseButton)
|
|
||||||
} else {
|
|
||||||
let new = ColumnView(owner: self, contentViewController: viewControllers[i], needsCloseButton: needsCloseButton)
|
|
||||||
stackView.addArrangedSubview(new)
|
|
||||||
}
|
|
||||||
i += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
while i < stackView.arrangedSubviews.count {
|
|
||||||
let toRemove = stackView.arrangedSubviews[i] as! ColumnView
|
|
||||||
toRemove.willRemoveColumn()
|
|
||||||
stackView.removeArrangedSubview(toRemove)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func show(_ vc: UIViewController, sender: Any?) {
|
|
||||||
if let sender = sender as? UIViewController {
|
|
||||||
var index: Int? = nil
|
|
||||||
var current: UIViewController? = sender
|
|
||||||
while let c = current {
|
|
||||||
index = viewControllers.firstIndex(of: c)
|
|
||||||
if index != nil {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
current = c.parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let index {
|
|
||||||
replaceViewControllers([vc], after: index, animated: true)
|
|
||||||
} else {
|
|
||||||
pushViewController(vc, animated: true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pushViewController(vc, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
SplitNavigationController.clearSelectedRow(sender: sender)
|
|
||||||
}
|
|
||||||
|
|
||||||
func replaceViewControllers(_ vcs: [UIViewController], after afterIndex: Int, animated: Bool) {
|
|
||||||
if afterIndex == viewControllers.count - 1 && vcs.count == 1 {
|
|
||||||
pushViewController(vcs[0], animated: animated)
|
|
||||||
} else {
|
|
||||||
viewControllers = Array(viewControllers[...afterIndex]) + vcs
|
|
||||||
scrollToEnd(animated: animated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func scrollToEnd(animated: Bool) {
|
|
||||||
if viewControllers.isEmpty {
|
|
||||||
scrollView.setContentOffset(.init(x: -scrollView.adjustedLeadingContentInset, y: -scrollView.adjustedContentInset.top), animated: false)
|
|
||||||
} else {
|
|
||||||
scrollColumnToEnd(columnIndex: viewControllers.count - 1, animated: animated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func scrollColumnToEnd(columnIndex: Int, animated: Bool) {
|
|
||||||
scrollView.layoutIfNeeded()
|
|
||||||
let column = stackView.arrangedSubviews[columnIndex]
|
|
||||||
let columnFrame = column.convert(column.bounds, to: scrollView)
|
|
||||||
let offset: CGFloat
|
|
||||||
if columnFrame.maxX < scrollView.bounds.width - scrollView.adjustedTrailingContentInset {
|
|
||||||
offset = -scrollView.adjustedLeadingContentInset
|
|
||||||
} else {
|
|
||||||
offset = columnFrame.minX + scrollView.adjustedLeadingContentInset - (scrollView.bounds.width - columnFrame.width)
|
|
||||||
}
|
|
||||||
scrollView.setContentOffset(CGPoint(x: offset, y: -scrollView.adjustedContentInset.top), animated: animated)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func closeColumn(_ vc: UIViewController) {
|
|
||||||
let index = viewControllers.firstIndex(of: vc)!
|
|
||||||
guard index > 0 else {
|
|
||||||
// Can't close the last column
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isManuallyUpdating = true
|
|
||||||
defer { isManuallyUpdating = false }
|
|
||||||
viewControllers.removeSubrange(index...)
|
|
||||||
animateChanges {
|
|
||||||
for column in self.stackView.arrangedSubviews[index...] {
|
|
||||||
column.layer.opacity = 0
|
|
||||||
}
|
|
||||||
self.scrollColumnToEnd(columnIndex: index - 1, animated: false)
|
|
||||||
} completion: {
|
|
||||||
self.updateViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private func animateChanges(_ animations: @escaping () -> Void, completion: (() -> Void)? = nil) {
|
|
||||||
let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters())
|
|
||||||
animator.addAnimations(animations)
|
|
||||||
animator.addCompletion { _ in
|
|
||||||
completion?()
|
|
||||||
}
|
|
||||||
animator.startAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MultiColumnNavigationController: NavigationControllerProtocol {
|
|
||||||
var topViewController: UIViewController? {
|
|
||||||
viewControllers.last
|
|
||||||
}
|
|
||||||
|
|
||||||
func popToRootViewController(animated: Bool) -> [UIViewController]? {
|
|
||||||
let removed = Array(viewControllers.dropFirst())
|
|
||||||
viewControllers = [viewControllers.first!]
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
func pushViewController(_ vc: UIViewController, animated: Bool) {
|
|
||||||
isManuallyUpdating = true
|
|
||||||
defer { isManuallyUpdating = false }
|
|
||||||
viewControllers.append(vc)
|
|
||||||
updateViews()
|
|
||||||
scrollToEnd(animated: animated)
|
|
||||||
if animated {
|
|
||||||
let column = stackView.arrangedSubviews.last!
|
|
||||||
column.layer.opacity = 0
|
|
||||||
animateChanges {
|
|
||||||
column.layer.opacity = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ColumnView: UIView {
|
|
||||||
private unowned let owner: MultiColumnNavigationController
|
|
||||||
private let contentView = UIView()
|
|
||||||
private let navigationController: ColumnNavigationController
|
|
||||||
private var contentViewController: UIViewController!
|
|
||||||
|
|
||||||
init(owner: MultiColumnNavigationController, contentViewController: UIViewController, needsCloseButton: Bool) {
|
|
||||||
self.owner = owner
|
|
||||||
self.navigationController = ColumnNavigationController(owner: owner)
|
|
||||||
super.init(frame: .zero)
|
|
||||||
|
|
||||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(contentView)
|
|
||||||
|
|
||||||
layer.shadowOpacity = 0.2
|
|
||||||
layer.shadowRadius = 8
|
|
||||||
layer.shadowOffset = .zero
|
|
||||||
contentView.layer.masksToBounds = false
|
|
||||||
contentView.layer.cornerRadius = 12.5
|
|
||||||
contentView.layer.cornerCurve = .continuous
|
|
||||||
contentView.layer.masksToBounds = true
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
contentView.topAnchor.constraint(equalTo: topAnchor),
|
|
||||||
contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
||||||
|
|
||||||
widthAnchor.constraint(equalToConstant: 400),
|
|
||||||
])
|
|
||||||
|
|
||||||
setContent(contentViewController, needsCloseButton: needsCloseButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setContent(_ viewController: UIViewController, needsCloseButton: Bool) {
|
|
||||||
guard viewController !== contentViewController || viewController.parent !== navigationController else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
contentViewController?.removeViewAndController()
|
|
||||||
|
|
||||||
if navigationController.parent != owner {
|
|
||||||
navigationController.removeViewAndController()
|
|
||||||
|
|
||||||
owner.addChild(navigationController)
|
|
||||||
navigationController.didMove(toParent: owner)
|
|
||||||
navigationController.view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
contentView.addSubview(navigationController.view)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
navigationController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
||||||
navigationController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
|
||||||
navigationController.view.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
||||||
navigationController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
contentViewController = viewController
|
|
||||||
navigationController.setViewControllers([viewController], animated: false)
|
|
||||||
|
|
||||||
if needsCloseButton {
|
|
||||||
installCloseBarButton(navigationItem: viewController.navigationItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func willRemoveColumn() {
|
|
||||||
navigationController.removeViewAndController()
|
|
||||||
navigationController.setViewControllers([], animated: false)
|
|
||||||
removeCloseBarButton(navigationItem: contentViewController.navigationItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func installCloseBarButton(navigationItem: UINavigationItem) {
|
|
||||||
let item = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .done, target: self, action: #selector(closeNavigationColumn))
|
|
||||||
item.accessibilityLabel = "Close Column"
|
|
||||||
if navigationItem.leftBarButtonItems != nil {
|
|
||||||
navigationItem.leftBarButtonItems!.insert(item, at: 0)
|
|
||||||
} else {
|
|
||||||
navigationItem.leftBarButtonItems = [item]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func removeCloseBarButton(navigationItem: UINavigationItem) {
|
|
||||||
navigationItem.leftBarButtonItems = (navigationItem.leftBarButtonItems ?? []).filter {
|
|
||||||
$0.action != #selector(closeNavigationColumn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func closeNavigationColumn() {
|
|
||||||
owner.closeColumn(contentViewController)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ColumnNavigationController: UINavigationController {
|
|
||||||
unowned let owner: MultiColumnNavigationController
|
|
||||||
|
|
||||||
init(owner: MultiColumnNavigationController) {
|
|
||||||
self.owner = owner
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func show(_ vc: UIViewController, sender: Any?) {
|
|
||||||
owner.show(vc, sender: sender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension UIScrollView {
|
|
||||||
var adjustedLeadingContentInset: CGFloat {
|
|
||||||
if traitCollection.layoutDirection == .leftToRight {
|
|
||||||
adjustedContentInset.left
|
|
||||||
} else {
|
|
||||||
adjustedContentInset.right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var adjustedTrailingContentInset: CGFloat {
|
|
||||||
if traitCollection.layoutDirection == .leftToRight {
|
|
||||||
adjustedContentInset.right
|
|
||||||
} else {
|
|
||||||
adjustedContentInset.left
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -49,7 +49,19 @@ class SplitNavigationController: UIViewController {
|
||||||
rootNav.showImpl = { [unowned self] vc, sender in
|
rootNav.showImpl = { [unowned self] vc, sender in
|
||||||
if self.canShowSecondaryNav {
|
if self.canShowSecondaryNav {
|
||||||
self.setSecondaryViewControllers([vc], animated: true)
|
self.setSecondaryViewControllers([vc], animated: true)
|
||||||
SplitNavigationController.clearSelectedRow(sender: sender)
|
|
||||||
|
// the split nav shouldn't really be reaching down into the inner VCs like this,
|
||||||
|
// but I can't think of a cleaner way
|
||||||
|
if let tableVC = sender as? UITableViewController,
|
||||||
|
let selectedIndexPath = tableVC.tableView.indexPathForSelectedRow {
|
||||||
|
tableVC.tableView.deselectRow(at: selectedIndexPath, animated: true)
|
||||||
|
} else if let sender = sender as? UIViewController,
|
||||||
|
let collectionView = (sender as? CollectionViewController)?.collectionView ?? sender.view as? UICollectionView {
|
||||||
|
// the collection view's animation speed is weirdly fast, so we do it slower
|
||||||
|
UIView.animate(withDuration: 0.5, delay: 0) {
|
||||||
|
collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.rootNav.pushViewController(vc, animated: true)
|
self.rootNav.pushViewController(vc, animated: true)
|
||||||
}
|
}
|
||||||
|
@ -106,21 +118,6 @@ class SplitNavigationController: UIViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func clearSelectedRow(sender: Any?) {
|
|
||||||
// the split nav shouldn't really be reaching down into the inner VCs like this,
|
|
||||||
// but I can't think of a cleaner way
|
|
||||||
if let tableVC = sender as? UITableViewController,
|
|
||||||
let selectedIndexPath = tableVC.tableView.indexPathForSelectedRow {
|
|
||||||
tableVC.tableView.deselectRow(at: selectedIndexPath, animated: true)
|
|
||||||
} else if let sender = sender as? UIViewController,
|
|
||||||
let collectionView = (sender as? CollectionViewController)?.collectionView ?? sender.view as? UICollectionView {
|
|
||||||
// the collection view's animation speed is weirdly fast, so we do it slower
|
|
||||||
UIView.animate(withDuration: 0.5, delay: 0) {
|
|
||||||
collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
@ -290,9 +287,6 @@ private class SplitSecondaryNavigationController: EnhancedNavigationViewControll
|
||||||
|
|
||||||
override var viewControllers: [UIViewController] {
|
override var viewControllers: [UIViewController] {
|
||||||
didSet {
|
didSet {
|
||||||
for vc in oldValue where vc.parent !== self {
|
|
||||||
removeSecondarySplitCloseButton(for: vc)
|
|
||||||
}
|
|
||||||
if let first = viewControllers.first {
|
if let first = viewControllers.first {
|
||||||
configureSecondarySplitCloseButton(for: first)
|
configureSecondarySplitCloseButton(for: first)
|
||||||
}
|
}
|
||||||
|
@ -327,12 +321,6 @@ private class SplitSecondaryNavigationController: EnhancedNavigationViewControll
|
||||||
viewController.navigationItem.leftBarButtonItem = item
|
viewController.navigationItem.leftBarButtonItem = item
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeSecondarySplitCloseButton(for viewController: UIViewController) {
|
|
||||||
if viewController.navigationItem.leftBarButtonItem?.tag == ViewTags.splitNavCloseSecondaryButton {
|
|
||||||
viewController.navigationItem.leftBarButtonItem = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func closeSecondary() {
|
@objc private func closeSecondary() {
|
||||||
closeSecondaryImpl()
|
closeSecondaryImpl()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue