SwiftUI

The shortest path to building great apps on every device
Better apps. Less code
Learn once, apply anywhere

1、什么是SwiftUI

苹果于2019年度WWDC全球开发者大会上,发布了基于Swift建立的声明式框架--SwiftUI,其可以用于watchOS、tvOS、macOS等苹果旗下产品的应用开发,统一了苹果平台的UI框架。

正如官网所言Better apps. Less code:用更少的代码构建更好的应用。

目前想要体验SwiftUI,需要以下的准备:Xcode 11macOS Mojave or Higher,如果想要体验实时预览和完整的Xcode 11功能,需要macOS 10.15 以上系统。

Q: 什么是声明式框架


image.png

Win32WebAndroid 和 iOS的框架通常使用命令式的 UI 编程风格。这可能是最熟悉的样式 - 手动构建全功能UI实体(如UIView或等效实体),然后在 UI 更改时使用方法和 setter 对其进行变更。

为了减轻开发人员的负担,无需编写如何在不同的 UI 状态之间进行切换的代码,相比之下,近些年来的微信小程序,以及Flutter 这些语言的UI框架让开发人员描述当前的 UI 状态,并把状态切换的的工作留给了框架。

命令式UI:构建全功能UI实体,然后在UI更改时使用方法对其进行变更。
声明式UI:描述当前的UI状态,并且不需要关心它是如何过渡到框架的。

image.png

2、演示

WWDCDemo -> ComposingComplexInterfaces

  • 功能界面的介绍
  • 代码的简单介绍
  • Canvas和代码的互动
  • HotReload

3、WWDC官方文档的总结

https://developer.apple.com/tutorials/swiftui/creating-and-combining-views

SwiftUI Essentials

  • 创建和组合视图:
v2-68546fb03c10b58684e564d303990ddb_r-2.jpg
  • 创建列表和导航栏:
创建列表和导航栏
  • 处理用户输入:(数据和UI的绑定)
处理用户输入

Drawing and Animation

绘图路径和形状:

绘图路径和形状
  • 动画视图和过渡

WWDCDemo -> ComposingComplexInterfaces ->HikeDetail

v2-433102db3c1179a9d3953229018bea4a_r.jpg
  • 复杂页面的组合

WWDCDemo -> ComposingComplexInterfaces

image.png
  • 如何嵌套UIKit

实现UIViewRepresentable协议

image.png
  • 多设备适配(画布)

我们可以在 canvas 中体验不同的设备,对比它们在渲染 view时的差异。

#if DEBUG
struct LandmarksList_Previews: PreviewProvider {
    static var previews: some View
        ForEach(["iPhone SE", "iPhone XS Max"], id: \.self) { deviceName in
            LandmarkList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
                .previewDisplayName(deviceName)
        }
        .environmentObject(UserData())
    }
}
#endif
image.png
  • 不同的设备比较

不推荐为 Mac,iPhone,Apple Watch, Apple TV 这些不同尺寸的设备采用一套界面设计,因此他们不推崇 Write once, run anywhere ,而是宣称 Learn once, apply anywhere

image.png
image.png

4、SwiftUI的特性

Opaque Result Type (不透明结果类型) some + 协议

var body: some View {
        HStack {
            landmark.image(forSize: 50)
            Text(verbatim: landmark.name)
            Spacer()

            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .imageScale(.medium)
                    .foregroundColor(.yellow)
            }
        }
    }

对于some View的出现,大家可能会觉得很突兀。一般情况下,闭包中返回的类型应该是用来指定body的类型,如果闭包中只有一个Text,那么body的类型应该就是Text

然而,很多时候在UI布局中是确定不了闭包中的具体类型,有可能是Text、Button、List等,为了解决这一问题,就产生了Opaque Result Type

其实ViewSwiftUI一个核心的协议,代表了闭包中元素描述。如下代码所示,其是通过一个associatedtype修饰的,带有这种修饰的协议不能作为类型来使用,只能作为类型约束来使用。

public protocol View : _View {
    // body 属性的类型
    associatedtype Body : View

    // 唯一属性 body 的类型是另一个具体类型 View
    var body: Self.Body { get }
}

为了搞清楚这个问题,我们先来看一下下面这段代码

protocol Shape {}

struct Rectangle: Shape {}

struct Union<A: Shape, B: Shape>: Shape {
    var a: Shape
    var b: Shape
}

struct Transformed<S: Shape>: Shape {
    var shape: S
}

protocol GameObject {
    associatedtype ShapeType: Shape
    var shape: ShapeType { get }
}

