第2章 使用SwiftUI構建watchOS app的界面

​ 在上一章中,咱們建立了第一個watchOS app項目,而後咱們修改了ContentView.swift的代碼,構建並運行了這個app。那麼app是怎麼啓動並找到ContentView來顯示的呢?編程

2.1 watchOS app的啓動過程和生命週期

​ 在用模板建立項目時咱們說過,『WatchKit App包含你應用的界面(storyboard)及界面所用的資源文件(assets),WatchKit Extension包含你應用的代碼』。如今咱們回到項目的文件導航欄,能夠看到WatchKit App底下有個Interface.storyboard文件,該文件包含一個Hosting Controller Scene場景,而場景下就是一個帶"->"表示的Hosting Controller,代表它就是咱們app的入口(或者叫主控制器)。經過聲明storyboard下的這個控制器的類型爲HostingController,就能與WatchKit Extension下的HostingController.swift這個代碼文件關聯起來。swift

11.jpg

​ 點擊HostingController.swift,它的代碼只有如下幾行:xcode

import WatchKit
import Foundation
import SwiftUI

class HostingController: WKHostingController<ContentView> {
    override var body: ContentView {
        return ContentView()
    }
}

​ 首先導入WatchKit、Foundation、SwiftUI三個框架,而後是HostingController類的定義,它繼承WKHostingController幷包含ContentView協議。WKHostingController是SwiftUI框架中的類,前綴WK則是WatchKit的縮寫表示。按住Command鍵而後點擊WKHostingController能夠查看它的定義:app

/// A `WKInterfaceController` which hosts a `View` hierarchy.
open class WKHostingController<Body> : WKInterfaceController where Body : View {

    /// The root `View` of the view hierarchy to display.
    open var body: Body { get }

    /// Invalidates the current `body` and triggers a body update during the
    /// next update cycle.
    public func setNeedsBodyUpdate()

    /// Update `body` immediately, if updates are pending.
    public func updateBodyIfNeeded()

    @objc override dynamic public init()
}

​ 經過閱讀以上代碼和註釋能夠知道,WKHostingController可使用SwiftUI的視圖來顯示和管理app的主界面。程序必須子類化WKHostingController並重寫(override)body屬性來提供咱們想要顯示的SwiftUI視圖。而上面的HostingController就是這個子類,body屬性返回的正是ContentView的實例。如今,咱們總算搞清楚本章最開始的問題了。框架

​ 接下來,咱們再看看WatchKit Extension下最後一個代碼文件ExtensionDelegate.swift,它的代碼大概有50行左右:ide

import WatchKit

class ExtensionDelegate: NSObject, WKExtensionDelegate {

    func applicationDidFinishLaunching() {
        // Perform any final initialization of your application.
    }

    func applicationDidBecomeActive() {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillResignActive() {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, etc.
    }

    func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
        // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
      
      //筆者注:此處省略若干行
        }
}

​ 相似地,經過上述代碼,咱們知道ExtensionDelegate是NSObject的子類並遵循WKExtensionDelegate代理協議,查看WKExtensionDelegate的定義,會發現它定義了各類可選的(optional)代理方法。實現這些代理方法就能夠響應app的各類生命週期事件,例如app的活躍和中止以及後臺任務等。WatchKit框架會根據WatchKit Extension下的Info.plist文件中的WKExtensionDelegateClassName對應的類名(默認狀況下爲ExtensionDelegate),自動爲watchOS app實例化一個擴展代理對象。而後,WatchKit將應用程序執行狀態的變化報告給這個擴展代理對象。工具

12.jpg

​ 在watchOS app的生命週期中,一般會有如下幾種狀態:未運行(Not running)、閒置(Inactive)、活躍(Active)、後臺(Background)、中止(Suspended)。各類狀態間的切換以下圖所示:oop

13.png

​ A. 當狀態從未運行切換到閒置或者後臺時,系統會調用擴展代理的applicationDidFinishLaunching()方法;佈局

​ B. 當狀態在閒置與活躍間切換時,系統會調用擴展代理的applicationDidBecomeActive()或者applicationWillResignActive()方法;學習

​ C. 當狀態在閒置與後臺間切換時,系統會調用擴展代理的applicationWillEnterForeground()或者applicationDidEnterBackground()方法。

​ 這三種狀態切換是watchOS app開發中最多見的,須要重點關注。

​ 瞭解完watchOS app的啓動過程和生命週期後,咱們要正式開始學習watchOS app開發的首選UI框架SwiftUI了。

2.2 SwiftUI簡介

