学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

Random Lessons from the SwiftUI Digital Lounge . Part 1 [译]

Animations

视图从一个位置到另一个位置的过程可以动画化吗?比如改变父级视图的场景?

当然,你可以去看一看 matchedGeometryEffect() ,这是去年的 SwiftUI 2.0 版本引入的 API

文本字体尺寸的变化可以动画化吗?

完整问题: 我们现在有办法可以动画化文本字号的变化吗?因为尺寸和背景颜色现在能够平滑过渡,但是字体会直接跳变,中间没有插值过程。

可以去参考一下 Fruta 范例工程里的 AnimatableFontModifier,那里边用了显式的字号作为可动画数据,使用场景是主视图和详情视图上的配料卡片的平滑过渡效果。这个实现对于 Fruta 来说已经够用,毕竟用例有限。

译者:作者对于进阶动画技术也有一系列博文,其中有一篇特别提到了 AnimatableModifier ( 「Swift花园」也发布过该博客的译文 —— “SwiftUI 动画进阶 - part3: AnimatableModifier”,感兴趣的读者可以查阅)

AppKit/UIKit

SwiftUI 里有像 UIView 的 drawHierarchy 那样可以把视图画到图像中的 API 吗?

SwiftUI 并没有支持这个功能的 API,但是借助 UIHostingController,我们可以把 SwiftUI 视图包装起来,然后在 hosting controller 视图上使用 drawHierarchy 实现目标。

如何控制UIViewRepresable视图的理想尺寸?

原问题: 我要如何控制一个 UIViewRepresentable 视图的理想尺寸呢?在获取被包装视图的自动尺寸时,我遇到过不少麻烦,特别是当被包装视图是 UIStackView 的时候。关于获取合适的自动尺寸有没有推荐的方式?好让我不需要过度依赖 fixedSize

回答:尝试在你的视图上实现intrinsicContentSize

UIHostingController 可以和 AnyView 一起使用吗?

原问题:我有一个框架,它会通过 UIHostingController 传出一个 SwiftUI 视图给主 app。这个视图在内部处理了它用到的所有东西,因此所有的类型都是 internal 的。唯一的 public 方法是对外给出 UIHostingController。 为了实现隔离维护,我是这样做的:return UIHostingController(rootView: AnyView(SchedulesView(store: store)))。这算是 AnyView 的一种正确用法吗?

答复:是的,这个用法没问题,尤其是当 AnyView 被用于视图层级的最基础层而不是用于实现动态性。不过也有别的方式,你可以封装精确类型的 hosting controller,比如返回一个向上转换或者自定义的协议类型:

  • UIViewController 的类型而不是实际的 UIHostingController<..> 类型返回
  • 创建一个客户端期望返回类型的精确 API,然后返回那个类型

或者,你还可以借助一个容器 UIViewController 来包裹你的 hosting controller,这种做法带来的额外好处是,调用模块可以移除对 SwiftUI 的依赖。

为什么 UIViewRepresentable 会在 makeUIView 之后和 dismantleUIView 之前更新一次?

更新函数可能因为多种原因被调用。当 UIView 存在时,它至少会被调用一次,并且在 UIView 被废弃之前可能调用多次。所以你不能依赖这个更新调用的频率。

追加的问题:我实现了一个 UIViewDiffableRepresentable ,它遵循 Hashable 协议,会在有效的更新之后检查各项属性,以防止开销很重的逻辑触发多余的 updateUIView 调用。这个做法是否优化过度了?有没有更合理的做法?

答复:这么做的确过度优化了。框架只有在 representable 结构外的属性实际改变时才会调用 updateUIView,你可以把更新放心地托付给它。

创建 UIViewRepresentable 的时候,让 Coordinator 持有通过 updateUIView() 传入的 UIView 是一种危险的行为吗?

这样做是安全的,你的 Coordinator 会在任何视图构建之前就被创建 —— 所以在 makeUIView 里你可以让 Coordinator 持有视图的引用。

在 SwiftUI 2 中有没有可以转换旧的 AppDelegate/SceneDelegate 生命周期的方法?

是的,你可以在 App 中使用 UIApplicationDelegateAdaptor 属性包装器,比如 UIApplicationDelegateAdaptor var myDelegate: MyAppDelegate

SwiftUI 会为你实例化一个 UIApplicationDelegate 并且按照 AppDelegate 的方式来回调它的方法。此外,你还可以通过 configurationForConnectingSceneSession 来返回自定义的 scene delegateSwiftUI 也会实例化并且按照 SceneDelegate 的方式回调它的方法。

向后兼容性

能说一说现有的 SwiftUI 代码要继承新版本特性有什么方法吗?我想要用一套代码同时支持 iOS 14 和 iOS 15。

大部分新特性不能向后发布到更早的系统版本。你可以用下面的方式来检查某个特性是否可用:

1
2
3
4
5
if #available(iOS 15, *) {
...
} else {
// 更早版本的回滚策略
}

这也就是说,确实有一些特性可以向后发布。比如,把对集合的绑定直接传入 ListForEach,然后取回每个元素的绑定:

1
2
3
ForEach($elements) { $element in
...
}

