mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Fix filters being jumpy when scrolling them (#2459)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user