Skip to content

Commit ef5b943

Browse files
committed
Initial support for dates
1 parent b9d885f commit ef5b943

4 files changed

Lines changed: 186 additions & 25 deletions

File tree

Example/PredicateViewExample/Item.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ final class Item {
2121

2222
init() {
2323
self.id = UUID()
24-
self.timestamp = Date(timeIntervalSinceNow: .random(in: -3600 * 24...3600 * 24))
24+
self.timestamp = Date(timeIntervalSinceNow: .random(in: -3600 * 24 * 30...3600 * 24 * 30))
2525
self.title = "Item \(Int.random(in: 1...1000))"
2626
self._status = Status.allCases.randomElement()!.rawValue
2727
}

Example/PredicateViewExample/SwiftDataDemoView.swift

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,32 +49,28 @@ struct SwiftDataDemoView: View {
4949

5050
var body: some View {
5151
VStack(alignment: .leading) {
52-
ScrollView(.horizontal) {
53-
PredicateView(predicate: $predicate, rowTemplates: [
54-
.init(keyPath: \.title, title: "Title"),
55-
.init(StatusExpressionView.self),
56-
])
57-
}
52+
predicateView(for: $predicate)
5853

59-
GroupBox("Cloned Predicates") {
60-
ForEach(Array($savedPredicates.enumerated()), id: \.offset) { index, $predicate in
61-
ScrollView(.horizontal) {
62-
PredicateView(predicate: $predicate, rowTemplates: [
63-
.init(keyPath: \.title, title: "Title"),
64-
.init(StatusExpressionView.self),
65-
])
54+
DisclosureGroup("Cloning") {
55+
VStack(alignment: .leading) {
56+
Button("New Clone") {
57+
savedPredicates.append(predicate)
58+
}
59+
60+
ForEach(Array($savedPredicates.enumerated()), id: \.offset) { index, $predicate in
61+
predicateView(for: $predicate)
6662
}
6763
}
68-
69-
Button("Clone") {
70-
savedPredicates.append(predicate)
71-
}
64+
.padding(.vertical)
65+
.frame(maxWidth: .infinity, alignment: .leading)
7266
}
7367

7468
Table(items) {
7569
TableColumn("Title", value: \.title)
7670
TableColumn("Status", value: \.status.rawValue)
77-
TableColumn("Timestamp", value: \.timestamp.description)
71+
TableColumn("Timestamp") { value in
72+
Text(value.timestamp, style: .date)
73+
}
7874
}
7975
}
8076
.padding()
@@ -89,6 +85,16 @@ struct SwiftDataDemoView: View {
8985
}
9086
}
9187

88+
private func predicateView(for predicate: Binding<Predicate<Item>>) -> some View {
89+
ScrollView(.horizontal) {
90+
PredicateView(predicate: predicate, rowTemplates: [
91+
.init(keyPath: \.title, title: "Title"),
92+
.init(keyPath: \.timestamp, title: "Timestamp"),
93+
.init(StatusExpressionView.self),
94+
])
95+
}
96+
}
97+
9298
private var items: [Item] {
9399
try! modelContext.fetch(.init(predicate: predicate))
94100
}

Sources/PredicateView/Expressions/DateExpression.swift

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,153 @@
55
// Created by Phil Zakharchenko on 9/22/24.
66
//
77

8-
import Foundation
8+
import SwiftUI
9+
10+
extension AnyExpression {
11+
public init(keyPath: KeyPath<Root, Date>, title: String) {
12+
self.init(wrappedValue: DateExpression(keyPath: keyPath, title: title))
13+
}
14+
15+
public init(keyPath: KeyPath<Root, Date?>, title: String) {
16+
self.init(wrappedValue: OptionalExpression<Root, DateExpression>(keyPath: keyPath, title: title))
17+
}
18+
}
19+
20+
struct DateExpression<Root>: ContentExpression, WrappablePredicateExpression {
21+
typealias AttributeValue = Date
22+
23+
enum Operator: String, ExpressionOperator {
24+
case before = "is before"
25+
case after = "is after"
26+
case sameDay = "is same day"
27+
case sameWeek = "is same week"
28+
case sameMonth = "is same month"
29+
30+
var comparisonOperator: PredicateExpressions.ComparisonOperator? {
31+
switch self {
32+
case .before: .lessThan
33+
case .after: .greaterThan
34+
default: nil
35+
}
36+
}
37+
38+
init?(_ comparisonOperator: PredicateExpressions.ComparisonOperator) {
39+
switch comparisonOperator {
40+
case .lessThan, .lessThanOrEqual: self = .before
41+
case .greaterThan, .greaterThanOrEqual: self = .after
42+
@unknown default: return nil
43+
}
44+
}
45+
}
46+
47+
static var defaultAttribute: StandardAttribute<Self> { .init(operator: .before, value: .now) }
48+
49+
var id = UUID()
50+
let keyPath: KeyPath<Root, Date>
51+
let title: String
52+
var attribute: StandardAttribute<Self> = Self.defaultAttribute
53+
54+
static func buildPredicate<V>(
55+
for variable: V,
56+
using attribute: StandardAttribute<Self>
57+
) -> (any StandardPredicateExpression<Bool>)? where V: StandardPredicateExpression<Value> {
58+
switch attribute.operator {
59+
case .before, .after:
60+
return PredicateExpressions.Comparison(
61+
lhs: variable,
62+
rhs: PredicateExpressions.Value(attribute.value),
63+
op: attribute.operator.comparisonOperator ?? .lessThan
64+
)
65+
case .sameDay, .sameWeek, .sameMonth:
66+
let interval: DateInterval
67+
switch attribute.operator {
68+
case .sameDay:
69+
interval = .init(
70+
start: attribute.value.startOfDay,
71+
end: attribute.value.endOfDay
72+
)
73+
case .sameWeek:
74+
interval = .init(
75+
start: attribute.value.startOfWeek,
76+
end: attribute.value.endOfWeek
77+
)
78+
case .sameMonth:
79+
interval = .init(
80+
start: attribute.value.startOfMonth,
81+
end: attribute.value.endOfMonth
82+
)
83+
default:
84+
return nil
85+
}
86+
87+
return PredicateExpressions.Conjunction(
88+
lhs: PredicateExpressions.Comparison(
89+
lhs: variable,
90+
rhs: PredicateExpressions.Value(interval.start),
91+
op: .greaterThanOrEqual
92+
),
93+
rhs: PredicateExpressions.Comparison(
94+
lhs: variable,
95+
rhs: PredicateExpressions.Value(interval.end),
96+
op: .lessThan
97+
)
98+
)
99+
}
100+
}
101+
102+
func decode<PredicateExpressionType: PredicateExpression<Bool>>(
103+
_ expression: PredicateExpressionType
104+
) -> (any Expression<Root>)? {
105+
switch expression {
106+
case let expression as PredicateExpressions.Comparison<KeyPathPredicateExpression, ValuePredicateExpression>:
107+
DateExpression(
108+
keyPath: expression.lhs.keyPath,
109+
title: title,
110+
attribute: .init(operator: .init(expression.op) ?? .before, value: expression.rhs.value)
111+
)
112+
case let expression as PredicateExpressions.Conjunction<Comparison, Comparison>:
113+
nil
114+
default:
115+
nil
116+
}
117+
}
118+
119+
static func makeContentView(_ value: Binding<Date>) -> some View {
120+
DatePicker("", selection: value, displayedComponents: .date)
121+
}
122+
123+
private typealias Comparison = PredicateExpressions.Comparison<KeyPathPredicateExpression, ValuePredicateExpression>
124+
}
125+
126+
private extension Date {
127+
var startOfDay: Date {
128+
Calendar.current.startOfDay(for: self)
129+
}
130+
131+
var endOfDay: Date {
132+
startOfDay.addingTimeInterval(3600 * 24 - 1)
133+
}
134+
135+
var startOfWeek: Date {
136+
let calendar = Calendar.current
137+
var components = calendar.dateComponents([.weekday, .year, .month, .weekOfYear], from: self)
138+
components.weekday = calendar.firstWeekday
139+
return calendar.date(from: components) ?? self
140+
}
141+
142+
var endOfWeek: Date {
143+
Calendar.current.date(byAdding: .second, value: 604799, to: startOfWeek) ?? startOfWeek
144+
}
145+
146+
var startOfMonth: Date {
147+
Calendar.current.date(from: Calendar.current.dateComponents([.year, .month], from: startOfDay)) ?? self
148+
}
149+
150+
var endOfMonth: Date {
151+
Calendar.current.date(byAdding: .month, value: 1, to: startOfMonth)?.addingTimeInterval(-1) ?? self
152+
}
153+
154+
var startOfYear: Date {
155+
return Calendar.current.date(from: Calendar.current.dateComponents([.year], from: startOfDay)) ?? self
156+
}
157+
}

Sources/PredicateView/Views/PredicateView.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,22 @@ import SwiftUI
1010
public struct PredicateView<Root>: View {
1111
@Binding public var predicate: Predicate<Root>
1212

13-
@State private var rootExpression: LogicalExpression<Root>
13+
@State private var rootExpression: LogicalExpression<Root> = .init()
1414
@Bindable private var configuration: PredicateViewConfiguration<Root>
1515

1616
public init(predicate: Binding<Predicate<Root>>, rowTemplates: [AnyExpression<Root>]) {
1717
self._predicate = predicate
1818
let templates = rowTemplates.map(\.wrappedValue)
1919
self.configuration = PredicateViewConfiguration(rowTemplates: templates)
20-
20+
}
21+
22+
private func updateExpression() {
23+
let templates = configuration.rowTemplates
2124
let expressions = (templates + [LogicalExpression<Root>()])
22-
.decode(from: predicate.wrappedValue.expression, as: Root.self)
25+
.decode(from: predicate.expression, as: Root.self)
2326

24-
self.rootExpression = if expressions.count == 1,
25-
let expression = expressions.first as? LogicalExpression<Root> {
27+
rootExpression = if expressions.count == 1,
28+
let expression = expressions.first as? LogicalExpression<Root> {
2629
expression
2730
} else {
2831
.init(children: expressions)
@@ -35,6 +38,9 @@ public struct PredicateView<Root>: View {
3538
.onPreferenceChange(PredicateAttributePreferenceKey.self) { _ in
3639
Task { buildPredicate() }
3740
}
41+
.onAppear {
42+
updateExpression()
43+
}
3844
}
3945

4046
private func buildPredicate() {

0 commit comments

Comments
 (0)