New filters ordering and faster animation (#2536)

This commit is contained in:
Mauro
2024-03-07 10:13:34 +01:00
committed by GitHub
parent 0d1dc4bf01
commit 4130e6fdf1
4 changed files with 52 additions and 39 deletions

View File

@@ -73,16 +73,15 @@ enum RoomListFilter: Int, CaseIterable, Identifiable {
struct RoomListFiltersState {
private(set) var activeFilters: OrderedSet<RoomListFilter>
private var inactiveFilters: OrderedSet<RoomListFilter>
init(activeFilters: OrderedSet<RoomListFilter> = []) {
self.activeFilters = .init(activeFilters)
inactiveFilters = OrderedSet(RoomListFilter.allCases).subtracting(activeFilters)
}
var availableFilters: [RoomListFilter] {
var availableFilters = inactiveFilters
var availableFilters = OrderedSet(RoomListFilter.allCases)
for filter in activeFilters {
availableFilters.remove(filter)
if let incompatibleFilter = filter.incompatibleFilter {
availableFilters.remove(incompatibleFilter)
}
@@ -90,6 +89,10 @@ struct RoomListFiltersState {
return availableFilters.elements
}
var orderedFilters: [RoomListFilter] {
activeFilters.elements + availableFilters
}
var isFiltering: Bool {
!activeFilters.isEmpty
}
@@ -101,20 +104,14 @@ struct RoomListFiltersState {
}
// We always want the most recently enabled filter to be at the bottom of the others.
activeFilters.append(filter)
inactiveFilters.remove(filter)
}
mutating func deactivateFilter(_ filter: RoomListFilter) {
activeFilters.remove(filter)
// We always want the most recently disabled filter to be on top of the others
inactiveFilters.insert(filter, at: 0)
}
mutating func clearFilters() {
// We iterate in reverse because filters should get disabled from the first to last that has been used.
for filter in activeFilters.reversed() {
deactivateFilter(filter)
}
activeFilters.removeAll()
}
func isFilterActive(_ filter: RoomListFilter) -> Bool {

View File

@@ -64,9 +64,7 @@ private struct FilterToggleStyle: ToggleStyle {
.drawingGroup()
// The button breaks the animation for some reason, so better to use the label directly with an onTapGesture
.onTapGesture {
withAnimation(.elementDefault) {
configuration.isOn.toggle()
}
configuration.isOn.toggle()
}
}
}

View File

@@ -17,39 +17,50 @@
import SwiftUI
struct RoomListFiltersView: View {
let leadingID = "leading"
@Binding var state: RoomListFiltersState
@Namespace private var namespace
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 8) {
if state.isFiltering {
clearButton
}
ForEach(state.activeFilters) { filter in
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
RoomListFilterView(filter: filter,
isActive: getBinding(for: filter))
.matchedGeometryEffect(id: filter.id, in: namespace)
ScrollViewReader { proxy in
ScrollView(.horizontal) {
HStack(spacing: 0) {
// Using an empty view makes the scroll a bit clunky, better a 0 frame spacer
Spacer()
.frame(width: 0, height: 0)
.id(leadingID)
HStack(spacing: 8) {
if state.isFiltering {
clearButton(scrollViewProxy: proxy)
}
ForEach(state.activeFilters) { filter in
RoomListFilterView(filter: filter,
isActive: getBinding(for: filter, scrollViewProxy: proxy))
.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
RoomListFilterView(filter: filter,
isActive: getBinding(for: filter, scrollViewProxy: proxy))
.matchedGeometryEffect(id: filter.id, in: namespace)
}
}
}
}
.scrollIndicators(.hidden)
.padding(.leading, 16)
.padding(.vertical, 12)
}
.scrollIndicators(.hidden)
}
private var clearButton: some View {
private func clearButton(scrollViewProxy: ScrollViewProxy) -> some View {
Button(action: {
withAnimation(.elementDefault) {
withAnimation(.easeInOut(duration: 0.2).disabledDuringTests()) {
state.clearFilters()
scrollViewProxy.scrollTo(leadingID, anchor: .leading)
}
}, label: {
Image(systemName: "xmark.circle.fill")
@@ -58,11 +69,18 @@ struct RoomListFiltersView: View {
})
}
private func getBinding(for filter: RoomListFilter) -> Binding<Bool> {
private func getBinding(for filter: RoomListFilter, scrollViewProxy: ScrollViewProxy) -> Binding<Bool> {
Binding<Bool>(get: {
state.isFilterActive(filter)
}, set: { isEnabled, _ in
isEnabled ? state.activateFilter(filter) : state.deactivateFilter(filter)
withAnimation(.easeInOut(duration: 0.2).disabledDuringTests()) {
if isEnabled {
state.activateFilter(filter)
scrollViewProxy.scrollTo(leadingID, anchor: .leading)
} else {
state.deactivateFilter(filter)
}
}
})
}
}

View File

@@ -51,7 +51,7 @@ final class RoomListFiltersStateTests: XCTestCase {
state.deactivateFilter(.people)
XCTAssertFalse(state.isFiltering)
XCTAssertEqual(state.activeFilters, [])
XCTAssertEqual(state.availableFilters, [.people, .unreads, .rooms, .favourites])
XCTAssertEqual(state.availableFilters, RoomListFilter.allCases)
state.activateFilter(.rooms)
XCTAssertTrue(state.isFiltering)
@@ -80,7 +80,7 @@ final class RoomListFiltersStateTests: XCTestCase {
state.clearFilters()
XCTAssertFalse(state.isFiltering)
XCTAssertEqual(state.activeFilters, [])
XCTAssertEqual(state.availableFilters, [.people, .unreads, .favourites, .rooms])
XCTAssertEqual(state.availableFilters, RoomListFilter.allCases)
}
func testOrder() {
@@ -90,11 +90,11 @@ final class RoomListFiltersStateTests: XCTestCase {
state.deactivateFilter(.favourites)
XCTAssertEqual(state.activeFilters, [])
XCTAssertEqual(state.availableFilters, [.favourites, .unreads, .people, .rooms])
XCTAssertEqual(state.availableFilters, RoomListFilter.allCases)
state.activateFilter(.rooms)
XCTAssertEqual(state.activeFilters, [.rooms])
XCTAssertEqual(state.availableFilters, [.favourites, .unreads])
XCTAssertEqual(state.availableFilters, [.unreads, .favourites])
state.activateFilter(.unreads)
XCTAssertEqual(state.activeFilters, [.rooms, .unreads])