struct EightPointedStar: GameObject {
    var shape: Union<Rectangle, Transformed<Rectangle>> {
        return Union(a:Rectangle(), b:Transformed(shape: Rectangle()))
    }
}

上述代码是可以编译通过的,但是 EightPointedStar 的 Shape 返回类型又臭又长,被暴露了出去;如果换成 Shape 则编译不通过,原因是

  • associatedtype ShapeType 要求必须指定具体的类型,而 Shape 不实现 Shape 本身。

  • 假如 Shape 协议中含有 Self 或者 associatedtype,无法作为函数的返回参数。这是 Swift 泛型系统长久以来的一个问题。

Swift 5.1 Opaque Result Type 特性,解决了上述问题,它为协议作为返回类型提供以下能力:

  • 语法上隐藏具体类型,所以叫做不透明结果类型

  • 强类型:类型参数不丢失

  • 允许带有 Self 或者 associatedtype 的协议作为返回类型

在 Swift 5.1 中,将返回类型改成some+ 协议名称的形式:

struct EightPointedStar: GameObject {
    var shape: some Shape {
        return Union(a:Rectangle(), b:Transformed(shape: Rectangle()))
    }
}

这类的泛型特性也被称作“反向泛型”,因为具体的类型参数是由“实现部分”指定并隐藏起来的,而一般的泛型是由“调用者”所指定的。

protocol Shape {
    func draw() -> String
}

struct Triangle: Shape {
    var size: Int

    // 会印出:
    // *
    // **
    // ***
    func draw() -> String {
        var result = [String]()
        for length in 1...size {
            result.append(String(repeating: "*", count: length))
        }
        return result.joined(separator: "n")
    }
}

struct FlippedShape<T: Shape>: Shape {
    var shape: T
    func draw() -> String {
        let lines = shape.draw().split(separator: "n")
        return lines.reversed().joined(separator: "n")
    }
}

let smallTriangle = Triangle(size: 3)
let flippedTriangle = FlippedShape(shape: smallTriangle) // 这里由 FlippedShape 决定 T == Triangle
print(flippedTriangle.draw())
// ***
// **
// *

所以 Opaque Result Type 最重要的精神就是让被调用方(callee),也就是实现方决定要回什么真实的类型;因此外部调用后得到的就会是一个抽象的类型,这点与先前在声明时给参数,外部需要明确地给予一个决定性的真实类型相反,可以视作是反向的泛型(reverse-generic)。

// 这个例子在Xcode 11 beta 2 之后能 work
func foo<T: Equatable>(_ x: T, _ y: T) -> some Equatable {
    let condition = x == y
    return condition ? 42 : 11
}

func test() {
  let x = foo("apples", "bananas")
  let y = foo("apples", "oranges")
  print(x == y) // 这里可以被调用是因为泛型系统保留了强类型
}

这个例子显示了不透明结果类型的三个特性:既对外隐藏了具体的 Equatable 类型;又保留了强类型(使得 x == y)可以比较;还支持了Equatable 这个带 Self 的泛型约束。
不透明结果类型对于函数实现有一个增强的要求:函数实现必须返回同一个具体类型,以上述代码为例:不能返回 Equatable 或者是 不同类型的 Equatable 的实现。

SwiftUI 最大特点的是声明式以及高度可组合,View 的唯一属性 body 是另一个满足View约束的具体 View 类型,我们在这里看到了组合以及递归两个特性。下面来看一个具体的View 类型 ContentView

struct ContentView : View {
    var body: some View {
      VStack {
        Text("Hello World")
        Text("Love & Peace")
      }
    }
}

ContentView 使用了不透明结果类型的特性,对外隐藏了具体类型 VStack。此外,ContentView 的具体类型都是通过它的 body 属性递归定义的(取决于它所包含的具体 View)

16b34c7eff9f571b.png

ViewModifier 链式语法

附加在某个视图上改变它原本的结构或样式

A modifier that you apply to a view or another view modifier, producing a different version of the original value.

image.png

除了内置的 modifierSwiftUI 也允许开发者定制自己的 modifier 的,比如下面是一个自定义例子:

struct PrimaryLabel: ViewModifier {
    func body(content: Content) -> some View {
        content.padding()
            .background(Color.red)
            .foregroundColor(Color.white)
            .font(.largeTitle)
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello, SwiftUI").modifier(PrimaryLabel())
    }
}

PropertyDelegate 现在改名为propertyWrapper

