New filters ordering and faster animation (#2536)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user