forked from shadowfacts/Tusker
Add widescreen navigation mode preference
This commit is contained in:
parent
9c368f295e
commit
88105f22a0
|
@ -171,6 +171,7 @@ 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
|
||||||
|
@ -224,6 +225,10 @@ 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
|
||||||
|
@ -425,6 +430,14 @@ extension Preferences {
|
||||||
|
|
||||||
extension Preferences {
|
extension Preferences {
|
||||||
public enum FeatureFlag: String, Codable {
|
public enum FeatureFlag: String, Codable {
|
||||||
case test
|
case iPadMultiColumn = "ipad-multi-column"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Preferences {
|
||||||
|
public enum WidescreenNavigationMode: String, Codable {
|
||||||
|
case stack
|
||||||
|
case splitScreen
|
||||||
|
case multiColumn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,6 +206,7 @@
|
||||||
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 */; };
|
||||||
|
@ -605,6 +606,7 @@
|
||||||
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>"; };
|
||||||
|
@ -1111,6 +1113,7 @@
|
||||||
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 */,
|
||||||
|
@ -2219,6 +2222,7 @@
|
||||||
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 */,
|
||||||
|
|
|
@ -35,7 +35,7 @@ struct AdvancedPrefsView : View {
|
||||||
isShowingFeatureFlagAlert = true
|
isShowingFeatureFlagAlert = true
|
||||||
}
|
}
|
||||||
.alert("Enable Feature Flag", isPresented: $isShowingFeatureFlagAlert) {
|
.alert("Enable Feature Flag", isPresented: $isShowingFeatureFlagAlert) {
|
||||||
TextField("Name", text: $featureFlagName)
|
TextField("Flag Name", text: $featureFlagName)
|
||||||
.textInputAutocapitalization(.never)
|
.textInputAutocapitalization(.never)
|
||||||
.autocorrectionDisabled()
|
.autocorrectionDisabled()
|
||||||
|
|
||||||
|
@ -45,6 +45,8 @@ 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,6 +45,7 @@ struct AppearancePrefsView : View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
themeSection
|
themeSection
|
||||||
|
interfaceSection
|
||||||
accountsSection
|
accountsSection
|
||||||
postsSection
|
postsSection
|
||||||
}
|
}
|
||||||
|
@ -87,6 +88,15 @@ 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) {
|
||||||
|
|
|
@ -0,0 +1,464 @@
|
||||||
|
//
|
||||||
|
// 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()
|
||||||
|
}
|
Loading…
Reference in New Issue