Fix filters being jumpy when scrolling them (#2459)

This commit is contained in:
Mauro
2024-02-13 02:09:55 +01:00
committed by GitHub
parent 50e6220076
commit 2b2accaca6
7 changed files with 40 additions and 36 deletions
@@ -121,8 +121,6 @@ struct HomeScreenViewState: BindableState {
return rooms return rooms
} }
var filtersState = RoomListFiltersState()
var bindings = HomeScreenViewStateBindings() var bindings = HomeScreenViewStateBindings()
var placeholderRooms: [HomeScreenRoom] { var placeholderRooms: [HomeScreenRoom] {
@@ -138,6 +136,7 @@ struct HomeScreenViewState: BindableState {
} }
struct HomeScreenViewStateBindings { struct HomeScreenViewStateBindings {
var filtersState = RoomListFiltersState()
var searchQuery = "" var searchQuery = ""
var isSearchFieldFocused = false var isSearchFieldFocused = false
@@ -89,7 +89,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
} }
if !value { if !value {
state.shouldShowFilters = false state.shouldShowFilters = false
state.filtersState.clearFilters() state.bindings.filtersState.clearFilters()
} else { } else {
state.shouldShowFilters = true state.shouldShowFilters = true
} }
@@ -106,9 +106,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
let isSearchFieldFocused = context.$viewState.map(\.bindings.isSearchFieldFocused) let isSearchFieldFocused = context.$viewState.map(\.bindings.isSearchFieldFocused)
let searchQuery = context.$viewState.map(\.bindings.searchQuery) let searchQuery = context.$viewState.map(\.bindings.searchQuery)
let enabledFilters = context.viewState.filtersState.$activeFilters let activeFilters = context.$viewState.map(\.bindings.filtersState.activeFilters)
isSearchFieldFocused isSearchFieldFocused
.combineLatest(searchQuery, enabledFilters) .combineLatest(searchQuery, activeFilters)
.removeDuplicates { $0 == $1 } .removeDuplicates { $0 == $1 }
.sink { [weak self] isSearchFieldFocused, _, _ in .sink { [weak self] isSearchFieldFocused, _, _ in
guard let self else { return } guard let self else { return }
@@ -209,9 +209,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
} else { } else {
if state.bindings.isSearchFieldFocused { if state.bindings.isSearchFieldFocused {
roomSummaryProvider?.setFilter(.include(.init(query: state.bindings.searchQuery, roomSummaryProvider?.setFilter(.include(.init(query: state.bindings.searchQuery,
filters: state.filtersState.activeFilters))) filters: state.bindings.filtersState.activeFilters)))
} else { } else {
roomSummaryProvider?.setFilter(.include(.init(filters: state.filtersState.activeFilters))) roomSummaryProvider?.setFilter(.include(.init(filters: state.bindings.filtersState.activeFilters)))
} }
} }
} }
@@ -71,8 +71,8 @@ enum RoomListFilter: Int, CaseIterable, Identifiable {
} }
} }
final class RoomListFiltersState: ObservableObject { struct RoomListFiltersState {
@Published private(set) var activeFilters: Set<RoomListFilter> private(set) var activeFilters: Set<RoomListFilter>
init(activeFilters: Set<RoomListFilter> = []) { init(activeFilters: Set<RoomListFilter> = []) {
self.activeFilters = activeFilters self.activeFilters = activeFilters
@@ -97,7 +97,7 @@ final class RoomListFiltersState: ObservableObject {
!activeFilters.isEmpty !activeFilters.isEmpty
} }
func activateFilter(_ filter: RoomListFilter) { mutating func activateFilter(_ filter: RoomListFilter) {
if let incompatibleFilter = filter.incompatibleFilter, if let incompatibleFilter = filter.incompatibleFilter,
activeFilters.contains(incompatibleFilter) { activeFilters.contains(incompatibleFilter) {
fatalError("[RoomListFiltersState] adding mutually exclusive filters is not allowed") fatalError("[RoomListFiltersState] adding mutually exclusive filters is not allowed")
@@ -105,11 +105,11 @@ final class RoomListFiltersState: ObservableObject {
activeFilters.insert(filter) activeFilters.insert(filter)
} }
func deactivateFilter(_ filter: RoomListFilter) { mutating func deactivateFilter(_ filter: RoomListFilter) {
activeFilters.remove(filter) activeFilters.remove(filter)
} }
func clearFilters() { mutating func clearFilters() {
activeFilters.removeAll() activeFilters.removeAll()
} }
@@ -18,15 +18,10 @@ import SwiftUI
struct RoomListFilterView: View { struct RoomListFilterView: View {
let filter: RoomListFilter let filter: RoomListFilter
@StateObject var state: RoomListFiltersState @Binding var isActive: Bool
var body: some View { var body: some View {
let binding = Binding<Bool>(get: { Toggle(isOn: $isActive) {
state.isFilterActive(filter)
}, set: { isEnabled, _ in
isEnabled ? state.activateFilter(filter) : state.deactivateFilter(filter)
})
Toggle(isOn: binding) {
Text(filter.localizedName) Text(filter.localizedName)
} }
.toggleStyle(FilterToggleStyle()) .toggleStyle(FilterToggleStyle())
@@ -35,8 +30,8 @@ struct RoomListFilterView: View {
struct RoomListFilterView_Previews: PreviewProvider, TestablePreview { struct RoomListFilterView_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
RoomListFilterView(filter: .people, state: .init()) RoomListFilterView(filter: .people, isActive: .constant(false))
RoomListFilterView(filter: .people, state: .init(activeFilters: [.people])) RoomListFilterView(filter: .people, isActive: .constant(true))
} }
} }
@@ -17,28 +17,30 @@
import SwiftUI import SwiftUI
struct RoomListFiltersView: View { struct RoomListFiltersView: View {
@StateObject var state: RoomListFiltersState @Binding var state: RoomListFiltersState
@Namespace private var namespace
var body: some View { var body: some View {
ScrollView(.horizontal) { ScrollView(.horizontal) {
LazyHStack(spacing: 8) { HStack(spacing: 8) {
if state.isFiltering { if state.isFiltering {
clearButton clearButton
} else {
// This solves a weird issue withe the LazyHStack
// where it is resized when the button appears and disappears
clearButton
.hidden()
.frame(width: 0)
} }
ForEach(state.sortedActiveFilters) { filter in ForEach(state.sortedActiveFilters) { filter in
RoomListFilterView(filter: filter, state: state) RoomListFilterView(filter: filter,
isActive: getBinding(for: filter))
.matchedGeometryEffect(id: filter.id, in: namespace)
// This will make the animation always render the enabled ones on top
.zIndex(1)
} }
ForEach(state.availableFilters) { filter in ForEach(state.availableFilters) { filter in
RoomListFilterView(filter: filter, state: state) RoomListFilterView(filter: filter,
isActive: getBinding(for: filter))
.matchedGeometryEffect(id: filter.id, in: namespace)
} }
} }
.padding(.leading, !state.isFiltering ? 8 : 16) .padding(.leading, 16)
.padding(.vertical, 12) .padding(.vertical, 12)
} }
.scrollIndicators(.hidden) .scrollIndicators(.hidden)
@@ -55,13 +57,21 @@ struct RoomListFiltersView: View {
.foregroundColor(.compound.bgActionPrimaryRest) .foregroundColor(.compound.bgActionPrimaryRest)
}) })
} }
private func getBinding(for filter: RoomListFilter) -> Binding<Bool> {
Binding<Bool>(get: {
state.isFilterActive(filter)
}, set: { isEnabled, _ in
isEnabled ? state.activateFilter(filter) : state.deactivateFilter(filter)
})
}
} }
// MARK: - Previews // MARK: - Previews
struct RoomListFiltersView_Previews: PreviewProvider, TestablePreview { struct RoomListFiltersView_Previews: PreviewProvider, TestablePreview {
static var previews: some View { static var previews: some View {
RoomListFiltersView(state: .init()) RoomListFiltersView(state: .constant(.init()))
RoomListFiltersView(state: .init(activeFilters: [.rooms, .favourites])) RoomListFiltersView(state: .constant(.init(activeFilters: [.rooms, .favourites])))
} }
} }
@@ -134,7 +134,7 @@ struct HomeScreenContent: View {
} }
private var filters: some View { private var filters: some View {
RoomListFiltersView(state: context.viewState.filtersState) RoomListFiltersView(state: $context.filtersState)
} }
@ViewBuilder @ViewBuilder
@@ -162,7 +162,7 @@ class HomeScreenViewModelTests: XCTestCase {
} }
func testFilters() async throws { func testFilters() async throws {
context.viewState.filtersState.activateFilter(.people) context.filtersState.activateFilter(.people)
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertEqual(roomSummaryProvider.currentFilter, RoomSummaryProviderFilter.include(.init(filters: [.people]))) XCTAssertEqual(roomSummaryProvider.currentFilter, RoomSummaryProviderFilter.include(.init(filters: [.people])))
context.isSearchFieldFocused = true context.isSearchFieldFocused = true