-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathContentView.swift
129 lines (110 loc) · 4.82 KB
/
ContentView.swift
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
//
// ContentView.swift
// Bodies
//
// Created by Helge Heß on 10.08.22.
//
import Foundation
import SwiftUI
@MainActor
struct ContentView: View {
let database : BodiesDB
@State private var bodies : [ SolarBody ] = []
/// The current search string.
@State private var searchString = ""
func loadFromCache() async {
let searchString = searchString // do not capture @State in concurrency
bodies = try! await database.solarBodies.fetch(orderBy: \.englishName) {
$0.englishName.contains(searchString, caseInsensitive: true)
}
print("Fetched #\(bodies.count) from database.")
}
var body: some View {
List(bodies) { body in
Label {
VStack(alignment: .leading) {
Text(verbatim: body.englishName)
.font(.headline)
Text("Type: \(body.bodyType.capitalized)")
if let s = body.discoveredBy, !s.isEmpty {
Text("Discovered by: \(s)")
}
}
} icon: {
switch body.bodyType {
case "Moon" : Image(systemName: "moon")
case "Planet" : Image(systemName: "globe")
case "Star" : Image(systemName: "star")
default:
let c0 = body.bodyType.first?.lowercased()
Image(systemName: "\(c0 ?? "?").circle")
}
}
}
.searchable(text: $searchString)
.refreshable { // pull-to-refresh
Task.detached { try await fetchAndUpdateCache() }
}
.task(id: searchString) {
// On startup we first load all the existing bodies from
// the cache database, which is superfast.
// It will be empty on the first run.
await loadFromCache()
}
.task {
// We run this detached, because decoding the network data
// using Codable isn't very fast and would block the UI.
Task.detached(priority: .background) {
// Then we re-fetch the Data from the network and
// update our cache with the fresh data.
try await fetchAndUpdateCache()
}
}
#if os(macOS)
.padding(.top, 1) // don't ask
#endif
}
private func fetchAndUpdateCache() async throws {
let url = URL(string: "https://api.le-systeme-solaire.net/rest/bodies")!
// Fetch raw data from endpoint.
let ( data, _ ) = try await URLSession.shared.data(from: url)
// Decode the JSON. The actual JSON struct doesn't contain the
// bodies at the root, so we need a little helper struct:
struct Result: Decodable {
let bodies : [ SolarBody ]
}
let result = try JSONDecoder().decode(Result.self, from: data)
// Now we need to merge the results we have in the database with the
// once we got from the web.
// We don't actually compare the values, but just overwrite existing
// records if they are still the same.
// Here we only fetch the IDs of the table using a `select`.
let oldIDs = Set(try await database.select(from: \.solarBodies, \.id))
// Calculate the changes on background thread.
let idsToDelete = oldIDs.subtracting(result.bodies.map(\.id))
let recordsToUpdate = result.bodies.filter { oldIDs.contains($0.id) }
let recordsToInsert = result.bodies.filter { !oldIDs.contains($0.id) }
print("Changes: #\(idsToDelete.count) deleted,",
"updating #\(recordsToUpdate.count),", // likely the same ...
"#\(recordsToInsert.count) new.")
// Apply changes in a transaction. Either succeeds completely or not.
try await database.transaction { tx in
// Using a transaction is also a little more efficient, because the
// same database thread and handle is reused.
// Note that a transaction _body_ is NOT asynchronous to avoid
// accidential deadlocks. The full transaction can be async though.
try tx.delete(from: \.solarBodies, where: { $0.id.in(idsToDelete) })
try tx.update(recordsToUpdate)
try tx.insert(recordsToInsert)
}
// Our sync worked, we should now have the records in the database.
await loadFromCache()
}
}
extension URLSession {
func data(from url: URL) async throws -> ( Data, URLResponse ) {
// This solely exist to silence a concurrency warning triggered by
// the default `delegate` parameter of `data(from:delegate:)`.
try await data(from: url, delegate: nil)
}
}