From 114a9d97c059386eae3f9477d3c2f5ded96cf355 Mon Sep 17 00:00:00 2001 From: Eric Andrews Date: Sun, 12 May 2024 16:57:16 -0400 Subject: [PATCH] Tab Reselection (#1061) --- CONTRIBUTING.md | 1 + Mlem.xcodeproj/project.pbxproj | 40 +++++---- .../Definitions/TabReselectTracker.swift | 24 +++++ ...onmentValues+TabReselectionHashValue.swift | 19 ---- ...ironmentValues+TabSelectionHashValue.swift | 20 ----- .../View+TabReselectConsumer.swift | 39 ++++++++ Mlem/App/Views/Root/ContentView.swift | 2 + .../App/Views/Root/Tabs/Feeds/FeedsView.swift | 88 +++++++++++-------- .../Views/Shared/CustomTabBarController.swift | 2 + Mlem/App/Views/Shared/ExpandedPostView.swift | 49 +++++++++++ .../Shared/Navigation/NavigationPage.swift | 4 + Mlem/App/Views/Shared/ScrollToView.swift | 54 ++++++++++++ 12 files changed, 252 insertions(+), 90 deletions(-) create mode 100644 Mlem/App/Globals/Definitions/TabReselectTracker.swift delete mode 100644 Mlem/App/Globals/EnvironmentValues/EnvironmentValues+TabReselectionHashValue.swift delete mode 100644 Mlem/App/Globals/EnvironmentValues/EnvironmentValues+TabSelectionHashValue.swift create mode 100644 Mlem/App/Utility/Extensions/Views/View Modifiers/View+TabReselectConsumer.swift create mode 100644 Mlem/App/Views/Shared/ExpandedPostView.swift create mode 100644 Mlem/App/Views/Shared/ScrollToView.swift diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d90d9f559..d09a51707 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,6 +57,7 @@ struct SomeView: View { @Environment entities @Binding variables @State variables + @Namespace variables Normal variables Computed properties diff --git a/Mlem.xcodeproj/project.pbxproj b/Mlem.xcodeproj/project.pbxproj index df2b56eb1..f8d7eb449 100644 --- a/Mlem.xcodeproj/project.pbxproj +++ b/Mlem.xcodeproj/project.pbxproj @@ -68,8 +68,6 @@ CD4D583F2B86855F00B82964 /* MlemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D583E2B86855F00B82964 /* MlemTests.swift */; }; CD4D58412B86858100B82964 /* MlemUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58402B86858100B82964 /* MlemUITests.swift */; }; CD4D58932B86BA5C00B82964 /* StandardPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58922B86BA5C00B82964 /* StandardPalette.swift */; }; - CD4D58972B86BAD900B82964 /* EnvironmentValues+TabReselectionHashValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58962B86BAD900B82964 /* EnvironmentValues+TabReselectionHashValue.swift */; }; - CD4D58992B86BB0300B82964 /* EnvironmentValues+TabSelectionHashValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58982B86BB0300B82964 /* EnvironmentValues+TabSelectionHashValue.swift */; }; CD4D58A52B86BD1B00B82964 /* PersistenceRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */; }; CD4D58AA2B86BE5900B82964 /* AccountListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58A92B86BE5900B82964 /* AccountListView.swift */; }; CD4D58AD2B86BE7100B82964 /* QuickSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D58AC2B86BE7100B82964 /* QuickSwitcherView.swift */; }; @@ -101,6 +99,10 @@ CD4D591C2B87B43D00B82964 /* Task+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D591B2B87B43D00B82964 /* Task+Extensions.swift */; }; CD4D59202B87B63300B82964 /* SettingsOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4D591F2B87B63300B82964 /* SettingsOptions.swift */; }; CD4E03532BE42E6F009D11D6 /* MlemMiddleware in Frameworks */ = {isa = PBXBuildFile; productRef = CD4E03522BE42E6F009D11D6 /* MlemMiddleware */; }; + CD4ED8472BF110FA00EFA0A2 /* TabReselectTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4ED8462BF110FA00EFA0A2 /* TabReselectTracker.swift */; }; + CD4ED84A2BF1113800EFA0A2 /* View+TabReselectConsumer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4ED8492BF1113800EFA0A2 /* View+TabReselectConsumer.swift */; }; + CD4ED84C2BF111BA00EFA0A2 /* ScrollToView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4ED84B2BF111BA00EFA0A2 /* ScrollToView.swift */; }; + CD4ED84E2BF113C800EFA0A2 /* ExpandedPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4ED84D2BF113C800EFA0A2 /* ExpandedPostView.swift */; }; CD59DE2C2BE467E600445EED /* MlemMiddleware in Frameworks */ = {isa = PBXBuildFile; productRef = CD59DE2B2BE467E600445EED /* MlemMiddleware */; }; CDA1E8212B8FC411007953EF /* LandingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA1E8202B8FC411007953EF /* LandingPage.swift */; }; CDA1E8272B90EF24007953EF /* AppFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA1E8262B90EF24007953EF /* AppFlow.swift */; }; @@ -184,8 +186,6 @@ CD4D583E2B86855F00B82964 /* MlemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MlemTests.swift; sourceTree = ""; }; CD4D58402B86858100B82964 /* MlemUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MlemUITests.swift; sourceTree = ""; }; CD4D58922B86BA5C00B82964 /* StandardPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardPalette.swift; sourceTree = ""; }; - CD4D58962B86BAD900B82964 /* EnvironmentValues+TabReselectionHashValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+TabReselectionHashValue.swift"; sourceTree = ""; }; - CD4D58982B86BB0300B82964 /* EnvironmentValues+TabSelectionHashValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+TabSelectionHashValue.swift"; sourceTree = ""; }; CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceRepository.swift; sourceTree = ""; }; CD4D58A92B86BE5900B82964 /* AccountListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListView.swift; sourceTree = ""; }; CD4D58AC2B86BE7100B82964 /* QuickSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSwitcherView.swift; sourceTree = ""; }; @@ -216,6 +216,10 @@ CD4D59192B87B3D100B82964 /* ErrorHandler+Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ErrorHandler+Dependency.swift"; sourceTree = ""; }; CD4D591B2B87B43D00B82964 /* Task+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Extensions.swift"; sourceTree = ""; }; CD4D591F2B87B63300B82964 /* SettingsOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsOptions.swift; sourceTree = ""; }; + CD4ED8462BF110FA00EFA0A2 /* TabReselectTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabReselectTracker.swift; sourceTree = ""; }; + CD4ED8492BF1113800EFA0A2 /* View+TabReselectConsumer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+TabReselectConsumer.swift"; sourceTree = ""; }; + CD4ED84B2BF111BA00EFA0A2 /* ScrollToView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToView.swift; sourceTree = ""; }; + CD4ED84D2BF113C800EFA0A2 /* ExpandedPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandedPostView.swift; sourceTree = ""; }; CDA1E8202B8FC411007953EF /* LandingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingPage.swift; sourceTree = ""; }; CDA1E8262B90EF24007953EF /* AppFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlow.swift; sourceTree = ""; }; CDA1E8532B952C3D007953EF /* StateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateManager.swift; sourceTree = ""; }; @@ -275,6 +279,7 @@ 03134A4E2BEAD23A002662CC /* Views */ = { isa = PBXGroup; children = ( + CD4ED8482BF1112A00EFA0A2 /* View Modifiers */, 03134A4F2BEAD245002662CC /* NavigationLink+NavigationPage.swift */, ); path = Views; @@ -499,21 +504,11 @@ isa = PBXGroup; children = ( CD4D59212B87BD0800B82964 /* Definitions */, - CD4D58952B86BAC400B82964 /* EnvironmentValues */, CD4D58672B86B38600B82964 /* Dependencies */, ); path = Globals; sourceTree = ""; }; - CD4D58952B86BAC400B82964 /* EnvironmentValues */ = { - isa = PBXGroup; - children = ( - CD4D58982B86BB0300B82964 /* EnvironmentValues+TabSelectionHashValue.swift */, - CD4D58962B86BAD900B82964 /* EnvironmentValues+TabReselectionHashValue.swift */, - ); - path = EnvironmentValues; - sourceTree = ""; - }; CD4D58A82B86BE4C00B82964 /* Shared */ = { isa = PBXGroup; children = ( @@ -528,6 +523,8 @@ 037386402BDAF3F7007492B5 /* Markdown.swift */, 03D3A1EE2BB9CA1D009DE55E /* MenuButton.swift */, 03267D802BED4714009D6268 /* Avatar */, + CD4ED84B2BF111BA00EFA0A2 /* ScrollToView.swift */, + CD4ED84D2BF113C800EFA0A2 /* ExpandedPostView.swift */, ); path = Shared; sourceTree = ""; @@ -649,10 +646,19 @@ CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */, CD4D58FB2B87B13B00B82964 /* ErrorHandler */, CD4D59042B87B19100B82964 /* Notifier */, + CD4ED8462BF110FA00EFA0A2 /* TabReselectTracker.swift */, ); path = Definitions; sourceTree = ""; }; + CD4ED8482BF1112A00EFA0A2 /* View Modifiers */ = { + isa = PBXGroup; + children = ( + CD4ED8492BF1113800EFA0A2 /* View+TabReselectConsumer.swift */, + ); + path = "View Modifiers"; + sourceTree = ""; + }; CDA1E81A2B8FC39A007953EF /* Onboarding */ = { isa = PBXGroup; children = ( @@ -876,8 +882,8 @@ 03134A582BEC1C46002662CC /* AccountListSettingsView.swift in Sources */, 031E2D5B2BEFC9460003BC45 /* ThemeSettingsView.swift in Sources */, 03D3A1F12BB9D48E009DE55E /* BasicAction.swift in Sources */, - CD4D58992B86BB0300B82964 /* EnvironmentValues+TabSelectionHashValue.swift in Sources */, CD4D590C2B87B1E700B82964 /* Notifier.swift in Sources */, + CD4ED84E2BF113C800EFA0A2 /* ExpandedPostView.swift in Sources */, 035BE08D2BDE88EC00F77D73 /* NavigationLayerView.swift in Sources */, 035BE0872BDD8DA000F77D73 /* NavigationRootView.swift in Sources */, 03D3A1E32BB8A40A009DE55E /* EmptyButtonStyle.swift in Sources */, @@ -890,10 +896,10 @@ CD4D58C02B86DBD100B82964 /* DefaultAvatarView.swift in Sources */, CD1446212A5B328E00610EF1 /* Privacy Policy.swift in Sources */, CD1446272A5B36DA00610EF1 /* EULA.swift in Sources */, + CD4ED84A2BF1113800EFA0A2 /* View+TabReselectConsumer.swift in Sources */, CD4D58FF2B87B15700B82964 /* EquatableError.swift in Sources */, CD4D58A52B86BD1B00B82964 /* PersistenceRepository.swift in Sources */, CD1446252A5B357900610EF1 /* Document.swift in Sources */, - CD4D58972B86BAD900B82964 /* EnvironmentValues+TabReselectionHashValue.swift in Sources */, CD4D591C2B87B43D00B82964 /* Task+Extensions.swift in Sources */, 03134A4D2BEAD058002662CC /* AvatarView.swift in Sources */, CD4D58AA2B86BE5900B82964 /* AccountListView.swift in Sources */, @@ -910,6 +916,7 @@ CD4D58C82B86DCED00B82964 /* AvatarType.swift in Sources */, CD4D58BB2B86DA7D00B82964 /* AccountListView+Logic.swift in Sources */, 031E2D5D2BEFCC630003BC45 /* SettingsView.swift in Sources */, + CD4ED8472BF110FA00EFA0A2 /* TabReselectTracker.swift in Sources */, 035BE0912BDEA01E00F77D73 /* NavigationPage+View.swift in Sources */, CD4D59142B87B36B00B82964 /* InternetConnectionManager.swift in Sources */, 03D3A1EF2BB9CA1D009DE55E /* MenuButton.swift in Sources */, @@ -951,6 +958,7 @@ 030FF67F2BC8544700F6BFAC /* CustomTabBarController.swift in Sources */, CD4D58F82B87B0D100B82964 /* InternetSpeed.swift in Sources */, CD4D58932B86BA5C00B82964 /* StandardPalette.swift in Sources */, + CD4ED84C2BF111BA00EFA0A2 /* ScrollToView.swift in Sources */, 030FF6812BC859FD00F6BFAC /* CustomTabViewHostingController.swift in Sources */, CD4D58AD2B86BE7100B82964 /* QuickSwitcherView.swift in Sources */, 6363D5C527EE196700E34822 /* MlemApp.swift in Sources */, diff --git a/Mlem/App/Globals/Definitions/TabReselectTracker.swift b/Mlem/App/Globals/Definitions/TabReselectTracker.swift new file mode 100644 index 000000000..db51f1dd5 --- /dev/null +++ b/Mlem/App/Globals/Definitions/TabReselectTracker.swift @@ -0,0 +1,24 @@ +// +// TabReselectTracker.swift +// Mlem +// +// Created by Eric Andrews on 2023-11-02. +// + +import Foundation +import SwiftUI + +@Observable +class TabReselectTracker { + private(set) var flag: Bool = false + + static var main: TabReselectTracker = .init() + + func signal() { + flag = true + } + + func reset() { + flag = false + } +} diff --git a/Mlem/App/Globals/EnvironmentValues/EnvironmentValues+TabReselectionHashValue.swift b/Mlem/App/Globals/EnvironmentValues/EnvironmentValues+TabReselectionHashValue.swift deleted file mode 100644 index fea383f61..000000000 --- a/Mlem/App/Globals/EnvironmentValues/EnvironmentValues+TabReselectionHashValue.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// EnvironmentValues+TabReselectionHashValue.swift -// Mlem -// -// Created by Eric Andrews on 2023-11-02. -// -import Foundation -import SwiftUI - -struct FancyTabBarReselectionEnvironmentKey: EnvironmentKey { - static var defaultValue: Int? { nil } -} - -extension EnvironmentValues { - var tabReselectionHashValue: Int? { - get { self[FancyTabBarReselectionEnvironmentKey.self] } - set { self[FancyTabBarReselectionEnvironmentKey.self] = newValue } - } -} diff --git a/Mlem/App/Globals/EnvironmentValues/EnvironmentValues+TabSelectionHashValue.swift b/Mlem/App/Globals/EnvironmentValues/EnvironmentValues+TabSelectionHashValue.swift deleted file mode 100644 index ef1378a63..000000000 --- a/Mlem/App/Globals/EnvironmentValues/EnvironmentValues+TabSelectionHashValue.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// EnvironmentValues+TabSelectionHashValue.swift -// Mlem -// -// Created by Eric Andrews on 2023-07-17. -// - -import Foundation -import SwiftUI - -struct FancyTabSelectionHashValueEnvironmentKey: EnvironmentKey { - static var defaultValue: Int? { nil } -} - -extension EnvironmentValues { - var tabSelectionHashValue: Int? { - get { self[FancyTabSelectionHashValueEnvironmentKey.self] } - set { self[FancyTabSelectionHashValueEnvironmentKey.self] = newValue } - } -} diff --git a/Mlem/App/Utility/Extensions/Views/View Modifiers/View+TabReselectConsumer.swift b/Mlem/App/Utility/Extensions/Views/View Modifiers/View+TabReselectConsumer.swift new file mode 100644 index 000000000..5f7acc74e --- /dev/null +++ b/Mlem/App/Utility/Extensions/Views/View Modifiers/View+TabReselectConsumer.swift @@ -0,0 +1,39 @@ +// +// View+TabReselectionConsumer.swift +// Mlem +// +// Created by Eric Andrews on 2024-05-11. +// + +import Foundation +import SwiftUI + +struct TabReselectionConsumer: ViewModifier { + @Environment(TabReselectTracker.self) var tabReselectTracker + + /// Reselect actions should only trigger when the view is shown, so we track it with this + @State var displayed: Bool = false + + var action: () -> Void + + func body(content: Content) -> some View { + content + .onChange(of: tabReselectTracker.flag) { + // Only execute the action if: + // - This view is currently displayed (this prevents it from triggering while in a different tab) + // - Flag is true--combined with the reset() call below, this ensures that only one consumer will consume this action, preventing the behavior where a "dismiss" action also scrolls the previous page + if displayed, tabReselectTracker.flag { + tabReselectTracker.reset() + action() + } + } + .onAppear { displayed = true } + .onDisappear { displayed = false } + } +} + +extension View { + func onReselectTab(action: @escaping () -> Void) -> some View { + modifier(TabReselectionConsumer(action: action)) + } +} diff --git a/Mlem/App/Views/Root/ContentView.swift b/Mlem/App/Views/Root/ContentView.swift index 2598f7be4..25123d625 100644 --- a/Mlem/App/Views/Root/ContentView.swift +++ b/Mlem/App/Views/Root/ContentView.swift @@ -18,6 +18,7 @@ struct ContentView: View { @State var palette: Palette = .main @State var selectedTabIndex: Int = 0 + @State var tabReselectTracker: TabReselectTracker = .main @State var navigationModel: NavigationModel = .init() @@ -28,6 +29,7 @@ struct ContentView: View { appState.cleanCaches() } .environment(palette) + .environment(tabReselectTracker) .environment(appState) } diff --git a/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift b/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift index 9a7addcd7..23538759c 100644 --- a/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift +++ b/Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift @@ -30,6 +30,9 @@ struct MinimalPostFeedView: View { @Environment(Palette.self) var palette @State var postTracker: StandardPostFeedLoader + @State private var scrollToTopAppeared = false + + @Namespace var scrollToTop init() { // need to grab some stuff from app storage to initialize with @@ -53,32 +56,41 @@ struct MinimalPostFeedView: View { } var body: some View { - content - .navigationTitle("Feeds") - .task { - if postTracker.items.isEmpty, postTracker.loadingState == .idle { - print("Loading initial PostTracker page...") + ScrollViewReader { scrollProxy in + content + .navigationTitle("Feeds") + .task { + if postTracker.items.isEmpty, postTracker.loadingState == .idle { + print("Loading initial PostTracker page...") + do { + try await postTracker.loadMoreItems() + } catch { + errorHandler.handle(error) + } + } + } + .task(id: appState.firstApi) { do { - try await postTracker.loadMoreItems() + try await postTracker.changeFeedType(to: .aggregateFeed(appState.firstApi, type: .subscribed)) } catch { errorHandler.handle(error) } } - } - .task(id: appState.firstApi) { - do { - try await postTracker.changeFeedType(to: .aggregateFeed(appState.firstApi, type: .subscribed)) - } catch { - errorHandler.handle(error) + .refreshable { + do { + try await postTracker.refresh(clearBeforeRefresh: false) + } catch { + errorHandler.handle(error) + } } - } - .refreshable { - do { - try await postTracker.refresh(clearBeforeRefresh: false) - } catch { - errorHandler.handle(error) + .onReselectTab { + if !scrollToTopAppeared { + withAnimation { + scrollProxy.scrollTo(scrollToTop) + } + } } - } + } } // This is a proof-of-concept; in the real frontend this code will go in InteractionBarView @@ -102,25 +114,31 @@ struct MinimalPostFeedView: View { var content: some View { ScrollView { LazyVStack(spacing: 0) { + ScrollToView(appeared: $scrollToTopAppeared) + .id(scrollToTop) + ForEach(postTracker.items, id: \.uid) { post in - HStack { - actionButton(post.upvoteAction) - actionButton(post.downvoteAction) - actionButton(post.saveAction) - - Text(post.title) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal) - .foregroundStyle(post.read ? .secondary : .primary) - } - .padding(10) - .background(palette.background) - .contentShape(.rect) - .contextMenu { - ForEach(post.menuActions.children, id: \.id) { action in - MenuButton(action: action) + NavigationLink(value: NavigationPage.expandedPost(post)) { + HStack { + actionButton(post.upvoteAction) + actionButton(post.downvoteAction) + actionButton(post.saveAction) + + Text(post.title) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + .foregroundStyle(post.read ? .secondary : .primary) + } + .padding(10) + .background(palette.background) + .contentShape(.rect) + .contextMenu { + ForEach(post.menuActions.children, id: \.id) { action in + MenuButton(action: action) + } } } + .buttonStyle(.plain) Divider() } } diff --git a/Mlem/App/Views/Shared/CustomTabBarController.swift b/Mlem/App/Views/Shared/CustomTabBarController.swift index 9c9349eb3..b3756b8c9 100644 --- a/Mlem/App/Views/Shared/CustomTabBarController.swift +++ b/Mlem/App/Views/Shared/CustomTabBarController.swift @@ -73,9 +73,11 @@ class CustomTabBarController: UITabBarController, UITabBarControllerDelegate { } func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { + TabReselectTracker.main.reset() // reset to prevent unconsumed actions from blocking the reselect flag if tabBarController.selectedViewController === viewController, let item = viewController as? CustomTabViewHostingController { print("\(item.rootView.title) tab re-selected") + TabReselectTracker.main.signal() } selectedIndexBinding = viewControllers?.firstIndex(of: viewController) ?? 0 return true diff --git a/Mlem/App/Views/Shared/ExpandedPostView.swift b/Mlem/App/Views/Shared/ExpandedPostView.swift new file mode 100644 index 000000000..e77cb83b2 --- /dev/null +++ b/Mlem/App/Views/Shared/ExpandedPostView.swift @@ -0,0 +1,49 @@ +// +// ExpandedPostView.swift +// Mlem +// +// Created by Eric Andrews on 2024-05-12. +// + +import Foundation +import MlemMiddleware +import SwiftUI + +struct ExpandedPostView: View { + @Environment(\.dismiss) var dismiss + + @State private var scrollToTopAppeared = false + + @Namespace var scrollToTop + + let post: any Post2Providing + + var body: some View { + ScrollViewReader { scrollProxy in + content + .onReselectTab { + if scrollToTopAppeared { + dismiss() + } else { + withAnimation { + scrollProxy.scrollTo(scrollToTop) + } + } + } + } + } + + var content: some View { + ScrollView { + VStack { + ScrollToView(appeared: $scrollToTopAppeared) + .id(scrollToTop) + + Text(post.title) + + Text("Some content really far below to scroll to") + .padding(.top, 700) + } + } + } +} diff --git a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift index cb020e9db..3a4975933 100644 --- a/Mlem/App/Views/Shared/Navigation/NavigationPage.swift +++ b/Mlem/App/Views/Shared/Navigation/NavigationPage.swift @@ -5,12 +5,14 @@ // Created by Sjmarf on 27/04/2024. // +import MlemMiddleware import SwiftUI enum NavigationPage: Hashable { case settings(_ page: SettingsPage = .root) case feeds, profile, inbox, search case quickSwitcher, addAccount + case expandedPost(_ post: Post2) } extension NavigationPage { @@ -32,6 +34,8 @@ extension NavigationPage { .presentationDetents([.medium, .large]) case .addAccount: LandingPage() + case let .expandedPost(post): + ExpandedPostView(post: post) } } diff --git a/Mlem/App/Views/Shared/ScrollToView.swift b/Mlem/App/Views/Shared/ScrollToView.swift new file mode 100644 index 000000000..efd81e3cb --- /dev/null +++ b/Mlem/App/Views/Shared/ScrollToView.swift @@ -0,0 +1,54 @@ +// +// 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 { + /// We don't have any horizontal scroll views yet, but this may need to be a LazyHStack if we do. [2023.09] + LazyVStack(spacing: 0) { + 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 { + /// We don't have any horizontal scroll views yet, but this may need to be a LazyHStack if we do. [2023.09] + LazyVStack(spacing: 0) { + EmptyView() + .frame(height: 1) + .onAppear { + appeared = true + } + .onDisappear { + appeared = false + } + } + } +}