Skip to content

Commit d3b569d

Browse files
committed
Add Yelp integration to demo app
1 parent 2304db6 commit d3b569d

File tree

8 files changed

+240
-45
lines changed

8 files changed

+240
-45
lines changed

Demo/Demo.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
A9B2328D29D346F700B85203 /* TheMovieDbScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B2328C29D346F700B85203 /* TheMovieDbScreen.swift */; };
1212
A9C008952E8A831600000CB6 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = A9C008942E8A831600000CB6 /* AppIcon.icon */; };
1313
A9C008992E8A859C00000CB6 /* View+ApiKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C008982E8A859C00000CB6 /* View+ApiKeys.swift */; };
14+
A9C0089B2E8A955C00000CB6 /* YelpScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C0089A2E8A955C00000CB6 /* YelpScreen.swift */; };
1415
A9C026AB29D2CEFA00371AE0 /* DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C026AA29D2CEFA00371AE0 /* DemoApp.swift */; };
1516
A9C026AD29D2CEFA00371AE0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C026AC29D2CEFA00371AE0 /* ContentView.swift */; };
1617
A9C026AF29D2CEFB00371AE0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A9C026AE29D2CEFB00371AE0 /* Assets.xcassets */; };
@@ -26,6 +27,7 @@
2627
A9B2328C29D346F700B85203 /* TheMovieDbScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TheMovieDbScreen.swift; sourceTree = "<group>"; };
2728
A9C008942E8A831600000CB6 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = "<group>"; };
2829
A9C008982E8A859C00000CB6 /* View+ApiKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ApiKeys.swift"; sourceTree = "<group>"; };
30+
A9C0089A2E8A955C00000CB6 /* YelpScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YelpScreen.swift; sourceTree = "<group>"; };
2931
A9C026A729D2CEFA00371AE0 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
3032
A9C026AA29D2CEFA00371AE0 /* DemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoApp.swift; sourceTree = "<group>"; };
3133
A9C026AC29D2CEFA00371AE0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -53,6 +55,7 @@
5355
isa = PBXGroup;
5456
children = (
5557
A9B2328C29D346F700B85203 /* TheMovieDbScreen.swift */,
58+
A9C0089A2E8A955C00000CB6 /* YelpScreen.swift */,
5659
);
5760
path = Screens;
5861
sourceTree = "<group>";
@@ -194,6 +197,7 @@
194197
isa = PBXSourcesBuildPhase;
195198
buildActionMask = 2147483647;
196199
files = (
200+
A9C0089B2E8A955C00000CB6 /* YelpScreen.swift in Sources */,
197201
A9C026AD29D2CEFA00371AE0 /* ContentView.swift in Sources */,
198202
A9B2328D29D346F700B85203 /* TheMovieDbScreen.swift in Sources */,
199203
A9C008992E8A859C00000CB6 /* View+ApiKeys.swift in Sources */,

Demo/Demo.xcodeproj/xcuserdata/danielsaidi.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,38 @@
33
uuid = "512808DE-AB88-454E-B5E2-A04823D3EA9B"
44
type = "1"
55
version = "2.0">
6+
<Breakpoints>
7+
<BreakpointProxy
8+
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
9+
<BreakpointContent
10+
uuid = "C8B2A896-E3E7-44F7-AB00-4517E92A1498"
11+
shouldBeEnabled = "Yes"
12+
ignoreCount = "0"
13+
continueAfterRunningActions = "No"
14+
filePath = "Demo/Screens/YelpScreen.swift"
15+
startingColumnNumber = "9223372036854775807"
16+
endingColumnNumber = "9223372036854775807"
17+
startingLineNumber = "103"
18+
endingLineNumber = "103"
19+
landmarkName = "fetchDefaultItems()"
20+
landmarkType = "7">
21+
</BreakpointContent>
22+
</BreakpointProxy>
23+
<BreakpointProxy
24+
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
25+
<BreakpointContent
26+
uuid = "CB77FC2D-B610-495A-B233-A62B145664E2"
27+
shouldBeEnabled = "Yes"
28+
ignoreCount = "0"
29+
continueAfterRunningActions = "No"
30+
filePath = "Demo/Screens/TheMovieDbScreen.swift"
31+
startingColumnNumber = "9223372036854775807"
32+
endingColumnNumber = "9223372036854775807"
33+
startingLineNumber = "90"
34+
endingLineNumber = "90"
35+
landmarkName = "fetchDefaultItems()"
36+
landmarkType = "7">
37+
</BreakpointContent>
38+
</BreakpointProxy>
39+
</Breakpoints>
640
</Bucket>

Demo/Demo/ContentView.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ struct ContentView: View {
2323
apiKey: $movieDbApiKey,
2424
screen: .theMovieDb(apiKey: movieDbApiKey)
2525
)
26+
screenSection(
27+
title: "Yelp",
28+
icon: "fork.knife",
29+
apiKey: $yelpApiKey,
30+
screen: .yelp(apiKey: yelpApiKey)
31+
)
2632
}
2733
.navigationTitle("ApiKit")
2834
.navigationDestination(for: DemoScreen.self) { $0.body }

