当前位置: 移动技术网 > IT编程>移动开发>IOS > SwiftUI学习(一)

SwiftUI学习(一)

2019年07月29日  | 移动技术网IT编程  | 我要评论

总览

如果你想要入门 swiftui 的使用,那 apple 这次给出的绝对给力。这个教程提供了非常详尽的步骤和说明,网页的交互也是一流,是觉得值得看和动手学习的参考。

不过,swiftui 中有一些值得注意的细节在教程里并没有太详细提及,也可能造成一些困惑。这篇文章以我的个人观点对教程的某些部分进行了补充说明,希望能在大家跟随教程学习 swiftui 的时候有点帮助。这篇文章的推荐阅读方式是,一边参照 swiftui 教程实际动手进行实现,一边在到达对应步骤时参照本文加深理解。在下面每段内容前我标注了对应的教程章节和链接,以供参考。

在开始学习 swiftui 之前,我们需要大致了解一个问题:为什么我们会需要一个新的 ui 框架。

为什么需要 swiftui

uikit 面临的挑战

对于 swift 开发者来说,昨天的 wwdc 19 首日 keynote 和 platforms state of the union 上最引人注目的内容自然是 swiftui 的公布了。从 ios sdk 2.0 开始,uikit 已经伴随广大 ios 开发者经历了接近十年的风风雨雨。uikit 的思想继承了成熟的 appkit 和 mvc,在初出时,为 ios 开发者提供了良好的学习曲线。

uikit 提供的是一套符合直觉的,基于控制流的命令式的编程方式。最主要的思想是在确保 view 或者 view controller 生命周期以及用户交互时,相应的方法 (比如 viewdidload 或者某个 target-action 等) 能够被正确调用,从而构建用户界面和逻辑。不过,不管是从使用的便利性还是稳定性来说,uikit 都面临着巨大的挑战。我个人勉强也能算是 ios 开发的“老司机”了,但是「掉到 uikit 的坑里」这件事,也几乎还是我每天的日常。uikit 的基本思想要求 view controller 承担绝大部分职责,它需要协调 model,view 以及用户交互。这带来了巨大的 side effect 以及大量的状态,如果没有妥善安置,它们将在 view controller 中混杂在一起,同时作用于 view 或者逻辑,从而使状态管理愈发复杂,最后甚至不可维护而导致项目失败。不仅是作为开发者我们自己写的代码,uikit 本身内部其实也经常受困于可变状态,各种奇怪的 bug 也频频出现。

声明式的界面开发方式

近年来,随着编程技术和思想的进步,使用声明式或者函数式的方式来进行界面开发,已经越来越被接受并逐渐成为主流。最早的思想大概是来源于 elm,之后这套方式被 react 和 flutter 采用,这一点上 swiftui 也几乎与它们一致。总结起来,这些 ui 框架都遵循以下步骤和原则:

  1. 使用各自的 dsl 来描述「ui 应该是什么样子」,而不是用一句句的代码来指导「要怎样构建 ui」。

    比如传统的 uikit,我们会使用这样的代码来添加一个 “hello world” 的标签,它负责“创建 label”,“设置文字”,“将其添加到 view 上”:

     func viewdidload() {
         super.viewdidload()
         let label = uilabel()
         label.text = "hello world"
         view.addsubview(label)
         // 省略了布局的代码
     }
    

    而相对起来,使用 swiftui 我们只需要告诉 sdk 我们需要一个文字标签:

     var body: some view {
         text("hello world")
     }
    
  2. 接下来,框架内部读取这些 view 的声明,负责将它们以合适的方式绘制渲染。

    注意,这些 view 的声明只是纯数据结构的描述,而不是实际显示出来的视图,因此这些结构的创建和差分对比并不会带来太多性能损耗。相对来说,将描述性的语言进行渲染绘制的部分是最慢的,这部分工作将交由框架以黑盒的方式为我们完成。

  3. 如果 view 需要根据某个状态 (state) 进行改变,那我们将这个状态存储在变量中,并在声明 view 时使用它:

     @state var name: string = "tom"
     var body: some view {
         text("hello \(name)")
     }
    

    关于代码细节可以先忽略,我们稍后会更多地解释这方面的内容。

  4. 状态发生改变时,框架重新调用声明部分的代码,计算出新的 view 声明,并和原来的 view 进行差分,之后框架负责对变更的部分进行高效的重新绘制。

