因爲 API 變更,此文章部份內容已失效,最新完整中文教程及代碼請查看 github.com/WillieWangW…git
SwiftUI
表明將來構建 App 的方向,歡迎加羣一塊兒交流技術,解決問題。github
本教程爲你提供一個將你已經學到的關於
SwiftUI
的知識應用到本身的產品上的機會,而且不費吹灰之力就能夠將 Landmarks app 遷移到 watchOS 上。swift首先,給項目添加一個 watchOS target,而後複製爲 iOS app 中建立的共享數據和視圖。當全部資源都準備好後,你就能夠經過自定義 SwiftUI 視圖,在 watchOS 上顯示詳細信息和列表視圖。bash
下載項目文件並按照如下步驟操做,也能夠打開已完成的項目自行瀏覽代碼。微信
- 預計完成時間:25 分鐘
- 項目文件:下載
要建立 watchOS app,首先要給項目添加一個 watchOS target。閉包
Xcode 會將 watchOS app 的組和文件,以及構建和運行 app 所需的 scheme 添加到項目中。app
1.1 選擇 File
> New
> Target
,當模版表單顯示後,選擇 watchOS
標籤,選擇 Watch App for iOS App
模版後點擊 Next
。ide
這個模版會給項目添加一個新的 watchOS app,將 iOS app 與它配對。工具
1.2 在表單的 Product Name
中輸入 WatchLandmarks
,將 Language
設置成 Swift
,將 User Interface
設置成 SwiftUI
。勾選 Include Notification Scene
複選框,而後點擊 Finish
。佈局
1.3 Xcode 彈出提示,點擊 Activate
。
這樣選擇 WatchLandmarks
scheme 後,就能夠構建和運行你的 watchOS app 了。
Whenever possible, create an independent watchOS app. Independent watchOS apps don’t require an iOS companion app.
1.4 在 WatchLandmarks Extension
的 General
標籤中,勾選 Supports Running Without iOS App Installation
複選框。
儘量建立一個獨立的 watchOS app。獨立的 watchOS app 不須要與 iOS app 配套使用。
設置了 watchOS target 後,你須要從 iOS target 中共享一些資源。好比重用 Landmark app 中的數據模型,一些資源文件,以及任何不須要修改就能夠跨平臺顯示的視圖。
2.1 在項目導航器中,按住 Command
鍵而後點擊選中如下文件:LandmarkRow.swift
, Landmark.swift
, UserData.swift
, Data.swift
, Profile.swift
, Hike.swift
, CircleImage.swift
。
Landmark.swift
, UserData.swift
, Data.swift
, Profile.swift
, Hike.swift
定義了 app 的數據模型。雖然你不會用到全部這些模型,可是須要保證這些文件都編譯到了 app 中。 LandmarkRow.swift
和 CircleImage.swift
是兩個不修改就能夠顯示在 watchOS 中的視圖。
2.2 打開 File
檢查器,勾選 Target Membership
中的 WatchLandmarks Extension
複選框。
這會讓你在上一步中選擇的文件在 watchOS app 中可用。
2.3 打開項目導航器,在 Landmark
組中選擇 Assets.xcassets
,而後在 File
檢查器的 Target Membership
中將它添加到 WatchLandmarks
target。
這與你上一步選擇到 target 不同, WatchLandmarks Extension
target 包含你的 app 的代碼,而 WatchLandmarks
target 則管理你的故事板,圖標和相關資源。
2.4 在項目導航器中,選擇 Resources
文件夾中的全部文件,而後在 File
檢查器的 Target Membership
中將它們添加到 WatchLandmarks Extension
target。
如今 iOS target 的資源在 watch app 上已經可用了,你須要建立一個 watch 獨有的視圖來顯示地標詳情。爲了測試這個視圖,你須要給最大和最小 watch 尺寸建立自定義預覽,而後給圓形視圖作一些修改來適配 watch 顯示。
3.1 在項目導航器中,單擊 WatchLandmarks Extension
文件夾旁邊的顯示三角形來顯示其內容,而後添加一個新 SwiftUI
視圖,命名爲 WatchLandmarkDetail
。
3.2 給 WatchLandmarkDetail
結構體添加 userData
, landmark
和 landmarkIndex
屬性。
這些和你在 處理用戶輸入 中添加到 LandmarkDetail
結構體中的屬性是同樣的。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
//
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
//
var body: some View {
Text("Hello World!")
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
WatchLandmarkDetail()
}
}
複製代碼
在上一步添加屬性後,你會在 Xcode 中獲得一個缺乏參數的錯誤。爲了修復這個錯誤,你須要二選一:提供屬性的默認值,或傳遞參數來設置視圖的屬性。
3.3 在預覽中,建立一個用戶數據的實例,而後用它給 WatchLandmarkView
結構體的初始化傳遞一個地標對象。另外還須要將這個用戶數據設置成視圖的環境對象。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
Text("Hello World!")
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
//
let userData = UserData()
return WatchLandmarkDetail(landmark: userData.landmarks[0])
.environmentObject(userData)
//
}
}
複製代碼
3.4 在 WatchLandmarkDetail.swift
中,從 body()
方法裏返回一個 CircleImage
視圖。
這就是你從 iOS 項目中複用 CircleImage
視圖的地方。由於建立了可調整大小的圖片, .scaledToFill()
的調用會讓圓形的尺寸自動適配顯示。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
//
CircleImage(image: self.landmark.image.resizable())
.scaledToFill()
//
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return WatchLandmarkDetail(landmark: userData.landmarks[0])
.environmentObject(userData)
}
}
複製代碼
3.5 給最大 (44mm) 和最小 (38mm) 錶盤建立預覽。
經過針對最大和最小錶盤的測試,你能夠看到你的 app 是如何縮放來適配顯示的。與往常同樣,你應該在全部支持的設備尺寸上測試用戶界面。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
CircleImage(image: self.landmark.image.resizable())
.scaledToFill()
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
//
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
//
}
}
複製代碼
圓形圖片從新調整大小來適配顯示的高度。但不幸,這依然裁剪了圓形的寬度。爲了修復這個裁剪問題,你須要把圖片嵌入到一個 VStack
中,而且作一些額外的佈局修改來讓圓形圖片適配任何 watch 的寬度。
3.6 把圖片嵌入到一個 VStack
中,在圖片下面顯示地標的名字和它的信息。
如你所見,信息並無徹底適配 watch 的屏幕,可是你能夠經過將這個 VStack
放在一個滾動視圖中來修復這個問題。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
//
VStack {
CircleImage(image: self.landmark.image.resizable())
.scaledToFill()
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
}
//
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
複製代碼
3.7 將豎直 stack 包裝中一個滾動視圖中。
這讓視圖能夠滾動,可是帶來了另一個問題:圓形圖片展開到了全屏,而且調整了其餘 UI 元素來匹配這個圖片。你須要調整這個圓形圖片的大小來讓它和地標名字顯示在屏幕上。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
//
ScrollView {
VStack {
CircleImage(image: self.landmark.image.resizable())
.scaledToFill()
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
}
}
//
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
複製代碼
3.8 把 scaleToFill()
改爲 scaleToFit()
。
這會讓圓形圖片縮放來匹配顯示的寬度。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
VStack {
CircleImage(image: self.landmark.image.resizable())
//
.scaledToFit()
//
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
}
}
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
複製代碼
3.9 添加填充使地標名字在圓形圖像下方可見。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
VStack {
CircleImage(image: self.landmark.image.resizable())
.scaledToFit()
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
}
//
.padding(16)
//
}
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
複製代碼
3.10 給返回按鈕添加一個標題。
這裏將返回按鈕的文字設置成來 Landmarks
,可是在本教程後面的部分中,只有添加 LandmarksList
視圖後,你才能看到返回按鈕。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
VStack {
CircleImage(image: self.landmark.image.resizable())
.scaledToFit()
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
}
.padding(16)
}
//
.navigationBarTitle("Landmarks")
//
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
複製代碼
如今你已經建立了基本的詳情視圖,能夠添加地圖視圖來顯示地標的位置了。與 CircleImage
不一樣,你不能僅僅重用 iOS app 的 MapView
。相對的,你須要建立一個 WKInterfaceObjectRepresentable
結構體來包裝 WatchKit 地圖。
4.1 給 WatchKit extension
添加一個自定義視圖,命名爲 WatchMapView
。
WatchMapView.swift
import SwiftUI
struct WatchMapView: View {
var body: some View {
Text("Hello World!")
}
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
WatchMapView()
}
}
複製代碼
4.2 在 WatchMapView
結構體中,將 View
改爲 WKInterfaceObjectRepresentable
。
在步驟 1 和 2 所示的代碼之間來回滾動來查看區別。
WatchMapView.swift
import SwiftUI
//
struct WatchMapView: WKInterfaceObjectRepresentable {
//
var body: some View {
Text("Hello World!")
}
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
WatchMapView()
}
}
複製代碼
Xcode 會顯示編譯錯誤,由於 WatchMapView
尚未遵循 WKInterfaceObjectRepresentable
屬性。
4.3 刪除 body()
方法,將其替換爲 landmark
屬性。
每當你建立一個地圖視圖你都須要給這個屬性傳遞一個值。好比,你能夠給預覽傳遞一個地標實例。
WatchMapView.swift
import SwiftUI
struct WatchMapView: WKInterfaceObjectRepresentable {
//
var landmark: Landmark
//
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
//
WatchMapView(landmark: UserData().landmarks[0])
//
}
}
複製代碼
4.4 實現 WKInterfaceObjectRepresentable
協議的 makeWKInterfaceObject(context:)
方法。
這個方法會建立 WatchMapView
用來顯示的 WatchKit 地圖。
WatchMapView.swift
import SwiftUI
struct WatchMapView: WKInterfaceObjectRepresentable {
var landmark: Landmark
//
func makeWKInterfaceObject(context: WKInterfaceObjectRepresentableContext<WatchMapView>) -> WKInterfaceMap {
return WKInterfaceMap()
}
//
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
WatchMapView(landmark: UserData().landmarks[0])
}
}
複製代碼
4.5 實現 WKInterfaceObjectRepresentable
協議的 updateWKInterfaceObject(_:, context:)
方法,根據地標座標設置地圖的範圍。
如今項目能夠成功構建而沒有任何錯誤了。
WatchMapView.swift
import SwiftUI
struct WatchMapView: WKInterfaceObjectRepresentable {
var landmark: Landmark
func makeWKInterfaceObject(context: WKInterfaceObjectRepresentableContext<WatchMapView>) -> WKInterfaceMap {
return WKInterfaceMap()
}
//
func updateWKInterfaceObject(_ map: WKInterfaceMap, context: WKInterfaceObjectRepresentableContext<WatchMapView>) {
let span = MKCoordinateSpan(latitudeDelta: 0.02,
longitudeDelta: 0.02)
let region = MKCoordinateRegion(
center: landmark.locationCoordinate,
span: span)
map.setRegion(region)
}
//
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
WatchMapView(landmark: UserData().landmarks[0])
}
}
複製代碼
4.6 選中 WatchLandmarkView.swift
文件,而後把地圖視圖添加到豎直 stack 的底部。
代碼在地圖視圖以後添加了一個分割線。.scaledToFit()
和 .padding()
修飾符讓地圖的尺寸很好的匹配了屏幕。
WatchLandmarkDetail.swift
import SwiftUI
struct WatchLandmarkDetail: View {
@EnvironmentObject var userData: UserData
var landmark: Landmark
var landmarkIndex: Int {
userData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
VStack {
CircleImage(image: self.landmark.image.resizable())
.scaledToFit()
Text(self.landmark.name)
.font(.headline)
.lineLimit(0)
Toggle(isOn:
$userData.landmarks[self.landmarkIndex].isFavorite) {
Text("Favorite")
}
Divider()
Text(self.landmark.park)
.font(.caption)
.bold()
.lineLimit(0)
Text(self.landmark.state)
.font(.caption)
//
Divider()
WatchMapView(landmark: self.landmark)
.scaledToFit()
.padding()
//
}
.padding(16)
}
.navigationBarTitle("Landmarks")
}
}
struct WatchLandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
let userData = UserData()
return Group {
WatchLandmarkDetail(landmark: userData.landmarks[0]).environmentObject(userData)
.previewDevice("Apple Watch Series 4 - 44mm")
WatchLandmarkDetail(landmark: userData.landmarks[1]).environmentObject(userData)
.previewDevice("Apple Watch Series 2 - 38mm")
}
}
}
複製代碼
對於地標列表,你能夠重用 iOS app 中的行視圖,可是每一個平臺須要展現其自身的詳情視圖。爲此,你須要將明肯定義詳情視圖的 LandmarkList
視圖轉換爲範型列表類型,
5.1 在工具欄中,選中 Landmarks scheme
。
Xcode 如今會構建和運行 app 的 iOS 版本。在把列表移動到 watchOS app 以前,你須要確認任何對 LandmarkList
視圖對修改在 iOS app 中依然生效。
5.2 選中 LandmarkList.swift
而後修改類型的聲明將其變成範型類型。
LandmarksList.swift
import SwiftUI
//
struct LandmarkList<DetailView: View>: View {
//
@EnvironmentObject private var userData: UserData
var body: some View {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
destination: LandmarkDetail(landmark: landmark).environmentObject(self.userData)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
}
.environmentObject(UserData())
}
}
複製代碼
添加範型聲明會讓你不管什麼時候建立一個 LandmarkList
結構體實例時都會出現 Generic parameter could not be inferred
錯誤。接下來的幾步會修復這些錯誤。
5.3 添加一個建立詳情視圖的閉包屬性。
LandmarksList.swift
import SwiftUI
struct LandmarkList<DetailView: View>: View {
@EnvironmentObject private var userData: UserData
//
let detailViewProducer: (Landmark) -> DetailView
//
var body: some View {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
destination: LandmarkDetail(landmark: landmark).environmentObject(self.userData)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
}
.environmentObject(UserData())
}
}
複製代碼
5.4 使用 detailViewProducer
屬性給地標建立詳情視圖。
LandmarksList.swift
import SwiftUI
struct LandmarkList<DetailView: View>: View {
@EnvironmentObject private var userData: UserData
let detailViewProducer: (Landmark) -> DetailView
var body: some View {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
//
destination: self.detailViewProducer(landmark).environmentObject(self.userData)) {
//
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
struct LandmarksList_Previews: PreviewProvider {
static var previews: some View {
ForEach(["iPhone SE", "iPhone XS Max"].identified(by: \.self)) { deviceName in
LandmarkList()
.previewDevice(PreviewDevice(rawValue: deviceName))
.previewDisplayName(deviceName)
}
.environmentObject(UserData())
}
}
複製代碼
當你建立了一個 LandmarkList
的實例後,你還須要提供一個給地標建立詳情視圖的閉包。
5.5 選中 Home.swift
,在 CategoryHome
結構體的 body()
方法中添加一個閉包來建立 LandmarkDetail
視圖。
Xcode 會根據閉包的返回類型來推斷 LandmarkList
結構體的類型。
Home.swift
import SwiftUI
struct CategoryHome: View {
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarkData,
by: { $0.category.rawValue }
)
}
var featured: [Landmark] {
landmarkData.filter { $0.isFeatured }
}
@State var showingProfile = false
var profileButton: some View {
Button(action: { self.showingProfile.toggle() }) {
Image(systemName: "person.crop.circle")
.imageScale(.large)
.accessibility(label: Text("User Profile"))
.padding()
}
}
var body: some View {
NavigationView {
List {
FeaturedLandmarks(landmarks: featured)
.scaledToFill()
.frame(height: CGFloat(200))
.clipped()
.listRowInsets(EdgeInsets())
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryRow(categoryName: key, items: self.categories[key]!)
}
.listRowInsets(EdgeInsets())
//
NavigationLink(destination: LandmarkList { LandmarkDetail(landmark: $0) }) {
//
Text("See All")
}
}
.navigationBarTitle(Text("Featured"))
.navigationBarItems(trailing: profileButton)
.sheet(isPresented: $showingProfile) {
ProfileHost()
}
}
}
}
struct FeaturedLandmarks: View {
var landmarks: [Landmark]
var body: some View {
landmarks[0].image.resizable()
}
}
// swiftlint:disable type_name
struct CategoryHome_Previews: PreviewProvider {
static var previews: some View {
CategoryHome()
.environmentObject(UserData())
}
}
複製代碼
5.6 在 LandmarkList.swift
中,給預覽添加相似的代碼。
在這裏,你須要使用條件編譯來根據 Xcode 的當前 scheme 來定義詳細視圖。Landmark app 如今能夠按預期在 iOS 上構建並運行了。
LandmarksList.swift
import SwiftUI
struct LandmarkList<DetailView: View>: View {
@EnvironmentObject private var userData: UserData
let detailViewProducer: (Landmark) -> DetailView
var body: some View {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
destination: self.detailViewProducer(landmark).environmentObject(self.userData)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
#if os(watchOS)
typealias PreviewDetailView = WatchLandmarkDetail
#else
typealias PreviewDetailView = LandmarkDetail
#endif
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList { PreviewDetailView(landmark: $0) }
.environmentObject(UserData())
}
}
複製代碼
如今你已經更新了 LandmarksList
視圖讓其能在兩個平臺上都工做,能夠將它添加到 watchOS app 中了。
6.1 在文件檢查器中,把 LandmarksList.swift
添加到 WatchLandmarks Extension
target 中。
你如今能夠中你的 watchOS app 的代碼中使用 LandmarkList
視圖了。
6.2 在工具欄中,將 scheme 改成 Watch Landmarks
。
6.3 打開 LandmarkList.swift
的同時,恢復預覽。
如今預覽會顯示 watchOS 的列表視圖。
LandmarksList.swift
import SwiftUI
struct LandmarkList<DetailView: View>: View {
@EnvironmentObject private var userData: UserData
let detailViewProducer: (Landmark) -> DetailView
var body: some View {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Show Favorites Only")
}
ForEach(userData.landmarks) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(
destination: self.detailViewProducer(landmark).environmentObject(self.userData)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
#if os(watchOS)
typealias PreviewDetailView = WatchLandmarkDetail
#else
typealias PreviewDetailView = LandmarkDetail
#endif
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList { PreviewDetailView(landmark: $0) }
.environmentObject(UserData())
}
}
複製代碼
watchOS app 的根是顯示默認 Hello World!
消息的 ContentView
。
6.4 修改 ContentView
來讓它顯示列表視圖。
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
LandmarkList { WatchLandmarkDetail(landmark: $0) }
.environmentObject(UserData())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
LandmarkList { WatchLandmarkDetail(landmark: $0) }
.environmentObject(UserData())
}
}
複製代碼
6.5 在模擬器中構建並運行 watchOS app。
經過在列表中滾動地標,點擊視圖的地標詳情,並將其標記爲收藏來測試 watchOS app 的行爲。點擊返回按鈕回到列表,而後打開 Favorite
開關來查看收藏的地標。
你的 Landmarks 的 watchOS 版本差很少要完成了。在這最後一節,你須要建立一個通知界面來顯示地標信息,當你在某一個收藏的地標附近時,就會收到這個通知。
注意
這一節只包含當你收到通知後如何顯示,並不描述如何設置或發送通知。
7.1 打開 NotificationView.swift
並建立一個視圖來顯示地標的信息,標題和消息。
由於任何通知的值均可覺得 nil,因此預覽會顯示通知的兩個版本。第一個僅顯示當沒有數據時的默認值,第二個顯示你提供的標題,信息,和位置。
NotificationView.swift
import SwiftUI
struct NotificationView: View {
//
let title: String?
let message: String?
let landmark: Landmark?
init(title: String? = nil,
message: String? = nil,
landmark: Landmark? = nil) {
self.title = title
self.message = message
self.landmark = landmark
}
//
var body: some View {
//
VStack {
if landmark != nil {
CircleImage(image: landmark!.image.resizable())
.scaledToFit()
}
Text(title ?? "Unknown Landmark")
.font(.headline)
.lineLimit(0)
Divider()
Text(message ?? "You are within 5 miles of one of your favorite landmarks.")
.font(.caption)
.lineLimit(0)
}
//
}
}
struct NotificationView_Previews: PreviewProvider {
//
//
static var previews: some View {
//
Group {
NotificationView()
NotificationView(title: "Turtle Rock",
message: "You are within 5 miles of Turtle Rock.",
landmark: UserData().landmarks[0])
}
.previewLayout(.sizeThatFits)
//
}
}
複製代碼
7.2 打開 NotificationController
並添加 landmark
, title
,和 message
屬性。
這些數據存儲發送進來的通知的相關值。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class NotificationController: WKUserNotificationHostingController<NotificationView> {
//
var landmark: Landmark?
var title: String?
var message: String?
//
override var body: NotificationView {
NotificationView()
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
}
}
複製代碼
7.3 更新 body()
方法來使用這些屬性。
此方法會實例化你以前建立的通知視圖。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class NotificationController: WKUserNotificationHostingController<NotificationView> {
var landmark: Landmark?
var title: String?
var message: String?
override var body: NotificationView {
//
NotificationView(title: title,
message: message,
landmark: landmark)
//
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
}
}
複製代碼
7.4 定義 LandmarkIndexKey
。
你須要使用這個鍵從通知中提取額地標的索引。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class NotificationController: WKUserNotificationHostingController<NotificationView> {
var landmark: Landmark?
var title: String?
var message: String?
//
let landmarkIndexKey = "landmarkIndex"
//
override var body: NotificationView {
NotificationView(title: title,
message: message,
landmark: landmark)
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
}
}
複製代碼
7.5 更新 didReceive(_:)
方法從推送中解析數據。
這個方法會更新控制器的屬性。調用這個方法後,系統會使控制器的 body
屬性無效,從而更新導航視圖。而後系統會在 Apple Watch 上顯示通知。
NotificationController.swift
import WatchKit
import SwiftUI
import UserNotifications
class NotificationController: WKUserNotificationHostingController<NotificationView> {
var landmark: Landmark?
var title: String?
var message: String?
let landmarkIndexKey = "landmarkIndex"
override var body: NotificationView {
NotificationView(title: title,
message: message,
landmark: landmark)
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
//
let userData = UserData()
let notificationData =
notification.request.content.userInfo as? [String: Any]
let aps = notificationData?["aps"] as? [String: Any]
let alert = aps?["alert"] as? [String: Any]
title = alert?["title"] as? String
message = alert?["body"] as? String
if let index = notificationData?[landmarkIndexKey] as? Int {
landmark = userData.landmarks[index]
}
//
}
}
複製代碼
當 Apple Watch 收到通知後,它會建立通知分類關聯當通知控制器。你須要打開並編輯 app 當故事板來給你當通知控制器設置分類。
7.6 這項目導航器中,選中 Watch Landmarks
文件夾,打開 Interface
故事板。在故事板中選擇指向靜態通知界面控制器的箭頭。
7.7 在 Attributes
檢查器中,將 Notification Category
的 Name
設置成 LandmarkNear
。
配置測試載荷來使用 LandmarkNear
分類,並傳遞通知控制器指望的數據。
7.8 選擇 PushNotificationPayload.apns
文件,而後更新 title
, body
, category
和 landmarkIndex
屬性。確認將分類設置成了 LandmarkNear
。另外,刪除在教程中任何沒有用到的鍵,好比 subtitle
, WatchKit Simulator Actions
,以及 customKey
。
載荷文件會模擬從服務發來的遠程通知數據。
PushNotificationPayload.apns
{
"aps": {
"alert": {
"body": "You are within 5 miles of Silver Salmon Creek."
"title": "Silver Salmon Creek",
},
"category": "LandmarkNear",
"thread-id": "5280"
},
"landmarkIndex": 1
}
複製代碼
7.9 選擇 Landmarks-Watch (Notification)
scheme,而後構建並運行你的 app。
當你第一次運行通知 scheme,系統會請求發送通知的權限。選擇 Allow
。以後模擬器會顯示一個可滾動的通知,它包括:用於標記 Landmarks app 爲發送方的框格,通知視圖以及用於通知操做的按鈕。