复杂的UI结构一直是前端布局的痛点,每次用户交互或者数据发生改变,都需要及时更新UI,否则会引起某些显示问题。但是,在SwiftUI里面,视图中声明的任何状态、内容和布局,源头一旦发生改变,会自动更新视图,因此,只需要一次布局。在属性前面加上@State等关键词,即可实现每次数据改动,UI动态更新的效果。

Swift 5.1的新特性Property Wrappers(一种属性装饰语法糖)来修饰State,内部实现的大概就是在属性Get、Set的时候,将部分可复用的代码包装起来。

struct OrderForm : View {
  @State private var order: Order
  
  var body: some View {
    Stepper(value: $order.quantity, in: 1...10) {
      Text("Quantity: \(order.quantity)")
    }
  }
}

这个语言特性非常通用,任何对于属性的存取有“套路”的访问,都可以用它来包装这种“套路”。我们先来学习一下几个套路。

  • 包装懒初始化逻辑
    为了实现属性 text 为懒初始化的属性,我们可以写成如下代码:
public struct MyType {
  var textStorage: String? = nil
  
  public var text: String {
    get {
      guard let value = textStorage else {
        fatalError("text has not yet been set!")
      }
      return value
    }
    
    set {
      textStorage = newValue
    }
  }
}

然而如果有很多属性都是这样的逻辑,这样的写法是很冗余的。所以属性代理就是解决这个问题的:

@propertyDelegate
public struct LateInitialized<Value> {
  private var storage: Value?
  
  public init() {
    storage = nil
  }
  
  public var value: Value {
    get{
      guard let value = storage else {
        fatalError("value has not yet been set!")
      }
      return value
    }
    set {
      storage = newValue
    }
  }
}

// 应用属性代理 LateInitialized
public struct MyType {
  @LateInitialized public var text: String?
}

上述代码的功能如上图所示。通过@propertyDelegate的修饰,能够解决不同类型的value进行特定的处理;上述包装的方法,能够建立视图与数据之间的关系,并且会判断在属性值发生变化的情况下,通知SwiftUI刷新视图,编译器能够为String类型的myValue生成如下的代码,经过修饰后的代码看起来很简洁。

public struct MyValue {
  var $myValue: LateInitialized<String> = LateInitialized<String>()

  public var myValue: String {
      get { $myValue }
      set { $myValue.value = newValue}
  }
}
  • 包装防御性拷贝
@propertyDelegate
public struct DefensiveCopying<Value: NSCopying> {
  private var storage: Value
  
  public init(initialValue value: Value) {
    storage = value.copy() as! Value
  }
  
  public var value: Value {
    get { storage }
    set {
      storage = newValue.copy() as! Value
    }
  }
}

// 应用属性代理 DefensiveCopying
public struct MyType {
  @DefensiveCopying public var path: UIBezierPath = UIBezierPath()
}

  • 包装 UserDefaults 的存取
    我们经常需要将属性写成针对UserDefaults存取的计算属性,而这个通用访问策略也能用属性代理实现:
@propertyWrapper
struct UserDefaultValue<Value: Codable> {

    let key: String
    let defaultValue: Value

    var wrappedValue: Value {
        get {
            let data = UserDefaults.standard.data(forKey: key)
            let value = data.flatMap { try? JSONDecoder().decode(Value.self, from: $0) }
            return value ?? defaultValue
        }
        set {
            let data = try? JSONEncoder().encode(newValue)
            UserDefaults.standard.set(data, forKey: key)
        }
    }
}
final class UserData: ObservableObject {
    let objectWillChange = PassthroughSubject<UserData, Never>()
    
    @UserDefaultValue(key: "allCurrencies", defaultValue: defaultCurrencies)
    var allCurrencies: [Currency] {
        didSet {
            objectWillChange.send(self)
        }
    }
    
    @UserDefaultValue(key: "baseCurrency", defaultValue: defaultCurrencies[0])
    var baseCurrency: Currency {
        didSet {
            objectWillChange.send(self)
        }
    }
    
    @UserDefaultValue(key: "userCurrency", defaultValue: defaultCurrencies)
    var userCurrency: [Currency] {
        didSet {
            objectWillChange.send(self)
        }
    }
}

在SwiftUI里面,不同类型的属性都能够通过该属性代理进行特定的处理:

@propertyDelegate public struct LateInitialized<Value> {
  private var storage: Value?
  
  public init() {
    storage = nil
  }
  