这个特性可以向后发布到支持 SwiftUI 的早期版本。

WWDC21 还提到了令一个向后发布的特性,那就是 enum-like 风格。

“解密 SwiftUI” 中提到用 @ViewBuilder 来消除对 AnyView 的使用,这个方法是否只能在 iOS 15 上使用?

不是,事实上这个方法可以向后发布到任何支持 SwiftUI 的版本。

编程策略

SwiftUI 中是否存在只能用 AnyView 而不能用其他替代方案构造视图的场景?

关于能不能使用 AnyView 的问题有不少。如果你能避免使用 AnyView,我们会建议你这么做,比方说采用 @ViewBuilder 或者泛型来传递视图。

不过,我们之所以会提供 AnyView,是因为我们明白,的确存在某些场景是其他方式无法解决的,或者权衡之下 AnyView 是可行的。

这里有一个小规则:假如被包装的视图很少改变或者几乎不改变,那么 AnyView 肯定没问题。但如果把 AnyView 用在那些会在不同的状态之间来回切换的场景,则可能会带来性能问题。这是因为为了管理这个过程,SwiftUI 需要承担额外的负荷。

Child 和 Parent 的 body ,哪一个会先被计算?

原问题:在“解密 SwiftUI” 的 Dependency Graph 部分,视频提到了两个视图,它们都依赖于相同的依赖项,需要生成新的 body。假如其中一个是另一个的子视图,那么哪个视图的 body 会被先计算 呢?

答复:父视图会先生成 body,然后递归遍历它的所有子视图。

如果不想借助 AnyView,我们要怎样传递一个视图给 ViewModifier ?

原问题:我创建了一个给视图添加自定义模态 overlayViewModifier,效果类似 sheet。有没有办法以构造器参数的方式把一个视图传给这个 ViewModifier,而不用求助于 AnyView ?我希望能把 overlay 的实际内容直接传入构造器。

答复:你可以通过实现你自己的带泛型参数的 ViewModifier 来实现这个目标,比如:struct MyModifier<C: View>: ViewModifier { ... },然后里面声明一个类似 var content: C 这样的属性:

这里提供一个完整的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ExampleView: View {
var body: some View {
RoundedRectangle(cornerRadius: 10)
.fill(.green)
.frame(width: 200, height: 80)
.modifier(MyHoverModifier(hoverView: Text("hello!").font(.largeTitle)))
}
}

struct MyHoverModifier<C: View>: ViewModifier {
@State var isHovering = false
var hoverView: C

func body(content: Content) -> some View {
content
.overlay(self.hoverView.opacity(isHovering ? 1.0 : 0.0))
.onHover {
isHovering = $0
}
}
}

Group 和 ViewBuilder 该怎么选?

原问题: Group { if whatever { … } else { … } }ViewBuilder.buildBlock(pageInfo == nil ? ViewBuilder.buildEither(first: EmptyView()) : ViewBuilder.buildEither(second: renderPage) )。这两种写法都能实现条件化构建视图,哪一种更好呢?

答复:这种场景下用 Group 更好。

追加的问题:这是出于可读性的考量?还是说一者的性能会优于另一者?

答复:主要是出于可读性 —— 通常我们不建议直接调用 result builder 的实现,而是让编译器去处理它们。

我们可以用 .id() 来保持视图的等价性吗?

原问题:如果我们在条件化构建中给视图应用了相同的 id,SwiftUI 会将它们视为相同的视图吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
var body: some View {
if isTrue {

Text("Hello")
.id(viewID)

} else {

Text("World")
.id(viewID)

}
}

答复:不,它们会是两个不同的视图。

这是因为 body 是一个隐式的 ViewBuilder。如果你并不是用 ViewBuilder,比如上面的代码是放在另外一个普通的属性里,那么它们将会是相同的视图。或者,你也可以这样写:

1
2
3
var body: some View {
Text(isTrue ? "Hello" : "World").id(viewID)
}

追加的问题:除了 body 之外,还有其他隐式 ViewBuilder 的例子吗?

答复:是的,例如 ViewModifier 的 body 函数,view style 的 makeBody, preview providers 等等,有很多。

追加的问题:所以我们应当在 view builder 尽量避免条件化吗?

答复:当然不是。条件化存在是有原因的,只是应该避免过度使用。

有没有一些场景我们应该优先使用 Hashable 而不是 Identifiable ?

如果你只需要识别一个单一值,那么 Identifiable 就是为此而生的,这意味着只有 id 属性要求是 Hashable,而不是整个类型。

对于样式,我要怎么实现条件化?

原问题:我们如何条件化设置不同的 modifier,比如说列表的样式?

1
2
3
4
5
List {

...

}.listStyle(isIpad ? .sidebar : .insets)

答复:SwiftUI 里的样式是静态的,不允许在运行时改变,上面的场景里分支语句会更合适。不过,你首先应该考虑是否真的有必要改变样式 —— 单一样式通常是正确的选择。

假如你正在寻找某种动态机制,可以向我们提出反馈。

对 SwiftUI 视图条件化应用 modifier 有没有某种最佳实践?

