From bb8c4bea4c34b0fbc703ee8191420c559b9b00a6 Mon Sep 17 00:00:00 2001 From: Bosco Ho Date: Mon, 9 Oct 2023 18:14:11 -0700 Subject: [PATCH 1/5] - Keep search bar visible on scroll. --- Mlem/Views/Tabs/Search/SearchView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mlem/Views/Tabs/Search/SearchView.swift b/Mlem/Views/Tabs/Search/SearchView.swift index 853cb5d44..4c909f81d 100644 --- a/Mlem/Views/Tabs/Search/SearchView.swift +++ b/Mlem/Views/Tabs/Search/SearchView.swift @@ -56,6 +56,7 @@ struct SearchView: View { searchModel.searchText = "" } } + .navigationSearchBarHiddenWhenScrolling(false) .autocorrectionDisabled(true) .textInputAutocapitalization(.never) .onAppear { From e2dceccd62cb7119bc0935fcb9fda1a835f70fbf Mon Sep 17 00:00:00 2001 From: Bosco Ho Date: Wed, 11 Oct 2023 01:37:11 -0700 Subject: [PATCH 2/5] - Reconfigure Search view hierarchy such that home/recents/results are always loaded. --- Mlem/Views/Tabs/Search/SearchView.swift | 49 ++++++++++++++++--------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/Mlem/Views/Tabs/Search/SearchView.swift b/Mlem/Views/Tabs/Search/SearchView.swift index 4c909f81d..698dea666 100644 --- a/Mlem/Views/Tabs/Search/SearchView.swift +++ b/Mlem/Views/Tabs/Search/SearchView.swift @@ -73,24 +73,39 @@ struct SearchView: View { @ViewBuilder private var content: some View { - ScrollView { - VStack { - switch page { - case .home: - SearchHomeView() - .transition(.opacity) - .environmentObject(homeSearchModel) - .environmentObject(homeContentTracker) - case .recents: - RecentSearchesView() - .transition(.opacity) - case .results: - SearchResultsView() - .environmentObject(searchModel) - .transition(.opacity) - } + ZStack { + ScrollView { + SearchHomeView() + .transition(.opacity) + .environmentObject(homeSearchModel) + .environmentObject(homeContentTracker) + } + .animation(.default, value: page) + .fancyTabScrollCompatible() + .scrollDismissesKeyboard(.immediately) + .zIndex(page == .home ? 1 : 0) + .opacity(page == .home ? 1 : 0) + + ScrollView { + RecentSearchesView() + .transition(.opacity) + } + .animation(.default, value: page) + .fancyTabScrollCompatible() + .scrollDismissesKeyboard(.immediately) + .zIndex(page == .recents ? 1 : 0) + .opacity(page == .recents ? 1 : 0) + + ScrollView { + SearchResultsView() + .environmentObject(searchModel) + .transition(.opacity) } .animation(.default, value: page) + .fancyTabScrollCompatible() + .scrollDismissesKeyboard(.immediately) + .zIndex(page == .results ? 1 : 0) + .opacity(page == .results ? 1 : 0) } .onChange(of: isSearching) { newValue in if newValue, searchModel.searchText.isEmpty { @@ -106,7 +121,5 @@ struct SearchView: View { } } } - .fancyTabScrollCompatible() - .scrollDismissesKeyboard(.immediately) } } From 8532411f6fda0b65a6e91839e7edf0440deaaa1d Mon Sep 17 00:00:00 2001 From: Bosco Ho Date: Wed, 11 Oct 2023 02:51:37 -0700 Subject: [PATCH 3/5] - Workaround some janky animations resulting from changed view hierarchy. --- Mlem/Views/Tabs/Search/SearchView.swift | 41 +++++++++++++++++++------ 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/Mlem/Views/Tabs/Search/SearchView.swift b/Mlem/Views/Tabs/Search/SearchView.swift index 698dea666..e43197997 100644 --- a/Mlem/Views/Tabs/Search/SearchView.swift +++ b/Mlem/Views/Tabs/Search/SearchView.swift @@ -76,37 +76,33 @@ struct SearchView: View { ZStack { ScrollView { SearchHomeView() - .transition(.opacity) .environmentObject(homeSearchModel) .environmentObject(homeContentTracker) } - .animation(.default, value: page) .fancyTabScrollCompatible() .scrollDismissesKeyboard(.immediately) + ._opacity(page == .home ? 1 : 0, speed: page == .home ? 1 : 0) .zIndex(page == .home ? 1 : 0) - .opacity(page == .home ? 1 : 0) ScrollView { RecentSearchesView() - .transition(.opacity) } - .animation(.default, value: page) .fancyTabScrollCompatible() .scrollDismissesKeyboard(.immediately) + ._opacity(page == .recents ? 1 : 0, speed: page == .recents ? 1 : 0) .zIndex(page == .recents ? 1 : 0) - .opacity(page == .recents ? 1 : 0) ScrollView { SearchResultsView() .environmentObject(searchModel) - .transition(.opacity) } - .animation(.default, value: page) .fancyTabScrollCompatible() .scrollDismissesKeyboard(.immediately) + ._opacity(page == .results ? 1 : 0, speed: page == .results ? 1 : 0) .zIndex(page == .results ? 1 : 0) - .opacity(page == .results ? 1 : 0) } + .animation(.default, value: page) + .transition(.opacity) .onChange(of: isSearching) { newValue in if newValue, searchModel.searchText.isEmpty { page = .recents @@ -123,3 +119,30 @@ struct SearchView: View { } } } + +extension View { + + @ViewBuilder + func _opacity(_ opacity: Double, speed: Double) -> some View { + if #available(iOS 17.0, *) { + self.transaction { transaction in + if speed > 0 { + transaction.animation = transaction.animation?.speed(speed) + } else { + transaction.animation = nil + } + } body: { view in + view.opacity(opacity) + } + } else { + self.opacity(opacity) + .transaction { transaction in + if speed > 0 { + transaction.animation = transaction.animation?.speed(speed) + } else { + transaction.animation = nil + } + } + } + } +} From 84f4e69034e4e26b1f6b6d93ca731eb23a728d74 Mon Sep 17 00:00:00 2001 From: Bosco Ho Date: Thu, 12 Oct 2023 02:53:11 -0700 Subject: [PATCH 4/5] - When search is canceled, scroll recents/results to top so they appear anchored to top on next appear. - When search field is cleared, scroll results to top. --- Mlem/Views/Tabs/Search/SearchView.swift | 112 +++++++++++++++--------- 1 file changed, 73 insertions(+), 39 deletions(-) diff --git a/Mlem/Views/Tabs/Search/SearchView.swift b/Mlem/Views/Tabs/Search/SearchView.swift index e43197997..3deededd7 100644 --- a/Mlem/Views/Tabs/Search/SearchView.swift +++ b/Mlem/Views/Tabs/Search/SearchView.swift @@ -37,6 +37,12 @@ struct SearchView: View { @State var isSearching: Bool = false @State var page: Page = .home + @State private var recentsScrollToTopSignal: Int = .min + @State private var resultsScrollToTopSignal: Int = .min + + @Namespace private var resultsScrollToTop + @Namespace private var recentsScrollToTop + init() { @AppStorage("internetSpeed") var internetSpeed: InternetSpeed = .fast _searchModel = StateObject(wrappedValue: .init(internetSpeed: internetSpeed)) @@ -54,6 +60,8 @@ struct SearchView: View { .onCancel { page = .home searchModel.searchText = "" + resultsScrollToTopSignal += 1 + recentsScrollToTopSignal += 1 } } .navigationSearchBarHiddenWhenScrolling(false) @@ -69,51 +77,68 @@ struct SearchView: View { } } } + .onChange(of: searchModel.searchText) { value in + if value.isEmpty { + resultsScrollToTopSignal += 1 + } + } } @ViewBuilder private var content: some View { - ZStack { - ScrollView { - SearchHomeView() - .environmentObject(homeSearchModel) - .environmentObject(homeContentTracker) - } - .fancyTabScrollCompatible() - .scrollDismissesKeyboard(.immediately) - ._opacity(page == .home ? 1 : 0, speed: page == .home ? 1 : 0) - .zIndex(page == .home ? 1 : 0) - - ScrollView { - RecentSearchesView() - } - .fancyTabScrollCompatible() - .scrollDismissesKeyboard(.immediately) - ._opacity(page == .recents ? 1 : 0, speed: page == .recents ? 1 : 0) - .zIndex(page == .recents ? 1 : 0) - - ScrollView { - SearchResultsView() - .environmentObject(searchModel) - } - .fancyTabScrollCompatible() - .scrollDismissesKeyboard(.immediately) - ._opacity(page == .results ? 1 : 0, speed: page == .results ? 1 : 0) - .zIndex(page == .results ? 1 : 0) - } - .animation(.default, value: page) - .transition(.opacity) - .onChange(of: isSearching) { newValue in - if newValue, searchModel.searchText.isEmpty { - page = .recents + ScrollViewReader { proxy in + ZStack { + ScrollView { + SearchHomeView() + .environmentObject(homeSearchModel) + .environmentObject(homeContentTracker) + } + .fancyTabScrollCompatible() + .scrollDismissesKeyboard(.immediately) + ._opacity(page == .home ? 1 : 0, speed: page == .home ? 1 : 0) + .zIndex(page == .home ? 1 : 0) + + ScrollView { + HStack { EmptyView() } + .id(recentsScrollToTop) + RecentSearchesView() + } + .fancyTabScrollCompatible() + .scrollDismissesKeyboard(.immediately) + ._opacity(page == .recents ? 1 : 0, speed: page == .recents ? 1 : 0) + .zIndex(page == .recents ? 1 : 0) + .onChange(of: recentsScrollToTopSignal) { _ in + proxy.scrollTo(recentsScrollToTop) + } + + ScrollView { + HStack { EmptyView() } + .id(resultsScrollToTop) + SearchResultsView() + .environmentObject(searchModel) + } + .fancyTabScrollCompatible() + .scrollDismissesKeyboard(.immediately) + ._opacity(page == .results ? 1 : 0, speed: page == .results ? 1 : 0) + .zIndex(page == .results ? 1 : 0) + .onChange(of: resultsScrollToTopSignal) { _ in + proxy.scrollTo(resultsScrollToTop) + } } - } - .onChange(of: searchModel.searchText) { newValue in - if page != .home { - if newValue.isEmpty { + .animation(.default, value: page) + .transition(.opacity) + .onChange(of: isSearching) { newValue in + if newValue, searchModel.searchText.isEmpty { page = .recents - } else { - page = .results + } + } + .onChange(of: searchModel.searchText) { newValue in + if page != .home { + if newValue.isEmpty { + page = .recents + } else { + page = .results + } } } } @@ -122,6 +147,15 @@ struct SearchView: View { extension View { + @ViewBuilder + func _defaultScrollAnchor(_ anchor: UnitPoint) -> some View { + if #available(iOS 17.0, *) { + self.defaultScrollAnchor(anchor) + } else { + self + } + } + @ViewBuilder func _opacity(_ opacity: Double, speed: Double) -> some View { if #available(iOS 17.0, *) { From 4b3de0b464962aac331821740b88adc1ccfa2c1f Mon Sep 17 00:00:00 2001 From: Bosco Ho Date: Thu, 12 Oct 2023 14:30:00 -0700 Subject: [PATCH 5/5] - Remove unused code. --- Mlem/Views/Tabs/Search/SearchView.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Mlem/Views/Tabs/Search/SearchView.swift b/Mlem/Views/Tabs/Search/SearchView.swift index 3deededd7..f8f697d53 100644 --- a/Mlem/Views/Tabs/Search/SearchView.swift +++ b/Mlem/Views/Tabs/Search/SearchView.swift @@ -147,15 +147,6 @@ struct SearchView: View { extension View { - @ViewBuilder - func _defaultScrollAnchor(_ anchor: UnitPoint) -> some View { - if #available(iOS 17.0, *) { - self.defaultScrollAnchor(anchor) - } else { - self - } - } - @ViewBuilder func _opacity(_ opacity: Double, speed: Double) -> some View { if #available(iOS 17.0, *) {