swiftui 的思想也完全一样,而且实际处理也不外乎这几个步骤。使用描述方式开发,大幅减少了在 app 开发者层面上出现问题的机率。

一些细节解读

中对声明式 ui 的编程思想有深刻的体现。另外,swiftui 中也采用了非常多 swift 5.1 的新特性,会让习惯了 swift 4 或者 5 的开发者“耳目一新”。接下来,我会分几个话题,对官方教程的一些地方进行解释和探索。

教程 1 - creating and combining views

section 1 - step 3: swiftui app 的启动

创建 app 之后第一件好奇的事情是,swiftui app 是怎么启动的。

教程示例 app 在 appdelegate 中通过 application(_:configurationforconnecting:options) 返回了一个名为 “default configuration” 的 uisceneconfiguration 实例:

func application(
    _ application: uiapplication,
    configurationforconnecting connectingscenesession: uiscenesession,
    options: uiscene.connectionoptions) -> uisceneconfiguration
{
    return uisceneconfiguration(name: "default configuration", sessionrole: connectingscenesession.role)
}

这个名字的 configuration 在 info.plist 的 “uiapplicationscenemanifest -> uisceneconfigurations” 中进行了定义,指定了 scene session delegate 类为 $(product_module_name).scenedelegate。这部分内容是 ios 13 中新加入的通过 scene 管理 app 生命周期的方式,以及多窗口支持部分所需要的代码。这部分不是我们今天的话题。在 app 完成启动后,控制权被交接给 scenedelegate,它的 scene(_:willconnectto:options:) 将会被调用,进行 ui 的配置:

func scene(
        _ scene: uiscene,
        willconnectto session: uiscenesession,
        options connectionoptions: uiscene.connectionoptions)
    {
        let window = uiwindow(frame: uiscreen.main.bounds)
        window.rootviewcontroller = uihostingcontroller(rootview: contentview())
        self.window = window
        window.makekeyandvisible()
    }

这部分内容就是标准的 ios app 启动流程了。uihostingcontroller 是一个 uiviewcontroller 子类,它将负责接受一个 swiftui 的 view 描述并将其用 uikit 进行渲染 (在 ios 下的情况)。uihostingcontroller 就是一个普通的 uiviewcontroller,因此完全可以做到将 swiftui 创建的界面一点点集成到已有的 uikit app 中,而并不需要从头开始就是基于 swiftui 的构建。

由于 swift abi 已经稳定,swiftui 是一个搭载在用户 ios 系统上的 swift 框架。因此它的最低支持的版本是 ios 13,可能想要在实际项目中使用,还需要等待一两年时间。

section 1 - step 4: 关于 some view

struct contentview: view {
    var body: some view {
        text("hello world")
    }
}

一眼看上去可能会对 some 比较陌生,为了讲明白这件事,我们先从 view 说起。

view 是 swiftui 的一个最核心的协议,代表了一个屏幕上元素的描述。这个协议中含有一个 associatedtype:

public protocol view : _view {
    associatedtype body : view
    var body: self.body { get }
}

这种带有 associatedtype 的协议不能作为类型来使用,而只能作为类型约束使用:

// error
func createview() -> view {

}

// ok
func createview<t: view>() -> t {

}

这样一来,其实我们是不能写类似这种代码的:

// error,含有 associatedtype 的 protocol view 只能作为类型约束使用
struct contentview: view {
    var body: view {
        text("hello world")
    }
}

想要 swift 帮助自动推断出 view.body 的类型的话,我们需要明确地指出 body 的真正的类型。在这里,body 的实际类型是 text

struct contentview: view {
    var body: text {
        text("hello world")
    }
}

当然我们可以明确指定出 body 的类型,但是这带来一些麻烦:

  1. 每次修改 body 的返回时我们都需要手动去更改相应的类型。
  2. 新建一个 view 的时候,我们都需要去考虑会是什么类型。
  3. 其实我们只关心返回的是不是一个 view,而对实际上它是什么类型并不感兴趣。