  public var value: Value {
    get{
      guard let value = storage 
      createDependency(view, value) // 建立视图与数据依赖关系
      return value
    }
    set {
      if(storage != newValue){
        storage = newValue
        notify(to: swiftui) // 通知 SwiftUI 数据有变化
      }
    }
  }
}
image.png
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyDelegate public struct State<Value> : DynamicViewProperty, BindingConvertible {

    /// Initialize with the provided initial value.
    public init(initialValue value: Value)

    /// The current state value.
    public var value: Value { get nonmutating set }

    /// Returns a binding referencing the state value.
    public var binding: Binding<Value> { get }

    /// Produces the binding referencing this state value
    public var delegateValue: Binding<Value> { get }

    /// Produces the binding referencing this state value
    /// TODO: old name for storageValue, to be removed
    public var storageValue: Binding<Value> { get }
}

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension State where Value : ExpressibleByNilLiteral {

    /// Initialize with a nil initial value.
    @inlinable public init()
}

Key Path Member Lookup

@State内部是在Get的时候建立数据源与视图的关系,并且返回当前的数据引用,使视图能够获取,在Set方法中会监听数据发生变化、会通知SwiftUI重新获取视图body,再通过Function Builders方法重构UI,绘制界面,在绘制过程中会自动比较视图中各个属性是否有变化,如果发生变化,便会更新对应的视图,避免全局绘制,资源浪费。

通过这种编程模式,SwiftUI帮助开发者建立了各种视图和数据的连接,并且处理两者之间的关系,开发者仅需要关注业务逻辑,其官方的数据结构图如下:

640-2.png

用户交互过程中,会产生一个用户的action,从上图可以看出,在SwiftUI中数据的流转过程如下:

该行为触发数据改变,并通过@State数据源进行包装;
@State检测到数据变化,触发视图重绘;
SwiftUI内部按上述所说的逻辑,判断对应视图是否需要更新UI,最终再次呈现给用户,等待交互;

以上就是SwiftUI的交互流程,其每一个节点之间的数据流转都是单向、独立的,无论应用程序的逻辑变得多么复杂,该模式与FluxRedux架构的数据模式相类似。

内部由无数这样的单向数据流组合而成,每个数据流都遵循相应的规范,这样开发者在排查问题的时候,不需要再去找所有与该数据相关的界面进行排查,只需要找到相应逻辑的数据流,分析数据在流程中运转是否正常即可。

不同场景中,SwiftUI提供了不同的关键词,其实现原理上如上文所示:

  • @State:定义一个响应式状态,它的变化会导致依赖它的视图自动更新(单向)
  • @Binding:视图和数据的双向绑定
  • @ObjectBinding:作用等价于 @Binding,但是支持使用一个外部对象
  • @BindableObject、CombineApple 官方新发布的 combine, 说是用来处理外部事件或服务端推送等场景, 是 RxJS 的 Swift 版本实现
  • @EnviromemntObject: 沿着 View 树的层级一直向下共享的数据,实现类似 Scoped Data(范围数据) 的效果
  • @Enviroment:可以全局共享的数据,同时它的变化会导致 UI 自动刷新,类似 React 这边的 Provider

以上特性的实现是基于SwiftCombine框架,下面简单介绍一下。该框架有两个非常重要的概念,观察者模式和响应式编程。

观察者模式是描述一对多关系:一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。这两类对象分别被称为被观察目标和观察者,一个观察目标可以对应多个观察者,观察者可以订阅它们感兴趣的内容,这也就是文中关键词@State的实现来源,将属性作为观察目标,观察者是存在该属性的多个View。

响应式编程的核心是面向异步数据流和变化的,响应式编程将所有事件转成为异步的数据流,更加方便的对这些数据流进行组合变换,最终只需要监听数据流的变化并做出处理即可,因此在SwiftUI中处理用户交互和响应等非常简洁。

FunctionBuilder

struct SlideViewer: View {
  @State private var isEditing = false
  @Binding var slide: Slide

  var body: some View {
    VStack {
      Text("Slide #\(slide.number)")
      if isEditing {
        TextFiled($slide.title)
      }
    }
  }
}
SwiftUI DSL 的需要

我们仔细分析下上述 DSL代码中的语法需要:

  • 从表达方式上从简:尽量省略不必要的逗号,return,中括号等等。
  • 支持简单的逻辑控制,比如 if控制语句。
  • 强类型:some View 代表了一个复合的强类型,在 View 发生改变的时候,复合的强类型有助于做 View diff 优化。
  • Swift已有的语法不冲突。
通过 ViewBuilder 来理解 @_functionBuilder

