Skip to content

Commit 4676495

Browse files
authored
Merge pull request #4 from ra1028/useAsync
feat: Add useAsync and useAsyncPerform hooks using Concurrency
2 parents a97a17a + c406964 commit 4676495

22 files changed

+770
-142
lines changed

Examples/BasicUsage/APIRequestPage.swift

+19-11
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@ struct Post: Codable {
77
let body: String
88
}
99

10-
func useFetchPosts() -> (phase: AsyncPhase<[Post], Error>, fetch: () -> Void) {
10+
func useFetchPosts() -> (phase: AsyncPhase<[Post], Error>, fetch: () async -> Void) {
1111
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
12-
let (phase, subscribe) = usePublisherSubscribe {
13-
URLSession.shared.dataTaskPublisher(for: url)
14-
.map(\.data)
15-
.decode(type: [Post].self, decoder: JSONDecoder())
16-
.receive(on: DispatchQueue.main)
12+
let (phase, fetch) = useAsyncPerform { () throws -> [Post] in
13+
let decoder = JSONDecoder()
14+
let (data, _) = try await URLSession.shared.data(from: url)
15+
return try decoder.decode([Post].self, from: data)
1716
}
1817

19-
return (phase: phase, fetch: subscribe)
18+
return (phase: phase, fetch: fetch)
2019
}
2120

2221
struct APIRequestPage: HookView {
@@ -44,7 +43,9 @@ struct APIRequestPage: HookView {
4443
}
4544
.navigationTitle("API Request")
4645
.background(Color(.systemBackground).ignoresSafeArea())
47-
.onAppear(perform: fetch)
46+
.task {
47+
await fetch()
48+
}
4849
}
4950

5051
func postRows(_ posts: [Post]) -> some View {
@@ -58,11 +59,18 @@ struct APIRequestPage: HookView {
5859
}
5960
}
6061

61-
func errorRow(_ error: Error, retry: @escaping () -> Void) -> some View {
62+
func errorRow(_ error: Error, retry: @escaping () async -> Void) -> some View {
6263
VStack {
63-
Text("Error: \(error.localizedDescription)").fixedSize(horizontal: false, vertical: true)
64+
Text("Error: \(error.localizedDescription)")
65+
.fixedSize(horizontal: false, vertical: true)
66+
6467
Divider()
65-
Button("Refresh", action: retry)
68+
69+
Button("Refresh") {
70+
Task {
71+
await retry()
72+
}
73+
}
6674
}
6775
}
6876
}

Examples/BasicUsage/IndexPage.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ struct IndexPage: View {
55
NavigationView {
66
Form {
77
NavigationLink(
8-
"Hook List",
9-
destination: HookListPage()
8+
"Showcase",
9+
destination: ShowcasePage()
1010
)
1111

1212
NavigationLink(

Examples/BasicUsage/HookListPage.swift Examples/BasicUsage/ShowcasePage.swift

+92-14
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,110 @@ import SwiftUI
44

55
typealias ColorSchemeContext = Context<Binding<ColorScheme>>
66

7-
struct HookListPage: HookView {
7+
struct ShowcasePage: HookView {
88
var hookBody: some View {
99
let colorScheme = useState(useEnvironment(\.colorScheme))
1010

1111
ColorSchemeContext.Provider(value: colorScheme) {
1212
ScrollView {
1313
VStack {
14-
useStateRow
15-
useReducerRow
16-
useEffectRow
17-
useLayoutEffectRow
18-
useMemoRow
19-
useRefRow
20-
useEnvironmentRow
21-
usePublisherRow
22-
usePublisherSubscribeRow
23-
useContextRow
14+
Group {
15+
useStateRow
16+
useReducerRow
17+
useEffectRow
18+
useLayoutEffectRow
19+
useMemoRow
20+
useRefRow
21+
}
22+
23+
Group {
24+
useAsyncRow
25+
useAsyncPerformRow
26+
usePublisherRow
27+
usePublisherSubscribeRow
28+
useEnvironmentRow
29+
useContextRow
30+
}
2431
}
2532
.padding(.vertical, 16)
2633
}
27-
.navigationTitle("Hook List")
34+
.navigationTitle("Showcase")
2835
.background(Color(.systemBackground).ignoresSafeArea())
2936
.colorScheme(colorScheme.wrappedValue)
3037
}
3138
}
3239

40+
var useAsyncRow: some View {
41+
let phase = useAsync(.once) { () -> UIImage? in
42+
let url = URL(string: "https://source.unsplash.com/random")!
43+
let (data, _) = try await URLSession.shared.data(from: url)
44+
return UIImage(data: data)
45+
}
46+
47+
return Row("useAsync") {
48+
Group {
49+
switch phase {
50+
case .pending, .running:
51+
ProgressView()
52+
53+
case .failure(let error):
54+
Text(error.localizedDescription)
55+
56+
case .success(let image):
57+
image.map { uiImage in
58+
Image(uiImage: uiImage)
59+
.resizable()
60+
.scaledToFit()
61+
}
62+
}
63+
}
64+
.frame(width: 100, height: 100)
65+
.clipped()
66+
}
67+
}
68+
69+
var useAsyncPerformRow: some View {
70+
let (phase, fetch) = useAsyncPerform { () -> UIImage? in
71+
let url = URL(string: "https://source.unsplash.com/random")!
72+
let (data, _) = try await URLSession.shared.data(from: url)
73+
return UIImage(data: data)
74+
}
75+
76+
return Row("useAsyncPerform") {
77+
HStack {
78+
Group {
79+
switch phase {
80+
case .pending, .running:
81+
ProgressView()
82+
83+
case .failure(let error):
84+
Text(error.localizedDescription)
85+
86+
case .success(let image):
87+
image.map { uiImage in
88+
Image(uiImage: uiImage)
89+
.resizable()
90+
.scaledToFit()
91+
}
92+
}
93+
}
94+
.frame(width: 100, height: 100)
95+
.clipped()
96+
97+
Spacer()
98+
99+
Button("Random") {
100+
Task {
101+
await fetch()
102+
}
103+
}
104+
}
105+
.task {
106+
await fetch()
107+
}
108+
}
109+
}
110+
33111
var useStateRow: some View {
34112
let count = useState(0)
35113

@@ -204,9 +282,9 @@ struct HookListPage: HookView {
204282
}
205283
}
206284

207-
struct HookListPage_Previews: PreviewProvider {
285+
struct ShowcasePage_Previews: PreviewProvider {
208286
static var previews: some View {
209-
HookListPage()
287+
ShowcasePage()
210288
}
211289
}
212290

Examples/Examples.xcodeproj/project.pbxproj

+12-8
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
/* Begin PBXBuildFile section */
1010
003A77A2E11520F9038CD7E0 /* MovieDetailPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4D224DD8896BC0368816053 /* MovieDetailPage.swift */; };
1111
0413865A55A886125F10E54C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6874F17F70C8E5EDF84F5 /* AppDelegate.swift */; };
12-
1EBF7320D4ABE8B5C4806D7B /* HookListPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7504F9563C0F894C2927F8F /* HookListPage.swift */; };
12+
18CA358FABAC2BC661161084 /* ShowcasePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5AB757E97B27DC9D7A96E5 /* ShowcasePage.swift */; };
1313
22B1EFFA1478D324171D5E75 /* MovieDBServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895CD000DCB5854C20C39119 /* MovieDBServiceMock.swift */; };
1414
2563030F0667D6982675CC0D /* PagedResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC5A16C0EDE1832418D3B44 /* PagedResponse.swift */; };
1515
2A64A442C817832C7117EB71 /* MovieDBService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EF0AE0F7D802B4683AD207 /* MovieDBService.swift */; };
1616
2C4C4451732A07DAEFC77B2B /* NetworkImageSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE6CAB1515CCDEDECEC3A1E /* NetworkImageSize.swift */; };
1717
36B24AEDD5199A51EE335F89 /* IndexPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D069A011D27489BADCA6B8C6 /* IndexPage.swift */; };
1818
3E9E4DA44DA2932FF3808619 /* Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1117F627EF5B370FD8AEA502 /* Dependency.swift */; };
1919
43FD37F20C5A05C1144D0B71 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD5F67CEEAFE74811B93864 /* SceneDelegate.swift */; };
20+
4A77333D66DAEEE5D4A47B17 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A2DF2C9F23B05D775347070 /* Utilities.swift */; };
2021
528D9EB2F42500C40019B919 /* UseTopRatedMoviesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B3D3D179C2DD7456748DBF /* UseTopRatedMoviesViewModel.swift */; };
2122
5A4EADB7439DD4B566DBC1FF /* Movie.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B5863034D96951456EB762 /* Movie.swift */; };
2223
5B2A5E7A0AA463A0BF685F71 /* UseTopRatedMoviesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20A38A9D88C5F0D2B9CCAC13 /* UseTopRatedMoviesViewModelTests.swift */; };
@@ -56,6 +57,7 @@
5657
1117F627EF5B370FD8AEA502 /* Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependency.swift; sourceTree = "<group>"; };
5758
1AABBBF7596FB58F4051F423 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
5859
20A38A9D88C5F0D2B9CCAC13 /* UseTopRatedMoviesViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseTopRatedMoviesViewModelTests.swift; sourceTree = "<group>"; };
60+
2A2DF2C9F23B05D775347070 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
5961
2AE6CAB1515CCDEDECEC3A1E /* NetworkImageSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkImageSize.swift; sourceTree = "<group>"; };
6062
2D5F81DFBD08056D0FD7F5BD /* TheMovieDB-MVVM-Tests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = "TheMovieDB-MVVM-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
6163
37A6874F17F70C8E5EDF84F5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -71,11 +73,11 @@
7173
86069121E82E72BAE07609D9 /* BasicUsage.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = BasicUsage.app; sourceTree = BUILT_PRODUCTS_DIR; };
7274
895CD000DCB5854C20C39119 /* MovieDBServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDBServiceMock.swift; sourceTree = "<group>"; };
7375
8AC5A16C0EDE1832418D3B44 /* PagedResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedResponse.swift; sourceTree = "<group>"; };
76+
8E5AB757E97B27DC9D7A96E5 /* ShowcasePage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowcasePage.swift; sourceTree = "<group>"; };
7477
94D8757F265C5526C297EFDB /* CounterPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterPage.swift; sourceTree = "<group>"; };
7578
959951B8FA9CB35BA897D008 /* TheMovieDB-MVVM.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "TheMovieDB-MVVM.app"; sourceTree = BUILT_PRODUCTS_DIR; };
7679
A01793F321BFB68D18E27818 /* TodoPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodoPage.swift; sourceTree = "<group>"; };
7780
A233EB4C5891C4899B737ACF /* TopRatedMoviesPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesPage.swift; sourceTree = "<group>"; };
78-
A7504F9563C0F894C2927F8F /* HookListPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HookListPage.swift; sourceTree = "<group>"; };
7981
A80807CB4E7902470EE2CF4E /* Todo.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Todo.app; sourceTree = BUILT_PRODUCTS_DIR; };
8082
B5CDB321485B19447B2E5863 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
8183
C56139636BCE9E6B7A5A1CF0 /* APIRequestPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRequestPage.swift; sourceTree = "<group>"; };
@@ -144,9 +146,9 @@
144146
C56139636BCE9E6B7A5A1CF0 /* APIRequestPage.swift */,
145147
5EE16AC7E6A5C62C32868DDE /* App.swift */,
146148
94D8757F265C5526C297EFDB /* CounterPage.swift */,
147-
A7504F9563C0F894C2927F8F /* HookListPage.swift */,
148149
D069A011D27489BADCA6B8C6 /* IndexPage.swift */,
149150
C9F47469489E2AC56001C720 /* Info.plist */,
151+
8E5AB757E97B27DC9D7A96E5 /* ShowcasePage.swift */,
150152
);
151153
path = BasicUsage;
152154
sourceTree = "<group>";
@@ -206,6 +208,7 @@
206208
895CD000DCB5854C20C39119 /* MovieDBServiceMock.swift */,
207209
5A76A9D3B40580BCF4FEE209 /* UseMovieImageTests.swift */,
208210
20A38A9D88C5F0D2B9CCAC13 /* UseTopRatedMoviesViewModelTests.swift */,
211+
2A2DF2C9F23B05D775347070 /* Utilities.swift */,
209212
);
210213
path = "TheMovieDB-MVVM-Tests";
211214
sourceTree = "<group>";
@@ -387,6 +390,7 @@
387390
22B1EFFA1478D324171D5E75 /* MovieDBServiceMock.swift in Sources */,
388391
E404ADC8590A7823470D6488 /* UseMovieImageTests.swift in Sources */,
389392
5B2A5E7A0AA463A0BF685F71 /* UseTopRatedMoviesViewModelTests.swift in Sources */,
393+
4A77333D66DAEEE5D4A47B17 /* Utilities.swift in Sources */,
390394
);
391395
runOnlyForDeploymentPostprocessing = 0;
392396
};
@@ -397,8 +401,8 @@
397401
8E4174CA12184B89C04932C3 /* APIRequestPage.swift in Sources */,
398402
6691B8C3D71144F41A10F4A6 /* App.swift in Sources */,
399403
C078F002FB29736B800B3774 /* CounterPage.swift in Sources */,
400-
1EBF7320D4ABE8B5C4806D7B /* HookListPage.swift in Sources */,
401404
36B24AEDD5199A51EE335F89 /* IndexPage.swift in Sources */,
405+
18CA358FABAC2BC661161084 /* ShowcasePage.swift in Sources */,
402406
);
403407
runOnlyForDeploymentPostprocessing = 0;
404408
};
@@ -473,7 +477,7 @@
473477
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
474478
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
475479
INFOPLIST_FILE = "TheMovieDB-MVVM/Info.plist";
476-
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
480+
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
477481
LD_RUNPATH_SEARCH_PATHS = (
478482
"$(inherited)",
479483
"@executable_path/Frameworks",
@@ -618,7 +622,7 @@
618622
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
619623
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
620624
INFOPLIST_FILE = "TheMovieDB-MVVM/Info.plist";
621-
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
625+
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
622626
LD_RUNPATH_SEARCH_PATHS = (
623627
"$(inherited)",
624628
"@executable_path/Frameworks",
@@ -642,7 +646,7 @@
642646
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
643647
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
644648
INFOPLIST_FILE = BasicUsage/Info.plist;
645-
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
649+
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
646650
LD_RUNPATH_SEARCH_PATHS = (
647651
"$(inherited)",
648652
"@executable_path/Frameworks",
@@ -666,7 +670,7 @@
666670
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
667671
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
668672
INFOPLIST_FILE = BasicUsage/Info.plist;
669-
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
673+
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
670674
LD_RUNPATH_SEARCH_PATHS = (
671675
"$(inherited)",
672676
"@executable_path/Frameworks",

Examples/TheMovieDB-MVVM-Tests/MovieDBServiceMock.swift

+10-14
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,19 @@ import UIKit
44
@testable import TheMovieDB_MVVM
55

66
final class MovieDBServiceMock: MovieDBServiceProtocol {
7-
let imageSubject = PassthroughSubject<UIImage?, URLError>()
8-
let moviesSubject = PassthroughSubject<[Movie], URLError>()
7+
var imageResult: Result<UIImage?, URLError>?
8+
var moviesResult: Result<[Movie], URLError>?
99
var totalPages = 100
1010

11-
func getImage(path: String?, size: NetworkImageSize) -> AnyPublisher<UIImage?, URLError> {
12-
imageSubject.eraseToAnyPublisher()
11+
func getImage(path: String?, size: NetworkImageSize) async throws -> UIImage? {
12+
try imageResult?.get()
1313
}
1414

15-
func getTopRated(page: Int) -> AnyPublisher<PagedResponse<Movie>, URLError> {
16-
moviesSubject
17-
.map { [totalPages] movies in
18-
PagedResponse(
19-
page: page,
20-
totalPages: totalPages,
21-
results: movies
22-
)
23-
}
24-
.eraseToAnyPublisher()
15+
func getTopRated(page: Int) async throws -> PagedResponse<Movie> {
16+
try PagedResponse(
17+
page: page,
18+
totalPages: totalPages,
19+
results: moviesResult?.get() ?? []
20+
)
2521
}
2622
}

0 commit comments

Comments
 (0)