diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index 474ef03d5..be91d967e 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -418,6 +418,9 @@ E40E018C2AABF85500410B2C /* NavigationRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40E018B2AABF85500410B2C /* NavigationRoutes.swift */; }; E40E018E2AABFBDE00410B2C /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40E018D2AABFBDE00410B2C /* NavigationRouter.swift */; }; E40E01902AABFC9300410B2C /* AnyNavigationPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40E018F2AABFC9300410B2C /* AnyNavigationPath.swift */; }; + E41FAD792AB12C2500557719 /* Environment+ScrollViewReaderProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41FAD782AB12C2500557719 /* Environment+ScrollViewReaderProxy.swift */; }; + E41FAD7B2AB12D5900557719 /* ScrollToView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41FAD7A2AB12D5900557719 /* ScrollToView.swift */; }; + E4516E472AAC4B3500F496BE /* DismissAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4516E462AAC4B3500F496BE /* DismissAction.swift */; }; E453477E2A9DE37300D1B46F /* Array+SafeIndexing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E453477D2A9DE37300D1B46F /* Array+SafeIndexing.swift */; }; E453A1D02A81C2140004BB8A /* QuickLookPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E453A1CF2A81C2140004BB8A /* QuickLookPreviewController.swift */; }; E47478132AAC350E001CB1AC /* NavigationLink+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E47478122AAC350E001CB1AC /* NavigationLink+Helpers.swift */; }; @@ -858,6 +861,9 @@ E40E018B2AABF85500410B2C /* NavigationRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRoutes.swift; sourceTree = ""; }; E40E018D2AABFBDE00410B2C /* NavigationRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouter.swift; sourceTree = ""; }; E40E018F2AABFC9300410B2C /* AnyNavigationPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyNavigationPath.swift; sourceTree = ""; }; + E41FAD782AB12C2500557719 /* Environment+ScrollViewReaderProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+ScrollViewReaderProxy.swift"; sourceTree = ""; }; + E41FAD7A2AB12D5900557719 /* ScrollToView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToView.swift; sourceTree = ""; }; + E4516E462AAC4B3500F496BE /* DismissAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissAction.swift; sourceTree = ""; }; E453477D2A9DE37300D1B46F /* Array+SafeIndexing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+SafeIndexing.swift"; sourceTree = ""; }; E453A1CF2A81C2140004BB8A /* QuickLookPreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickLookPreviewController.swift; sourceTree = ""; }; E47478122AAC350E001CB1AC /* NavigationLink+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NavigationLink+Helpers.swift"; sourceTree = ""; }; @@ -1316,6 +1322,8 @@ 63A200522A2DDD38005CDDE3 /* Dictionary - Append.swift */, E4DDB4312A81819300B3A7E0 /* Double.swift */, B1955A202A6145C00056CF99 /* Environment - EasterFlagSetter.swift */, + E41FAD782AB12C2500557719 /* Environment+ScrollViewReaderProxy.swift */, + CDE8F2382A68DA7D00E0AE68 /* Environment - Force Onboard.swift */, 507573902A5AD53C00AA7ABD /* Error+Equatable.swift */, 6D8601ED2A43C0B1002A56FC /* Image.swift */, 6DA61F842A568F99001EA633 /* Int.swift */, @@ -1746,6 +1754,7 @@ B11D72822A49FAA7009DC22F /* Cached Image.swift */, 50F2851B2A5C5C1500CF8865 /* TokenRefreshView.swift */, CD863FBB2A6B026400A31ED9 /* DocumentView.swift */, + E41FAD7A2AB12D5900557719 /* ScrollToView.swift */, ); path = Shared; sourceTree = ""; @@ -2237,6 +2246,7 @@ E49F0E752A90395400BC4EE3 /* NavigationPath+Helpers.swift */, E47B2B772A902E3C00629AF7 /* Router */, E47B2B742A902DB400629AF7 /* Route */, + E4516E462AAC4B3500F496BE /* DismissAction.swift */, ); path = Navigation; sourceTree = ""; @@ -2509,6 +2519,7 @@ CDEBC3282A9A57F200518D9D /* Content Model Identifier.swift in Sources */, B1955A1D2A606B950056CF99 /* Easter Rewards.swift in Sources */, CDB0117F2A6F70A000D043EB /* Editor Tracker.swift in Sources */, + E4516E472AAC4B3500F496BE /* DismissAction.swift in Sources */, 6354F30A2A2E20040074C08D /* Alert - Multiple Alerts.swift in Sources */, 6318EDC727EE4E1500BFCAE8 /* Post.swift in Sources */, 6372186C2A3A2AAD008C4816 /* SaveComment.swift in Sources */, @@ -2598,6 +2609,7 @@ 637218502A3A2AAD008C4816 /* APIPersonAggregates.swift in Sources */, 6D693A422A5114DF009E2D76 /* APIPostReport.swift in Sources */, 5064D03F2A6DE0DB00B22EE3 /* Notifier+Dependency.swift in Sources */, + E41FAD792AB12C2500557719 /* Environment+ScrollViewReaderProxy.swift in Sources */, 6D8003792A45FD1300363206 /* Bundle.swift in Sources */, 63344C712A098060001BC616 /* Sidebar View.swift in Sources */, 6DE118392A4A20D600810C7E /* Lazy Load Post Link.swift in Sources */, @@ -2777,6 +2789,7 @@ 6318DE5427FB958800CC2AD6 /* Stickied Tag.swift in Sources */, CD7B53B72A5F258B00006E81 /* APIPrivateMessageReportView.swift in Sources */, 030AC04F2A6464DA00037155 /* CommunitySettingsView.swift in Sources */, + E41FAD7B2AB12D5900557719 /* ScrollToView.swift in Sources */, 6386E03A2A0455BC006B3C1D /* String - Contains Elements From Array.swift in Sources */, 63F0C7A62A05225100A18C5D /* Saved Account.swift in Sources */, 637218462A3A2AAD008C4816 /* APIComment.swift in Sources */, diff --git a/Mlem/ContentView.swift b/Mlem/ContentView.swift index 63061076e..99aaf6486 100644 --- a/Mlem/ContentView.swift +++ b/Mlem/ContentView.swift @@ -24,7 +24,6 @@ struct ContentView: View { // tabs @State private var tabSelection: TabSelection = .feeds - @State private var tabNavigation: any FancyTabBarSelection = TabSelection._tabBarNavigation @State private var showLoading: Bool = false @GestureState private var isDetectingLongPress = false @@ -36,7 +35,7 @@ struct ContentView: View { var accessibilityFont: Bool { UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory } var body: some View { - FancyTabBar(selection: $tabSelection, navigationSelection: $tabNavigation, dragUpGestureCallback: showAccountSwitcherDragCallback) { + FancyTabBar(selection: $tabSelection, dragUpGestureCallback: showAccountSwitcherDragCallback) { Group { FeedRoot(showLoading: showLoading) .fancyTabItem(tag: TabSelection.feeds) { diff --git a/Mlem/Custom Tab Bar/FancyTabBar.swift b/Mlem/Custom Tab Bar/FancyTabBar.swift index 8d30200ea..5423c0b9c 100644 --- a/Mlem/Custom Tab Bar/FancyTabBar.swift +++ b/Mlem/Custom Tab Bar/FancyTabBar.swift @@ -16,7 +16,7 @@ struct FancyTabBar: View { @Binding private var selection: Selection /// Keeps track of tab "re-selected" state. - @Binding private var navigationSelection: NavigationSelection + @State private var navigationSelection: NavigationSelection @State private var __tempNavigationSelection: Int = -1 /// We only toggle this to trigger an `onChange` event. @State private var __navigationSelectionSignal: Bool = false @@ -30,12 +30,11 @@ struct FancyTabBar: View { init( selection: Binding, - navigationSelection: Binding, dragUpGestureCallback: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content ) { self._selection = selection - self._navigationSelection = navigationSelection + self._navigationSelection = .init(wrappedValue: selection.wrappedValue) self.content = content self.dragUpGestureCallback = dragUpGestureCallback } diff --git a/Mlem/Extensions/Environment+ScrollViewReaderProxy.swift b/Mlem/Extensions/Environment+ScrollViewReaderProxy.swift new file mode 100644 index 000000000..9e02b86af --- /dev/null +++ b/Mlem/Extensions/Environment+ScrollViewReaderProxy.swift @@ -0,0 +1,19 @@ +// +// Environment+ScrollViewReaderProxy.swift +// Mlem +// +// Created by Bosco Ho on 2023-09-12. +// + +import SwiftUI + +private struct ScrollViewReaderProxy: EnvironmentKey { + static let defaultValue: ScrollViewProxy? = nil +} + +extension EnvironmentValues { + var scrollViewProxy: ScrollViewProxy? { + get { self[ScrollViewReaderProxy.self] } + set { self[ScrollViewReaderProxy.self] = newValue } + } +} diff --git a/Mlem/Extensions/IconSettingsView.swift b/Mlem/Extensions/IconSettingsView.swift index f10446e1a..7290ccfe2 100644 --- a/Mlem/Extensions/IconSettingsView.swift +++ b/Mlem/Extensions/IconSettingsView.swift @@ -24,6 +24,8 @@ let iconFinder = Regex { // struct AlternativeIcons: View { struct IconSettingsView: View { @State var currentIcon: String? = UIApplication.shared.alternateIconName + + @Environment(\.dismiss) private var dismiss @EnvironmentObject var easterTracker: EasterFlagsTracker var body: some View { @@ -33,6 +35,7 @@ struct IconSettingsView: View { } } .fancyTabScrollCompatible() + .hoistNavigation(dismiss: dismiss) } func getAllIcons() -> [AlternativeIcon] { diff --git a/Mlem/Navigation/DismissAction.swift b/Mlem/Navigation/DismissAction.swift new file mode 100644 index 000000000..defc83e45 --- /dev/null +++ b/Mlem/Navigation/DismissAction.swift @@ -0,0 +1,136 @@ +// +// DismissAction.swift +// Mlem +// +// Created by Bosco Ho on 2023-09-08. +// + +import Dependencies +import Foundation +import SwiftUI + +// MARK: - Navigation +final class Navigation: ObservableObject { + + /// Return `true` to indicate that an auxiliary action was performed. + typealias AuxiliaryAction = () -> Bool + + var pathActions: [Int: (dismiss: DismissAction?, auxiliaryAction: AuxiliaryAction?)] = [:] + + /// Navigation always performs dismiss action (if available), but may choose to perform an auxiliary action first. + /// + /// This action includes support for popping back to sidebar view in a `NavigationSplitView`. + var dismiss: DismissAction? + /// An auxiliary action may consist of multiple sub-actions: To do so, simply configure this action to return false once all sub-actions have been (or can no longer be) performed. + /// + /// - Warning: Navigation may skip this action, depending on user preference or other factors. Do not perform critical logic in this action. + var auxiliaryAction: AuxiliaryAction? +} + +// MARK: - Hoist dismiss action +extension View { + + func hoistNavigation( + dismiss: DismissAction, + auxiliaryAction: Navigation.AuxiliaryAction? = nil + ) -> some View { + modifier( + NavigationDismissHoisting( + dismiss: dismiss, + auxiliaryAction: auxiliaryAction + ) + ) + } +} + +struct NavigationDismissHoisting: ViewModifier { + + @EnvironmentObject private var navigation: Navigation + @Environment(\.navigationPathWithRoutes) private var navigationPath + + /// - Note: Unfortunately, we can't access the dismiss action via View.environment...doing so causes SwiftUI to enter into infinite loop. [2023.09] + let dismiss: DismissAction + let auxiliaryAction: Navigation.AuxiliaryAction? + + @State private var didAppear = false + + func body(content: Content) -> some View { + content + .onAppear { + defer { didAppear = true } + + /// This must only be called once: + /// For example, user may wish to drag to peek at the previous view, but then cancel that drag action. During this, the previous view's .onAppear will get called. If we run this logic for that view again, the actual top view's dismiss action will get lost. [2023.09] + if didAppear == false { + print("hoist navigation dismiss action") + navigation.dismiss = dismiss + navigation.auxiliaryAction = auxiliaryAction + let pathIndex = max(0, navigationPath.count) + print("adding path action at index -> \(pathIndex)") + navigation.pathActions[pathIndex] = (dismiss, auxiliaryAction) + } + } + .onDisappear { + print("onDisappear: path count -> \(navigationPath.count), action count -> \(navigation.pathActions.count)") + print("removing path action at index -> \(navigationPath.count + 1)") + navigation.pathActions.removeValue(forKey: navigationPath.count + 1) + } + } +} + +// MARK: - Enable tab bar navigation +extension View { + + /// Unconditionally enable tab bar navigation. + func tabBarNavigationEnabled(_ tab: TabSelection, _ navigator: Navigation) -> some View { + modifier(PerformTabBarNavigation(tab: tab, navigator: navigator)) + } +} + +struct PerformTabBarNavigation: ViewModifier { + + @Dependency(\.hapticManager) private var hapticManager + @Environment(\.navigationPathWithRoutes) private var navigationPath + @Environment(\.tabNavigationSelectionHashValue) private var selectedNavigationTabHashValue + + let tab: TabSelection + let navigator: Navigation + + func body(content: Content) -> some View { + content.onChange(of: selectedNavigationTabHashValue) { newValue in + if newValue == tab.hashValue { + hapticManager.play(haptic: .gentleInfo, priority: .high) + // Customization based on user preference should occur here, for example: + // performSystemPopToRootBehaviour() + // noOp() + // performDimsissOnly() + performDismissAfterAuxiliary() + } + } + } + + /// Runs all auxiliary actions before calling system dismiss action. + private func performDismissAfterAuxiliary() { + print("perform action on path index -> \(navigationPath.count)") + guard let pathAction = navigator.pathActions[navigationPath.count] else { + print("path action not found at index -> \(navigationPath.count)") + return + } + + if let auxiliaryAction = pathAction.auxiliaryAction { + let performed = auxiliaryAction() + if !performed, let dismiss = pathAction.dismiss { + print("found auxiliary action, but that logic has been exhausted...perform standard dismiss action") + print("perform tab navigation on \(tab) tab") + dismiss() + } else { + print("performed auxiliary action") + } + } else if let dismiss = pathAction.dismiss { + print("perform dismiss action via tab navigation on \(tab) tab") + dismiss() + } else { + print("attempted tab navigation -> action(s) not found") + } + } +} diff --git a/Mlem/Views/Shared/Accounts/Accounts Page.swift b/Mlem/Views/Shared/Accounts/Accounts Page.swift index 899a9e56e..b30994642 100644 --- a/Mlem/Views/Shared/Accounts/Accounts Page.swift +++ b/Mlem/Views/Shared/Accounts/Accounts Page.swift @@ -80,6 +80,14 @@ struct AccountsPage: View { } } } + .hoistNavigation(dismiss: dismiss) + .onAppear { + selectedAccount = appState.currentActiveAccount + } + .onChange(of: selectedAccount) { account in + guard let account else { return } + appState.setActiveAccount(account) + } .sheet(isPresented: $isShowingInstanceAdditionSheet) { AddSavedInstanceView(onboarding: false) } diff --git a/Mlem/Views/Shared/DocumentView.swift b/Mlem/Views/Shared/DocumentView.swift index 966cf22fd..540ad08bf 100644 --- a/Mlem/Views/Shared/DocumentView.swift +++ b/Mlem/Views/Shared/DocumentView.swift @@ -10,6 +10,8 @@ import SwiftUI /// Displays a document struct DocumentView: View { + + @Environment(\.dismiss) private var dismiss let text: String var body: some View { @@ -18,5 +20,6 @@ struct DocumentView: View { .padding() } .fancyTabScrollCompatible() + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Shared/Posts/Expanded Post.swift b/Mlem/Views/Shared/Posts/Expanded Post.swift index 2526082a9..0fe1d9717 100644 --- a/Mlem/Views/Shared/Posts/Expanded Post.swift +++ b/Mlem/Views/Shared/Posts/Expanded Post.swift @@ -49,6 +49,8 @@ struct ExpandedPost: View { @AppStorage("showCommentJumpButton") var showCommentJumpButton: Bool = true @AppStorage("commentJumpButtonSide") var commentJumpButtonSide: JumpButtonLocation = .bottomTrailing + @Environment(\.dismiss) private var dismiss + @EnvironmentObject var appState: AppState @EnvironmentObject var editorTracker: EditorTracker @EnvironmentObject var layoutWidgetTracker: LayoutWidgetTracker @@ -74,6 +76,7 @@ struct ExpandedPost: View { contentView .environmentObject(commentTracker) .navigationBarTitle(post.community.name, displayMode: .inline) + .hoistNavigation(dismiss: dismiss) .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { toolbarMenu } } @@ -85,6 +88,12 @@ struct ExpandedPost: View { commentTracker.comments = sortComments(commentTracker.comments, by: newSortingType) } } + .onAppear { + print("ExpandedPost appeared") + } + .onDisappear { + print("ExpandedPost disappeared") + } } private var contentView: some View { diff --git a/Mlem/Views/Shared/Posts/Lazy Load Expanded Post.swift b/Mlem/Views/Shared/Posts/Lazy Load Expanded Post.swift index f3395455c..3933fc795 100644 --- a/Mlem/Views/Shared/Posts/Lazy Load Expanded Post.swift +++ b/Mlem/Views/Shared/Posts/Lazy Load Expanded Post.swift @@ -14,6 +14,8 @@ import SwiftUI struct LazyLoadExpandedPost: View { @Dependency(\.errorHandler) var errorHandler + @Environment(\.dismiss) private var dismiss + let post: APIPost let scrollTarget: Int? @@ -38,6 +40,7 @@ struct LazyLoadExpandedPost: View { progressView } } + .hoistNavigation(dismiss: dismiss) } private var progressView: some View { diff --git a/Mlem/Views/Shared/ScrollToView.swift b/Mlem/Views/Shared/ScrollToView.swift new file mode 100644 index 000000000..85ab445d6 --- /dev/null +++ b/Mlem/Views/Shared/ScrollToView.swift @@ -0,0 +1,50 @@ +// +// ScrollToView.swift +// Mlem +// +// Created by Bosco Ho on 2023-09-12. +// + +import SwiftUI + +/// To enable scroll to behaviour: Assign a @Namespace id, and place this view inside a scroll view in the desired position. +/// +/// This view is not visible to users. +/// - Note: Use `ListScrollToView` in `List`. +/// - Warning: Do not set this view to hidden. +struct ScrollToView: View { + + @Binding var appeared: Bool + + var body: some View { + HStack(spacing: 0) { + EmptyView() + } + .frame(height: 1) + .onAppear { + appeared = true + } + .onDisappear { + appeared = false + } + } +} + +/// For use inside `List`. +/// +/// See also: `ScrollToView`. +struct ListScrollToView: View { + + @Binding var appeared: Bool + + var body: some View { + EmptyView() + .frame(height: 1) + .onAppear { + appeared = true + } + .onDisappear { + appeared = false + } + } +} diff --git a/Mlem/Views/Tabs/Feeds/Community List/Community List View.swift b/Mlem/Views/Tabs/Feeds/Community List/Community List View.swift index d04398d7f..aa96498c2 100644 --- a/Mlem/Views/Tabs/Feeds/Community List/Community List View.swift +++ b/Mlem/Views/Tabs/Feeds/Community List/Community List View.swift @@ -18,8 +18,15 @@ struct CommunitySection: Identifiable { struct CommunityListView: View { - @StateObject private var model: CommunityListModel = .init() + @Environment(\.tabNavigationSelectionHashValue) private var selectedNavigationTabHashValue + + @Namespace var scrollToTop + @StateObject private var model: CommunityListModel = .init() + + /// Set to `false` on disappear. + @State private var appeared: Bool = false + @Binding var selectedCommunity: CommunityLinkWithContext? init(selectedCommunity: Binding) { @@ -73,9 +80,25 @@ struct CommunityListView: View { .navigationBarColor() .listStyle(PlainListStyle()) .scrollIndicators(.hidden) - + .onAppear { + appeared = true + } + .onDisappear { + appeared = false + } + SectionIndexTitles(proxy: scrollProxy, communitySections: model.allSections()) } + .onChange(of: selectedNavigationTabHashValue) { newValue in + guard appeared else { + return + } + if newValue == TabSelection.feeds.hashValue { + withAnimation { + scrollProxy.scrollTo("top", anchor: .bottom) + } + } + } } .refreshable { await model.load() diff --git a/Mlem/Views/Tabs/Feeds/Components/Sidebar View.swift b/Mlem/Views/Tabs/Feeds/Components/Sidebar View.swift index 3108a9cd9..b647d5ba4 100644 --- a/Mlem/Views/Tabs/Feeds/Components/Sidebar View.swift +++ b/Mlem/Views/Tabs/Feeds/Components/Sidebar View.swift @@ -12,6 +12,8 @@ struct CommunitySidebarView: View { @Dependency(\.communityRepository) var communityRepository @Dependency(\.errorHandler) var errorHandler + @Environment(\.dismiss) private var dismiss + // parameters let community: APICommunity @State var communityDetails: GetCommunityResponse? @@ -32,6 +34,7 @@ struct CommunitySidebarView: View { } .navigationTitle("Sidebar") .navigationBarTitleDisplayMode(.inline) + .hoistNavigation(dismiss: dismiss) .task(priority: .userInitiated) { // Load community details if they weren't provided // when we loaded diff --git a/Mlem/Views/Tabs/Feeds/Feed Root.swift b/Mlem/Views/Tabs/Feeds/Feed Root.swift index d4cb7f7d4..28dc37354 100644 --- a/Mlem/Views/Tabs/Feeds/Feed Root.swift +++ b/Mlem/Views/Tabs/Feeds/Feed Root.swift @@ -12,36 +12,54 @@ struct FeedRoot: View { @Environment(\.scenePhase) var phase @Environment(\.tabSelectionHashValue) private var selectedTagHashValue @Environment(\.tabNavigationSelectionHashValue) private var selectedNavigationTabHashValue - + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @AppStorage("defaultFeed") var defaultFeed: FeedType = .subscribed @AppStorage("defaultPostSorting") var defaultPostSorting: PostSortType = .hot @StateObject private var feedRouter: NavigationRouter = .init() + @StateObject private var navigation: Navigation = .init() @State var rootDetails: CommunityLinkWithContext? let showLoading: Bool + + @State private var columnVisibility: NavigationSplitViewVisibility = .automatic var body: some View { - NavigationSplitView { - CommunityListView(selectedCommunity: $rootDetails) - } detail: { - if let rootDetails { + /* + Implementation Note: + - The conditional content in `detail` column must be inside the `NavigationStack`. To be clear, the root view for `detail` column must be `NavigationStack`, otherwise navigation may break in odd ways. [2023.09] + - For tab bar navigation (scroll to top) to work, ScrollViewReader must wrap the entire `NavigationSplitView`. Furthermore, the proxy must be passed into the environment on the split view. Attempting to do so on a column view doesn't work. [2023.09] + */ + ScrollViewReader { proxy in + NavigationSplitView(columnVisibility: $columnVisibility) { + CommunityListView(selectedCommunity: $rootDetails) + .id(appState.currentActiveAccount.id) + } detail: { NavigationStack(path: $feedRouter.path) { - FeedView( - community: rootDetails.community, - feedType: rootDetails.feedType, - sortType: defaultPostSorting, - showLoading: showLoading - ) - .environmentObject(appState) - .handleLemmyViews() + if let rootDetails { + FeedView( + community: rootDetails.community, + feedType: rootDetails.feedType, + sortType: defaultPostSorting, + showLoading: showLoading, + rootDetails: $rootDetails, + splitViewColumnVisibility: $columnVisibility + ) + .environmentObject(appState) + .tabBarNavigationEnabled(.feeds, navigation) + .handleLemmyViews() + } else { + Text("Please select a community") + } } - .id(rootDetails.id) - } else { - Text("Please select a community") + .id((rootDetails?.id ?? 0) + appState.currentActiveAccount.id) } + .environment(\.scrollViewProxy, proxy) } + .environment(\.navigationPathWithRoutes, $feedRouter.path) + .environmentObject(navigation) .handleLemmyLinkResolution( navigationPath: .constant(feedRouter) ) diff --git a/Mlem/Views/Tabs/Feeds/Feed View.swift b/Mlem/Views/Tabs/Feeds/Feed View.swift index d092d3e37..cad8bd140 100644 --- a/Mlem/Views/Tabs/Feeds/Feed View.swift +++ b/Mlem/Views/Tabs/Feeds/Feed View.swift @@ -9,6 +9,7 @@ import Dependencies import Foundation import SwiftUI +// swiftlint:disable type_body_length struct FeedView: View { // MARK: Environment and settings @@ -24,6 +25,9 @@ struct FeedView: View { @AppStorage("postSize") var postSize: PostSize = .large @AppStorage("showReadPosts") var showReadPosts: Bool = true + @Environment(\.navigationPathWithRoutes) private var navigationPath + @Environment(\.dismiss) private var dismiss + @Environment(\.scrollViewProxy) private var scrollViewProxy @EnvironmentObject var appState: AppState @EnvironmentObject var filtersTracker: FiltersTracker @EnvironmentObject var editorTracker: EditorTracker @@ -33,12 +37,17 @@ struct FeedView: View { let community: APICommunity? let showLoading: Bool @State var feedType: FeedType + @Binding var rootDetails: CommunityLinkWithContext? + /// Applicable when presented as root view in a column of NavigationSplitView. + @Binding var splitViewColumnVisibility: NavigationSplitViewVisibility init( community: APICommunity?, feedType: FeedType, sortType: PostSortType, - showLoading: Bool = false + showLoading: Bool = false, + rootDetails: Binding? = nil, + splitViewColumnVisibility: Binding? = nil ) { // need to grab some stuff from app storage to initialize post tracker with @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast @@ -54,6 +63,9 @@ struct FeedView: View { internetSpeed: internetSpeed, upvoteOnSave: upvoteOnSave )) + + self._rootDetails = rootDetails ?? .constant(nil) + self._splitViewColumnVisibility = splitViewColumnVisibility ?? .constant(.automatic) } // MARK: State @@ -67,6 +79,14 @@ struct FeedView: View { @AppStorage("hasTranslucentInsets") var hasTranslucentInsets: Bool = true + @Namespace var scrollToTop + @State private var scrollToTopAppeared = false + private var scrollToTopId: Int? { + postTracker.items.first?.id + } + + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + // MARK: Destructive confirmation @State private var isPresentingConfirmDestructive: Bool = false @@ -89,12 +109,40 @@ struct FeedView: View { ToolbarItemGroup(placement: .navigationBarTrailing) { ellipsisMenu } } .navigationBarTitleDisplayMode(.inline) - /// [2023.08] Set to `.visible` to workaround bug where navigation bar background may disappear on certain devices when device rotates. + /// [2023.08] Set to `.visible` to workaround bug where navigation bar background may disappear on certain devices when device rotates. .navigationBarColor(visibility: .visible) + .hoistNavigation( + dismiss: dismiss, + auxiliaryAction: { + /// Need to check `scrollToTopAppeared` because we want to scroll to top before popping back to sidebar. [2023.09] + if navigationPath.isEmpty, scrollToTopAppeared == false { + print("scroll to top") + withAnimation { + scrollViewProxy?.scrollTo(scrollToTop, anchor: .top) + } + return true + } else { + if horizontalSizeClass == .regular { + print("show/hide sidebar in regular size class") + splitViewColumnVisibility = { + if splitViewColumnVisibility == .all { + return .automatic + } else { + return .all + } + }() + return true + } else { + print("exhausted auxiliary actions") + return false + } + } + } + ) .environmentObject(postTracker) .task(priority: .userInitiated) { await initFeed() } .task(priority: .background) { await fetchCommunityDetails() } - // using hardRefreshFeed() for these three so that the user gets immediate feedback, also kills the ScrollViewReader + // using hardRefreshFeed() for these three so that the user gets immediate feedback, also kills the ScrollViewReader .onChange(of: feedType) { _ in Task(priority: .userInitiated) { await hardRefreshFeed() @@ -123,6 +171,12 @@ struct FeedView: View { } } .refreshable { await refreshFeed() } + .onAppear { + print("FeedView appeared") + } + .onDisappear { + print("FeedView disappeared") + } } @ViewBuilder @@ -132,6 +186,9 @@ struct FeedView: View { noPostsView() } else { LazyVStack(spacing: 0) { + ScrollToView(appeared: $scrollToTopAppeared) + .id(scrollToTop) + // note: using .uid here because .id causes swipe actions to break--state changes still seem to properly trigger rerenders this way 🤔 ForEach(postTracker.items, id: \.uid) { post in feedPost(for: post) @@ -282,3 +339,4 @@ struct FeedView: View { } } } +// swiftlint:enable type_body_length diff --git a/Mlem/Views/Tabs/Inbox/Inbox View.swift b/Mlem/Views/Tabs/Inbox/Inbox View.swift index 0adae107f..27dd8d774 100644 --- a/Mlem/Views/Tabs/Inbox/Inbox View.swift +++ b/Mlem/Views/Tabs/Inbox/Inbox View.swift @@ -37,13 +37,17 @@ struct InboxView: View { // MARK: Global + @Namespace var scrollToTop + @State private var scrollToTopAppeared = false + @EnvironmentObject var appState: AppState @EnvironmentObject var editorTracker: EditorTracker @EnvironmentObject var unreadTracker: UnreadTracker @Environment(\.tabSelectionHashValue) private var selectedTagHashValue @Environment(\.tabNavigationSelectionHashValue) private var selectedNavigationTabHashValue - + @Environment(\.dismiss) private var dismiss + @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast // MARK: Internal @@ -85,7 +89,8 @@ struct InboxView: View { // utility @StateObject private var inboxRouter: NavigationRouter = .init() - + @StateObject private var navigation: Navigation = .init() + var body: some View { // NOTE: there appears to be a SwiftUI issue with segmented pickers stacked on top of ScrollViews which causes the tab bar to appear fully transparent. The internet suggests that this may be a bug that only manifests in dev mode, so, unless this pops up in a build, don't worry about it. If it does manifest, we can either put the Picker *in* the ScrollView (bad because then you can't access it without scrolling to the top) or put a Divider() at the bottom of the VStack (bad because then the material tab bar doesn't show) NavigationStack(path: $inboxRouter.path) { @@ -97,6 +102,7 @@ struct InboxView: View { ToolbarItemGroup(placement: .navigationBarTrailing) { ellipsisMenu } } .listStyle(PlainListStyle()) + .tabBarNavigationEnabled(.inbox, navigation) .handleLemmyViews() .onChange(of: selectedTagHashValue) { newValue in if newValue == TabSelection.inbox.hashValue { @@ -109,6 +115,7 @@ struct InboxView: View { } } } + .environmentObject(navigation) } @ViewBuilder var contentView: some View { @@ -121,27 +128,41 @@ struct InboxView: View { .pickerStyle(.segmented) .padding(.horizontal) - ScrollView(showsIndicators: false) { - if errorOccurred { - errorView() - } else { - switch curTab { - case .all: - inboxFeedView() - case .replies: - repliesFeedView() - case .mentions: - mentionsFeedView() - case .messages: - messagesFeedView() + ScrollViewReader { proxy in + ScrollView(showsIndicators: false) { + ScrollToView(appeared: $scrollToTopAppeared) + .id(scrollToTop) + + if errorOccurred { + errorView() + } else { + switch curTab { + case .all: + inboxFeedView() + case .replies: + repliesFeedView() + case .mentions: + mentionsFeedView() + case .messages: + messagesFeedView() + } } } - } - .fancyTabScrollCompatible() - .refreshable { - Task(priority: .userInitiated) { - await refreshFeed() + .fancyTabScrollCompatible() + .refreshable { + Task(priority: .userInitiated) { + await refreshFeed() + } } + .hoistNavigation( + dismiss: dismiss, + auxiliaryAction: { + withAnimation { + proxy.scrollTo(scrollToTop) + } + return true + } + ) } } // load view if empty or account has changed diff --git a/Mlem/Views/Tabs/Profile/Profile View.swift b/Mlem/Views/Tabs/Profile/Profile View.swift index e7dec5d58..779f95251 100644 --- a/Mlem/Views/Tabs/Profile/Profile View.swift +++ b/Mlem/Views/Tabs/Profile/Profile View.swift @@ -18,21 +18,28 @@ struct ProfileView: View { @Environment(\.tabNavigationSelectionHashValue) private var selectedNavigationTabHashValue @StateObject private var profileRouter: NavigationRouter = .init() - + @StateObject private var navigation: Navigation = .init() + var body: some View { - NavigationStack(path: $profileRouter.path) { - UserView(userID: userID) - .handleLemmyViews() - } - .handleLemmyLinkResolution(navigationPath: .constant(profileRouter)) - .onChange(of: selectedTagHashValue) { newValue in - if newValue == TabSelection.profile.hashValue { - print("switched to Profile tab") + ScrollViewReader { proxy in + NavigationStack(path: $profileRouter.path) { + UserView(userID: userID) + .handleLemmyViews() + .tabBarNavigationEnabled(.profile, navigation) } - } - .onChange(of: selectedNavigationTabHashValue) { newValue in - if newValue == TabSelection.profile.hashValue { - print("re-selected \(TabSelection.profile) tab") + .environment(\.navigationPathWithRoutes, $profileRouter.path) + .environment(\.scrollViewProxy, proxy) + .environmentObject(navigation) + .handleLemmyLinkResolution(navigationPath: .constant(profileRouter)) + .onChange(of: selectedTagHashValue) { newValue in + if newValue == TabSelection.profile.hashValue { + print("switched to Profile tab") + } + } + .onChange(of: selectedNavigationTabHashValue) { newValue in + if newValue == TabSelection.profile.hashValue { + print("re-selected \(TabSelection.profile) tab") + } } } } diff --git a/Mlem/Views/Tabs/Profile/User Moderator View.swift b/Mlem/Views/Tabs/Profile/User Moderator View.swift index ed68a1bcc..fc0ee4dc0 100644 --- a/Mlem/Views/Tabs/Profile/User Moderator View.swift +++ b/Mlem/Views/Tabs/Profile/User Moderator View.swift @@ -11,6 +11,8 @@ import SwiftUI A view that displays the list of communities a user moderates */ struct UserModeratorView: View { + @Environment(\.dismiss) private var dismiss + // parameters var userDetails: APIPersonView var moderatedCommunities: [APICommunityModeratorView] @@ -25,6 +27,7 @@ struct UserModeratorView: View { .navigationTitle("Moderator Details") .navigationBarColor() .navigationBarTitleDisplayMode(.inline) + .hoistNavigation(dismiss: dismiss) .headerProminence(.standard) .listStyle(.plain) } diff --git a/Mlem/Views/Tabs/Profile/User View.swift b/Mlem/Views/Tabs/Profile/User View.swift index a725b60ed..f572051a1 100644 --- a/Mlem/Views/Tabs/Profile/User View.swift +++ b/Mlem/Views/Tabs/Profile/User View.swift @@ -5,6 +5,8 @@ // Created by David Bureš on 02.04.2022. // +// swiftlint:disable file_length + import Dependencies import SwiftUI @@ -17,11 +19,16 @@ struct UserView: View { @Dependency(\.apiClient) var apiClient @Dependency(\.errorHandler) var errorHandler @Dependency(\.notifier) var notifier - + + @Namespace var scrollToTop + // appstorage @AppStorage("shouldShowUserHeaders") var shouldShowUserHeaders: Bool = true // environment + @Environment(\.navigationPathWithRoutes) private var navigationPath + @Environment(\.dismiss) private var dismiss + @Environment(\.scrollViewProxy) private var scrollViewProxy @EnvironmentObject var appState: AppState // parameters @@ -37,6 +44,8 @@ struct UserView: View { @State private var selectionSection = UserViewTab.overview @State private var errorDetails: ErrorDetails? + @State private var scrollToTopAppeared = false + init(userID: Int, userDetails: APIPersonView? = nil) { @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast @AppStorage("upvoteOnSave") var upvoteOnSave = false @@ -58,8 +67,22 @@ struct UserView: View { if let errorDetails { ErrorView(errorDetails) .fancyTabScrollCompatible() + .hoistNavigation(dismiss: dismiss) } else { contentView + .hoistNavigation( + dismiss: dismiss, + auxiliaryAction: { + if navigationPath.isEmpty { + withAnimation { + scrollViewProxy?.scrollTo(scrollToTop) + } + return true + } else { + return false + } + } + ) .sheet(isPresented: $isPresentingAccountSwitcher) { AccountsPage() } @@ -110,6 +133,9 @@ struct UserView: View { private func view(for userDetails: APIPersonView) -> some View { ScrollView { + ScrollToView(appeared: $scrollToTopAppeared) + .id(scrollToTop) + header(for: userDetails) if let bio = userDetails.person.bio { diff --git a/Mlem/Views/Tabs/Search/Search View.swift b/Mlem/Views/Tabs/Search/Search View.swift index 135e3c7a3..b11003ea1 100644 --- a/Mlem/Views/Tabs/Search/Search View.swift +++ b/Mlem/Views/Tabs/Search/Search View.swift @@ -13,12 +13,15 @@ struct SearchView: View { @Dependency(\.apiClient) var apiClient @Dependency(\.errorHandler) var errorHandler + @Namespace var scrollToTop + // environment @EnvironmentObject var communitySearchResultsTracker: CommunitySearchResultsTracker @EnvironmentObject var recentSearchesTracker: RecentSearchesTracker @Environment(\.tabSelectionHashValue) private var selectedTagHashValue @Environment(\.tabNavigationSelectionHashValue) private var selectedNavigationTabHashValue + @Environment(\.dismiss) private var dismiss // private state @State private var isSearching: Bool = false @@ -30,19 +33,24 @@ struct SearchView: View { @State private var searchPage: Int = 1 @State private var hasMorePages: Bool = true - @StateObject private var searchRouter: NavigationRouter = .init() + @State private var scrollToTopAppeared = false + @StateObject private var searchRouter: NavigationRouter = .init() + @StateObject private var navigation: Navigation = .init() + // constants private let searchPageSize = 50 var body: some View { NavigationStack(path: $searchRouter.path) { content + .tabBarNavigationEnabled(.search, navigation) .handleLemmyViews() .navigationBarTitleDisplayMode(.inline) .navigationBarColor() .navigationTitle("Search") } + .environmentObject(navigation) .handleLemmyLinkResolution(navigationPath: .constant(searchRouter)) .searchable(text: getSearchTextBinding(), prompt: "Search for communities") .autocorrectionDisabled(true) @@ -73,29 +81,44 @@ struct SearchView: View { @ViewBuilder private var recentSearches: some View { - List { - Section { - ForEach(recentSearchesTracker.recentSearches, id: \.self) { recentlySearchedText in - Button(recentlySearchedText) { - searchText = recentlySearchedText - performSearch() + ScrollViewReader { proxy in + List { + ListScrollToView(appeared: $scrollToTopAppeared) + .id(scrollToTop) + + Section { + ForEach(recentSearchesTracker.recentSearches, id: \.self) { recentlySearchedText in + Button(recentlySearchedText) { + searchText = recentlySearchedText + performSearch() + } } + } header: { + Text(recentSearchesTracker.recentSearches.isEmpty ? "No recent searches" : "Recent searches") } - } header: { - Text(recentSearchesTracker.recentSearches.isEmpty ? "No recent searches" : "Recent searches") - } - - Button(role: .destructive) { - recentSearchesTracker.clearRecentSearches() - } label: { - HStack { - Spacer() - Text("Clear recent searches") - .foregroundColor(.red) - Spacer() + + Button(role: .destructive) { + recentSearchesTracker.clearRecentSearches() + } label: { + HStack { + Spacer() + Text("Clear recent searches") + .foregroundColor(.red) + Spacer() + } } } - }.listStyle(.insetGrouped) + .listStyle(.insetGrouped) + .hoistNavigation( + dismiss: dismiss, + auxiliaryAction: { + withAnimation { + proxy.scrollTo(scrollToTop, anchor: .bottom) + } + return true + } + ) + } } @ViewBuilder @@ -105,28 +128,42 @@ struct SearchView: View { } else if communitySearchResultsTracker.foundCommunities.isEmpty { Text("No communities found for search") } else { - List { - ForEach(communitySearchResultsTracker.foundCommunities) { community in - CommunityLinkView( - community: community.community, - extraText: "\(community.counts.subscribers.roundedWithAbbreviations) subscribers" - ) - .frame(maxWidth: .infinity, alignment: .leading) - .onAppear { - let communityIndex = communitySearchResultsTracker.foundCommunities.firstIndex(of: community) - if let index = communityIndex { - // If we are half a page from the end, ask for more - let distanceFromEnd = communitySearchResultsTracker.foundCommunities.count - index - if distanceFromEnd == searchPageSize / 2 { - if hasMorePages { - performSearch() + ScrollViewReader { proxy in + List { + ListScrollToView(appeared: $scrollToTopAppeared) + .id(scrollToTop) + + ForEach(communitySearchResultsTracker.foundCommunities) { community in + CommunityLinkView( + community: community.community, + extraText: "\(community.counts.subscribers.roundedWithAbbreviations) subscribers" + ) + .frame(maxWidth: .infinity, alignment: .leading) + .onAppear { + let communityIndex = communitySearchResultsTracker.foundCommunities.firstIndex(of: community) + if let index = communityIndex { + // If we are half a page from the end, ask for more + let distanceFromEnd = communitySearchResultsTracker.foundCommunities.count - index + if distanceFromEnd == searchPageSize / 2 { + if hasMorePages { + performSearch() + } } } } } } + .fancyTabScrollCompatible() + .hoistNavigation( + dismiss: dismiss, + auxiliaryAction: { + withAnimation { + proxy.scrollTo(scrollToTop, anchor: .bottom) + } + return true + } + ) } - .fancyTabScrollCompatible() } } diff --git a/Mlem/Views/Tabs/Settings/Components/Settings View.swift b/Mlem/Views/Tabs/Settings/Components/Settings View.swift index f78f75334..e11d73eea 100644 --- a/Mlem/Views/Tabs/Settings/Components/Settings View.swift +++ b/Mlem/Views/Tabs/Settings/Components/Settings View.swift @@ -11,16 +11,18 @@ struct SettingsView: View { @EnvironmentObject var layoutWidgetTracker: LayoutWidgetTracker @StateObject private var settingsRouter: NavigationRouter = .init() + @StateObject private var navigation: Navigation = .init() @Environment(\.openURL) private var openURL @Environment(\.tabSelectionHashValue) private var selectedTagHashValue @Environment(\.tabNavigationSelectionHashValue) private var selectedNavigationTabHashValue - + @Environment(\.dismiss) private var dismiss + @Namespace var scrollToTop var body: some View { - NavigationStack(path: $settingsRouter.path) { - ScrollViewReader { _ in + ScrollViewReader { proxy in + NavigationStack(path: $settingsRouter.path) { List { Section { NavigationLink(value: SettingsRoute.accountsPage) { @@ -60,24 +62,30 @@ struct SettingsView: View { } } } - .onChange(of: selectedNavigationTabHashValue) { newValue in - if newValue == TabSelection.settings.hashValue { - print("re-selected \(TabSelection.settings) tab") + .tabBarNavigationEnabled(.settings, navigation) + .environmentObject(settingsRouter) + .fancyTabScrollCompatible() + .handleLemmyViews() + .navigationTitle("Settings") + .navigationBarColor() + .navigationBarTitleDisplayMode(.inline) + .useSettingsNavigationRouter() + .hoistNavigation( + dismiss: dismiss, + auxiliaryAction: { + withAnimation { + proxy.scrollTo(scrollToTop, anchor: .bottom) + } + return true } - } + ) } - .environmentObject(settingsRouter) - .fancyTabScrollCompatible() - .handleLemmyViews() - .navigationTitle("Settings") - .navigationBarColor() - .navigationBarTitleDisplayMode(.inline) - .useSettingsNavigationRouter() - } - .handleLemmyLinkResolution(navigationPath: .constant(settingsRouter)) - .onChange(of: selectedTagHashValue) { newValue in - if newValue == TabSelection.settings.hashValue { - print("switched to Settings tab") + .environmentObject(navigation) + .handleLemmyLinkResolution(navigationPath: .constant(settingsRouter)) + .onChange(of: selectedTagHashValue) { newValue in + if newValue == TabSelection.settings.hashValue { + print("switched to Settings tab") + } } } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/About/AboutView.swift b/Mlem/Views/Tabs/Settings/Components/Views/About/AboutView.swift index 3b09722db..5e8389495 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/About/AboutView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/About/AboutView.swift @@ -9,6 +9,7 @@ import SwiftUI struct AboutView: View { @Binding var navigationPath: NavigationPath + @Environment(\.dismiss) private var dismiss var body: some View { Group { @@ -62,6 +63,7 @@ struct AboutView: View { } .navigationTitle("About") .navigationBarColor() + .hoistNavigation(dismiss: dismiss) } var versionString: String { diff --git a/Mlem/Views/Tabs/Settings/Components/Views/About/ContributorsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/About/ContributorsView.swift index 0c068d97b..92d7b825b 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/About/ContributorsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/About/ContributorsView.swift @@ -31,6 +31,7 @@ struct DeveloperView: View { } struct ContributorsView: View { + @Environment(\.dismiss) private var dismiss var body: some View { List { Section("Development Team") { @@ -73,5 +74,6 @@ struct ContributorsView: View { .fancyTabScrollCompatible() .navigationTitle("Contributors") .navigationBarColor() + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/About/LicensesView.swift b/Mlem/Views/Tabs/Settings/Components/Views/About/LicensesView.swift index 9d1d33074..3f0423595 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/About/LicensesView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/About/LicensesView.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI struct LicensesView: View { + @Environment(\.dismiss) private var dismiss var body: some View { VStack(alignment: .labelStart) { List { @@ -45,5 +46,6 @@ struct LicensesView: View { } .navigationTitle("Licenses") .navigationBarColor() + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Accessibility/AccessibilitySettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Accessibility/AccessibilitySettingsView.swift index aa2982afd..1c1badd3c 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Accessibility/AccessibilitySettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Accessibility/AccessibilitySettingsView.swift @@ -14,6 +14,8 @@ struct AccessibilitySettingsView: View { @AppStorage("hasTranslucentInsets") var hasTranslucentInsets: Bool = true @AppStorage("showSettingsIcons") var showSettingsIcons: Bool = true + @Environment(\.dismiss) private var dismiss + @State private var readBarThicknessSlider: CGFloat = 3.0 var body: some View { @@ -92,5 +94,6 @@ struct AccessibilitySettingsView: View { .fancyTabScrollCompatible() .navigationTitle("Accessibility") .navigationBarColor() + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Advanced/AdvancedSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Advanced/AdvancedSettingsView.swift index 95a692459..f809f8afe 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Advanced/AdvancedSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Advanced/AdvancedSettingsView.swift @@ -11,6 +11,8 @@ import SwiftUI struct AdvancedSettingsView: View { @AppStorage("developerMode") var developerMode: Bool = false + @Environment(\.dismiss) private var dismiss + @State private var diskUsage: Int64 = 0 var body: some View { @@ -53,5 +55,6 @@ struct AdvancedSettingsView: View { } .navigationTitle("Advanced") .navigationBarColor() + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/AppearanceSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/AppearanceSettingsView.swift index 9bd166de1..9349c3c68 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/AppearanceSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/AppearanceSettingsView.swift @@ -10,6 +10,8 @@ import SwiftUI struct AppearanceSettingsView: View { @AppStorage("lightOrDarkMode") var lightOrDarkMode: UIUserInterfaceStyle = .unspecified + @Environment(\.dismiss) private var dismiss + var body: some View { List { Section { @@ -67,5 +69,6 @@ struct AppearanceSettingsView: View { .navigationTitle("Appearance") .navigationBarColor() .navigationBarTitleDisplayMode(.inline) + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Comment/CommentSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Comment/CommentSettingsView.swift index 734fe38ea..3e31633c8 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Comment/CommentSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Comment/CommentSettingsView.swift @@ -24,6 +24,9 @@ enum JumpButtonLocation: String, SettingsOptions { } struct CommentSettingsView: View { + + @Environment(\.dismiss) private var dismiss + @EnvironmentObject var layoutWidgetTracker: LayoutWidgetTracker @AppStorage("compactComments") var compactComments: Bool = false @@ -114,5 +117,6 @@ struct CommentSettingsView: View { .navigationTitle("Comments") .navigationBarColor() .navigationBarTitleDisplayMode(.inline) + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Community/CommunitySettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Community/CommunitySettingsView.swift index a424a640e..c2278a786 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Community/CommunitySettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Community/CommunitySettingsView.swift @@ -6,6 +6,9 @@ // import SwiftUI struct CommunitySettingsView: View { + + @Environment(\.dismiss) private var dismiss + @AppStorage("shouldShowCommunityHeaders") var shouldShowCommunityHeaders: Bool = true @AppStorage("shouldShowCommunityIcons") var shouldShowCommunityIcons: Bool = true @@ -24,5 +27,7 @@ struct CommunitySettingsView: View { } .fancyTabScrollCompatible() .navigationTitle("Communities") + .navigationBarColor() + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Post/PostSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Post/PostSettingsView.swift index ee3fcaf15..a1f24a53f 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Post/PostSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Post/PostSettingsView.swift @@ -9,6 +9,8 @@ import Foundation import SwiftUI struct PostSettingsView: View { + + @Environment(\.dismiss) private var dismiss @EnvironmentObject var layoutWidgetTracker: LayoutWidgetTracker @AppStorage("postSize") var postSize: PostSize = .headline @@ -180,5 +182,6 @@ struct PostSettingsView: View { .navigationTitle("Posts") .navigationBarColor() .navigationBarTitleDisplayMode(.inline) + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Shared/LayoutWidgetEditView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Shared/LayoutWidgetEditView.swift index e94397cd9..893d201e2 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Shared/LayoutWidgetEditView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Shared/LayoutWidgetEditView.swift @@ -10,6 +10,7 @@ import SwiftUI struct LayoutWidgetEditView: View { @Environment(\.isPresented) var isPresented + @Environment(\.dismiss) private var dismiss var onSave: (_ widgets: [LayoutWidgetType]) -> Void @@ -107,6 +108,7 @@ struct LayoutWidgetEditView: View { } .navigationTitle("Widgets") .navigationBarColor() + .hoistNavigation(dismiss: dismiss) } var infoText: some View { diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/TabBar/TabBarSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/TabBar/TabBarSettingsView.swift index 87a9cccdd..f57c11563 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/TabBar/TabBarSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/TabBar/TabBarSettingsView.swift @@ -14,6 +14,8 @@ struct TabBarSettingsView: View { @AppStorage("showInboxUnreadBadge") var showInboxUnreadBadge: Bool = true @AppStorage("showUserAvatarOnProfileTab") var showUserAvatar: Bool = true + @Environment(\.dismiss) private var dismiss + @EnvironmentObject var appState: AppState @State var textFieldEntry: String = "" @@ -91,5 +93,6 @@ struct TabBarSettingsView: View { print("new nickname: \(nickname)") textFieldEntry = nickname } + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Theme/ThemeSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Theme/ThemeSettingsView.swift index 0e94a7a2d..0be5574fa 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Theme/ThemeSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/Theme/ThemeSettingsView.swift @@ -56,6 +56,8 @@ struct ThemeLabel: View { struct ThemeSettingsView: View { @AppStorage("lightOrDarkMode") var lightOrDarkMode: UIUserInterfaceStyle = .unspecified + @Environment(\.dismiss) private var dismiss + var body: some View { List { Picker("Appearance", selection: $lightOrDarkMode) { @@ -72,5 +74,6 @@ struct ThemeSettingsView: View { .fancyTabScrollCompatible() .navigationTitle("Theme") .navigationBarColor() + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/User/UserSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/User/UserSettingsView.swift index c3d4d6919..5466945b0 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Appearance/User/UserSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Appearance/User/UserSettingsView.swift @@ -8,6 +8,9 @@ import SwiftUI struct UserSettingsView: View { + + @Environment(\.dismiss) private var dismiss + @AppStorage("shouldShowUserHeaders") var shouldShowUserHeaders: Bool = true @AppStorage("shouldShowUserAvatars") var shouldShowUserAvatars: Bool = true @@ -26,5 +29,7 @@ struct UserSettingsView: View { } .fancyTabScrollCompatible() .navigationTitle("Users") + .navigationBarColor() + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Views/Tabs/Settings/Components/Views/Filters/FiltersSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/Filters/FiltersSettingsView.swift index 0ed3775f7..1ba6d57a9 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/Filters/FiltersSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/Filters/FiltersSettingsView.swift @@ -15,6 +15,7 @@ struct FiltersSettingsView: View { @Dependency(\.persistenceRepository) var persistenceRepository @EnvironmentObject var filtersTracker: FiltersTracker + @Environment(\.dismiss) private var dismiss @State private var newFilteredKeyword: String = "" @State private var isShowingKeywordImporter: Bool = false @@ -151,6 +152,7 @@ struct FiltersSettingsView: View { .navigationTitle("Filters") .navigationBarColor() .navigationBarTitleDisplayMode(.inline) + .hoistNavigation(dismiss: dismiss) .toolbar { ToolbarItem(placement: .automatic) { EditButton() diff --git a/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift b/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift index 59546ceb7..c3d7d6a98 100644 --- a/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift +++ b/Mlem/Views/Tabs/Settings/Components/Views/General/GeneralSettingsView.swift @@ -25,6 +25,7 @@ struct GeneralSettingsView: View { @AppStorage("showSettingsIcons") var showSettingsIcons: Bool = false @EnvironmentObject var appState: AppState + @Environment(\.dismiss) private var dismiss @State private var isShowingFavoritesDeletionConfirmation: Bool = false @@ -148,5 +149,6 @@ struct GeneralSettingsView: View { .fancyTabScrollCompatible() .navigationTitle("General") .navigationBarColor() + .hoistNavigation(dismiss: dismiss) } } diff --git a/Mlem/Window.swift b/Mlem/Window.swift index 1aa743452..7a8d58f80 100644 --- a/Mlem/Window.swift +++ b/Mlem/Window.swift @@ -20,6 +20,8 @@ struct Window: View { @StateObject var filtersTracker: FiltersTracker = .init() @StateObject var recentSearchesTracker: RecentSearchesTracker = .init() @StateObject var layoutWidgetTracker: LayoutWidgetTracker = .init() + /// This is only here so that sheet views that double as navigation views don't crash when they expect a navigation object. [2023.09] + @StateObject private var navigation: Navigation = .init() @StateObject var appState: AppState = .init() @State var flow: AppFlow @@ -73,6 +75,8 @@ struct Window: View { .environmentObject(recentSearchesTracker) .environmentObject(easterFlagsTracker) .environmentObject(layoutWidgetTracker) + /// Some views that participate in tab bar navigation may also be presented as sheets. This is purely a dummy, unused object passed to these sheets so that they don't crash. [2023.09] + .environmentObject(navigation) } private func setFlow(_ flow: AppFlow) {