some view 这种写法使用了 swift 5.1 的 opaque return types 特性。它向编译器作出保证,每次 body 得到的一定是某一个确定的,遵守 view 协议的类型,但是请编译器“网开一面”,不要再细究具体的类型。返回类型确定单一这个条件十分重要,比如,下面的代码也是无法通过的:

let somecondition: bool

// error: function declares an opaque return type, 
// but the return statements in its body do not have 
// matching underlying types.
var body: some view {
    if somecondition {
        // 这个分支返回 text
        return text("hello world")
    } else {
        // 这个分支返回 button,和 if 分支的类型不统一
        return button(action: {}) {
            text("tap me")
        }
    }
}

这是一个编译期间的特性,在保证 associatedtype protocol 的功能的前提下,使用 some 可以抹消具体的类型。这个特性用在 swiftui 上简化了书写难度,让不同 view 声明的语法上更加统一。

section 2 - step 1: 预览 swiftui

swiftui 的 preview 是 apple 用来对标 rn 或者 flutter 的 hot reloading 的开发工具。由于 ibdesignable 的性能上的惨痛教训,而且得益于 swiftui 经由 uikit 的跨 apple 平台的特性,apple 这次选择了直接在 macos 上进行渲染。因此,你需要使用搭载有 swiftui.framework 的 macos 10.15 才能够看到 xcode previews 界面。

xcode 将对代码进行静态分析 (得益于 swiftsyntax 框架),找到所有遵守 previewprovider 协议的类型进行预览渲染。另外,你可以为这些预览提供合适的数据,这甚至可以让整个界面开发流程不需要实际运行 app 就能进行。

笔者自己尝试下来,这套开发方式带来的效率提升相比 hot reloading 要更大。hot reloading 需要你有一个大致界面和准备相应数据,然后运行 app,停在要开发的界面,再进行调整。如果数据状态发生变化,你还需要 restart app 才能反应。swiftui 的 preview 相比起来,不需要运行 app 并且可以提供任何的 dummy 数据,在开发效率上更胜一筹。

经过短短一天的使用,option + command + p 这个刷新 preview 的快捷键已经深入到我的肌肉记忆中了。

section 3 - step 5: 关于 viewbuilder

创建 stack 的语法很有趣:

vstack(alignment: .leading) {
    text("turtle rock")
        .font(.title)
    text("joshua tree national park")
        .font(.subheadline)
}

一开始看起来好像我们给出了两个 text,似乎是构成的是一个类似数组形式的 [view],但实际上并不是这么一回事。这里调用了 vstack 类型的初始化方法:

public struct vstack<content> where content : view {
    init(
        alignment: horizontalalignment = .center, 
        spacing: length? = nil, 
        content: () -> content)
}

前面的 alignment 和 spacing 没啥好说,最后一个 content 比较有意思。看签名的话,它是一个 () -> content 类型,但是我们在创建这个 vstack 时所提供的代码只是简单列举了两个 text,而并没有实际返回一个可用的 content

这里使用了 swift 5.1 的另一个新特性:funtion builders。如果你实际观察 vstack 的,会发现 content 前面其实有一个 @viewbuilder 标记:

init(
    alignment: horizontalalignment = .center, 
    spacing: length? = nil, 
    @viewbuilder content: () -> content)

而 viewbuilder 则是一个由 @_functionbuilder 进行标记的 struct:

@_functionbuilder public struct viewbuilder { /* */ }

使用 @_functionbuilder 进行标记的类型 (这里的 viewbuilder),可以被用来对其他内容进行标记 (这里用 @viewbuilder 对 content 进行标记)。被用 function builder 标记过的 viewbuilder 标记以后,content 这个输入的 function 在被使用前,会按照 viewbuilder 中合适的 buildblock 进行 build 后再使用。如果你阅读 viewbuilder的,会发现有很多接受不同个数参数的 buildblock 方法,它们将负责把闭包中一一列举的 text 和其他可能的 view 转换为一个 tupleview,并返回。由此,content 的签名 () -> content 可以得到满足。

