SwiftUI的一些新语法特性


苹果swift开源之后,对每个新增的特性都会做公开透明的讨论,自己也主要是参考swift的官方Evolution,对一些比较有用的特性做一些总结,方便之后的查看,下面主要是swift5.1的新特性。下面的特性主要是针对2019年wwdc发布的swiftUI框架,使用swift5.1的一些新的语法特性。

Property Wrapper

属性封装是一种更通用的方式封装属性。苹果在swift语言进化中,举了Lazy和@NSCopying这两个关键字的例子,来说明了为何swift5.1要设计这个语法功能。下面就拿Lazy的例子来分析下这个语法的好处。Lazy也就是惰性加载是之前swift语法的一个关键字,用法是程序实际调用这个属性的时候,才会真正的使用这个属性自定义的初始化的方法,可以避免定义的时候就初始化,从而提升程序的运行效率。其实把之前的Lazy关键字语法展开就是如下的代码:

struct Foo {
  // lazy var foo = 100
  private var _foo: Int?
  var foo: Int {
    get {
      if let value = _foo { return value }
      let initialValue = 100
      _foo = initialValue
      return initialValue
    }
    set {
      _foo = newValue
    }
  }
}

上面这些代码本质上就是Lazy的实现,其实如果你想在get方法中定义一些自己想要的方法,比如做一些日志打印或者边界判断,直接用Lazy关键字就无能为力了,swift的语言设计者也认为这种硬编码的方式不是很优雅,应该用一种更通用的方式来定义这种语法,所以就诞生了属性封装这种语法特性。下面我们看下属性封装是如何来定义上面的用法的。

@propertyWrapper
enum Lazy<Value> {
  case uninitialized(() -> Value)
  case initialized(Value)

  init(wrappedValue: @autoclosure @escaping () -> Value) {
    self = .uninitialized(wrappedValue)
  }

  var wrappedValue: Value {
    mutating get {
      switch self {
      case .uninitialized(let initializer):
        let value = initializer()
        self = .initialized(value)
        // TODO 做一些属性条件的判断
        return value
      case .initialized(let value):
        return value
      }
    }
    set {
      self = .initialized(newValue)
    }
  }
}

在使用这个属性封装时,只要简单的调用@Lazy var foo = 100,用法和之前swift的Lazy关键字几乎一样,但是开发者却能灵活的控制属性的用法,比如上面代码中注释的地方,就可以增加一些条件控制的方法。用属性封装主要可以对哪些特性做封装哪?下面我列出来一些用法

  • 约束属性的范围的时候,例如对某些整形设置为0-100
  • 转换属性的数据时候,比如有些属性设置的时候,需要计算转换成其他数据,最常用的就是PI的设置,需要转换成度数表示法,就可以用这种方案
  • 改变属性比较语法的时候

如果想要上述这些特性的详细用法的,建议可以看下Mat大神的博客,里面都有详细说明。 其实本质上属性封装有点像C语言的宏的定义和java中的注解语法,给开发者一定的能力改变属性的一些语法特性,这种语法特性在很多脚本语言都具备,尤其做一些自定义的DSL,这个特点非常有用。所以在SwiftUI里面,标记语法中有大量的应用,熟悉这个特性对今后学习SwiftUI挺有帮助。

Opaque Result Types

如果这个特性按照字面的翻译意思是不透明结果类型,但是我个人认为这种字面翻译很难理解这个语法特性,自己也想不到什么好的翻译方法,所以暂时就用英文的原称来解释这个语法了。其实本质上这个语法特性,是让swift的泛型编程中返回的类型结构更简单、易懂。swift的api设计者用了一个Shape类的例子,详细解释了这个语法特性设计的由来。下面看一个例子

protocol Shape {
  func draw(to: Surface)

  func collides<Other: Shape>(with: Other) -> Bool
}

struct Rectangle: Shape { /* ... */ }
struct Circle: Shape { /* ... */ }
struct Union<A: Shape, B: Shape>: Shape {
  var a: A, b: B
  // ...
}
struct Transformed<S: Shape>: Shape {
  var shape: S
  var transform: Matrix3x3
  // ...
}
protocol GameObject {
  // The shape of the object
  associatedtype Shape

  var shape: Shape { get }
}

如果我们想定义一个八角星的结构体,往往之前是这样写的