就像 @propertyDelegate用来修饰 State一样,@_functionBuilder 用来修饰 ViewBuilder,这里同样 ViewBuilder也不过是一个编译器会使用它、并且对它所包含的方法有一定要求的类型。那么ViewBuilder在哪里呢?其实就在各种容器类型的最后一个闭包参数中,以VStack为例:

// 定义
struct VStack<Content> where Content : View {
  init(alignment: HorizontalAlignment = .center, spacing: Length? = nil,
  @ViewBuilder content: () -> Content)
    
}

// 使用
struct ContentView : View {
  var body: some View {
    VStack(alignment: .leading) {
      Text("Hello, World")
      Text("Leon Lu")
    }
  }
}

上面这个例子中,我们看到 SwiftUI 中如何在一个容器类型 VStack 的构造函数的闭包中平铺其包含的两个 Text;另一方面,在闭包的函数声明中,我们看到了 @ViewBuidler 的修饰。其实不难推断,为了能编译过,ViewBuidler对于这个闭包中的代码在编译阶段“动了手脚”,那么这是如何做到的呢?来看 ViewBuilder中的关键方法:

static func buildBlock() -> EmptyView
static func buildBlock<Content>(Content) -> Content
static func buildBlock<C0, C1>(C0, C1) -> TupleView<(C0, C1)>
static func buildBlock<C0, C1, C2>(C0, C1, C2) -> TupleView<(C0, C1, C2)>
static func buildBlock<C0, C1, C2, C3>(C0, C1, C2, C3) -> TupleView<(C0, C1, C2, C3)>
static func buildBlock<C0, C1, C2, C3, C4>(C0, C1, C2, C3, C4) -> TupleView<(C0, C1, C2, C3, C4)>

我们的两个 Text 的例子中,编译器自动(根据名称的约定)使用了

static func buildBlock<C0, C1>(C0, C1) -> TupleView<(C0, C1)> 

方法,这时候VStack的类型就成为了 VStack<TupleView<(Text,Text)>>了。经过 ViewBuilder 转换后的代码:

struct ContentView : View {
  var body: some View {
    VStack(alignment: .leading) {
      ViewBuilder.buildBlock(Text("Hello, World"), Text("Leon Lu"))
    }
  }
}

值得一提的是,由于 buildBlock 的 overload 版本最多泛型参数是 10 个。所以当超过 10 个的时候可以使用 Group包一下; 如果有循环可以展开,则可以使用 ForEach。

FunctionBuilder 分支条件的情况

ViewBuilder 中还有两个函数被用来构建含分支条件时候的类型

static func buildEither<TrueContent, FalseContent>(first: TrueContent) ->
 ConditionalContent<TrueContent, FalseContent>

static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> 
 ConditionalContent<TrueContent, FalseContent>


如果根据不同条件返回不同的视图,那么生成的类型中包含两个类型。

struct SlideViewer: View {
  @State private var isEditing = false
  @Binding var slide: Slide

  var body: some View {
    VStack {
      Text("Slide #\(slide.number)")
      if isEditing {
        TextFiled($slide.title)
      } else {
        Text(slide.title)
      }
    }
  }
}

此时,VStack的类型变成了 VStack<TupleView<(Text, ConditionalContent<TextField,Text>)>>

Q: 何为 DSL
DSL(Domain Specific Language) 翻译成中文就是:“领域特定语言”。首先,从定义就可以看出,DSL 也是一种编程语言,只不过它主要是用来处理某个特定领域的问题。
广为人知的编程语言有 C、Java、PHP 等,他们被称为 GPL(General Purpose Language),即通用目的语言。与这些语言相比,DSL 相对显得比较神秘,他们中的大多数甚至连一个名字都没有。这主要是因为 DSL 通常用来处理某个特定的、很小的领域中的问题,因此起名字这事没有太大的必要和意义。
说了这么多废话, 一定有读者在想:“能不能举个例子讲解一下,什么是 DSL。实际上,DSL 只是对一类语言的描述,它可以非常简单

UIView (0, 0, 100, 100) black
UILabel (50, 50, 200, 200) yellow

比如可以没有括号(这取决于你如何设计),因此开发、阅读的效率更高。但作为代价,DSL 调试很麻烦,很难做类型检查,因此几乎难以想象可以用DSL开发一个大型的程序。