实际上构建这个 vstack 的代码会被转换为类似下面这样:

// 等效伪代码,不能实际编译。
vstack(alignment: .leading) { viewbuilder -> content in
    let text1 = text("turtle rock").font(.title)
    let text2 = text("joshua tree national park").font(.subheadline)
    return viewbuilder.buildblock(text1, text2)
}

当然这种基于 funtion builder 的方式是有一定限制的。比如 viewbuilder 就只实现了最多的 buildblock,因此如果你在一个 vstack 中放超过十个 view 的话,编译器就会不太高兴。不过对于正常的 ui 构建,十个参数应该足够了。如果还不行的话,你也可以考虑直接使用 tupleview 来用多元组的方式合并 view

tupleview<(text, text)>(
    (text("hello"), text("hello"))
)

除了按顺序接受和构建 view 的 buildblock 以外,viewbuilder 还实现了两个特殊的方法:buildeither 和 buildif。它们分别对应 block 中的 if...else 的语法和 if 的语法。也就是说,你可以在 vstack 里写这样的代码:

var somecondition: bool

vstack(alignment: .leading) {
    text("turtle rock")
        .font(.title)
    text("joshua tree national park")
        .font(.subheadline)
    if somecondition {
        text("condition")
    } else {
        text("not condition")
    }
}

其他的命令式的代码在 vstack 的 content 闭包里是不被接受的,下面这样也不行:

vstack(alignment: .leading) {
    // let 语句无法通过 function builder 创建合适的输出
    let somecondition = model.condition
    if somecondition {
        text("condition")
    } else {
        text("not condition")
    }
}

到目前为止,只有以下三种写法能被接受 (有可能随着 swiftui 的发展出现别的可接受写法):

  • 结果为 view 的语句
  • if 语句
  • if...else... 语句

section 4 - step 7: 链式调用修改 view 的属性

教程到这一步的话,相信大家已经对 swiftui 的超强表达能力有所感悟了。

var body: some view {
    image("turtlerock")
        .clipshape(circle())
        .overlay(
            circle().stroke(color.white, linewidth: 4))
        .shadow(radius: 10)
}

可以试想一下,在 uikit 中要动手撸一个这个效果的困难程度。我大概可以保证,99% 的开发者很难在不借助文档或者 copy paste 的前提下完成这些事情,但是在 swiftui 中简直信手拈来。在创建 view 之后,用链式调用的方式,可以将 view 转换为一个含有变更后内容的对象。这么说比较抽象,我们可以来看一个具体的例子。比如简化一下上面的代码:

let image: image = image("turtlerock")
let modified: _modifiedcontent<image, _shadoweffect> = image.shadow(radius: 10)

image 通过一个 .shadow 的 modifier,modified 变量的类型将转变为 _modifiedcontent<image, _shadoweffect>。如果你查看 view 上的 shadow 的定义,它是这样的:

extension view {
    func shadow(
        color: color = color(.srgblinear, white: 0, opacity: 0.33), 
        radius: length, x: length = 0, y: length = 0) 
    -> self.modified<_shadoweffect>
}

modified 是 view 上的一个 typealias,在 struct image: view 的实现里,我们有:

public typealias modified<t> = _modifiedcontent<self, t>

_modifiedcontent 是一个 swiftui 的私有类型,它存储了待变更的内容,以及用来实施变更的 modifier

struct _modifiedcontent<content, modifier> {
    var content: content
    var modifier: modifier
}

在 content 遵守 viewmodifier 遵守 viewmodifier 的情况下,_modifiedcontent 也将遵守 view,这是我们能够通过 view 的各个 modifier extension 进行链式调用的基础:

extension _modifiedcontent : _view 
    where content : view, modifier : viewmodifier 
{
}

在 shadow 的例子中,swiftui 内部会使用 _shadoweffect 这个 viewmodifier,并把 image 自身和 _shadoweffect实例存放到 _modifiedcontent 里。不论是 image 还是 modifier,都只是对未来实际视图的描述,而不是直接对渲染进行的操作。在最终渲染前,viewmodifier 的 body(content: self.content) -> self.body 将被调用,以给出最终渲染层所需要的各个属性。

