GitHub移动端APP:支持代码浏览、Git事件和AI一键翻译等
一、简介
1、SwiftUI和Combine分别是什么?
(1) SwiftUI是声明式UI框架
(2) Combine是响应式编程框架,是用于处理数据流的,SwiftUI背后的数据处理的部分工作便是依赖于Combine框架。
2、Swift与ObjC的最重要区别
Swift提倡面向协议编程,ObjC提倡面向对象编程
3、响应式编程的核心是什么?
由状态驱动UI
4、编程思维分为哪两类?
传统指令式编程和函数响应式编程
二、你好,SwiftUI
1、指令式编程和声明式编程的区别
(1) 指令式编程:是教会计算机“怎么做”,它偏向于是“写给计算机的语言”。
(2) 声明式编程:是告诉计算机“做什么”,它偏向于是“写给人看的语言”。它通常是使用函数式或DSL(如数据库查询的SQL)方式来实现。
2、什么是声明式UI开发
(1) 代表UI层的View,一般来说并不是真实负责渲染的传统意义的视图层级,而是一个“虚拟的” 对View组织关系的描述(声明)。
(2) 决定UI的用户状态State被存储在某个或某几个对象中。
(3) 用一个函数描述View,这个函数的输入参数是State,即View = f(State)。
(4) 框架在State 改变时,调用上述函数获取对应新的State 的View,并与当前的View 进行差分计算,并重新渲染更改的部分。
(5) View = f(State),其中f是纯函数,也就是对于某个特定的State输入,所对应的View是确定的。
3、什么是view modifier?
作用于某个view,并生成原来值的另一个版本。它分为两类:
(1) 原地型modifier:定义在具体类型上,然后返回同样类型的modifier
(2) 封装类modifier:定义在view扩展中,将原view进行包装并返回新view的modifier
4、SwiftUI的用户界面是如何被驱动的?
在SwiftUI里,用户界面是严格被数据驱动的:在运行时,任何对于界面的修改,都只能通过修改数据来达成,而不能直接对界面进行调整和操作。相比于传统的UIKit 或AppKit,这在一定程度上对灵活性进行了限制,强制了我们必须使用更合理的数据处理方式。不过另一方面,它也规范了SwiftUI 中数据流动的方式,让开发者更不容易犯错。
三、数据状态和绑定
1、Redux架构
(1) 将app当作一个状态机,状态决定用户界面。
(2) 这些状态都保存在一个Store对象中,被称为State。
(3) View不能直接操作State,而只能通过发送Action的方式,间接改变存储在Store中的State。
(4) Reducer接受原有的State和发送过来的Action,生成新的State。
(5) 用新的State替换Store中原有的状态,并用新状态来驱动更新界面。
2、@State和@Binding的作用?
(1) @State属性值仅只能在属性本身被设置时会触发UI刷新,这个特性让它非常适合用来声明一个值类型的值:因为对值类型的属性的变更,也会触发整个值的重新设置,进而刷新UI。不过,在把这样的值在不同对象间传递时,状态值将会遵守值语义发生复制。
(2) @Binding就是用来解决这个问题的。和@State类似,@Binding也是对属性的修饰,它做的事情是将属性的值语义“转换”为引用语义。对被声明为@Binding的属性进行赋值,改变的将不是属性本身,而是它的引用,这个改变将被向外传递。
(3) 在Swift 5.1中,对一个由@符号修饰的属性,在它前面使用的投影访问方式。
3、什么是ObservableObject协议
(1) ObservableObject协议要求实现类型是class,它只有一个需要实现的属性:objectWillChange。在数据将要发生改变时,这个属性用来向外进行“广播”,它的订阅者(一般是View相关的逻辑) 在收到通知后,对View进行刷新。
(2) 创建ObservableObject后,实际在View 里使用时,我们需要将它声明为@ObservedObject。这也是一个属性包装,它负责通过订阅objectWillChange这个“广播”,将具体管理数据的ObservableObject和当前的View关联起来。
(3) 在ObservableObject中,如果没有定义objectWillChange,编译器会为你自动生成它,并在被标记为@Published的属性发生变更时,自动去调用objectWillChange.send()。
4、什么是@EnvironmentObject?
(1) 在SwiftUI中,View提供了environmentObject(_:)方法,来把某个ObservableObject的值注入到当前View层级及其子层级中去。在这个View的子层级中,可以使用@EnvironmentObject来直接获取这个绑定的环境值。
(2) 可能一开始你会认为@EnvironmentObject和“臭名昭著”的单例很像:只要我们在View的层级上,不论何处都可以访问到这个环境对象。看似这会带来状态管理上的困难和混乱,但是Swift提供了清晰的状态变更和界面刷新的循环,如果我们能选择正确的设计和架构模式,完全可以避免这种风险。使用@EnvironmentObject带来很大的便捷性,也避免了大量不必要的属性传递,这会为之后代码变更带来更多的好处。
5、@State、@Binding、@ObservedObject和@EnvironmentObject分别在什么场景下使用?
它们根据适用范围和存储状态的复杂度的不同而进行不同的选取。
(1) @State和@Binding提供View内部的状态存储,它们应该是被标记为private的简单值类型,仅在内部使用。
(2) ObservableObject和@ObservedObject则针对跨越View层级的状态共享,它可以处理更复杂的数据类型,其引用类型的特点,也让我们需要在数据变化时通过某种手段向外发送通知(比如手动调用objectWillChange.send或者使用@Published)来触发界面刷新。(跨越式View层级)
(3) 对于“跳跃式”跨越多个View层级的状态,@EnvironmentObject能让我们更方便地使用ObservableObject,以达到简化代码的目的。(跳跃式View层级)
四、真实世界的SwiftUI
1、SwiftUI中的动画类型
(1) 隐式动画:通过View上的animtion修饰,就可以在View中支持动画的属性发生变化时自动为整个View添加上动画支持了。这种动画并没有指明运行时机,它会在View中属性改变时自动触发。
(2) 显式动画:显式动画通过明确的withAnimation调用触发,我们可以将改变app状态的操作放在withAnimation的闭包中,这时由闭包中状态变化所触发的View变化,将以动画形式呈现。
2、Form表单的作用
Form表单时专门处理类似设置页这类需求的,它在不同平台(比如iOS或者MacOS)中会按照该平台的特点进行渲染,在iOS中它是一个分组列表(Grouped List)。
3、UIKit中常见的处理异步任务的设计模式
闭包、代理和通知
五、Combine和异步编程
1、响应式异步编程的特点
(1) 事件会适合的时机进行发布,它会携带一些数据。
(2) 会有一个订阅者来处理这些事件和数据,从而驱动程序的其他部分(比如UI界面)。
(3) 订阅通常是在事件完成之前进行的,而源源不断地事件形成事件流。
2、什么是Combine框架
(1) Combine框架是苹果提供的一套响应式异步编程接口。
(2) 它的处理流程通常为:
① 一个事件及其附带的数据被发布出来
② 订阅者接收这个事件
③ 事件及其数据可以通常一系列的变形操作来进行转换为所想要的。
(3) Combine框架的三个角色:负责发布事件的发布者(Publisher)、负责订阅事件的订阅者(Subscriber)和负责事件及其数据变形的操作者(Operator)
3、事件发布者(Publisher)
(1) Publisher协议包含两个关联类型(Output和Failure)和一个receive方法,用于发布新的事件及其数据,以及准备好被Subscriber订阅。
(2) Publisher可以发布的三种事件:
① Output类型的新值:代表事件流中出现了新的值
② Failure类型的错误:代表事件流中发生了问题,事件流到此终止
③ 完成事件:表示事件流中所有的元素都已经发布结束,事件流到此终止。
(3) 事件流按事件的数量分为:有限事件流和无限事件流,可理解为Publisher的生命周期
4、事件操作者(Operator)
(1) Publisher所提供的事件和数据,往往并不能够直接用来驱动决定UI的状态。因此,需要通过Operator来对原有的Publisher进行变形等逻辑操作。
(2) 每个Operator的行为模式都一样:它们使用上游Publisher所发布的数据作为输入,以此产生的新的数据,然后自身成为新的Publisher,并将这些新的数据作为输出,发布给下游。
(3) 通过一系列组合,我们可以得到一个响应式的Publisher链条:当链条最上端的Publisher 发布某个事件后,链条中的各个Operator对事件和数据进行处理。在链条的末端我们希望最终能得到可以直接驱动UI状态的事件和数据。这样,终端的消费者可以直接使用这些准备好的数据,而这个消费者的角色由Subscriber来担任。
5、事件订阅者(Subscriber)
(1) Subscriber协议包含两个关联类型(Input和Failure)和三个receive方法。
(2) sink方法用于直接订阅,assign方法用于驱动UI。
6、Combine框架中的Subject
(1) Subject协议提供了两个send方法,用于实现将传统的指令式异步API里的事件和信号转换到响应式的世界中去。
(2) Combine框架内置了两个Subject的实现:
① PassthroughSubject:简单地将send接收到的事件转发给下游的其他Publisher或Subscriber,它不会对接受到的值进行保留,当订阅开始后,它将监听并响应接下来的事件。
② CurrentValueSubject:会包装和持有一个值,并在设置该值时发送事件并保留新的值。在订阅发生的瞬间,CurrentValueSubject会把当前保存的值发送给订阅者。
7、Combine框架中的Scheduler
(1) Scheduler用于解决发布事件的地方(where)和时机(when)。
(2) Where:在更新UI时,我们需要保证相关操作发生在主线程。因为异步API往往涉及到在不同线程间切换,这个问题就显得尤为重要。RunLoop就是一个实现了Scheduler协议的类型,它知道要如何执行后续的订阅任务。如果没有receive(on: RunLoop.main) 的话,sink的闭包将会在后台线程执行,这在更新UI时将带来问题。
(3) When:Scheduler的另一个重要工作是为事件的发布指定和规划时间。默认情况下,被订阅的Publisher将尽可能快地把事件传递给后续的处理流程,不过有些情况下,我们会希望改变事件链的传递时间,比如加入延迟或者等待空闲时再进行传递(如delay和debounce等),这些延时也是由Scheduler负责调度的。
六、Publisher和常见Operator
1、发布者和订阅者的订阅过程
(1) Publisher.subscribe:发起订阅
(2) Subscriber.receive(subscription:):收到订阅
(3) Subscription.request(:):请求数据
(4) Subscriber.receive(:):收到数据
(5) Subscriber.receive(_:):更多数据
(6) Subscriber.receive(completion:):收到完成
2、常用的发布者(Publisher)和操作符(Operator)
(1) Empty:仅一个完成事件
(2) Just:1个输出事件和1个完成事件
(3) Fail:仅一个错误事件
(4) Sequence:数组或区间转换为发布者
(5) map:对输出进行变形
(6) reduce:对输出进行合并
(7) scan:对输出进行合并,并记录每一步
(8) compactMap:压缩变形
(9) flatMap:展平变形
(10) removeDuplicates:移除重复输出
3、Publisher的结束事件有哪两种?
正常完成事件(.finished)和异常错误事件(.failure)
4、异步编程的错误处理有哪三种方式?
(1) 转换错误类型:mapError
(2) 尝试抛出错误:tryXXX(如:tryMap、tryScan、tryFilter和tryReduce等)
(3) 从错误中恢复:replaceError或catch
5、Combine编程的核心工作是什么?
组合各种Operator来生成满足最终逻辑的Publisher
6、eraseToAnyPublisher方法的作用?
用于将复杂类型的Publisher进行类型抹消
七、响应式编程边界
1、Combine框架中的Subject作用?
(1) 它除了需要遵循Publisher协议,还暴露了一些send方法,从而可以让外界通过它们来发送事件。
(2) 通过它可以将传统指令式编程转换为响应式的。
2、事件序列中的逻辑运算是什么?
(1) zip(当且):当Publisher1发布值,且Publisher2发布值时,将两个值合并,作为新的事件发布出去。【按相同index合并】
场景:同时发出了多个网络请求,希望在它们全部完成的时候把结果合并在一起。
(2) combineLatest(当或):不论是哪个输入Publisher,只要发生了新的事件,它就把新发生的事件值和另一个Publisher中当前的最新值合并。【按最新值合并】
场景:注册界面上有多个TextField,你可能想要在其中某一个值变动时获取到所有TextField 中的值并对它们进行检查。
3、如何将异步的指令式编程转换为响应式编程?
(1) Future方式:只适用于产生且只产生一个结果的场景,比如网络请求。
(2) Subject方式:适用于产生至少两个结果的场景,比如弹出键盘或定时间隔多次调用。
4、Foundation框架中Publisher接口有哪些?
(1) URLSession的dataTaskPublisher方法:用于在网络请求成功时发布一个值为(Response, Data)的事件,请求过程失败时,发送类型为URLError的错误。
(2) Timer的publish方法:用于创建一个按照一定间隔发送事件的Publisher。注意,它是一个ConnectablePublisher类型,需要调用connect方法才开始发送事件。
(3) NotificationCenter的publisher方法:其Output值是Notification,与传统的通知体系没有任何区别。
5、什么是@Published属性包装器?
(1) 用于修饰class中的属性,从而将该属性转换为Publisher,使属性同时具有存储值和发布值(通过投影符号$获取)的功能。
(2) 在被订阅时,当前值也会被发送给订阅者,它的底层其实就是一个CurrentValueSubject。
6、如何进行订阅和绑定?
(1) sink方法:用于订阅Publisher,并返回一个AnyCancellable值。
(2) assign方法:用于将Publisher的输出值通过keypath绑定到一个对象的属性上去。在SwiftUI中,这种值通常会是ObservableObject中的属性值,它进一步会被用来驱动View的更新。
7、share操作符的作用?
对于值类型的Publisher,它在进行变形的时候会被复制一份,当我们存在多个Subscriber对应一个Publisher的场景,就需要共享这个Publisher,便可使用share操作符将它从值类型转变为引用类型来达到目的。
8、Combine框架的内存管理
(1) 对于需要connect的Publisher,在connect后需要保存返回的Cancellable,并在合适的时候调用cancel()以结束事件的持续发布。
(2) 对于sink或assign的返回值,一般将其存储在实例的变量中,等待属性持有者被释放时一同自动取消。不过,你也完全可以在不需要时提前释放这个变量或者明确地调用cancel()以取消订阅或绑定关系。
(3) 对于情况一,也完全可以将Cancellable作为参数传递给AnyCancellable的初始化方法,将它包装成为一个可以自动取消的对象。
八、SwiftUI架构
1、Redux架构中Reducer的作用?
(1) 避免将业务逻辑直接放在View中
(2) 便于将业务逻辑统一放在Reduce中
(3) 把单纯的状态变化逻辑统一放到Reducer中,那单元测试就将轻而易举
2、什么是Redux架构中的“副作用”?
(1) “副作用”表示异步任务
(2) reduce方法仅用于改变state,它可以触发“副作用”
(3) “副作用”结束后,可以发送一个新的action给reduce来改变state
(4) 对于不会改变state的“副作用”被称为“纯副作用”,可以将它通过属性的didSet来处理,而跳过action的方式
九、用户体验和布局进阶
1、SwiftUI遵循的布局规则是什么?
协商解决、层层上报:父级的View根据某种规则,向子View“提议”一个可行的建议尺寸;子View以这个尺寸为参考,按照自己的需求进行布局:或占满所有可能的尺寸(比如Rectangle和Circle),或按照自己要显示的内容确定新的尺寸(比如Text),或把这个任务再委托给自己的子View继续进行布局(比如各类Stack View)。在子View确定自己的尺寸后,它将这个需要的尺寸汇报回父View,父View最后把这个确定好尺寸的子View放置在座标系合适的位置上。
2、一句话描述View Modifier的作用?
用一个带有相关属性的新View来包装原有的View