Skip to content

Commit 57ecfb8

Browse files
Async throwing support (#40)
* Add `Shared.loadError` * wip * wip * wip * wip * Tests * Add issue reporting to load/save errors * Revert "Add issue reporting to load/save errors" This reverts commit 920371a. * wip * wip * wip * test * wip * breaking API changes for throwing save/load. * async load * fix * throwing save * wip * wip * wip * wip * wip * wip * wip * wip * wip * continuations * wip * wip * wip * wip * wip * wip * wip * wip * wip * introduced LoadContext to clarifying when state is loaded * basics of LoadContinuation * wip * workaround swift bug * save context * wip * wip * wip * wip * wip * wip * renames * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Moved load to be async * Made save async * save cleanup * async stream subscription * Revert "async stream subscription" This reverts commit c4bb1f5. * clean up * fix * Revert "fix" This reverts commit d9a428f. * Revert "clean up" This reverts commit 6fb1137. * Reapply "async stream subscription" This reverts commit cc9422c. * Revert "async stream subscription" This reverts commit c4bb1f5. * Revert "save cleanup" This reverts commit efd2260. * Revert "Made save async" This reverts commit 2f84316. * Revert "Moved load to be async" This reverts commit 2053bde. * wip * wip * wip * wip * wip * wip * Stub * wip * wip * wip * wip * wip * wip * wip * wip * cancel inflight load in FactKey * fix loading glitch * some tests * wip * wip * wip * wip * wip * wip * wip * wip * format * wip * wip * wip * Resume continuations that are deallocated and not run. * wip * wip * wip * wip * wip --------- Co-authored-by: Brandon Williams <[email protected]>
1 parent dbcd9d4 commit 57ecfb8

File tree

71 files changed

+2706
-922
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+2706
-922
lines changed

Examples/APIClientDemo/App.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import SwiftUI
2+
3+
@main
4+
struct APIClientDemoApp: App {
5+
var body: some Scene {
6+
WindowGroup {
7+
NavigationStack {
8+
ContentView()
9+
}
10+
}
11+
}
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"colors" : [
3+
{
4+
"idiom" : "universal"
5+
}
6+
],
7+
"info" : {
8+
"author" : "xcode",
9+
"version" : 1
10+
}
11+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "universal",
5+
"platform" : "ios",
6+
"size" : "1024x1024"
7+
},
8+
{
9+
"appearances" : [
10+
{
11+
"appearance" : "luminosity",
12+
"value" : "dark"
13+
}
14+
],
15+
"idiom" : "universal",
16+
"platform" : "ios",
17+
"size" : "1024x1024"
18+
},
19+
{
20+
"appearances" : [
21+
{
22+
"appearance" : "luminosity",
23+
"value" : "tinted"
24+
}
25+
],
26+
"idiom" : "universal",
27+
"platform" : "ios",
28+
"size" : "1024x1024"
29+
}
30+
],
31+
"info" : {
32+
"author" : "xcode",
33+
"version" : 1
34+
}
35+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import IssueReporting
2+
import Sharing
3+
import SwiftUI
4+
5+
private let readMe = """
6+
This app demonstrates how one can use data from an external API service to power the state held \
7+
in a '@SharedReader' property. The 'fact' property represents a fact loaded from a server about \
8+
a number. The way you fetch a new fact for a number is by assigning '$fact' with a new \
9+
'@SharedReader' instance that provides the number.
10+
"""
11+
12+
struct ContentView: View {
13+
@Shared(.appStorage("count")) var count = 0
14+
@SharedReader(.fact(nil)) var fact
15+
@State var isAboutPresented = false
16+
17+
var body: some View {
18+
Form {
19+
Section {
20+
Text("\(count)")
21+
Button("Decrement") {
22+
$count.withLock { $0 -= 1 }
23+
}
24+
Button("Increment") {
25+
$count.withLock { $0 += 1 }
26+
}
27+
}
28+
}
29+
.toolbar {
30+
Button("About") { isAboutPresented = true }
31+
}
32+
.sheet(isPresented: $isAboutPresented) {
33+
Form {
34+
Text(readMe)
35+
}
36+
}
37+
.task(id: count) {
38+
do {
39+
$fact = try await SharedReader(require: .fact(nil))
40+
} catch {
41+
reportIssue(error)
42+
}
43+
}
44+
.refreshable {
45+
do {
46+
$fact = try await SharedReader(require: .fact(count))
47+
} catch {
48+
reportIssue(error)
49+
}
50+
}
51+
VStack(spacing: 24) {
52+
if $fact.isLoading {
53+
ProgressView()
54+
} else if let loadError = $fact.loadError {
55+
Text(loadError.localizedDescription)
56+
} else if let fact {
57+
Text(fact)
58+
}
59+
Button("Get Fact") {
60+
$fact = SharedReader(.fact(count))
61+
}
62+
}
63+
.padding()
64+
}
65+
}
66+
67+
#Preview {
68+
NavigationStack {
69+
ContentView()
70+
}
71+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Foundation
2+
import Sharing
3+
import Synchronization
4+
5+
extension SharedReaderKey where Self == FactKey {
6+
static func fact(_ number: Int?) -> Self {
7+
Self(number: number)
8+
}
9+
}
10+
11+
// NB: This 'SharedReaderKey` conformance is designed to allow one to hold onto a 'fact: String?'
12+
// in their featues, while secretly it is powered by a network request to fetch the fact.
13+
// Conformances to 'SharedReaderKey' must be Sendable, and so this is why the 'loadTask'
14+
// variable is held in a mutex.
15+
final class FactKey: SharedReaderKey {
16+
let id = UUID()
17+
let number: Int?
18+
let loadTask = Mutex<Task<Void, Never>?>(nil)
19+
20+
init(number: Int?) {
21+
self.number = number
22+
}
23+
24+
func load(context _: LoadContext<String?>, continuation: LoadContinuation<String?>) {
25+
guard let number else {
26+
continuation.resume(returning: nil)
27+
return
28+
}
29+
loadTask.withLock { task in
30+
task?.cancel()
31+
task = Task {
32+
do {
33+
// NB: This dependence on 'URLSession.shared' should be hidden behind a dependency
34+
// so that features that use this shared state are still testable.
35+
let (data, _) = try await URLSession.shared.data(
36+
from: URL(string: "http://numbersapi.com/\(number)")!
37+
)
38+
// NB: The Numbers API can be quite fast. Let's simulate a slower connection.
39+
try await Task.sleep(for: .seconds(1))
40+
continuation.resume(returning: String(decoding: data, as: UTF8.self))
41+
} catch {
42+
continuation.resume(throwing: error)
43+
}
44+
}
45+
}
46+
}
47+
48+
func subscribe(
49+
context _: LoadContext<String?>, subscriber _: SharedSubscriber<String?>
50+
) -> SharedSubscription {
51+
SharedSubscription {}
52+
}
53+
}

Examples/APIClientDemo/Info.plist

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSAppTransportSecurity</key>
6+
<dict>
7+
<key>NSAllowsArbitraryLoads</key>
8+
<true/>
9+
</dict>
10+
</dict>
11+
</plist>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import SwiftUI
2+
3+
struct APIClientView: SwiftUICaseStudy {
4+
let readMe = """
5+
See the dedicated API Client demo in the repo at ./Examples/APIClientDemo.
6+
"""
7+
let caseStudyTitle = "API Client"
8+
9+
var body: some View {
10+
Text("See the dedicated API Client demo in the repo at ./Examples/APIClientDemo.")
11+
}
12+
}

Examples/CaseStudies/CaseStudiesApp.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ private struct RootView: View {
3232
}
3333
CaseStudyGroupView("Custom SharedKey") {
3434
NotificationsView()
35+
APIClientView()
3536
GRDBView()
3637
FirebaseView()
3738
}

0 commit comments

Comments
 (0)