SwiftUI 是一種創新、簡潔的編程方式,經過 Swift 的強大功能,在全部 Apple 平臺上構建用戶界面。藉助它,您只需一套工具和 API,便可建立面向任何 Apple 設備的用戶界面。SwiftUI 採用簡單易懂、編寫方式天然的聲明式 Swift 語法,可無縫支持新的 Xcode 設計工具,讓您的代碼與設計保持高度同步。SwiftUI 原生支持「動態字體」、「深色模式」、本地化和輔助功能——第一行您寫出的 SwiftUI 代碼,就已是您編寫過的、功能最強大的 UI 代碼。

​ SwiftUI支持iOS 13+、watchOS 6+、tvOS 13+和macOS 10.15+,目前第一個版本雖然對於iPhone和mac應用開發可能還顯得不夠強大,但做爲watchOS app的首選UI框架,比起原始的UIKit已經體現出無比的優點。首先,手錶的屏幕較小布局簡單;其次,各類界面的交互也足夠方便;最後,數據與視圖的綁定使得界面的更新變得實時且不易出錯。

SwiftUI 採用聲明式語法,您只需聲明用戶界面應具有的功能便可。例如,您能夠寫明您須要一個由文本欄組成的項目列表,而後描述各個欄位的對齊方式、字體和顏色。您的代碼比以往更加簡單直觀和易於理解,能夠節省您的時間和維護工做。

1.png

這種聲明式風格甚至適用於動畫等複雜的概念。只需幾行代碼,就能輕鬆地向幾乎任何控件添加動畫並選擇一系列即時可用的特效。在運行時,系統會處理全部必要的步驟和中斷因素,來保證您的代碼流暢運行、保持穩定。實現動畫效果是如此簡單,您還能探索新的方式讓 app 更生動出彩。

​ 下面,咱們經過實例來說解SwiftUI構建界面的一些基本操做。

2.3 建立你的第二個watchOS項目

​ 有了第一個項目"👋🍎⌚️‼️"的經驗,咱們的第二個watchOS項目將建立一個簡單的遊戲——Emoji成語。遊戲的邏輯很簡單:通常地,咱們的成語會有4個文字(非4字的先不考慮),咱們把其中三個經過Emoji顯示出來,剩下一個使用"❓"來代替;而後提供4個Emoji選項做爲"❓"的備選答案,在用戶點擊某個選項後返回結果是否正確,而後進入下一個成語,直到所有成語展現完畢;最後顯示用戶的得分,計分規則是答對1題加10分,答錯1題減5分(到0分則再也不減),一共10題滿分是100分;爲了讓遊戲能有更好的交互性,咱們還將提供『求助』和『跳過』功能,求助會直接顯示正確答案但只能得5分,而跳過則不顯示答案也不會加減分,每一個功能限定最多隻能用一次。

​ 第一個界面的視圖規劃大概是下圖這個樣子,其中黃色區域是咱們的成語顯示區,綠色區域是咱們的備選答案區,紅色區域是功能交互區,三個區域間有兩條灰色的分隔線。

2.jpg

​ 如今,咱們先按第一章的步驟,以EmojiIdioms爲名稱建立這個項目。而後仍是選中WatchKit Extension下的ContentView.swift來編輯咱們的界面視圖:

3.jpg

​ 能夠看到ContentView.swift已經在最開始的地方就導入了SwiftUI。在SwiftUI中,全部的UI組件均可以當作是View,各類各樣的View構建成app的界面與交互。全部的View都是struct類型,由於struct能比class更快速地渲染和更新視圖。ContentView也是View的一個子類,它有一個類型爲some View的變量body,只要把視圖的代碼寫在body內,系統就會自動生成並顯示對應的視圖,好比這裏的文本"Hello, World!"。底下的ContentView_Previews是PreviewProvider類型,是爲了讓Xcode能在Canvas實時更新咱們視圖的黑科技,它有一個類型一樣爲some View的靜態變量previews,返回的就是ContentView的實例。若是使用真機或者模擬器運行程序,即便去掉ContentView_Previews也不會影響程序的實際運行。

​ SwiftUI採用的是相似HTML的流式佈局方式,方向爲從上到下從左到右,默認會居中。點擊Xcode右上角的"+"按鈕,能夠看到目前支持的全部視圖類型,包括控件視圖、佈局視圖、繪畫以及其它視圖。回到開始的視圖規劃,咱們須要1個Text視圖來顯示成語、4個Button視圖來顯示備選答案、2個Button視圖來處理功能交互,另外還有兩個Divider視圖來分隔各個區域,最後使用Vertical Stack 和 Horizontal Stack處理各視圖的佈局。

4.jpg

​ 1)黃色區域,先把文本更新爲"1️⃣💎2️⃣❓",其中"❓"就是遊戲中這個成語要補充的字:

struct ContentView: View {
    var body: some View {
        Text("1️⃣💎2️⃣❓")
            .font(.title)
    }
}

