[譯] SwiftUI 官方教程 (十)

因爲 API 變更,此文章部份內容已失效,最新完整中文教程及代碼請查看 github.com/WillieWangW…git

微信技術羣

SwiftUI 表明將來構建 App 的方向,歡迎加羣一塊兒交流技術,解決問題。github

建立 watchOS App

本教程爲你提供一個將你已經學到的關於 SwiftUI 的知識應用到本身的產品上的機會,而且不費吹灰之力就能夠將 Landmarks app 遷移到 watchOS 上。swift

首先,給項目添加一個 watchOS target,而後複製爲 iOS app 中建立的共享數據和視圖。當全部資源都準備好後,你就能夠經過自定義 SwiftUI 視圖,在 watchOS 上顯示詳細信息和列表視圖。bash

下載項目文件並按照如下步驟操做,也能夠打開已完成的項目自行瀏覽代碼。微信

  • 預計完成時間:25 分鐘
  • 項目文件:下載

1. 添加一個 watchOS Target

要建立 watchOS app,首先要給項目添加一個 watchOS target。閉包

Xcode 會將 watchOS app 的組和文件,以及構建和運行 app 所需的 scheme 添加到項目中。app

1.1 選擇 File > New > Target,當模版表單顯示後,選擇 watchOS 標籤,選擇 Watch App for iOS App 模版後點擊 Nextide

這個模版會給項目添加一個新的 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 ExtensionGeneral 標籤中,勾選 Supports Running Without iOS App Installation 複選框。

儘量建立一個獨立的 watchOS app。獨立的 watchOS app 不須要與 iOS app 配套使用。

2. 在多個 Target 中共享文件

設置了 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.swiftCircleImage.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。

3. 建立詳情視圖

如今 iOS target 的資源在 watch app 上已經可用了,你須要建立一個 watch 獨有的視圖來顯示地標詳情。爲了測試這個視圖,你須要給最大和最小 watch 尺寸建立自定義預覽,而後給圓形視圖作一些修改來適配 watch 顯示。

3.1 在項目導航器中,單擊 WatchLandmarks Extension 文件夾旁邊的顯示三角形來顯示其內容,而後添加一個新 SwiftUI 視圖,命名爲 WatchLandmarkDetail

3.2 給 WatchLandmarkDetail 結構體添加 userDatalandmarklandmarkIndex 屬性。

這些和你在 處理用戶輸入 中添加到 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")
        }
    }
}
複製代碼

4. 添加 watchOS 地圖視圖

如今你已經建立了基本的詳情視圖,能夠添加地圖視圖來顯示地標的位置了。與 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")
        }
    }
}
複製代碼

5. 建立一個跨平臺的列表視圖

對於地標列表,你能夠重用 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())
    }
}
複製代碼

6. 添加地標列表

如今你已經更新了 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 開關來查看收藏的地標。

7. 建立自定義通知界面

你的 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 並添加 landmarktitle ,和 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 CategoryName 設置成 LandmarkNear

配置測試載荷來使用 LandmarkNear 分類,並傳遞通知控制器指望的數據。

7.8 選擇 PushNotificationPayload.apns 文件,而後更新 titlebodycategorylandmarkIndex 屬性。確認將分類設置成了 LandmarkNear 。另外,刪除在教程中任何沒有用到的鍵,好比 subtitleWatchKit 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 爲發送方的框格,通知視圖以及用於通知操做的按鈕。

相關文章
相關標籤/搜索