Skip to content

Commit

Permalink
Tab Reselection (#1061)
Browse files Browse the repository at this point in the history
  • Loading branch information
EricBAndrews authored May 12, 2024
1 parent 3d70573 commit 114a9d9
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 90 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ struct SomeView: View {
@Environment entities
@Binding variables
@State variables
@Namespace variables
Normal variables
Computed properties
Expand Down
40 changes: 24 additions & 16 deletions Mlem.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -184,8 +186,6 @@
CD4D583E2B86855F00B82964 /* MlemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MlemTests.swift; sourceTree = "<group>"; };
CD4D58402B86858100B82964 /* MlemUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MlemUITests.swift; sourceTree = "<group>"; };
CD4D58922B86BA5C00B82964 /* StandardPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardPalette.swift; sourceTree = "<group>"; };
CD4D58962B86BAD900B82964 /* EnvironmentValues+TabReselectionHashValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+TabReselectionHashValue.swift"; sourceTree = "<group>"; };
CD4D58982B86BB0300B82964 /* EnvironmentValues+TabSelectionHashValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+TabSelectionHashValue.swift"; sourceTree = "<group>"; };
CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistenceRepository.swift; sourceTree = "<group>"; };
CD4D58A92B86BE5900B82964 /* AccountListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountListView.swift; sourceTree = "<group>"; };
CD4D58AC2B86BE7100B82964 /* QuickSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickSwitcherView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -216,6 +216,10 @@
CD4D59192B87B3D100B82964 /* ErrorHandler+Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ErrorHandler+Dependency.swift"; sourceTree = "<group>"; };
CD4D591B2B87B43D00B82964 /* Task+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Extensions.swift"; sourceTree = "<group>"; };
CD4D591F2B87B63300B82964 /* SettingsOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsOptions.swift; sourceTree = "<group>"; };
CD4ED8462BF110FA00EFA0A2 /* TabReselectTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabReselectTracker.swift; sourceTree = "<group>"; };
CD4ED8492BF1113800EFA0A2 /* View+TabReselectConsumer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+TabReselectConsumer.swift"; sourceTree = "<group>"; };
CD4ED84B2BF111BA00EFA0A2 /* ScrollToView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToView.swift; sourceTree = "<group>"; };
CD4ED84D2BF113C800EFA0A2 /* ExpandedPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandedPostView.swift; sourceTree = "<group>"; };
CDA1E8202B8FC411007953EF /* LandingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingPage.swift; sourceTree = "<group>"; };
CDA1E8262B90EF24007953EF /* AppFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlow.swift; sourceTree = "<group>"; };
CDA1E8532B952C3D007953EF /* StateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -275,6 +279,7 @@
03134A4E2BEAD23A002662CC /* Views */ = {
isa = PBXGroup;
children = (
CD4ED8482BF1112A00EFA0A2 /* View Modifiers */,
03134A4F2BEAD245002662CC /* NavigationLink+NavigationPage.swift */,
);
path = Views;
Expand Down Expand Up @@ -499,21 +504,11 @@
isa = PBXGroup;
children = (
CD4D59212B87BD0800B82964 /* Definitions */,
CD4D58952B86BAC400B82964 /* EnvironmentValues */,
CD4D58672B86B38600B82964 /* Dependencies */,
);
path = Globals;
sourceTree = "<group>";
};
CD4D58952B86BAC400B82964 /* EnvironmentValues */ = {
isa = PBXGroup;
children = (
CD4D58982B86BB0300B82964 /* EnvironmentValues+TabSelectionHashValue.swift */,
CD4D58962B86BAD900B82964 /* EnvironmentValues+TabReselectionHashValue.swift */,
);
path = EnvironmentValues;
sourceTree = "<group>";
};
CD4D58A82B86BE4C00B82964 /* Shared */ = {
isa = PBXGroup;
children = (
Expand All @@ -528,6 +523,8 @@
037386402BDAF3F7007492B5 /* Markdown.swift */,
03D3A1EE2BB9CA1D009DE55E /* MenuButton.swift */,
03267D802BED4714009D6268 /* Avatar */,
CD4ED84B2BF111BA00EFA0A2 /* ScrollToView.swift */,
CD4ED84D2BF113C800EFA0A2 /* ExpandedPostView.swift */,
);
path = Shared;
sourceTree = "<group>";
Expand Down Expand Up @@ -649,10 +646,19 @@
CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */,
CD4D58FB2B87B13B00B82964 /* ErrorHandler */,
CD4D59042B87B19100B82964 /* Notifier */,
CD4ED8462BF110FA00EFA0A2 /* TabReselectTracker.swift */,
);
path = Definitions;
sourceTree = "<group>";
};
CD4ED8482BF1112A00EFA0A2 /* View Modifiers */ = {
isa = PBXGroup;
children = (
CD4ED8492BF1113800EFA0A2 /* View+TabReselectConsumer.swift */,
);
path = "View Modifiers";
sourceTree = "<group>";
};
CDA1E81A2B8FC39A007953EF /* Onboarding */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand All @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
24 changes: 24 additions & 0 deletions Mlem/App/Globals/Definitions/TabReselectTracker.swift
Original file line number Diff line number Diff line change
@@ -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
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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))
}
}
2 changes: 2 additions & 0 deletions Mlem/App/Views/Root/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -28,6 +29,7 @@ struct ContentView: View {
appState.cleanCaches()
}
.environment(palette)
.environment(tabReselectTracker)
.environment(appState)
}

Expand Down
88 changes: 53 additions & 35 deletions Mlem/App/Views/Root/Tabs/Feeds/FeedsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()
}
}
Expand Down
2 changes: 2 additions & 0 deletions Mlem/App/Views/Shared/CustomTabBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 114a9d9

Please sign in to comment.