​ 2)綠色區域,備選按鈕不能跟上面的文本直接合在body中顯示,須要先建立一個Vertical Stack包含起來,按住Command鍵而後點擊Text能夠調出快捷菜單,選擇"Embed in VStack",相似地4個備選按鈕也要經過Horizontal Stack包含起來:

17.png

struct ContentView: View {
    var body: some View {
        VStack {
            Text("1️⃣💎2️⃣❓")
                .font(.title)
            
            HStack {
                Button(action: {}) { Text("🐶") }
                Button(action: {}) { Text("🐞") }
                Button(action: {}) { Text("🐦") }
                Button(action: {}) { Text("🐟") }
            }
        }
    }
}

​ Button中的action是點擊後的回調處理,這個咱們留到後面再補充。目前是4個備選項,若是下一版本更新到8個甚至更多,上面這種寫法顯然太不優雅了。還好,SwiftUI爲咱們提供了ForEach枚舉,來處理這種循環的需求:

struct ContentView: View {
    let options = ["🐶", "🐞", "🐦", "🐟"]
    
    var body: some View {
        VStack {
            Text("1️⃣💎2️⃣❓")
                .font(.title)
            
            HStack {
                ForEach(0..<options.count) { index in
                    Button(action: {}) { Text(self.options[index]) }
                }
            }
        }
    }
}

​ 從預覽中能夠看到當前視圖的效果,到目前爲止還算比較符合咱們的規劃的,能夠看到視圖垂直方向上由以前的"1️⃣💎2️⃣❓"居中,變成了文本與備選按鈕一塊兒居中了,這都是SwiftUI在背後幫咱們自動處理好的。

5.jpg

​ 3)紅色區域,一樣地把2個按鈕經過Horizontal Stack包含起來就能夠了,按鈕的點擊處理也是放到後面。最後,在三個區域間添上Divider分隔線。此時,刷新預覽界面,你會發現最上面的"1️⃣💎2️⃣❓"有部分居然超出屏幕外顯示不全了,而Text與第一個分隔線的間距卻比指望中的要大。

6.jpg

​ 這時,咱們能夠手動設置Text的padding屬性,來調整它與Divider的位置關係。同時咱們美化了備選按鈕的背景色的形狀,再更換了功能按鈕的樣式(並利用Spacer視圖幫助佈局),最後還顯式地設置了各區域的高度來適應不一樣的屏幕分辨率,最終的代碼以下。

struct ContentView: View {
    let options = ["🐶", "🐞", "🐦", "🐟"]
    
    var body: some View {
        VStack {
            Text("1️⃣💎2️⃣❓")
                .font(.title)
                .padding(.bottom, -15)
                .frame(height: 30)
            
            Divider()
            
            HStack {
                ForEach(0..<options.count) { index in
                    Button(action: {}) { Text(self.options[index]) }
                        .background(Color.green)
                        .clipShape(Circle())
                }
            }
            .frame(height: 35)
            
            Divider()
            
            HStack {
                Spacer()
                Button(action: { }) { Text("🆘").font(.largeTitle) }
                    .buttonStyle(PlainButtonStyle())
                Spacer()
                Button(action: { }) { Text("⏭").font(.largeTitle) }
                    .buttonStyle(PlainButtonStyle())
                Spacer()
            }
            .frame(height: 35)
        }
    }
}

​ 上面的font、padding、frame、background、clipShape、buttonStyle這些都是View 的 Modifier(修飾器),它們在View聲明以後經過方法調用的方式,做用於原來的View並生成一個新的版本。須要注意的是,SwiftUI 的 Modifier 所形成的佈局影響是嚴格按照順序執行的,好比上面Button的background和clipShape若是順序換一下,將會看到方形的綠色背景按鈕。全部可用的Modifier能夠經過點擊Xcode右上角的"+"按鈕並切換到第二欄中查看:

16.jpg

​ 在38mm到44mm各個模擬器上都運行一下,如今均可以正常顯示了。此外,咱們也能夠經過previewDevice修飾器指定顯示預覽的設備。

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
//        .previewDevice("Apple Watch Series 3 - 38mm")
        .previewDevice("Apple Watch Series 5 - 44mm")
    }
}

7.png9.jpg10.jpg8.jpg

​ 至此,咱們僅使用不到40行代碼,便初步繪製出以前規劃的視圖了。SwiftUI在佈局方面的簡潔與便利可見一斑。固然這只是一個靜態視圖,遊戲還不能真正玩起來。

​ 下一章,咱們將詳細講解SwiftUI中的數據流並完成咱們的遊戲邏輯,敬請期待。

參考內容:

  1. https://developer.apple.com/d...
  2. https://developer.apple.com/c...
  3. https://developer.apple.com/d...
  4. SwiftUI on watchOS:https://developer.apple.com/v...
相關文章
相關標籤/搜索