Demo/Demo/DemoScreen.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ import SwiftUI
1111
enum DemoScreen: Hashable, View {
1212

1313
case theMovieDb(apiKey: String)
14+
case yelp(apiKey: String)
1415
}
1516

1617
extension DemoScreen {
1718

19+
@ViewBuilder
1820
var body: some View {
1921
switch self {
2022
case .theMovieDb(let apiKey): TheMovieDbScreen(apiKey: apiKey)
23+
case .yelp(let apiKey): YelpScreen(apiKey: apiKey)
2124
}
2225
}
2326
}

Demo/Demo/Screens/TheMovieDbScreen.swift

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,39 +22,42 @@ struct TheMovieDbScreen: View {
2222
@StateObject
2323
private var model = ViewModel()
2424

25-
typealias Movie = TheMovieDb.Movie
26-
typealias MovieResult = TheMovieDb.MoviesPaginationResult
25+
typealias Item = TheMovieDb.Movie
26+
typealias ItemResult = TheMovieDb.MoviesPaginationResult
2727

2828
class ViewModel: ObservableObject {
2929

30-
@Published var discoverMovies = [Movie]()
31-
@Published var searchMovies = [Movie]()
30+
@Published var defaultItems = [Item]()
31+
@Published var searchItems = [Item]()
3232
@Published var searchQuery = ""
3333
}
3434

3535
var body: some View {
3636
ScrollView(.vertical) {
3737
LazyVGrid(columns: gridColumns) {
38-
ForEach(movies) { movie in
39-
gridItem(for: movie)
38+
ForEach(items) {
39+
gridItem(for: $0)
4040
}
41-
}.padding()
41+
}
42+
.padding()
4243
}
43-
.task { fetchDiscoverData() }
44+
.task { fetchDefaultItems() }
4445
.searchable(text: $model.searchQuery)
45-
.onReceive(model.$searchQuery
46-
.throttle(for: 1, scheduler: RunLoop.main, latest: true), perform: search
47-
)
46+
.onReceive(model.$searchQuery.throttle(
47+
for: 1,
48+
scheduler: RunLoop.main,
49+
latest: true
50+
), perform: search)
4851
.navigationTitle("The Movie DB")
4952
}
5053
}
5154