struct EightPointedStar: GameObject {
  var shape: Union<Rectangle, Transformed<Rectangle>> {
    return Union(Rectangle(), Transformed(Rectangle(), by: .fortyFiveDegrees)
  }
}

如果这个时候想修改这个八角形的绘制方法,就是绘制的基础图像用Circle换成Rectangle,如下

struct EightPointedStar: GameObject {
  var shape: Union<Circle, Transformed<Circle>> {
    return Union(Circle(), Transformed(Circle(), by: .fortyFiveDegrees)
  }
}

因为shape返回的类型做了改变,这样源码中用八角形绘制调用的地方就要做相应的更改。如果使用了这种新的泛型返回值的特性,我们只要简单的这样写就好了。

struct EightPointedStar: GameObject {
  var shape: some Shape {
    return Union(Circle(), Transformed(Rectangle(), by: .fortyFiveDegrees)
  }
}

至于内部如何实现,都可以随时重构,不会影响外面的调用,可以说既利于代码的重构,也利于代码的简洁和阅读。 所以在这个语法特性出现之前,如果想要在泛型函数中返回泛型,通常的做法如下:

func generic<T: Shape>() -> T { ... }

let x: Rectangle = generic() // T == Rectangle, chosen by caller
let x: Circle = generic() // T == Circle, chosen by caller

有了Opaque Result Types语法特性只要简单的在Shape前面加上some就完全可以达到这种效果了。

// Proposed syntax
func reverseGeneric() -> some Shape { return Rectangle(...) }

let x = reverseGeneric() // abstracted type chosen by reverseGeneric's implementation

所以这个通用的泛型返回的类型,在你编写泛型的一些类和函数的时候,就很方便的。同样在swiftUI框架中你也会看到有大量的some View这种用法。

Identifiable Protocol

这个语法特性,就相当于给一个struct或者class定义一个唯一的标志符,对于比较两个类数据的区别非常有用。SwiftUI中的数据模型其实大量用到Identifiable这个协议,因为通过Identifiable协议,SwiftUI框架可以判断出那些数据模型做了更改,对做UI的增,删,改操作非常有用。这个协议的源码实现如下:

/// A class of types whose instances hold the value of an entity with stable identity.
protocol Identifiable {

    /// A type representing the stable identity of the entity associated with `self`.
    associatedtype ID: Hashable

    /// The stable identity of the entity associated with `self`.
    var id: ID { get }
}

比如你用SwiftUI定义了ListView,显示所有通信录的对象往往应该这些写代码:

struct Contact: Identifiable {
    var id: Int
    var name: String
}

struct ContactList: View {
    var favorites: [Contact]

    var body: some View {
        List(favorites) { contact in
            FavoriteCell(contact)
        }
    }
}

这样当你的contact对象做改变的时候,比如修改名字,SwiftUI框架通过Identifiable就很容易判断出来这个对象是新加的还是修改的,就很容易对此做UI方面的变化。其实这个属性对SwiftUI框架做数据响应式编程是非常有用的,框架会判断数据状态的改变,然后根据这些数据状态的变化,做响应的UI操作。

Synthesize default values for the memberwise initializer

增加了struct和class默认的构造函数,当struct和class有默认属性的时候,可以在调用初始化函数的时候,省略这些属性。下面给了一个例子做了解释

struct Dog {
  var age: Int = 0
  var name: String
  
  // 这个初始化函数是swift的语言默认的实现
  init(age: Int = 0, name: String) {
    self.age = age
    self.name = name
  }
}

在之前的swift语法中如果这样调用let sparky = Dog(name: “Sparky”)会报语法的错误,因为系统找不到这个构造方法,如果想要这样调用必须用下面的方法重新定义

struct Dog {
  var age: Int = 0
  var name: String
  
  // 这个构造方法必须定义,因为系统默认不提供。
  init(age: Int = 0, name: String) {
    self.age = age
    self.name = name
  }
}

而在swift5.1中,这种构造方法的定义就完全没有必要了,只要有默认的赋值,就会自动就生成默认的构造方法。

其实google的Flutter的框架中,Dart语言就有此种特性,可以看出这种特性可以减少大量UI控件初始化的冗余,对于声明式的UI构造非常有用。所以在SwiftUI中很多控件都可以使用这种简便的构造函数初始化,使代码看起来非常的简洁。

Ordered Collection Diffing

这个特性其实是swift5.1提供的一个做差异运算的库函数,尤其在集合中筛选不同的元素和操作,用这个函数就非常方便。

从一个集合中方便找出不同的对象,有一篇博客描述的非常详细,如果想要用这个函数可以看下。其实苹果在wwdc2019中也讲到过如何利用差异化比较,来改造UI。尤其是tableView和collectionView需要重用的这些控件,差异化比较如何在其中发挥作用,做了详细的讲解。有兴趣可以看wwdc教程

总结

由于最近苹果SwiftUI的release版本已经发布了,自己也看了下swift5.1的一些语法特性,也拿了几个跟swiftUI关联比较大的特性做了一些分析,为之后学习swiftUI框架打一点基础。

如果你喜欢这篇文章,谢谢你的赞赏

图3

如有疑问请联系我