SwiftUI是一种新颖的构建UI方式和全新的编码风格,本文以通俗易懂的语言,从Swift 5.1语法新特性和SwiftUI的优点方面进行分享,但愿对热爱移动端的同窗有必定的帮助,让你们尽量快速、全面和透彻地理解SwiftUI。前端
苹果于2019年度WWDC全球开发者大会上,发布了基于Swift创建的声明式框架–SwiftUI,其能够用于watchOS、tvOS、macOS等苹果旗下产品的应用开发,统一了苹果平台的UI框架。面试
正如官网所言Better apps. Less code:用更少的代码构建更好的应用。目前想要体验SwiftUI,须要如下的准备:Xcode 11 beta和macOS Mojave or Higher,若是想要体验实时预览和完整的Xcode 11功能,须要macOS 10.15 beta。编程
本文主要从如下三个方面讲述SwiftUI的特性:swift
本节对Opaque Result Type, PropertyDelegate, FunctionBuilder三个语法新特性进行讲解,结合部分伪代码和数据流分析,由浅入深地理解,其在SwiftUI中的做用。数据结构
新建一个SwiftUI的新项目,会出现以下代码:一个Text展现在body中。闭包
struct ContentView : View { var body: some View { Text("Hello World") } }
对于some View的出现,你们可能会以为很突兀。通常状况下,闭包中返回的类型应该是用来指定body的类型,以下代码所示,若是闭包中只有一个Text,那么body的类型应该就是Text。架构
struct ContentView : View { var body: Text { Text("Hello World") } }
然而,不少时候在UI布局中是肯定不了闭包中的具体类型,有多是Text、Button、List等,为了解决这一问题,就产生了Opaque Result Type。app
其实View是SwiftUI一个核心的协议,表明了闭包中元素描述。以下代码所示,其是经过一个associatedtype修饰的,带有这种修饰的协议不能做为类型来使用,只能做为类型约束来使用。框架
经过Some View的修饰,其向编译器保证:每次闭包中返回的必定是一个肯定,并且遵照View协议的类型,不要去关心究竟是哪一种类型。这样的设计,为开发者提供了一个灵活的开发模式,抹掉了具体的类型,不须要修改公共API来肯定每次闭包的返回类型,也下降了代码书写难度。异步
public protocol View : _View { associatedtype Body : View var body: Self.Body { get } }
这是个人iOS开发交流群:519832104无论你是小白仍是大牛欢迎入驻,能够一块儿分享经验,讨论技术,共同窗习成长!
另附上一份各好友收集的大厂面试题,须要iOS开发学习资料、面试真题,能够进
群可自行下载!
点击此处,当即与iOS大牛交流学习
复杂的UI结构一直是前端布局的痛点,每次用户交互或者数据发生改变,都须要及时更新UI,不然会引发某些显示问题。可是,在SwiftUI里面,视图中声明的任何状态、内容和布局,源头一旦发生改变,会自动更新视图,所以,只须要一次布局。在属性前面加上@State关键词,便可实现每次数据改动,UI动态更新的效果。
@propertyDelegate public struct State: DynamicViewProperty, BindingConvertible
上述代码中,一个@State关键词继承了DynamicViewProperty和BindingConvertible,BindingConvertible是对属性值的绑定,DynamicViewProperty是动态绑定了View和属性。
也就是说,声明一个属性时,SwiftUI会将当前属性的状态与对应视图的绑定,当属性的状态发生改变的时候,当前视图会销毁之前的状态并及时更新,下面具体分析一下这个过程。通常状况下实现一个String属性的初始化,代码以下:
public struct MyValue { var myValueStorage: String? = nil public var myValue: String { get { myValue = myValueStorage return myValueStorage } set { myValueStorage = newValue } } }
若是代码中有不少这样的属性,并且对某些属性进行特定的处理,上面的写法无疑会产生不少冗余。属性代理(propertyDelegate)的出现就是解决这个问题的,属性代理是一个泛型类型,不一样类型的属性都可以经过该属性代理进行特定的处理:
@propertyDelegate public struct LateInitialized{ 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 数据有变化 } } } }
上述代码的功能如上图所示。经过@propertyDelegate的修饰,可以解决不一样类型的value进行特定的处理;上述包装的方法,可以创建视图与数据之间的关系,而且会判断在属性值发生变化的状况下,通知SwiftUI刷新视图,编译器可以为String类型的myValue生成以下的代码,通过修饰后的代码看起来很简洁。
public struct MyValue { var $myValue: LateInitialized= LateInitialized() public var myValue: String { get { $myValue } set { $myValue.value = newValue} } }
接下来,咱们看一下@State的源码:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) @propertyDelegate public struct State: 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{ get } /// Produces the binding referencing this state value public var delegateValue: Binding{ get } /// Produces the binding referencing this state value /// TODO: old name for storageValue, to be removed public var storageValue: Binding{ 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() }
Swift 5.1的新特性Property Wrappers(一种属性装饰语法糖)来修饰State,内部实现的大概就是在属性Get、Set的时候,将部分可复用的代码包装起来,上文中说的“属性代理是一个泛型类型”正可以高效的实现这部分功能。
@State内部是在Get的时候创建数据源与视图的关系,而且返回当前的数据引用,使视图可以获取,在Set方法中会监听数据发生变化、会通知SwiftUI从新获取视图body,再经过Function Builders方法重构UI,绘制界面,在绘制过程当中会自动比较视图中各个属性是否有变化,若是发生变化,便会更新对应的视图,避免全局绘制,资源浪费。
经过这种编程模式,SwiftUI帮助开发者创建了各类视图和数据的链接,而且处理二者之间的关系,开发者仅须要关注业务逻辑,其官方的数据结构图以下:
用户交互过程当中,会产生一个用户的action,从上图能够看出,在SwiftUI中数据的流转过程以下:
以上就是SwiftUI的交互流程,其每个节点之间的数据流转都是单向、独立的,不管应用程序的逻辑变得多么复杂,该模式与Flux和Redux架构的数据模式相相似。
内部由无数这样的单向数据流组合而成,每一个数据流都遵循相应的规范,这样开发者在排查问题的时候,不须要再去找全部与该数据相关的界面进行排查,只须要找到相应逻辑的数据流,分析数据在流程中运转是否正常便可。
不一样场景中,SwiftUI提供了不一样的关键词,其实现原理上如上文所示:
以上特性的实现是基于Swift的Combine框架,下面简单介绍一下。该框架有两个很是重要的概念,观察者模式和响应式编程。
观察者模式是描述一对多关系:一个对象发生改变时将自动通知其余对象,其余对象将相应作出反应。这两类对象分别被称为被观察目标和观察者,一个观察目标能够对应多个观察者,观察者能够订阅它们感兴趣的内容,这也就是文中关键词@State的实现来源,将属性做为观察目标,观察者是存在该属性的多个View。
响应式编程的核心是面向异步数据流和变化的,响应式编程将全部事件转成为异步的数据流,更加方便的对这些数据流进行组合变换,最终只须要监听数据流的变化并作出处理便可,所以在SwiftUI中处理用户交互和响应等很是简洁。
在认识FunctionBuilder以前,必须先了解一下ViewBuilder,其是用 @_functionBuilder来修饰的,编译器会使用。而且对它所包含的方法有必定要求,其隐藏在各个容器类型的最后一个闭包参数中。下面具体介绍所谓的“要求”。
在组合视图中,闭包中会处理大量的UI组件,FunctionBuilder是经过闭包创建样式,将闭包中的UI描述传递给专门的构造器,提供了相似DSL的开发模式。以下实现一个简单的View:
struct RowCell : View { let image : UIImage let title : String let tip : String var body: some View { HStack{ Image(uiImage: image) Text(title) Text(tip) } } }
查看HStack的初始化代码,以下所示:其最后的content是用ViewBuilder进行修饰的,也就是经过functionBuilder对闭包表达式进行了特殊处理,最终构造出视图。
init(alignment: VerticalAlignment = .center, spacing: Length? = nil, @ViewBuilder content: () -> Content)
若是没有FunctionBuilder这一新特性,那么开发者必须对容器视图进行管理,以HStack为例(以下代码所示)。若存在大量的表达式,无疑会让开发者感受到头疼,并且代码也会很杂乱,结构也不够清晰。
struct RowCell : View { let image : UIImage let title : String let tip : String var body: some View { var builder = HStackBuilder() builder.add(Image(uiImage: image)) builder.add(Text(title)) builder.add(Text(tip)) return builder.build() } }
用@_functionBuilder修饰的内容,均会实现一个构造器,构造器的功能如上述代码所示。构建器声明几种buildBlock方法用来构造视图,这几种方法可以知足各类各样的闭包表达式。下面是SwiftUI的ViewBuilder几种方法:
Building Blocks static func buildBlock() -> EmptyView //Builds an empty view from a block containing no statements. static func buildBlock(Content) -> Content //Passes a single view written as a child view through unmodified. static func buildBlock(C0, C1) -> TupleView static func buildBlock(C0, C1, C2) -> TupleView static func buildBlock(C0, C1, C2, C3) -> TupleView ...
上文被ViewBuilder修饰的content,content在调用的时候,会按照上述合适的buildBlock进行构建视图,将闭包中出现的Text或者其余的组件build成一个TupleView,而且返回。
可是,@_functionBuilder也存在必定局限性,ViewBuilder的buildBlock最多传入十个参数,也就是布局中最多只能有十个View;若是超过十个View,能够考虑使用TupleView来用多元的方式合并View。
做为SwiftUI的新特色之一,FunctionBuilder倾向于目前流行的编程方式,开发者可以使用基于DSL的架构,像SwiftUI,而不用去考虑具体的实现细节,由于构建器实现的就是一个DSL自己。
本节经过DSL视图的分析,分析SwfitUI在布局上的特色,以及利用该特色在组件化过程当中的优点。
目前,组件化编程是主流的开发方式,SwfitUI带来了全新的功能–能够构建可重用的组件,采用了声明式编程思想。将单1、简单的响应视图组合到繁琐、复杂的视图中去,并且在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包含起来,RootView是SwiftUI在Window上建立出来了。经过简单的几行代码,设置了按钮的点击事件,样式和文案。
其视图DSL结构以下图所示,SwiftUI会直接读取 DSL内部描述信息并收集起来,而后转换成基本的图形单元,最终交给底层Metal或OpenGL渲染出来。
经过该结构发现,与UIKit的布局结构有很大的不一样,像按钮的一些属性background、padding、cornerRadius等不该该出如今视图主结构中,应该出如今Button视图的结构中。
由于,在 SwiftUI中这些属性的设置在内部都会用一个View来承载,而后在布局的时候就会按照上面示例的布局流程,一层层View的计算布局下来,这样作的优势是:方便底层在设计渲染函数时更容易作到monomorphic call,省去无用的分支判断,提升效率。
同时SwiftUI中也是支持frame设定,但也不会像UIKit中那样做用于当前元素,在内部也是造成一个虚拟的View来承载frame设定,在布局过程当中进行frame计算最终显示出想要的结果。
总之在SwiftUI中给一个View设置属性,已经不是为当前元素提供约束,而是用一系列容器来包含当前元素,为后续布局计算作准备。
SwiftUI的界面再也不像UIKit那样,用ViewController 承载各类UIVew控件,而是一切皆View,因此能够把View切分红各类细致化的组件,而后经过组合的方式拼装成最终的界面,这种视图的拼装方式提升了界面开发的灵活性和复用性。所以,视图组件化是SwiftUI很大的亮点。
SwiftUI的Preview是Apple的一大突破,相似RN、Flutter的Hot Reloading。Apple选择了直接在macOS上进行渲染,不过须要搭载有SwiftUI.framework的macOS 10.15才可以看到Xcode Previews界面。
Xcode将对代码进行静态分析 (得益于SwiftSyntax框架),找到全部遵照PreviewProvider 协议的类型进行预览渲染。在Xcode 11中提供了实时预览和静态预览两项功能,实时预览:代码的修改可以实时呈如今Xcode的预览窗口中;此外,Xcdoe还提供了快捷功能,经过command+鼠标点击组件,能够快速、方便地添加组件和设置组件属性。
做者介绍:
梁启健,携程金融支付中心开发工程师,主要负责支付iOS端的开发与优化工做,喜欢研究大前端和跨平台技术。
本文转载自公众号携程技术中心(ID:ctriptech)原文连接
点击此处,当即与iOS大牛交流学习