Dec5de9d13922dfc176516b2604f75f9
SwiftUI 多设备兼容

Session 240 - SwiftUI On All Devices
作者:冬瓜

对于 iOS 开发而言,本次 WWDC 2019 中 SwiftUI 无疑是开发者最关注的一个新的技术。因为 SwiftUI 可能是构建任意平台(iOS、macOS、watchOS、tvOS)的 App的最简方案(The shortest path to building great apps on every device),这里面我们关注的词是每个(Every)。但是对于有经验的人来说,在任何设备上,都有自己相对应的开发框架,例如在 iPhone 和 iPad 上依托于 UIKit,在 macOS 上依托于 AppKit,而在 Apple Watch 和 Apple TV 上分别是 WatchKitTVUIKit。这些 UI 框架是基于各个平台的优势和功能设计的,因此 在界面的呈现上十分自然。但其中也有很多设计和体验上的差异,因此到现在为止,我们无法做到一个设备上的 App 以完全相同的方式迁移到另一台其他场景的设备,直到 SwiftUI 的出现!

总览 SwiftUI

我们使用 SwiftUI 从零构建一款 App,无论你使用的是键盘和鼠标,还是多点触控的显示器,还是 Siri 的遥控器,甚至是手表上的旋钮开关,它都可以满足不同平台上的操作习惯。通过这套约定的开发范式,也有助于我们的设计规范。

拥有这套多端的框架支持,我们可以通过同一表达范式根据平台不同来区分各种场景。例如我现在要在某个界面中构建一个“蓝牙的开关”,我们可以通过相同的描述方式来描述这个控件,但是根据平台呈现出了不同的表现:

当然对于布局方式(Layout)或者其他的控件也是相同的。我们可以通过一种规则来编写多种样式来适配不同的平台。关于更多的开发细节,你可以查看 Session 216 - SwiftUI Essentials 获取到更多 SwiftUI 通用控件的使用方法和介绍。

所以拥有了这些,我们就可以真正的做到完美适配所有设备的场景吗?并不是的,One size does not fit all!没有一种方法可以满足 One size does not fit all,我们借鉴的只有在设计布局上的思路参考及交互逻辑。所有我们提倡的并不是:Write once, run anywhere,而是 Learn once, apply anywhere

下面我们会用这套思想来构建 4 个 App,示例是 Landmark。下图说明了我们的 Landmark 这个产品的主要的需求以及在各个端上的独立 App 需要完成那些功能:

在这个 Session 中我们会更加关注 Apple TV、Mac 和 Apple Watch 端上对于 SwiftUI 的使用。

SwiftUI on Apple TV

Apple TV 可以说是在所有的设备中屏幕最大的,并且 tvOS 的体验不是移动体验,而是与起居室中的用户进行互动体验。所以在进行 Apple TV 的应用开发时,我们要记住这三个要点:

10-英寸的视觉 (10-foot experience)

什么是 10-英寸的视觉呢。用户想要的是在起居室中就可以轻松浏览到的感官体验,强调图像和音频的优质播放,而不是去完成负责的笔记和记事编辑等工作。根据以上使用经验,我们可以梳理出比较重要的几点:

  1. 注意是超大的屏幕;
  2. 远距离的感官;
  3. 长时间的使用;
  4. 同一时刻会有多名用户同时观看。

我们根据以上特点来对部分功能做出取舍:

焦点与 Siri 远程遥控

与电视互动的关键是 Siri 远程遥控。根据其操作的特点,我们需要让我们的界面易被浏览、所有的页面导航分明,这也是 tvOS 应用的重要关键。SwiftUI 的很多原生的控件可以直接切换到 tvOS 风格,采用 TV 风格的 UI,适配焦点选择,使得对应设备可以高度兼容。

struct MyFocusableView: View {
    let canBecomeFocused: Bool
    var body: some View {
    return Text("Hello WWDC")
        .focusable(canBecomeFocused) { isFocused in
            // Focus changed
            // 焦点变化时候调用
        }
        .onPlayPauseCommand {
            // Play/pause button pressed
            // 播放/暂停按钮点击后触发
        }
        .onExitCommand {
            // Menu button pressed
            // 菜单按钮点击后触发
        }
    }
}

简化导航

使用这些专门为 tvOS 平台定制的回调方法,可以轻松地为用户设置逻辑。在电视的大屏中,我们还要充分利用其屏幕自身的宽长来为用户呈现更好的视觉体验,这时候可以使用 TabbedViewNavigationView 配合使用来构建一个良好观感的 tvOS 界面。

// TabbedView and NavigationView on tvOS
struct MainView: View {
    enum Tab { case explore, hikes, tours }
    @State var selection: Tab
    var body: some View {
        // NavigationView 允许内容页面和选择页面的嵌套
        // 其中的 NavigationBar 更容易去了解当前所在的位置
        return NavigationView {
            // TabbedView 适用于多数的 tvOS 交互界面
            TabbedView(selection: $selection) {
                // 其中各类的选项卡适用于 tvOS 应用中对内容的分类筛选
                // 更加容易的让 tvOS 用户去控制并切换类别
                ExploreView().tabItemLabel(Text("Explore")).tag(Tab.explore)
                HikesView().tabItemLabel(Text("Hikes")).tag(Tab.hikes)  
                ToursView().tabItemLabel(Text("Tours")).tag(Tab.tours)
            }
        }
    }
}

tvOS 和 iOS 的导航层次结构

在 iOS 中,我们顶层页面往往是一个 TabbedView,然后根据不同的 tab 来分类不同的内容。然而在 tvOS 中,我们的顶层层级确是 NavigationView,然后用户根据 TabbedView 的类目 tab 深入到各个 Detail 页面中。根据这种层级关系,我们将两个层级用 Swift 代码来描述一下:

如此编写后,我们的 iOS 端和 tvOS 端的界面如下方样式排列:

Demo

虽然我没有尝试将全部的 Landmarks tvOS 应用制作出,但是我制作了地标的列表页面,来尝试了一下 tvOS 的简单页面开发。我门使用官方的 Landmarks 示例工程(这个工程是官方的 Composing Complex Interfaces的完整工程)。

首先创建一个 tvOS App:

然后把 Landmark 中的数据层、资源和公用视图层增加 tvOS App 的 Member Target:

进入 tvOS Target 的 ContentView.swift 就可以编写 SwiftUI 的代码了。我们简单的来做一个横向滑动的 Landmarks 列表,从而利用上 Apple TV 的宽屏优势:

```Swift
import SwiftUI

struct LandmakrsListView : View {
@EnvironmentObject var userData: UserData

var body: some View {
    NavigationView {
        ScrollView {
            HStack {
                ForEach(userData.landmarks) { l in
                    Button(action: {
                        print("Detail View")
                    }) {
                        CircleImage(image: l.image(forSize: 250))
                        Spacer()
                        Text(l.name)
                    }
                }
            }
            .offset(x: 0, y: 30)
        }
        .navigationBarItem(title: Text("Landmarks"))
    }
}

}

if DEBUG

struct LandmakrsListView_Previews : PreviewProvider {
static var previews: some View {
LandmakrsListView()

top Created with Sketch.