Skip to content

Commit b246ec8

Browse files
committed
Add "Creating a watchOS App"
1 parent 3c3f9b5 commit b246ec8

Some content is hidden

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

47 files changed

+1598
-44
lines changed

Framework-Integration/Assets.xcassets/AppIcon.appiconset/Contents.json

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,90 @@
104104
"idiom" : "ios-marketing",
105105
"filename" : "landmark_app_icon_1024x1024.png",
106106
"scale" : "1x"
107+
},
108+
{
109+
"size" : "24x24",
110+
"idiom" : "watch",
111+
"filename" : "landmark_app_icon_48x48.png",
112+
"scale" : "2x",
113+
"role" : "notificationCenter",
114+
"subtype" : "38mm"
115+
},
116+
{
117+
"size" : "27.5x27.5",
118+
"idiom" : "watch",
119+
"filename" : "landmark_app_icon_55x55.png",
120+
"scale" : "2x",
121+
"role" : "notificationCenter",
122+
"subtype" : "42mm"
123+
},
124+
{
125+
"size" : "29x29",
126+
"idiom" : "watch",
127+
"filename" : "landmark_app_icon_58x58-2.png",
128+
"role" : "companionSettings",
129+
"scale" : "2x"
130+
},
131+
{
132+
"size" : "29x29",
133+
"idiom" : "watch",
134+
"filename" : "landmark_app_icon_87x87-1.png",
135+
"role" : "companionSettings",
136+
"scale" : "3x"
137+
},
138+
{
139+
"size" : "40x40",
140+
"idiom" : "watch",
141+
"filename" : "landmark_app_icon_80x80-2.png",
142+
"scale" : "2x",
143+
"role" : "appLauncher",
144+
"subtype" : "38mm"
145+
},
146+
{
147+
"size" : "44x44",
148+
"idiom" : "watch",
149+
"filename" : "landmark_app_icon_88x88.png",
150+
"scale" : "2x",
151+
"role" : "appLauncher",
152+
"subtype" : "40mm"
153+
},
154+
{
155+
"size" : "50x50",
156+
"idiom" : "watch",
157+
"filename" : "landmark_app_icon_100x100.png",
158+
"scale" : "2x",
159+
"role" : "appLauncher",
160+
"subtype" : "44mm"
161+
},
162+
{
163+
"size" : "86x86",
164+
"idiom" : "watch",
165+
"filename" : "landmark_app_icon_172x172.png",
166+
"scale" : "2x",
167+
"role" : "quickLook",
168+
"subtype" : "38mm"
169+
},
170+
{
171+
"size" : "98x98",
172+
"idiom" : "watch",
173+
"filename" : "landmark_app_icon_196x196.png",
174+
"scale" : "2x",
175+
"role" : "quickLook",
176+
"subtype" : "42mm"
177+
},
178+
{
179+
"size" : "108x108",
180+
"idiom" : "watch",
181+
"filename" : "landmark_app_icon_216x216.png",
182+
"scale" : "2x",
183+
"role" : "quickLook",
184+
"subtype" : "44mm"
185+
},
186+
{
187+
"size" : "1024x1024",
188+
"idiom" : "watch-marketing",
189+
"filename" : "landmark_app_icon_1024x1024-1.png",
190+
"scale" : "1x"
107191
}
108192
],
109193
"info" : {
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading

Framework-Integration/Home/Home.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ struct CategoryHome: View {
4545
}
4646
.listRowInsets(EdgeInsets())
4747

48-
NavigationLink(destination: LandmarkList()) {
48+
NavigationLink(destination: LandmarkList { LandmarkDetail(landmark: $0) }) {
4949
Text("See All")
5050
}
5151
}

Framework-Integration/Landmark Views/LandmarkList.swift

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ A view showing a list of landmarks.
77

88
import SwiftUI
99

10-
struct LandmarkList: View {
10+
struct LandmarkList<DetailView: View>: View {
1111
@EnvironmentObject private var userData: UserData
1212

13+
let detailViewProducer: (Landmark) -> DetailView
14+
1315
var body: some View {
1416
List {
1517
Toggle(isOn: $userData.showFavoritesOnly) {
@@ -19,8 +21,7 @@ struct LandmarkList: View {
1921
ForEach(userData.landmarks) { landmark in
2022
if !self.userData.showFavoritesOnly || landmark.isFavorite {
2123
NavigationLink(
22-
destination: LandmarkDetail(landmark: landmark)
23-
) {
24+
destination: self.detailViewProducer(landmark).environmentObject(self.userData)) {
2425
LandmarkRow(landmark: landmark)
2526
}
2627
}
@@ -30,15 +31,15 @@ struct LandmarkList: View {
3031
}
3132
}
3233

33-
struct LandmarksList_Previews: PreviewProvider {
34+
#if os(watchOS)
35+
typealias PreviewDetailView = WatchLandmarkDetail
36+
#else
37+
typealias PreviewDetailView = LandmarkDetail
38+
#endif
39+
40+
struct LandmarkList_Previews: PreviewProvider {
3441
static var previews: some View {
35-
ForEach(["iPhone SE", "iPhone XS Max"], id: \.self) { deviceName in
36-
NavigationView {
37-
LandmarkList()
38-
.previewDevice(PreviewDevice(rawValue: deviceName))
39-
.previewDisplayName(deviceName)
40-
}
41-
}
42-
.environmentObject(UserData())
42+
LandmarkList { PreviewDetailView(landmark: $0) }
43+
.environmentObject(UserData())
4344
}
4445
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
See LICENSE folder for this sample’s licensing information.
3+
4+
Abstract:
5+
Helpers for loading images and data.
6+
*/
7+
8+
import Foundation
9+
import CoreLocation
10+
import SwiftUI
11+
import ImageIO
12+
13+
let landmarkData: [Landmark] = load("landmarkData.json")
14+
let features = landmarkData.filter { $0.isFeatured }
15+
let hikeData: [Hike] = load("hikeData.json")
16+
17+
func load<T: Decodable>(_ filename: String, as type: T.Type = T.self) -> T {
18+
let data: Data
19+
20+
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
21+
else {
22+
fatalError("Couldn't find \(filename) in main bundle.")
23+
}
24+
25+
do {
26+
data = try Data(contentsOf: file)
27+
} catch {
28+
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
29+
}
30+
31+
do {
32+
let decoder = JSONDecoder()
33+
return try decoder.decode(T.self, from: data)
34+
} catch {
35+
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
36+
}
37+
}
38+
39+
final class ImageStore {
40+
typealias _ImageDictionary = [String: CGImage]
41+
fileprivate var images: _ImageDictionary = [:]
42+
43+
fileprivate static var scale = 2
44+
45+
static var shared = ImageStore()
46+
47+
func image(name: String) -> Image {
48+
let index = _guaranteeImage(name: name)
49+
50+
return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(verbatim: name))
51+
}
52+
53+
static func loadImage(name: String) -> CGImage {
54+
guard
55+
let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
56+
let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
57+
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
58+
else {
59+
fatalError("Couldn't load image \(name).jpg from main bundle.")
60+
}
61+
return image
62+
}
63+
64+
fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
65+
if let index = images.index(forKey: name) { return index }
66+
67+
images[name] = ImageStore.loadImage(name: name)
68+
return images.index(forKey: name)!
69+
}
70+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
See LICENSE folder for this sample’s licensing information.
3+
4+
Abstract:
5+
The model for a hike.
6+
*/
7+
8+
import SwiftUI
9+
10+
struct Hike: Codable, Hashable, Identifiable {
11+
var name: String
12+
var id: Int
13+
var distance: Double
14+
var difficulty: Int
15+
var observations: [Observation]
16+
17+
static var formatter = LengthFormatter()
18+
19+
var distanceText: String {
20+
return Hike.formatter
21+
.string(fromValue: distance, unit: .kilometer)
22+
}
23+
24+
struct Observation: Codable, Hashable {
25+
var distanceFromStart: Double
26+
27+
var elevation: Range<Double>
28+
var pace: Range<Double>
29+
var heartRate: Range<Double>
30+
}
31+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
See LICENSE folder for this sample’s licensing information.
3+
4+
Abstract:
5+
The model for an individual landmark.
6+
*/
7+
8+
import SwiftUI
9+
import CoreLocation
10+
11+
struct Landmark: Hashable, Codable, Identifiable {
12+
var id: Int
13+
var name: String
14+
fileprivate var imageName: String
15+
fileprivate var coordinates: Coordinates
16+
var state: String
17+
var park: String
18+
var category: Category
19+
var isFavorite: Bool
20+
var isFeatured: Bool
21+
22+
var locationCoordinate: CLLocationCoordinate2D {
23+
CLLocationCoordinate2D(
24+
latitude: coordinates.latitude,
25+
longitude: coordinates.longitude)
26+
}
27+
28+
var featureImage: Image? {
29+
guard isFeatured else { return nil }
30+
31+
return Image(
32+
ImageStore.loadImage(name: "\(imageName)_feature"),
33+
scale: 2,
34+
label: Text(verbatim: name))
35+
}
36+
37+
enum Category: String, CaseIterable, Codable, Hashable {
38+
case featured = "Featured"
39+
case lakes = "Lakes"
40+
case rivers = "Rivers"
41+
case mountains = "Mountains"
42+
}
43+
}
44+
45+
extension Landmark {
46+
var image: Image {
47+
ImageStore.shared.image(name: imageName)
48+
}
49+
}
50+
51+
struct Coordinates: Hashable, Codable {
52+
var latitude: Double
53+
var longitude: Double
54+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
See LICENSE folder for this sample’s licensing information.
3+
4+
Abstract:
5+
An object that models a user profile.
6+
*/
7+
import Foundation
8+
9+
struct Profile {
10+
var username: String
11+
var prefersNotifications: Bool
12+
var seasonalPhoto: Season
13+
var goalDate: Date
14+
15+
static let `default` = Self(username: "g_kumar", prefersNotifications: true, seasonalPhoto: .winter)
16+
17+
init(username: String, prefersNotifications: Bool = true, seasonalPhoto: Season = .winter) {
18+
self.username = username
19+
self.prefersNotifications = prefersNotifications
20+
self.seasonalPhoto = seasonalPhoto
21+
self.goalDate = Date()
22+
}
23+
24+
enum Season: String, CaseIterable {
25+
case spring = "🌷"
26+
case summer = "🌞"
27+
case autumn = "🍂"
28+
case winter = "☃️"
29+
}
30+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
See LICENSE folder for this sample’s licensing information.
3+
4+
Abstract:
5+
A model object that stores app data.
6+
*/
7+
8+
import Combine
9+
import SwiftUI
10+
11+
final class UserData: ObservableObject {
12+
@Published var showFavoritesOnly = false
13+
@Published var landmarks = landmarkData
14+
@Published var profile = Profile.default
15+
}

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323

2424
完成本教程,你将获得一个这样的 App 以及一套精彩的 `SwiftUI` 代码:
2525

26-
![](https://github.com/WillieWangWei/SwiftUI-Tutorials/blob/master/preview.gif)
26+
![](https://github.com/WillieWangWei/SwiftUI-Tutorials/blob/master/preview-iOS.gif)
27+
28+
![](https://github.com/WillieWangWei/SwiftUI-Tutorials/blob/master/preview-watchOS.gif)
2729

2830
> **觉得不错?给个 Star 或 Follow 👌**
2931
@@ -34,6 +36,7 @@
3436
| v1.0.0 | 完成初版 |
3537
| v1.0.1 | 同步官方文档<br>增加成品预览 |
3638
| v1.0.2 | 同步官方文档<br>优化翻译,修复翻译错误,让阅读更流畅<br>增加了代码高亮<br>标记出了代码修改范围,便于上下文对比 |
39+
| v1.0.3 | 新增章节《创建 watchOS App》<br>修复代码bug<br>增加 watchOS App 成品预览<br> |
3740

3841
## 调整
3942

0 commit comments

Comments
 (0)