原问题:给 SwiftUI 视图条件化应用 modifier 有没有最佳实践呢?我自己实现了一个 .if modifier,当状态变化时整个视图都会刷新

答复:可以考虑采用惰性(inert) modifier,如果哪个 modifier 缺少惰性版本,请反馈给我们。

在使用新的 SwiftUI Table 视图时,我可以把 10 个以上的 TableColumn 以 Group 的方式组织起来吗?

是的,你当然可以这么做,就像我们对视图那那样做!

追加的问题:那我是否可以理解为: @ViewBuilder 里不再有对象数量的限制了?

答复:@ViewBuilder 今年没有变化,它可以构建的元素数量仍然是有限制的。但是 Group 和嵌套 builder 可以帮助你将很多视图组合起来。

经过测试,我发现 ForEach 和控制流语句对 TableColumnBuilder 不起作用。这真遗憾,因为我能够预见,有许多场景会有这种需求。对于这个问题,我已经提出了反馈: FB9189673 (ForEach) 和 FB9189678 (控制流)。

在 UIHostingController 中使用 Core Data 要怎么回避 AnyView 呢?

原问题:在 SwiftUI 中使用 UIHostingControllerCore Data 时,我们要如何避免因为 environment modifier 导致视图类型变化而不得不使用 AnyView 的问题呢?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import UIKit
import SwiftUI
import CoreData

struct MyView: View {
var body: some View {
Text("!")
}
}
class MyHostingController: UIHostingController<MyView> {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// custom stuff here
}
}
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let persistentContainer = NSPersistentContainer(name: "MyStore")
let rootView = MyView().environment(\.managedObjectContext, persistentContainer.viewContext)
let hostingController = MyHostingController(rootView: rootView) // this will not work anymore because type has changed with the environment modifier
// more stuff
}
}

答复:这个问题很棒!有一个解决方案,不过里面的方括号会多到瞎… 在 class MyHostingController: UIHostingController 中,MyView 其实并不是我们的目标类型,你需要的是 MyView().environment(.managedObjectContext, persistentContainer.viewContext) 的类型,它的完整形式是 ModifiedContent<MyView,...>(… 部分太长了,这里省略)。

通常我的做法是拷贝这个类型,然后声明一个顶级类型别名:typealias MyModifiedView = ModifiedContent<MyView, ...>,其中右边的类型是从错误消息中复制的。这样一来,你就可以把代码写成:class MyHostingController: UIHostingController 了。

答复推荐的解决方案在之前是可以工作的。但由于现在 swiftc 已经不再报告具体类型,而是报告 some View,所以这个办法行不通了。
有一个变通的办法是先打印出具体类型:

1
2
let rootView = MyView().environment(\.managedObjectContext, persistentContainer.viewContext)
print("\(type(of: rootView))")

但这样做还不够,你还得强制转换类型,否则无法通过编译:

rootView as! MyModifiedView

完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import UIKit
import SwiftUI
import CoreData

typealias MyModifiedView = ModifiedContent<MyView, _EnvironmentKeyWritingModifier<NSManagedObjectContext>>

struct MyView: View {
var body: some View {
Text("!")
}
}

class MyHostingController: UIHostingController<MyModifiedView> {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// custom stuff here
}
}

class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let persistentContainer = NSPersistentContainer(name: "MyStore")
let rootView = MyView().environment(\.managedObjectContext, persistentContainer.viewContext)
let hostingController = MyHostingController(rootView: rootView as! MyModifiedView)
// more stuff
}
}

假如 SE-309 生效并且应用于 View,届时一个 View 和一个 AnyView 在视图等价性这一点上还会有实质性的区别吗?

原问题: SE-309 使得我们可以把关联类型纳入枚举的 existential type,假设它会涵盖 View 类型,那么到那个时候,一个 View 的 existentialAnyView 在视图等价性上还有区别吗?

答复:我喜欢这个提案!对于实现的细节我无法评价,但相较于 existentialAnyView 擦除了更多信息,所以 existential 仍然会是区分的边界。

现在我们有了 task,那么是否还有应该使用 onAppear 而非 task 的场景吗?

答复(工程师 #1): onAppear() 仍然可以使用。对于之前的代码可以不需要更新。我认为 task() 提供了更宽泛的解决方案,即便对于耗时很短的同步任务来说也是(适用的),因为它让你可以在将来需要的时候升级到异步的任务。

追加的问题:这么说,新代码你总是会使用 task(),还是说 onAppear() 仍然有它自己适用的地方?我可能会认为 onAppear 类似于被弃用了?

答复(工程师 #1):我个人会始终采用 task(),但一些人可能会喜欢 onAppearonDisppear() 的对称性。

追加的问题:那么二者有区别吗?

答复(工程师 #1): task() 会在 onDisappear 时取消异步任务,并且不会再触发新的任务。

答复(工程师 #2):通常我们会避免废弃 API,除非它们真的有害。之前提到,onAppear 相比 task 有更多的限制,所以我会建议在新代码中使用 task。但不管怎么说, onAppear 是无害的。

参考

Random Lessons from the SwiftUI Digital Lounge