-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSwiftUISyncDemo.swift
More file actions
171 lines (150 loc) · 4.84 KB
/
SwiftUISyncDemo.swift
File metadata and controls
171 lines (150 loc) · 4.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import SharingInstant
import SwiftUI
// NOTE: Schema.todos is now auto-generated in Sources/Generated/Schema.swift
// The generated Todo type uses `createdAt: Double` (epoch timestamp).
struct SwiftUISyncDemo: SwiftUICaseStudy {
let readMe = """
This demo shows how to use the `@Shared` annotation with `.instantSync` \
for bidirectional synchronization with InstantDB.
Changes you make are applied optimistically—they show immediately in the UI \
while being sent to the server in the background. If the server rejects \
a change, it will be rolled back.
Try adding, editing, and deleting todos. Open the app on multiple devices \
or simulators to see real-time sync in action!
"""
let caseStudyTitle = "Sync Demo"
var body: some View {
TodoListView()
.onAppear {
InstantLogger.viewAppeared("SwiftUISyncDemo")
}
.onDisappear {
InstantLogger.viewDisappeared("SwiftUISyncDemo")
}
}
}
/// The main todo list view with shared state
private struct TodoListView: View {
/// Type-safe sync using EntityKey.
///
/// The `@Shared` property wrapper handles:
/// - Connecting to InstantDB
/// - Subscribing to the "todos" collection
/// - Bidirectional sync with optimistic updates
/// - Automatic cleanup on view disappear
///
/// Uses `Schema.todos` for type safety - no string literals needed!
@Shared(.instantSync(Schema.todos.orderBy(\Todo.createdAt, EntityKeyOrderDirection.desc)))
private var todos: IdentifiedArrayOf<Todo> = []
@State private var newTodoTitle = ""
@FocusState private var isInputFocused: Bool
@State private var toast: Toast?
/// Track previous count to detect data received from server
@State private var previousTodoCount: Int = 0
var body: some View {
List {
Section {
HStack {
Text("Total Todos")
Spacer()
Text("\(todos.count)")
.font(.title2)
.bold()
}
}
Section("Add New Todo") {
HStack {
TextField("What needs to be done?", text: $newTodoTitle)
.focused($isInputFocused)
.onSubmit(addTodo)
Button(action: addTodo) {
Image(systemName: "plus.circle.fill")
.font(.title2)
}
.disabled(newTodoTitle.trimmingCharacters(in: .whitespaces).isEmpty)
}
}
Section("Todos (Read-Only)") {
if todos.isEmpty {
ContentUnavailableView {
Label("No Todos", systemImage: "checklist")
} description: {
Text("Add your first todo above!")
}
} else {
ForEach(todos) { todo in
TodoRowReadOnly(todo: todo)
}
}
}
}
.toast($toast)
.onChange(of: todos.count) { oldCount, newCount in
// Log when data is received from server (count changes without user action)
if oldCount != previousTodoCount {
InstantLogger.dataReceived(
"Todos updated",
count: newCount,
details: ["previousCount": oldCount, "newCount": newCount]
)
}
previousTodoCount = newCount
}
.onAppear {
InstantLogger.info("TodoListView appeared", json: ["initialCount": todos.count])
previousTodoCount = todos.count
}
}
private func addTodo() {
let title = newTodoTitle.trimmingCharacters(in: .whitespaces)
guard !title.isEmpty else { return }
// Log user action
InstantLogger.userAction("Add todo", details: ["title": title])
// Use generated mutation with callbacks for toast feedback
$todos.createTodo(
createdAt: Date().timeIntervalSince1970 * 1_000,
done: false,
title: title,
callbacks: .init(
onSuccess: { _ in
withAnimation {
toast = Toast(type: .success, message: "Todo created!")
}
},
onError: { error in
withAnimation {
toast = Toast(type: .error, message: "Failed: \(error.localizedDescription)")
}
}
)
)
newTodoTitle = ""
isInputFocused = false
}
}
/// A simple read-only todo row to avoid AttributeGraph cycles
private struct TodoRowReadOnly: View {
let todo: Todo
var body: some View {
HStack {
Image(systemName: todo.done ? "checkmark.circle.fill" : "circle")
.font(.title2)
.foregroundStyle(todo.done ? .green : .secondary)
VStack(alignment: .leading, spacing: 4) {
Text(todo.title)
.strikethrough(todo.done)
// Normalize seconds vs milliseconds for display.
Text(InstantEpochTimestamp.date(from: todo.createdAt), style: .relative)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
#Preview {
NavigationStack {
CaseStudyView {
SwiftUISyncDemo()
}
}
}