5255
extension TheMovieDbScreen {
5356

54-
func gridItem(for movie: Movie) -> some View {
57+
func gridItem(for item: Item) -> some View {
5558
VStack {
5659
AsyncImage(
57-
url: movie.posterUrl(width: 300),
60+
url: item.posterUrl(width: 300),
5861
content: { image in
5962
image.resizable()
6063
.cornerRadius(5)
@@ -63,35 +66,32 @@ extension TheMovieDbScreen {
6366
placeholder: {
6467
ProgressView()
6568
}
66-
).accessibilityLabel(movie.title)
69+
)
70+
.accessibilityLabel(item.title)
6771
}
6872
}
6973
}
7074

7175
extension TheMovieDbScreen {
7276

73-
var movies: [Movie] {
74-
model.searchMovies.isEmpty ? model.discoverMovies : model.searchMovies
77+
var items: [Item] {
78+
model.searchItems.isEmpty ? model.defaultItems : model.searchItems
7579
}
7680

77-
func fetchDiscoverData() {
81+
func fetchDefaultItems() {
7882
Task {
7983
do {
80-
let result = try await fetchDiscoverData()
81-
updateDiscoverResult(with: result)
84+
let result: ItemResult = try await session.request(
85+
at: TheMovieDb.Route.discoverMovies(page: 1),
86+
in: environment
87+
)
88+
updateDefaultItems(with: result)
8289
} catch {
8390
print(error)
8491
}
8592
}
8693
}
8794

88-
func fetchDiscoverData() async throws -> MovieResult {
89-
try await session.request<MovieResult>(
90-
at: TheMovieDb.Route.discoverMovies(page: 1),
91-
in: environment
92-
)
93-
}
94-
9595
func search(with query: String) {
9696
Task {
9797
do {
@@ -103,7 +103,7 @@ extension TheMovieDbScreen {
103103
}
104104
}
105105

106-
func search(with query: String) async throws -> MovieResult {
106+
func search(with query: String) async throws -> ItemResult {
107107
try await session.request(
108108
at: TheMovieDb.Route.searchMovies(query: query, page: 1),
109109
in: environment
@@ -114,19 +114,28 @@ extension TheMovieDbScreen {
114114
@MainActor
115115
extension TheMovieDbScreen {
116116

117-
func updateDiscoverResult(with result: MovieResult) {
118-
model.discoverMovies = result.results
117+
func updateDefaultItems(with result: ItemResult) {
118+
model.defaultItems = result.results
119119
}
120120

121-
func updateSearchResult(with result: MovieResult) {
122-
model.searchMovies = result.results
121+
func updateSearchResult(with result: ItemResult) {
122+
model.searchItems = result.results
123123
}
124124
}
125125

126126
#Preview {
127-
128-
TheMovieDbScreen(apiKey: "c9237ef2809ed01e64d7b37b0f951c7b")
129-
#if os(macOS)
130-
.frame(minWidth: 500)
131-
#endif
127+
128+
struct Preview: View {
129+
130+
@AppStorage(Self.movieDbApiKey) var apiKey = ""
131+
132+
var body: some View {
133+
TheMovieDbScreen(apiKey: apiKey)
134+
#if os(macOS)
135+
.frame(minWidth: 500)
136+
#endif
137+
}
138+
}
139+
140+
return Preview()
132141
}

Demo/Demo/Screens/YelpScreen.swift

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//
2+
// YelpScreen.swift
3+
// Demo
4+
//
5+
// Created by Daniel Saidi on 2025-09-29.
6+
// Copyright © 2025 Daniel Saidi. All rights reserved.
7+
//
8+
9+
import ApiKit
10+
import SwiftUI
11+
12+
struct YelpScreen: View {
13+
14+
init(apiKey: String) {
15+
self.environment = .v3(apiToken: apiKey)
16+
}
17+
18+
let session = URLSession.shared
19+
let environment: Yelp.Environment
20+
let gridColumns = [GridItem(.adaptive(minimum: 100), alignment: .top)]
21+
22+
@StateObject
23+
private var model = ViewModel()
24+
25+
typealias Item = Yelp.Restaurant
26+
typealias ItemResult = Yelp.RestaurantSearchResult
27+
28+
class ViewModel: ObservableObject {
29+
30+
@Published var defaultItems = [Item]()
31+
@Published var searchItems = [Item]()
32+
@Published var searchQuery = ""
33+
}
34+
35+
var body: some View {
36+
ScrollView(.vertical) {
37+
LazyVGrid(columns: gridColumns) {
38+
ForEach(items) {
39+
gridItem(for: $0)
40+
}
41+
}.padding()
42+
}
43+
.task { fetchDefaultItems() }
44+
// .searchable(text: $model.searchQuery)
45+
// .onReceive(model.$searchQuery.throttle(
46+
// for: 1,
47+
// scheduler: RunLoop.main,
48+
// latest: true
49+
// ), perform: search)
50+
.navigationTitle("Yelp")
51+
}
52+
}
53+
54+
extension YelpScreen {
55+
56+
func gridItem(for item: Item) -> some View {
57+
VStack {
58+
AsyncImage(
59+
url: item.imageUrl?.url,
60+
content: { image in
61+
image.resizable()
62+
.cornerRadius(5)
63+
.aspectRatio(contentMode: .fit)
64+
},
65+
placeholder: {
66+
ProgressView()
67+
}
68+
)
69+
.accessibilityLabel(item.name ?? item.id)
70+
}
71+
}
72+
}
73+
74+
private extension String {
75+
76+
var url: URL? {
77+
.init(string: self)
78+
}
79+
}
80+
81+
extension YelpScreen {
82+
83+
var items: [Item] {
84+
model.searchItems.isEmpty ? model.defaultItems : model.searchItems
85+
}
86+
87+
func fetchDefaultItems() {
88+
Task {
89+
do {
90+
let result: ItemResult = try await session.request(
91+
at: Yelp.Route.search(
92+
params: .init(
93+
skip: 0,
94+
take: 25,
95+
radius: 5_000,
96+
coordinate: (lat: 59.3327, long: 18.0645)
97+
)
98+
),
99+
in: environment
100+
)
101+
updateDefaultItems(with: result)
102+
} catch {
103+
print(error)
104+
}
105+
}
106+
}
107+
}
108+
109+
@MainActor
110+
extension YelpScreen {
111+
112+
func updateDefaultItems(with result: ItemResult) {
113+
model.defaultItems = result.businesses
114+
}
115+
}
116+
117+
#Preview {
118+
119+
struct Preview: View {
120+
121+
@AppStorage(Self.yelpApiKey) var apiKey = ""
122+
123+
var body: some View {
124+
YelpScreen(apiKey: apiKey)
125+
#if os(macOS)
126+
.frame(minWidth: 500)
127+
#endif
128+
}
129+
}
130+
131+
return Preview()
132+
}

RELEASE_NOTES.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ Until then, breaking changes can happen in any version, and deprecated features
66

77

88

9+
## 1.1.0
10+
11+
### 💡 Adjustments
12+
13+
* The package now uses Swift 6.1.
14+
* The demo app now targets iOS 26.
15+
16+
17+
918
## 1.0.3
1019

1120
### 💡 Adjustments

0 commit comments

Comments
 (0)