更具体来说,_shadoweffect 是一个满足 environmentalmodifier 协议的类型,这个协议要求在使用前根据使用环境将自身解析为具体的 modifier。

其他的几个修改 view 属性的链式调用与 shadow 的原理几乎一致。

小结

上面是对 swiftui 的第一部分进行的一些说明,在之后的一篇文章里,我会对剩余的几个教程中有意思的部分再做些解释。

虽然公开还只有一天,但是 swiftui 已经经常被用来和 flutter 等框架进行比较。试用下来,在 view 的描述表现力上和与 app 的结合方面,swiftui 要胜过 flutter 和 dart 的组合很多。swift 虽然开源了,但是 apple 对它的掌控并没有减弱。swift 5.1 的很多特性几乎可以说都是为了 swiftui 量身定制的,我们已经在本文中看到了一些例子,比如 opaque return types 和 function builder 等。在接下来对后面几个教程的解读中,我们还会看到更多这方面的内容。

另外,apple 在背后使用 combine.framework 这个响应式编程框架来对 swiftui.framework 进行驱动和数据绑定,相比于现有的 rxswift/rxcocoa 或者是 reactiveswift 的方案来说,得到了语言和编译器层级的大力支持。如果有机会,我想我也会对这方面的内容进行一些探索和介绍。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

  • ios uicollectionview实现横向滚动

    现在使用卡片效果的app很多,之前公司让实现一种卡片效果,就写了一篇关于实现卡片的文章。文章最后附有demo实现上我选择了使用uicollectionview ... [阅读全文]
  • iOS UICollectionView实现横向滑动

    本文实例为大家分享了ios uicollectionview实现横向滑动的具体代码,供大家参考,具体内容如下uicollectionview的横向滚动,目前我使... [阅读全文]
  • iOS13适配深色模式(Dark Mode)的实现

    iOS13适配深色模式(Dark Mode)的实现

    好像大概也许是一年前, mac os系统发布了深色模式外观, 看着挺刺激, 时至今日用着也还挺爽的终于, 随着iphone11等新手机的发售, ios 13系统... [阅读全文]
  • ios 使用xcode11 新建项目工程的步骤详解

    ios 使用xcode11 新建项目工程的步骤详解

    xcode11新建项目工程,新增了scenedelegate这个类,转而将原appdelegate负责的对ui生命周期的处理担子接了过来。故此可以理解为:ios... [阅读全文]
  • iOS实现转盘效果

    本文实例为大家分享了ios实现转盘效果的具体代码,供大家参考,具体内容如下demo下载地址: ios转盘效果功能:实现了常用的ios转盘效果,轮盘抽奖效果的实现... [阅读全文]
  • iOS开发实现转盘功能

    本文实例为大家分享了ios实现转盘功能的具体代码,供大家参考,具体内容如下今天给同学们讲解一下一个转盘选号的功能,直接上代码直接看viewcontroller#... [阅读全文]
  • iOS实现轮盘动态效果

    本文实例为大家分享了ios实现轮盘动态效果的具体代码,供大家参考,具体内容如下一个常用的绘图,主要用来打分之类的动画,效果如下。主要是ios的绘图和动画,本来想... [阅读全文]
  • iOS实现九宫格连线手势解锁

    本文实例为大家分享了ios实现九宫格连线手势解锁的具体代码,供大家参考,具体内容如下demo下载地址:效果图:核心代码://// clockview.m// 手... [阅读全文]
  • iOS实现卡片堆叠效果

    本文实例为大家分享了ios实现卡片堆叠效果的具体代码,供大家参考,具体内容如下如图,这就是最终效果。去年安卓5.0发布的时候,当我看到安卓全新的material... [阅读全文]
  • iOS利用余弦函数实现卡片浏览工具

    iOS利用余弦函数实现卡片浏览工具

    本文实例为大家分享了ios利用余弦函数实现卡片浏览工具的具体代码,供大家参考,具体内容如下一、实现效果通过拖拽屏幕实现卡片移动,左右两侧的卡片随着拖动变小,中间... [阅读全文]
验证码:
移动技术网