第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...
相关文章
相关标签/搜索