From 88105f22a0226e5b18fe1c90de00ccd24a35f683 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 3 Sep 2023 17:39:58 -0400 Subject: [PATCH] Add widescreen navigation mode preference --- .../TuskerPreferences/Preferences.swift | 15 +- Tusker.xcodeproj/project.pbxproj | 4 + .../Preferences/AdvancedPrefsView.swift | 4 +- .../Preferences/AppearancePrefsView.swift | 10 + .../WidescreenNavigationPrefsView.swift | 464 ++++++++++++++++++ 5 files changed, 495 insertions(+), 2 deletions(-) create mode 100644 Tusker/Screens/Preferences/WidescreenNavigationPrefsView.swift diff --git a/Packages/TuskerPreferences/Sources/TuskerPreferences/Preferences.swift b/Packages/TuskerPreferences/Sources/TuskerPreferences/Preferences.swift index 2f894db8..9e68232d 100644 --- a/Packages/TuskerPreferences/Sources/TuskerPreferences/Preferences.swift +++ b/Packages/TuskerPreferences/Sources/TuskerPreferences/Preferences.swift @@ -171,6 +171,7 @@ public class Preferences: Codable, ObservableObject { @Published public var showLinkPreviews = true @Published public var leadingStatusSwipeActions: [StatusSwipeAction] = [.favorite, .reblog] @Published public var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share] + @Published public var widescreenNavigationMode = WidescreenNavigationMode.multiColumn // MARK: Composing @Published public var defaultPostVisibility = Visibility.public @@ -224,6 +225,10 @@ public class Preferences: Codable, ObservableObject { @Published public var hasShownLocalTimelineDescription = false @Published public var hasShownFederatedTimelineDescription = false + public func hasFeatureFlag(_ flag: FeatureFlag) -> Bool { + enabledFeatureFlags.contains(flag) + } + private enum CodingKeys: String, CodingKey { case theme case pureBlackDarkMode @@ -425,6 +430,14 @@ extension Preferences { extension Preferences { 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 } } diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index da1f0785..f7feb71f 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -206,6 +206,7 @@ D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; }; D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; }; D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */; }; + D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */; }; D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.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 */; }; @@ -605,6 +606,7 @@ D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = ""; }; D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = ""; }; 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 = ""; }; D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowSceneDelegate+Close.swift"; sourceTree = ""; }; D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = ""; }; D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = ""; }; @@ -1111,6 +1113,7 @@ 04586B4022B2FFB10021BD04 /* PreferencesView.swift */, 04586B4222B301470021BD04 /* AppearancePrefsView.swift */, D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */, + D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */, 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */, D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */, D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */, @@ -2219,6 +2222,7 @@ D60088F22980DAA0005B4D00 /* TipJarView.swift in Sources */, D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */, D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */, + D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */, D65B4B562971F98300DABDFB /* ReportView.swift in Sources */, D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */, D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */, diff --git a/Tusker/Screens/Preferences/AdvancedPrefsView.swift b/Tusker/Screens/Preferences/AdvancedPrefsView.swift index 7e9d68bb..638185a5 100644 --- a/Tusker/Screens/Preferences/AdvancedPrefsView.swift +++ b/Tusker/Screens/Preferences/AdvancedPrefsView.swift @@ -35,7 +35,7 @@ struct AdvancedPrefsView : View { isShowingFeatureFlagAlert = true } .alert("Enable Feature Flag", isPresented: $isShowingFeatureFlagAlert) { - TextField("Name", text: $featureFlagName) + TextField("Flag Name", text: $featureFlagName) .textInputAutocapitalization(.never) .autocorrectionDisabled() @@ -45,6 +45,8 @@ struct AdvancedPrefsView : View { 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")) } diff --git a/Tusker/Screens/Preferences/AppearancePrefsView.swift b/Tusker/Screens/Preferences/AppearancePrefsView.swift index 5a3e0ee5..a6ea68d5 100644 --- a/Tusker/Screens/Preferences/AppearancePrefsView.swift +++ b/Tusker/Screens/Preferences/AppearancePrefsView.swift @@ -45,6 +45,7 @@ struct AppearancePrefsView : View { var body: some View { List { themeSection + interfaceSection accountsSection postsSection } @@ -87,6 +88,15 @@ struct AppearancePrefsView : View { .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 { Section(header: Text("Accounts")) { Toggle(isOn: useCircularAvatars) { diff --git a/Tusker/Screens/Preferences/WidescreenNavigationPrefsView.swift b/Tusker/Screens/Preferences/WidescreenNavigationPrefsView.swift new file mode 100644 index 00000000..89bf1792 --- /dev/null +++ b/Tusker/Screens/Preferences/WidescreenNavigationPrefsView.swift @@ -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() + + var body: some View { + HStack { + Spacer() + + OptionView( + value: .stack, + selection: $preferences.widescreenNavigationMode, + startAnimation: startAnimation + ) { + Text("Stack") + } + + Spacer(minLength: 32) + + OptionView( + value: .splitScreen, + selection: $preferences.widescreenNavigationMode, + startAnimation: startAnimation + ) { + Text("Split Screen") + } + + if preferences.hasFeatureFlag(.iPadMultiColumn) { + Spacer(minLength: 32) + + OptionView( + 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: View { + let value: Preferences.WidescreenNavigationMode + @Binding var selection: Preferences.WidescreenNavigationMode + let startAnimation: PassthroughSubject + @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(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) +} + +private struct NavigationModeRepresentable: UIViewRepresentable { + let startAnimation: PassthroughSubject + + 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) { + 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) { + 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) { + 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() +}