如果同时接触过编译型语言和脚本语言,你可以把 DSL 理解为一种比脚本语言更加轻量、灵活的语言。

  • DSL 的执行过程
    了解过 C 语言的开发者应该知道,从 C 语言源码到最后的可执行文件,需要经过预编译、编译(词法分析、语法分析、语义分析)、汇编、链接等步骤,最终生成 CPU 相关的机器码,也就是一堆 0 和 1。

脚本语言不需要编译(有些也可以编译),他们在运行时被解释,当然也需要做词法分析和语法分析,最终生成机器码。

于是问题来了,自定义的 DSL 如何被执行呢?

对于词法分析和语法分析,由于语言简单,通常只是少数关键字,即使使用最简单的字符串解析,工作量和复杂度也在可接受的范围内。然而最后生成汇编代码就显得不是很有必要了,DSL 的特点不是追求执行效率,而是高效,对开发者友好。

因此一种常见的做法是,用别的语言(可以理解为宿主语言)来解析 DSL,并执行宿主语言。继续以上面的DSL 为例,我们可以用 OC 读取这个文本文件,了解到我们要创建一个 UIView 对象,因此会执行以下代码:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
view.backgroundColor = [UIColor blackColor];
  • 如何实现 DSL

可以看到,DSL 的定义与实现毫无技术难度可言,与其说是一门语言,不如说是一种信息的标记格式,一种存储信息的协议。从这个角度来说,JSON、XML 等数据格式也可以被称为 DSL。

DSL白话解释

Components

本节通过DSL视图的分析,分析SwfitUI在布局上的特点,以及利用该特点在组件化过程中的优势。

目前,组件化编程是主流的开发方式,SwfitUI带来了全新的功能--可以构建可重用的组件,采用了声明式编程思想。将单一、简单的响应视图组合到繁琐、复杂的视图中去,而且在Apple的任何平台上都能使用该组件,达到了跨平台(仅限苹果设备)的效果。按照用途大概能够分为基础组件、布局组件和功能组件。

更多的组件详见 example link

下面以一个Button为例子:

struct ContentView : View {
    var body: some View {
        Button(action: {
            // did tap
        },label: {Text("Click me")}
        )
        .foregroundColor(Color.white)
        .cornerRadius(5)
        .padding(20)
        .background(Color.blue)
    }
}

其中包含了一个Button,其父视图是一个ContenView,其实ContenView还会被一个RootView包含起来,RootViewSwiftUI在Window上创建出来了。通过简单的几行代码,设置了按钮的点击事件,样式和文案。

其视图DSL结构如下图所示,SwiftUI会直接读取 DSL内部描述信息并收集起来,然后转换成基本的图形单元,最终交给底层Metal或OpenGL渲染出来。

通过该结构发现,与UIKit的布局结构有很大的不同,像按钮的一些属性background、padding、cornerRadius等不应该出现在视图主结构中,应该出现在Button视图的结构中。

因为,在 SwiftUI中这些属性的设置在内部都会用一个View来承载,然后在布局的时候就会按照上面示例的布局流程,一层层View的计算布局下来,这样做的优点是:方便底层在设计渲染函数时更容易做到monomorphic call(单独调用),省去无用的分支判断,提高效率。

640-3.png

同时SwiftUI中也是支持frame设定,但也不会像UIKit中那样作用于当前元素,在内部也是形成一个虚拟的View来承载frame设定,在布局过程中进行frame计算最终显示出想要的结果。

总之在SwiftUI中给一个View设置属性,已经不是为当前元素提供约束,而是用一系列容器来包含当前元素,为后续布局计算做准备。

SwiftUI的界面不再像UIKit那样,用ViewController承载各种UIVew控件,而是一切皆View,所以可以把View切分成各种细致化的组件,然后通过组合的方式拼装成最终的界面,这种视图的拼装方式提高了界面开发的灵活性和复用性。因此,视图组件化是SwiftUI很大的亮点。

畅想

  • SwiftUI不仅为Apple的平台带来了一种新的构建UI的方式,还有全新的Swift编码风格;
  • 可以推断出:SwiftUI会出现很多组件库,方便前端开发;
    -支持热更新,这一点可能让更多的开发者拥抱SwiftUI
  • 虽然SwiftUI优点很多,但是其使用的门槛很高,只能在iOS 13以上的系统使用;仅这点,很多公司和开发者望而却步,目前主流应用最低支持iOS 9,至少3年之内,SwiftUI只能作为一个理论的知识储备,所以其还有很长的路要走;
    -SwiftUI这种与平台无关、纯描述的UI框架,恰恰是跨平台方案的正确方向,将来其能否统一整个大前端呢?这点非常值得期待;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容