Интересно, что в начале этого года я построил нечто похожее на коммерческий проект.Это сложно сделать в общем, но большинство проблем возникает из-за того, что мы думаем задом наперед.«Начните с конца».
// I want to be able to filter a sequence like this:
let newArray = myArray.filteredBy([
MyModel.Filters.DueDateFilter(selectedOptions: [.in24hours(past: false)]),
MyModel.Filters.StatusFilter(selectedOptions: [.a, .b])
])
Эта часть очень проста.Это даже не требует filteredBy
.Просто добавьте .filter
к каждому элементу:
let newArray = myArray
.filter(MyModel.Filters.DueDateFilter(selectedOptions: [.in24hours(past: false)]).filter)
.filter(MyModel.Filters.StatusFilter(selectedOptions: [.a, .b]).filter)
Если вы хотите, вы можете написать фильтрованный таким образом и сделать то же самое:
func filteredBy(_ filters: [(Element) -> Bool]) -> [Element] {...}
Дело в том, чтоFilter
здесь на самом деле не «фильтр».Это описание фильтра, с множеством других вещей, связанных с пользовательским интерфейсом (мы поговорим об этом позже).Чтобы на самом деле фильтровать, все, что вам нужно, это (Element) -> Bool
.
Что нам действительно нужно, так это способ создать ([Element]) -> Element
с хорошим, выразительным синтаксисом.На функциональном языке это было бы довольно просто, потому что у нас были бы такие вещи, как частичное применение и функциональная композиция.Но Свифт на самом деле не любит делать такие вещи, поэтому, чтобы сделать его красивее, давайте построим некоторые структуры.
struct Filter<Element> {
let isIncluded: (Element) -> Bool
}
struct Map<Input, Output> {
let transform: (Input) -> Output
}
Нам понадобится способ начать, поэтому давайте воспользуемся картой идентичности
extension Map where Input == Output {
init(on: Input.Type) { transform = { $0 }}
}
И нам нужен способ подумать о keyPaths
extension Map {
func keyPath<ChildOutput>(_ keyPath: KeyPath<Input, ChildOutput>) -> Map<Input, ChildOutput> {
return Map<Input, ChildOutput>(transform: { $0[keyPath: keyPath] })
}
}
И, наконец, мы хотим создать реальный фильтр
extension Map {
func inRange<RE: RangeExpression>(_ range: RE) -> Filter<Input> where RE.Bound == Output {
let transform = self.transform
return Filter(isIncluded: { range.contains(transform($0)) })
}
}
Добавить помощника для«последние 24 часа»
extension Range where Bound == Date {
static var last24Hours: Range<Date> { return Date(timeIntervalSinceNow: -24*60*60)..<Date() }
}
И теперь мы можем создать фильтр, который выглядит следующим образом:
let filters = [Map(on: MyModel.self).keyPath(\.dueDate).inRange(Range.last24Hours)]
filters
имеет тип Filter<MyModel>
, так что любая другая вещь, которая фильтруетMyModel
является законным здесь.Настройка filteredBy
:
extension Sequence {
func filteredBy(_ filters: [Filter<Element>]) -> [Element] {
return filter{ element in filters.allSatisfy{ $0.isIncluded(element) } }
}
}
OK, это шаг фильтрации.Но ваша проблема также в основном в «конфигурации пользовательского интерфейса», и для этого вы хотите захватить гораздо больше элементов, чем это делает.
Но ваш пример использования не поможет вам:
// Also I want to be able to save the state of all filters like this
var activeFilters: [AnyFilter] = [ // ???
MyModel.Filters.DueDateFilter(selectedOptions: [.in24hours(past: false)]),
MyModel.Filters.StatusFilter(selectedOptions: [.a, .b])
]
Как вы можете преобразовать AnyFilter
в элементы пользовательского интерфейса?Ваш протокол фильтра позволяет буквально любой тип опции.Как бы вы отобразили пользовательский интерфейс для этого, если бы тип опции был OutputStream
или DispatchQueue
?Созданный вами тип не решает проблему.
Вот один из способов решения этой проблемы.Создайте структуру FilterComponent, которая определяет необходимые элементы пользовательского интерфейса и предоставляет способ создания фильтра.
struct FilterComponent<Model> {
let optionTitles: [String]
let allowsMultipleSelection: Bool
var selectedOptions: IndexSet
let makeFilter: (IndexSet) -> Filter<Model>
}
Затем для создания компонента фильтра даты нам понадобятся некоторые параметры для дат.
enum DateOptions: String, CaseIterable {
case inPast24hours = "In the past 24 hours"
case inNext24hours = "In the next 24 hours"
var dateRange: Range<Date> {
switch self {
case .inPast24hours: return Date(timeIntervalSinceNow: -24*60*60)..<Date()
case .inNext24hours: return Date()..<Date(timeIntervalSinceNow: -24*60*60)
}
}
}
И затем мы хотим создать такой компонент с правильным makeFilter
:
extension FilterComponent {
static func byDate(ofField keyPath: KeyPath<Model, Date>) -> FilterComponent<Model> {
return FilterComponent(optionTitles: DateOptions.allCases.map{ $0.rawValue },
allowsMultipleSelection: false,
selectedOptions: [],
makeFilter: { indexSet in
guard let index = indexSet.first else {
return Filter<Model> { _ in true }
}
let range = DateOptions.allCases[index].dateRange
return Map(on: Model.self).keyPath(keyPath).inRange(range)
})
}
}
. При этом мы можем создавать компоненты типа FilterComponent<MyModel>
.Никакие внутренние типы (такие как Date
) не должны быть выставлены.Протоколы не нужны.
let components = [FilterComponent.byDate(ofField: \MyModel.dueDate)]