• <noscript id="e0iig"><kbd id="e0iig"></kbd></noscript>
  • <td id="e0iig"></td>
  • <option id="e0iig"></option>
  • <noscript id="e0iig"><source id="e0iig"></source></noscript>
  • SwiftUI 官方教程 (十)

    標簽: SwiftUI  iOS  Swift  Xcode  watchOS

    由于 API 變動,此文章部分內容已失效,最新完整中文教程及代碼請查看 https://github.com/WillieWangWei/SwiftUI-Tutorials

    image

    創建 watchOS App

    本教程為你提供一個將你已經學到的關于 SwiftUI 的知識應用到自己的產品上的機會,并且不費吹灰之力就可以將 Landmarks app 遷移到 watchOS 上。

    首先,給項目添加一個 watchOS target,然后復制為 iOS app 中創建的共享數據和視圖。當所有資源都準備好后,你就可以通過自定義 SwiftUI 視圖,在 watchOS 上顯示詳細信息和列表視圖。

    下載項目文件并按照以下步驟操作,也可以打開已完成的項目自行瀏覽代碼。

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

    1. 添加一個 watchOS Target

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

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

    image

    1.1 選擇 File > New > Target,當模版表單顯示后,選擇 watchOS 標簽,選擇 Watch App for iOS App 模版后點擊 Next

    這個模版會給項目添加一個新的 watchOS app,將 iOS app 與它配對。

    1.2 在表單的 Product Name 中輸入 WatchLandmarks ,將 Language 設置成 Swift ,將 User Interface 設置成 SwiftUI 。勾選 Include Notification Scene 復選框,然后點擊 Finish

    image

    1.3 Xcode 彈出提示,點擊 Activate

    這樣選擇 WatchLandmarks scheme 后,就可以構建和運行你的 watchOS app 了。

    image

    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 配套使用。

    image

    2. 在多個 Target 中共享文件

    設置了 watchOS target 后,你需要從 iOS target 中共享一些資源。比如重用 Landmark app 中的數據模型,一些資源文件,以及任何不需要修改就可以跨平臺顯示的視圖。

    image

    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 則管理你的故事板,圖標和相關資源。

    image

    2.4 在項目導航器中,選擇 Resources 文件夾中的所有文件,然后在 File 檢查器的 Target Membership 中將它們添加到 WatchLandmarks Extension target。

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Qhg4zWC8-1571757358455)(https://upload-images.jianshu.io/upload_images/4518631-210ebb2a6e00d7c5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

    3. 創建詳情視圖

    現在 iOS target 的資源在 watch app 上已經可用了,你需要創建一個 watch 獨有的視圖來顯示地標詳情。為了測試這個視圖,你需要給最大和最小 watch 尺寸創建自定義預覽,然后給圓形視圖做一些修改來適配 watch 顯示。

    image

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

    image

    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)
            //
        }
    }
    

    image

    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)
        }
    }
    

    image

    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")
            }
            //
        }
    }
    

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-y9ikqOvT-1571757358457)(https://upload-images.jianshu.io/upload_images/4518631-1519d175447e7144.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

    圓形圖片重新調整大小來適配顯示的高度。但不幸,這依然裁剪了圓形的寬度。為了修復這個裁剪問題,你需要把圖片嵌入到一個 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")
            }
        }
    }
    

    image

    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")
            }
        }
    }
    

    image

    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")
            }
        }
    }
    

    image

    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")
            }
        }
    }
    

    image

    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")
            }
        }
    }
    

    image

    4. 添加 watchOS 地圖視圖

    現在你已經創建了基本的詳情視圖,可以添加地圖視圖來顯示地標的位置了。與 CircleImage 不同,你不能僅僅重用 iOS app 的 MapView 。相對的,你需要創建一個 WKInterfaceObjectRepresentable 結構體來包裝 WatchKit 地圖。

    image

    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()
        }
    }
    

    image

    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])
        }
    }
    

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-0inx0rIT-1571757358462)(https://upload-images.jianshu.io/upload_images/4518631-cb7a981289c40a66.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

    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")
            }
        }
    }
    

    image

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

    對于地標列表,你可以重用 iOS app 中的行視圖,但是每個平臺需要展示其自身的詳情視圖。為此,你需要將明確定義詳情視圖的 LandmarkList 視圖轉換為范型列表類型,

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-mCQv10g4-1571757358463)(https://upload-images.jianshu.io/upload_images/4518631-7f735bec942d313a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

    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 中了。

    image

    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())
        }
    }
    

    image

    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())
        }
    }
    

    image

    6.5 在模擬器中構建并運行 watchOS app。

    通過在列表中滾動地標,點擊視圖的地標詳情,并將其標記為收藏來測試 watchOS app 的行為。點擊返回按鈕回到列表,然后打開 Favorite 開關來查看收藏的地標。

    7. 創建自定義通知界面

    你的 Landmarks 的 watchOS 版本差不多要完成了。在這最后一節,你需要創建一個通知界面來顯示地標信息,當你在某一個收藏的地標附近時,就會收到這個通知。

    注意

    這一節只包含當你收到通知后如何顯示,并不描述如何設置或發送通知。

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qnzABrmF-1571757358481)(https://upload-images.jianshu.io/upload_images/4518631-feab17eacf578ae2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

    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 故事板。在故事板中選擇指向靜態通知界面控制器的箭頭。

    image

    7.7 在 Attributes 檢查器中,將 Notification CategoryName 設置成 LandmarkNear

    image

    配置測試載荷來使用 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 為發送方的框格,通知視圖以及用于通知操作的按鈕。

    image

    版權聲明:本文為A45824430原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
    本文鏈接:https://blog.csdn.net/A45824430/article/details/102692911

    智能推薦

    SwiftUI 新屬性InlinePickerStyle 教程含源碼

    實戰需求 SwiftUI 新屬性InlinePickerStyle,該屬性允許Picker與周圍容器中的其余內容保持一致。該樣式將適應不同容器和平臺的外觀,例如菜單中的單個菜單項。本文將演示一下InlinePickerStyle的基礎使用 本文價值與收獲 看完本文后,您將能夠作出下面的界面 看完本文您將掌握的技能 掌握Picker基礎使用 掌握InlinePickerStyle基礎使用 實現內置效...

    SwiftUI macOS 教程之WindowStyle設置

    SwiftUI 目前支持窗體樣式 func windowStyle<S>(S) -> some Scene 設置此場景創建的窗口的樣式。 struct DefaultWindowStyle 默認窗口樣式。 DefaultWindowStyle struct HiddenTitleBarWindowStyle 一種窗口樣式,它同時隱藏窗口的標題和標題欄區域的背景,從而允許顯示更多窗...

    SwiftUI 動畫效果大全 (教程含源碼)

    實戰需求 SwiftUI 動畫效果大全 本文價值與收獲 看完本文后,您將能夠作出下面的界面 看完本文您將掌握的技能 掌握 easeIn 掌握 easeOut 掌握 easeInOut 掌握 linear 掌握 default 掌握 spring 掌握 interact 基礎知識 animation(_?? 將給定的動畫應用于此視圖中的所有可設置動畫的值。 返回值 封裝此視圖并將動畫應用于視圖中使用...

    SpringBoot非官方教程 | 第十篇: 用spring Restdocs創建API文檔

    轉載請標明出處:  http://blog.csdn.net/forezp/article/details/71023510  本文出自方志朋的博客 這篇文章將帶你了解如何用spring官方推薦的restdoc去生成api文檔。本文創建一個簡單的springboot工程,將http接口通過Api文檔暴露出來。只需要通過 JUnit單元測試和Spring的MockMVC就可以生成...

    SpringBoot非官方教程 | 第十篇: 用spring Restdocs創建API文檔

    這篇文章將帶你了解如何用spring官方推薦的restdoc去生成api文檔。本文創建一個簡單的springboot工程,將http接口通過Api文檔暴露出來。只需要通過 JUnit單元測試和Spring的MockMVC就可以生成文檔。 準備工作 你需要15min Jdk 1.8 maven 3.0+ idea 創建工程 引入依賴,其pom文件: 通過@SpringBootApplication,...

    猜你喜歡

    SpringBoot非官方教程 | 第十篇: 用spring Restdocs創建API文檔

    轉載請標明出處: http://blog.csdn.net/forezp/article/details/71023510 本文出自方志朋的博客 這篇文章將帶你了解如何用spring官方推薦的restdoc去生成api文檔。本文創建一個簡單的springboot工程,將http接口通過Api文檔暴露出來。只需要通過 JUnit單元測試和Spring的MockMVC就可以生成文檔。 準備工作 你需要...

    SwiftUI macOS教程之Sheets組件經典教程

    一、Sheet 組件介紹 Sheet屬于內含式組件,該組件是特殊的窗口組件,用于信息展示與輔助性選擇。從macOS 11開始,Sheet不再從父窗口工具欄的底部邊緣彈出。在macOS 11中,Sheet是一個圓角矩形視圖,它漂浮在變暗的父窗口頂部。下面是實際效果圖: 二、本文價值與收獲 看完本文后,您將能夠作出下面的界面 看完本文您將掌握的技能 macOS下掌握Sheet基本使用 掌握sheet組...

    SwiftUI 復用舊項目代碼之UIViewcontroller(2020版教程)

    SwiftUI 復用舊項目代碼之UIViewcontroller(2020版教程) SwiftUI 非常好用,開發界面高效快捷,但是具體到業務層面,復用歷史代碼是必須要研究到事情。 如何在SwiftUI中調用UIViewcontroller 1、 創建一個 Wrapper 例如我們有個Example1ViewController舊代碼,我們可以先通過UIViewControllerRepresen...

    HTML中常用操作關于:頁面跳轉,空格

    1.頁面跳轉 2.空格的代替符...

    freemarker + ItextRender 根據模板生成PDF文件

    1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...

    精品国产乱码久久久久久蜜桃不卡