系列文章深度解读|SwiftUI 背后那些事儿

发表于 3年以前  | 总阅读数:358 次

今年苹果的WWDC你看了吗?苹果在2019年的WWDC的重头戏当然非SwiftUI莫属:全新的声明式语法、绑定式API、和响应式变成框架Combine。这一切的一切都预示着即将在Apple Native布局系统掀起一场革命。为此,苹果在很多方面都做了努力,这才促成了SwiftUI现在的样子。想要了解Swift的新特性、SwiftUI数据流和SwiftUI布局系统等新知识吗?一起来看吧。

Swift 5.1 新语法

单表达式隐式返回值

在 Swift 5.0 之前的语法中,如果一个闭包表达式只有一个表达式,那么可以省略 <span style="font-size: 14px;">return 关键字。 现在 Swift 5.1 以后的版本中计算属性和函数语句同样适用。 如:

// before swift 5.0 

struct Rectangle {
    var width = 0.0, height = 0.0
    var area1: Double {
        return width * height
    }

    func area2() -> Double {
        return width * height 
    }
}

// after switft 5.1
struct Rectangle {
    var width = 0.0, height = 0.0
    var area1: Double { width * height }

    func area2() -> Double { width * height }
}

关于这个新特性的完整提案可以参考(https://github.com/apple/Swift-evolution/blob/master/proposals/0255-omit-return.md)

根据结构体默认成员合成默认初始化器

在 Swift 5.0 之前结构体声明,编译器会默认生成一个逐一成员初始化器,同时如果都有默认值,还会生成一个无参的初始化器。 但如果此时结构体成员属性过多,且较多都有默认值,则只能使用逐一成员初始化器,会使每处调用的地方写法过于冗余,在传统 OOP 语言中可以使用 <span style="font-size: 14px;">Builder 模式解决, 但在 Swift 5.1 之后编译器会按需合成初始化器,避免初始化写法的冗余。 如:

struct Dog {
    var name = "Generic dog name"
    var age = 0
}
let boltNewborn = Dog()
let daisyNewborn = Dog(name: "Daisy", age: 0)
// before swift 5.0 ❎
let benjiNewborn = Dog(name: "Benji")
// after switft 5.1 ✅
let benjiNewborn = Dog(name: "Benji")

关于这个新特性的完整提案可以参考(https://github.com/apple/Swift-evolution/blob/master/proposals/0242-default-values-memberwise.md)

字符串插入运算符新设计

这个特性主要扩大了字符串插入运算符的使用范围,以前我们只能用在 String 的初始化中,但是不能在参数处理中使用字符串插入运算符。 在以前的语法中只能分开书写,虽然没什么大问题,但总归要多一行代码,现在可以直接使用了, 尤其是对于 SwiftUI,Text 控件就使用到了这种新语法,可以使我们在单行表达式中即可初始化 Tetx。


// before swift 5.0 ❎
let quantity = 10
label.text = NSLocalizedString(
    "You have \(quantity) apples,
    comment: "Number of apples"
)

label.text = String(format: formatString, quantity)

// after switft 5.1 ✅
let quantity = 10
return Text(.
"You have \(quantity) apples"
).

// 实际上编译器会翻译为如下几句
var builder = LocalizedStringKey.StringInterpolation(
    literalCapacity: 16, interpolationCount: 1
)
builder.appendLiteral("You have ")
builder.appendInterpolation(quantity)
builder.appendLiteral(" apples")
LocalizedStringKey(stringInterpolation: builder)

关于这个新特性的完整提案可以参考(https://github.com/apple/Swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md)

属性包装器

当我们在一个类型中声明计算属性时,大部分属性的访问和获取都是有相同的用处,这些代码是可抽取的,如我们标记一些用户偏好设置,在计算属性的设置和获取中直接代理到 UserDefault的实现中,我们可以通过声明 @propertyWarpper 来修饰,可以减少大量重复代码。 在 SwiftUI中, @State`` @EnviromemntObject`` @bindingObject`` @Binding 都是通过属性包装器代理到 SwiftUI 框架中使其自动响应业务状态的变化。

// before swift 5.0 ❎
struct User {
    static var usesTouchID: Bool {
        get {
            return UserDefaults.standard.bool(forKey: "USES_TOUCH_ID")
        }
        set {
            UserDefaults.standard.set(newValue, forKey: "USES_TOUCH_ID")
        }
    }
    static var isLoggedIn: Bool {
        get {
            return UserDefaults.standard.bool(forKey: "LOGGED_IN")
        }
        set {
            UserDefaults.standard.set(newValue, forKey: "LOGGED_IN")
        }
    }
}

// after switft 5.1 ✅
@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    init(_ key: String, defaultValue: T) {
        print("UserDefault init")
        self.key = key
        self.defaultValue = defaultValue
        UserDefaults.standard.register(defaults: [key: defaultValue])
    }
    var value: T {
        get {
            print("getter")
            return UserDefaults.standard.object(forKey: key) as? T ??  defaultValue
        }
        set {
            print("setter")
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}
struct User2 {
    @UserDefault("USES_TOUCH_ID", defaultValue: false)
    static var usesTouchID: Bool
    @UserDefault("LOGGED_IN", defaultValue: false)
    var isLoggedIn: Bool
}

print("hello world")
let user = User2()
User2.usesTouchID = true
let delegate = User2.$usesTouchID
print("\(delegate)")
let detelate2 = user.$isLoggedIn

实际上属性包装器是在编译时期翻译为以下的代码, 并且编译器禁止使用 <span style="font-size: 14px;">$ 开头的标识符。

struct User2 {
    static var $usesTouchID = UserDefault<Bool>("USES_TOUCH_ID", defaultValue: false)
    static var usesTouchID: Bool {
        set {
            $usesTouchID.value = newValue
        }
        get {
            $usesTouchID.value 
        }
    }
    @UserDefault("LOGGED_IN", defaultValue: false)
    var isLoggedIn: Bool
}

使用属性包装器的好处除了可以减少重复代码,Swift Runtime 还保证了以下几点:

  • 对于实例的属性包装器是即时加载的
  • 对于类属性的属性保证器是懒加载的
  • 属性包装器是线程安全的
  • 通过 <span style="font-size: 14px;">$ 运算符可以获取到原始的属性包装器实例,这大量使用在 SwiftUI 的数据依赖中

关于这个新特性的完整提案可以参考(https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md)

注:在目前的 Beta 版本中 @PropertyDelegate@PropertyWrapper 是一致的,正式版本发布会应该只会保留一种语法。

不透明返回类型

在 Swift 5.0 之前我们如果想返回抽象类型一般使用 Generic Type 或者 Protocol, 使用泛型会显示的暴露一些信息给 API 使用者,不是完整的类型抽象。 但是使用 Protocol 也有几个限制: 泛型返回值在运行时都是一个容器,效率较差,返回值不能调用自身类型的方法,协议不允许拥有关联类型,由于编译时丢失了类型信息,编译器无法推断类型,导致无法使用 == 运算符。

在 Swift 5.1 中新增了 opaque result type,这个特性使用 some 修饰协议返回值,具有以下特性:

  • 所有的条件分支只能返回一个特定类型,不同则会编译报错
  • 方法使用者依旧无法知道类型,(使用方不透明)
  • 编译器知情具体类型,因此可以使用类型推断。
// before swift 5.0 ❎
public protocol View : _View {
    associatedtype Body : View

    var body: Self.Body { get }
}
// compile error
func getText() -> View {
    return Text("") 
}

func getText<T: Text>() -> T {
    return Text("")
}
// after switft 5.1 ✅
struct ContentView: View {
    var body: some View {
        Text("")
    }
}

关于这个新特性的完整提案可以参考(https://github.com/apple/Swift-evolution/blob/master/proposals/0244-opaque-result-types.md)

Swift Style DSL / Function Builder

此项提案是一个比较特殊的提案,原因在于目前还在审核中,并没有正式加入 Swift 语言中,但是 Xcode11 自带的 Swift 5.1 已经集成了这项语法. 使用 Function Builder 可以使表达式语句隐含的返回在函数返回值中, 这样可以在嵌套逻辑和返回值表达式的 DSL 中十分具有表现力。如下面的语句是等同的:

// Original source code:
@TupleBuilder
func build() -> (Int, Int, Int) {
  1
  2
  3
}

// This code is interpreted exactly as if it were this code:
func build() -> (Int, Int, Int) {
  let _a = 1
  let _b = 2
  let _c = 3
  return TupleBuilder.buildBlock(_a, _b, _c)
}

在 SwiftUI 中的所有布局类控件,几乎全部使用了 Function Builder 特性,可读性要远远大于 Flutter 的 DSL语法,以下是一个苹果 WWDC Session 的例子。

head { 
    meta().charset("UTF-8")
  if cond { 
    title("Title 1")
  } else { 
    title("Title 2")
  } 
}

实际上会被翻译为
head {
  let a: HTML = meta().charset("UTF-8") 
  let d: HTML
  if cond {
    let b: HTML = title("Title 1")
    d = HTMLBuilder.buildEither(first: b) 
  } else {
    let c: HTML = title("Title 2")
    d = HTMLBuilder.buildEither(second: c) 
  }
  return HTMLBuilder.buildBlock(a, d) 
}

可读性大大加强,但值得注意的是目前在 SwiftUI 中使用 Function Builder 实现的 ViewBuilder 功能仅仅支持 10 个泛型参数,在 SwiftUI 中,如果同级别元素中,超过 10 个则会有奇怪的编译错误。

这点官方推荐使用组合降低 View 结构,这点和 Flutter 的推荐写法不同,超过 10 个 则会编译错误 -_-。

目前这份提议[function-builders.md](https://github.com/apple/swift-evolution/blob/9992cf3c11c2d5e0ea20bee98657d93902d5b174/proposals/XXXX-function-builders.md),还在草案阶段,所以实现是一份下划线关键字。

不排除苹果会在 9月份正式版本来临之前稍加修改语法,但相信这份功能的放开也会在正式版本发布之期修改完毕。

其他新特性

本文主要介绍了一些新的语法特性,且大部分都和 SwiftUI 相关,从 WWDC 的Session 演讲者都透露出 Swift 语言组的核心目标,Make Your Swift API Better

主要从以下几个体现。

  • Expressive 有表现力
  • Clear 清晰没二义性
  • Easy to use 简单易用

本文还有大量的 Swift 5.1 新特性没有提到,如 @dynamicCallable`` @dynamicMemberLookup`` WritableKeyPath 有兴趣的读者可以参考 Swift的完整演变提案 swift-evolution。(https://github.com/apple/swift-evolution/tree/master/proposals)

★ Swift 从 3.x Attribute

Swift 自 3.0 版本至今添加了很多 属性(Attribut) 标记,这些 Attribute 通过给编译器提供信息,增强了 Swift 的元编程能力,如果读者有兴趣可以参考这份完整的正式版本 Attribute大全。(https://docs.swift.org/swift-book/ReferenceManual/Attributes.html)

Swift/SwiftUI API Design Guide

Swift 已经经过 9(4年内部孵化) 个年头的发展,语言的设计指南已经区域问题,其中涉及到了很多不同于其他语言的观点,今年的 Session 着重的梳理了以下几点。

值类型和引用类型

当你设计一个数据结构时,优先选择结构体或者枚举,它们都是值类型,值类型具有以下几个有点。

1.值类型在栈上分配,性能要远远大于引用类型,且 Swift Runtime 有 COW 优化。

2.值类型没有引用计数,不会引起奇怪的多线程安全问题。

3.值类型的存储属性是扁平化的,避免在类继承情况下一个子类继承过多的存储属性导致实例在内存中过大,如 SwiftUI 使用 Modifier的结构体优化设计。

那什么时候我们才需要使用引用类型呢? 只有当以下几个场景存在时才有必要使用。

  • 你需要引用计数和构造和析构的时机。
  • 数据需要集中管理或共享,如单例或者数据缓存实例等。
  • ID 语义和 Equal语义冲突的时候。

协议还是泛型

随着 Swift 中 POP 的流行,很多人涉及 API 越来越习惯使用 协议,但是官方指出设计的要点, 设计是演进的而不是一步到位的,应该遵守以下步骤。

1、不要直接声明协议

2、从实际的业务场景出发

3、从中抽象出公用代码

4、从已有的协议中组合协议,而不是重新构建一套新的协议结构

DynamicMemberLookup & dynamicCallable

这个语法其实是 Swift 4.2 的语法了,但是在 Xcode11 中, IDE进一步获得了增强,可以轻松获得代码提示,如果使用过 ruby 和 python的开发者,可以轻易地理解,把一个实例当方法调用, 静态语言在保证安全的前提下可以通过编译,并可以动态调用,相信未来在和其他语言互通时可以大方光彩。

抽象数据访问

前面已经介绍过来 Swift 5.1 新增语法 PropertyWarpper, 在 Swift 中很多计算属性都围绕着数据访问如 懒加载,防御性复制,TLS数据区,而它们的模式都是一致的, 多使用 PropertyWarpper 可以抽象公用代码,加强 API 语义。 在 SwiftUI 中核心的数据流,均使用 PropertyWarpper 如 @Binding`` @State`` @EnviromentObject`` @Enviroment。

<span style="font-size: 14px;"><br></br>

SwiftUI 360° 分析

在 SwiftUI 中我们不再使用传统的 Cocoa 命令式布局系统,该用声明式布局,那么到底什么事声明式布局?什么是命令式布局?。 举个例子,假设你要设计如下布局。

在命令式布局系统中,你要做以下事情,

  • 初始化一个 ImageView 实例
  • 设置它的位置信息
  • 设置它的缩放级别
  • 把它添加到当前的视图树中
  • 如果有事件,设置下事件的代理或者回掉

开发者要做的事情繁多且易错,开发效率极为原始低下,但在声明式时代你只需要描述一下信息。

屏幕上有个图片,在什么位置,是什么缩放比例即可, 至于怎么布局,一切交给框架,Coder do less, Framework do more。

至于声明式编程和命令式编程,可以参考维基百科 声明式编程 命令式编程。 实际上声明式编程早在上个世纪就已经提出并且演化很久,和命令式不同的是,声明式编程一般要求框架做的事情非常多,在早期计算机性能一般的年代,声明式编程并未大火, 一般多用于解析 DSL (Domain Specific Language) 中,比如早期的 HTML 布局,SQL 语言等。

顺便提下早在 2006 年微软便提出 WPF 框架,其中使用 XAML 语言编写声明式 UI 代码,同时支持事件/数据绑定机制,配合宇宙第一 IDE Visual Studio 强大的拖拉拽功能,开发者体验爽到机制。 但是遗憾的是 Window Phone 并未在移动操作系统平台争得一番天地,以至于很多人对声明式 UI 不甚了解。

声明式 UI 会是未来吗?

结合最近多年大火的前端大火的框架 React 移动平台的 Reactive Native`` WeexFlutter 包括暂时只能编写Native平台的 SwiftUIJetpack Compose. 多方的数据论证和流行度可以说明,声明式编程在 UI 布局方面有得天独厚的优势, 相信未来 UI 布局也会是声明式编程的天下。

SwiftUI 中的 View

对于传统的的 Cocoa 编程中,一个 View 可能是一个 UIView 也可能是一个 NSView 代表了屏幕上可视的元素,且依赖于对应的平台。 而在 SwiftUI 的实现中 <span style="font-size: 14px;">A View Defines a Piece of UI 一个 View 是一个真实屏幕上可见元素的描述,在不同的平台可以是 UIView 也可以说 NSView 或 其他实现, View 是跨平台的描述信息,底层的实现被封装在框架内部实现。

SwiftUI View & Modifier

在传统的命令式编程布局系统中,我们对一些 UI 系统结构是通常是通过继承实现的,再编写代码时通过对属性的调用来修改视图的外观,如颜色透明度等。 但这会带来导致类继承结构比较复杂,如果设计不够好会造成 OOP 的通病类爆炸,并且通过继承来的数据结构,子类会集成父类的存储属性,会导致子类实例在内存占据比较庞大,即便很多属性都是默认值并不使用。 如图:

在 SwiftUI 中奖视图的修饰期抽象为 Modifier, Modifier通常只含有 1-2 个存储属性,通过原始控件的 Extension 可以在视图定义中添加各种 Modifier,它们在运行时的实现通常是一个闭包,在运行时 SwiftUI 构建出真实的视图。

★ SwiftUI 元控件

在 SwiftUI 系统中我们使用结构体遵守 View 协议,通过组合现有的控件描述,实现 Body 方法,但 Body 的方法会不会无限递归下去?

在 SwiftUI 系统中定义了 6 个元/主 View Text`` Color`` Spacer`` Image`` Shape`` Divider, 它们都不遵守 View 协议,只是基本的视图数据结构。

其他常见的视图组件都是通过组合元控件和修饰器来组合视图结构的。如 Button`` Toggle 等。

关于 SwiftUI 的视图和修饰器可以参考 Github 整理的速查表Jinxiansen/SwiftUI。(https://github.com/Jinxiansen/SwiftUI/blob/master/README_CN.md#Text)

DataFlow in SwiftUI

任何程序都不可能是静态的,充满着数据状态,函数充满着副作用,传统的命令式编程通过成员变量来管理状态,这使得状态的复杂度成指数级增长。举一个最简单的例子。假设一个视图只有 4 种状态,组合起来就有16种, 但是人脑处理状态的复杂度是有限的,状态的复杂度一旦超过人脑的复杂度,就会产生大量的 Bug,并且修掉了这个产生了那个,试问哪位同学看见一个类充斥着几十上百个成员属性和全局变量不想拍桌子的。

在 Flutter 中控件分为StateLessStateFull 控件,数据流是单向的,随之诞生了 Bloc`` RxDart`` Redux 许多框架,虽然作者本人是 SwiftUI 的粉丝,但不得不说 Flutter 的很多思想非常先进。 这里简单分析下 SwiftUI 中如何处理完们的数据流。 这里先放出数据流的原则,我们关注两个点 Source of Truth 是指我们真是的业务逻辑数据,Dervied Value 是指 SwiftUI 框架中使用的数据。

★ Constant

通常部分 UI 数据是不可改变的,比如一个 Text 的 Color, 这部分我们可以直接使用 Modifier 的构造器讲属性传递进去即可,这里就不做多解释了。

Text("Hello world")
                .color(.red)

★ @State

那在某些情况下我们某些 UI 元素会有一系列的响应事件,会导致视图发现变化,那么 SwiftUI 是怎么做到的? 就是通过前面的新语法 Property warpper, 对所有使用 State 标记的属性 都会代理到 SwiftUI 中 State 的方法实现,同时框架,计算Diff,刷新界面。 顺便这里强调下在 SwiftUI 中 Views area a function of State not of a sequence of Event, View 是真实视图的状态,并不是一系列变化的事件。

struct PlayerView: View {
    let episode: Episode
    @State private var isPlaying = false
    var body: some View {
        VStack {
            Text(episode.title)
            Button(action: {
                self.isPlaying.toggle()
            }) {
                Image(systemName: isPlaying ? "pause.cicle" : "play.circle")
            }
        }
    }
}

★ @Binding

再很多时候我们会总归抽象减少我们的代码长度,比如将上文中的 Button 抽象为一个 PlayerButton , 这时候存在一个问题,State 属性我们是再重新声明一份吗?

struct PlayButton: View {
    @State private var isPlaying = false
    var body: some View {
        Button(action: {
            self.isPlaying.toggle()
        }) {
            Image(systemName: isPlaying ? "pause.cicle" : "play.circle")
        }
    }
}

这样写会有一个问题,我们会产生两个 Derived Value,虽然两者都可通知 SwiftUI 做界面刷新,但是 PlayerViewPlayerButton 的数据同步又成了问题。

SwiftUI 推荐使用 @Binding 解决,我们来看下 Binding 的实现。

@propertyDelegate public struct Binding<Value> {
    public var value: Value { get nonmutating set }

    /// Initializes from functions to read and write the value.
    public init(getValue: @escaping () -> Value, setValue: @escaping (Value) -> Void)

}

Binding 结构体使用闭包捕获了原本的属性值,使得属性可以用引用的方式保留。

struct PlayerView: View {
    let episode: Episode
    @State private var isPlaying = false
    var body: some View {
        VStack {
            Text(episode.title)
            PlayButton($isPlaying)
        }
    }
}

这里 State 实现了 BindingConvertible 协议,使得 State 可以直接转换为 Binding。

★ @BingableObject & Combine

UI 除了受用户点击影响 有时候还来自于外部的通知,如一个IM类消息,收到远程的消息,或者一个定时器被触发。

在 SwiftUI 中通过最新的 Combine 框架可以很方便的响应外部变化,开发者只需实现 BindableObject 协议即可。

class PodcastPlayerStore: BindableObject {
    var didChange = PassthoughSubject<Void, Never>()
    func advance() {
        // ..
        didChange.send()
    }
}

struct PlayerView: View {
    let episode: Episode
    @State private var isPlaying = false
    @State private var currentTime: TimeInterval = 0.0 
    var body: some View {
        VStack {
            Text(episode.title)
            PlayButton($isPlaying)
        }
        .onReceive(PodcastPlayerStore.currentTimePublisher) { newTime in 
            self.currentTime = newTime
        }
    }
}

★ @ObjectBinding

使用 State 的方式可以通知单个视图的变化,但是有时候我们需要多个视图共享一个元素信息,并且在数据信息发送变化时通知 SwiftUI 刷新所有布局,这时候可以使用

@ObjectBinding。

final class PodcastPlayer: BindableObject {
    var isPlaying: Bool = false {
        didSet {
            didChange.send(self)
        }
    }

    func play() {
        isPlaying = true
    }

    func pause() {
        isPlaying = false
    }

    var didChange = PassthroughSubject<PodcastPlayer, Never>()
}

struct EpisodesView: View {
    @ObjectBinding var player: PodcastPlayer
    let episodes: [Episode]

    var body: some View {
        List {
            Button(
                action: {
                    if self.player.isPlaying {
                        self.player.pause()
                    } else {
                        self.player.play()
                    }
            }, label: {
                    Text(player.isPlaying ? "Pause": "Play")
                }
            )
            ForEach(episodes) { episode in
                Text(episode.title)
            }
        }
    }
}
@propertyDelegate public struct ObjectBinding<BindableObjectType> : DynamicViewProperty where BindableObjectType : BindableObject {
    @dynamicMemberLookup public struct Wrapper {
        public subscript<Subject>(dynamicMember keyPath: ReferenceWritableKeyPath<BindableObjectType, Subject>) -> Binding<Subject> { get }
    }

    public var value: BindableObjectType

    public init(initialValue: BindableObjectType)

    public var delegateValue: ObjectBinding<BindableObjectType>.Wrapper { get }

    public var storageValue: ObjectBinding<BindableObjectType>.Wrapper { get }
}

系统通过 ObjectBinding<BindableObjectType>.Wrapper 感知外部数据的变化。

★ @EnviromemntObject

有时候一些环境变量是共享的,我们可以通过 EnviromentObject 获取共享的信息,这些共享的数据信息回沿着 View 树的的结构向下传递。 类似于 Flutter 的 Theme 和 ScopeModel ,比较简单这里就不多做解释了。

let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIHostingController(rootView: LandmarkList().environmentObject(UserData()))

struct LandmarkList: View {
    @EnvironmentObject private var userData: UserData

    var body: some View {
        Text("")
    }

★ @Enviroment

前面提到的环境信息一般是指用户自定义共享信息,但是同时系统存在大量的内置环境信息,如时区,颜色模式等,可以直接订阅系统的环境信息,使得 SwiftUI 自动获取到环境信息的变化,自动刷新布局。

struct CalendarView: View {
    @Environment(\.calendar) var calendar: Calendar
    @Environment(\.locale) var locale: Locale
    @Environment(\.colorScheme) var colorScheme: ColorScheme

    var body: some View {
        return Text(locale.identifier)
    }
}

★ 总结

以上是 SwiftUI 官方在各种教程和 WWDC session 提到的数据流管理方案,总结下来是以下几点:

  • 对于不变的常量直接传递给 SwiftUI 即可。
  • 对于控件上需要管理的状态使用 @State 管理。
  • 对于外部的事件变化使用 BindableObject 发送通知。
  • 对于需要共享的视图可变数据使用 @ObjectBinding 管理。
  • 不要出现多个状态同步管理,使用 @Binding 共享一个 Source of truth
  • 对于系统环境使用 @Enviroment 管理。
  • 对于需要共享的不可变数据使用 @EnviromemntObject 管理。
  • @Binding 具有引用语义,可以很好的和 @Binding`` @objectBinding`` @State 协作,避免出现多个数据不同步。

SwiftUI 的布局算法

相信很多人看过 SwiftUI 后对 SwiftUI 内部的布局算法是比较好奇的,在 Session 237 Building Custom Views with SwiftUI 摘要介绍了一下。 SwiftUI 会通过 body 的返回值获取描述视图的控件信息,转换为对应的内部视图信息,交给 2D 绘图引擎 Metal 或者 Open GL 绘制,其中比较复杂的 Toggle 可能引用自原本的UIKit实现。

★ Content View

对于以下的代码:

import SwiftUI

struct ContentView : View {
    var body: some View {
        Text("Hello World!")
    }
}

它们的结构是如下的 RootView -> ContentView -> Text, 那么 Text 是如何出现在屏幕上的?官方的介绍是如下三个步骤

1、父视图为子视图提供预估尺寸

2、子视图计算自己的实际尺寸

3、父视图根据子视图的尺寸将子视图放在自身的坐标系中

比较重要的是第二步,对于一个视图描述,通常有三种设置尺寸的方式。

  • 无需计算,根据内容推断,如 Image 是和图片等大,Text 是计算出来的可视范围,类似 NSString 根据字体计算宽高。
  • Frame 强制指定宽高
  • 设置缩放比例 如 Image 设置 aspectRatio。

顺便苹果提了一点 SwiftUI 中将计算出的模糊坐标点会对齐到清晰的像素点,避免出现锯齿感。

那么对于使用 Modifier 的布局结构如下:

import SwiftUI

struct ContentView : View {
    var body: some View {
        Text("Hello World!")
        .padding(10)
        .background(Color.Green)
    }
}
struct Avocado : View {
    var body: some View {
        Image("20x20_avocado")
            .frame(width: 30, height: 30)
    }
}

看上去它们 Frame 和 Background Padding 等元素都出现在了视图结构中,但它们其实都是 View 的约束信息,并不是真正的 View。

★ HStack/VStack

HStack 和 ZStack 的非常类似安卓的 LinerLayout,算法也同 Flex 布局比较相似。 对于如下的布局, 苹果都会在控件之间添加上符合苹果人机交互指南的间距,保证 UI 的优雅和一致性。

HStack {
    VStack {
        Text("★★★★★")
        Text("5 stars")
    }
    .font(.caption)
    VStack {
        HStack {
            Text("Avocado Toast").font(.title)
            Spacer()
            Image("20x20_avocado")
        }
        Text("Ingredients: Avocado, Almond Butter, Bread, Red Pepper Flakes")
            .font(.caption)
            .lineLimit(1)
    }
}

对于如上的 Stack 是怎么计算的?设 Stack 主轴方向长度为 W1。

1、根据人机交互指南的预留出边距 S, 边距根据元素的排列可能有多个

2、得到剩余的主轴宽度 W2= W1 - N * S

3、平均分配一个预估宽度

4、计算一些具备明确宽高的元素 如 Image 设置了 Frame的元素的等。

5、沿主轴方向从前到后计算,,如果计算出来的宽度小于预估宽度则正常显示,不够则截断。

6、最后的元素为剩余宽度,如果不够显示则阶段

7、默认的交叉轴对齐方式为 Center,Stack 占据包括最大元素的边界。

默认的计算是顺序计算布局,如果某些元素比较重要,可以使用 LayoutPriority Modifier 提高布局优先级避免出现视图截断。

★ 交叉轴对齐方式

很多时候我们开发布局系统会嵌套很多 HStack 和 ZStack 有时候我们除了内部对齐,还有按照自定义的对齐比例的对齐。

extension VerticalAlignment {
    public static let top: VerticalAlignment
    public static let center: VerticalAlignment
    public static let bottom: VerticalAlignment
    public static let firstTextBaseline: VerticalAlignment
    public static let lastTextBaseline: VerticalAlignment
}

extension HorizontalAlignment {

    public static let leading: HorizontalAlignment
    public static let center: HorizontalAlignment
    public static let trailing: HorizontalAlignment
}

extension VerticalAlignment {
    private enum MidStarAndTitle: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> Length {
            return d[.bottom]
        }
    }
    static let midStarAndTitle = VerticalAlignment(MidStarAndTitle.self)
}

// 自定义对齐方式
HStack(alignment: .midStarAndTitle) {
    VStack {
        Text("★★★★★")
        Text("5 stars")
    }
    .font(.caption)
    VStack {
        HStack {
            Text("Avocado Toast").font(.title)
            Spacer()
            Image("20x20_avocado")
        }
        Text("Ingredients: Avocado, Almond Butter, Bread, Red Pepper Flakes")
            .font(.caption)
            .lineLimit(1)
    }
}

★ ZStack

除了 HStack 和 VStack 还有 ZStack, ZStack 的布局方式类似绝对定位, 在 UIKit 中类似 向一个 View 添加不同的 SubView。

VStack {
            Text("Hello")
            Text("World").padding(10);
}

VStack 的对齐方式组合自水平和垂直方向。

public struct Alignment : Equatable {

    public var horizontal: HorizontalAlignment

    public var vertical: VerticalAlignment

    @inlinable public init(horizontal: HorizontalAlignment, vertical: VerticalAlignment)

    public static let center: Alignment

    public static let leading: Alignment

    public static let trailing: Alignment

    public static let top: Alignment

    public static let bottom: Alignment

    public static let topLeading: Alignment

    public static let topTrailing: Alignment

    public static let bottomLeading: Alignment

    public static let bottomTrailing: Alignment

    public static func == (a: Alignment, b: Alignment) -> Bool
}

★ Shape in SwiftUI

在 SwiftUI 中绘图类似之前使用 UIBezierPath 的 API,只需要实现 Shape 的描述信息即可这里就不多赘述了。

struct WedgeShape: Shape {
    var wedge: Ring.Wedge

    func path(in rect: CGRect) -> Path {
        var p = Path()
        let g = WedgeGeometry(wedge, in: rect)
        p.addArc(center: g.cen, radius: g.r0, startAngle: g.a0, endAngle: g.a1, clockwise: false)
        p.addLine(to: g.topRight)
        p.addArc(center: g.cen, radius: g.r1, startAngle: g.a1, endAngle: g.a0, clockwise: true)
        p.closeSubpath()
        return p
    }

}

SwiftUI 混合布局

很多现有的 APP 大量的使用 Cocoa 传统的命令式 UI 布局方式,即便不用兼容 iOS 13.x 以下直接引入 SwiftUI,也是不可能全部改造为 SwiftUI,势必会存在较长时间的混合编程。 苹果已经考虑到这点,并且提供了非常简单的方式供混合编程。

★ UIKit 嵌入 SwiftUI View

这个是最为常见的场景 SwiftUI 提供了 UI/NS/WKHostingController 包装一个 SwiftUI View,并且提供了对于的视图更新时机 setNeedsBodyUpdate updateBodyIfNeeded

★ SwiftUI View 嵌入 UIKit View

开发者已经封装好了大量可用的视图组件,可以直接嵌入到 SwiftUI 中无缝开发。

这块就不做过多解释了,这部分的代码和时机分成清晰。 Demo 可以参考 Session 231 integrating swiftui (https://developer.apple.com/videos/play/wwdc2019/231/)

SwiftUI On All Device

苹果考虑的跨平台不是像 RN Flutter Weex 的跨手机操作系统平台,而是跨 TV Watch Mac iPad 平台,所以理念也不一致,苹果特意指出针对不同的设备,是没有一种方法,能万能的适配到所有设备上。 如果有势必回抹掉很多平台特有的体验,这虽然对开发者友好,但不见得对用户是一件好事,苹果指出了针对多设备的设计理念。

1、针对对应的平台选择对应的合适的设计

2、认真分析共享模块的设计

3、共享视图是个有风险的行为,要考虑的更加深入

4、Learn once apply anyware.

在 SwiftUI 中开发者描述的视图层只是数据的定义和描述, SwiftUI 框架内部已经做的足够多,相信学习了 SwiftUI 可以轻易地在其他平台上开始开发。 关于更多细节请参考 Session 240 SwiftUI on all Device 和 Session 219 SwiftUI on watchOS (https://developer.apple.com/videos/play/wwdc2019/240/)

accessibility in SwiftUI

今年 SwiftUI 也充分考虑了无障碍访问功能,不得不说苹果是非常有人性化的公司,SwiftUI 给无障碍开发减少了很多的工作量。不过作者对这方面了解甚少,就不误导各位读者了, 详细的大家可以参考 Session 238 accessibility in swiftui。(https://developer.apple.com/videos/play/wwdc2019/238/)

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ciiauLB__o-cXXfKn7lL1Q

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录