Ask Apple 为开发者与苹果工程师发明了在 WWDC 之外进行直接沟通的时机。本文对本次活动中与 SwiftUI 有关的一些问答进行了收拾,并增加了一点个人见解。本文为下篇。

原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

Q&A

Form vs List

Q:这可能是一个十分愚蠢的问题,但我一直对 Form 和 List 感到困惑。它们之间有什么差异,什么时候应该运用 Form ,什么时候应该运用 List ?谢谢!

A:Form 是一种将许多相关控件组合在一同的办法。尽管 Form 和 List 在 iOS 上看起来差不多,但假如你看一下 macOS,就会发现它们之间的不少差异。与 macOS 上的 List 比较,许多控件在 Form 中的外观和行为都有所不同。与 Form 不同的是,List 内置了对修正方法( Edit Mode )的支撑。因此,假如你正在创立一个视图来显现可翻滚的内容,并可能进行挑选操作,那么在 iOS 和 macOS 上运用 List 将有最好的体会。假如你要渲染许多相关的控件,运用 Form 会在 iOS 和 macOS 上有最好的默认体会。

除了前期的 SwiftUI 版别,Form、List、LazyStack 以及 LazyGrid 之间在执行功率和子视图的生命周期方面的体现都相当挨近。SwiftUI 4.0 的 Form 在 Ventura 上的体现与以往版别有很大的不同。方法上更挨近 iOS 的状况,一同也对 mac 进行了更多的适配。

Ask Apple 2022 与 SwiftUI 有关的问答(下)

在辅佐状况躲藏图画

Q:关于辅佐功用,Image(decorative:) 和 .accessibilityHidden 之间是否有差异?

A:没有差异,运用这两种办法可以适当地躲藏图画,使其不被辅佐技术所发现!

accessibilityHidden 支撑任意符合 View 协议的元素,一同可以动态调整它的躲藏状况。

Table 中上下文菜单

Q:假如我在 TABLE 上增加了一个上下文菜单,我怎样确定哪一行导致了菜单的显现(无需挑选该行)?

A:在 TABLE 外运用 contextMenu(forSelectionType:) 。

在 上篇第一个问题中 已经介绍了 contextMenu(forSelectionType:) 的运用办法。同常常运用的 contextMenu 不同,contextMenu(forSelectionType:) 是针对整个 List 或 Table 运用的( 非单元格 )。阅览 用 Table 在 SwiftUI 下创立表格 ,了解 Table 的具体用法。

视图的功用优化

Q:面对杂乱的用户界面时,操控视图中的更新规模的最佳做法是什么( 以防止不需求的转发以及重复核算 )。在更杂乱的 UI 中,由于视图的更新速度过快,功用( 至少在 macOS 上 )迅速下降。

A:有不同的策略。

  • ObservableObject 是使视图或视图层次结构的失效( 引发重新核算 )的单元。你可以运用符合 ObservableObject 协议的不同方针来切割失效的规模
  • 有时,不依赖 @Published 而取得一些手动操控并直接向 objectWillChange 发布改变是很有用的
  • 增加一个中心视图,只提取你需求的特点,并依靠 SwiftUI 的 equality 检查来提早中止无效核算

苹果工程师给出的答案与 防止 SwiftUI 视图的重复核算 一文中的许多主张都一致。视图的功用优化是一个体系工程,在对其运作机制、注入原理、更新时机等方面有了归纳认识后,可以更好地做出有针对性的处理计划。

快速检索数组元素

Q:为什么没有简单的办法将 TABLE 挑选的行映射到供给表内容的数组元素上?似乎仅有的办法是在数组中搜索匹配的 id 值,这关于大表来说似乎功率很低。

A:用数组索引来存储挑选是很软弱的:假如数组发生了骤变,挑选就会变得不同步。Swift Collections 有一个 OrderedDictionary,可能会对你有所协助。

这正是 Swift Identified Collections 项目存在的含义。Swift Identified Collections 是依据 OrderedDictionary 完结的一个具有键特点的类数组。它的仅有要求是元素有必要符合 Identifiable 协议。

struct Todo: Identifiable {
  var description = ""
  let id: UUID
  var isComplete = false
}
class TodosViewModel: ObservableObject {
  @Published var todos: IdentifiedArrayOf<Todo> = []
  ...
}
// 可以用相似字典的办法对元素进行操作,快速定位,一同在更新 IdentifiedArray 时,也不容易引发 ForEach 的反常
todos[id:id] = newTodo

自定义布局

Q:在完结自定义布局时,处理十分小或十分大的可用空间的边际状况有多重要?

A:和许多作业相同,这个问题的答案是取决于你的运用状况( 无论这个答案多么不令人满意:sweat_smile: )。假如容器对 zero 和 infinite 的可用空间提出要求,需求用以确定最小和最大的尺度,至少应该考虑这些状况。除此以外,当你企图完结一个可以在各种状况下运用通用的布局时,一定要考虑!可是,假如你只是自己运用它,而且条件可控,那么不处理这些状况也是合理的。

创立一个考虑到一切状况的通用布局( 例如:VStack、HStack )是一项相当艰巨的作业。开发者即便无法完结这样的布局容器,也应对各种尺度需求的定义有清晰的了解。在 SwiftUI 布局 —— 尺度( 上 ) 一文中,对主张尺度的几种方法都进行了介绍。

怎样削减主线程的担负

Q:怎样防止一切操作都被放置在主线上?任何标记 @Published 的变量都应该在主线上被修正,所以应该运用 @MainActor 。但任何触及该特点的代码都将被影响。是否有主张的规范方法或办法来改善这一点?

A:一般来说,你确实需求在主线程上与 UI 结构互动。在运用引证类型时,这一点尤其重要,由于你有必要确保总是有对它进行序列化的读取。实际上,咱们有一个十分棒的 WWDC 演讲,具体介绍了并发性和 SwiftUI ,特别提到了有关运用 ObservableObject 的状况。一般来说,功用瓶颈不在写入 @Published 特点的周围。我主张的办法是在主线程之外做任何昂贵的或阻塞的作业,然后只在需求写入 ObservableObject 上的特点时再跳回主线程。

@State 是线程安全的,@StateObject 会主动将 wrappedValue( 符合 ObservableObject 协议的引证类型 )标示为 @MainActor 。

自定义布局

Q:我常常想依据列表中最长或最短的文字来安置各种小组件。鉴于动态文本巨细在应用程序运转时可能会发生改变,衡量给定字体的文本巨细的最佳办法是什么?

A:你好!咱们新的布局协议支撑这个功用。任何自定义布局的完好完结都比我在这里的帖子中快速勾勒出来的要长,但整体思路是,你可以创立一个布局来查询其子级的理想巨细并相应地对它们进行排序。然后,您可以运用垂直或水平堆栈布局来组合它,这样您就不需求自己完结一切的完结作业。

Jane 的 主动依据宽度排版 视频与该问题十分符合。阅览 The SwiftUI Layout Protocol 了解怎样创立自定义布局。

创立从底部开端的翻滚视图

Q:我怎样完结一个在底部对齐的翻滚视图,在 macOS 上会不会有糟糕的功用?我选用了常见的处理计划,即旋转翻滚视图和里边的每个单元格,以取得预期的倒置列表,在 iOS 上,这很有效。但在 macOS 上,它使 CPU 运用率保持在 100%。

A:你最好的挑选是运用 ScrollView 和 ScrollViewReader,并在 onAppear 或新内容进来时翻滚到最底部的视图。我不主张测验旋转翻滚视图。

Swiftcord 的代码展现了怎样在 SwiftUI 下完结倒置列表。阅览 优化在 SwiftUI List 中显现大数据集的呼应功率 一文,了解苹果工程师推荐的办法。在两种计划中,假如在数据量很大的状况下,我更倾向于第一种办法,这样可以按需求读取数据。

定制 List

Q:是否有办法以完全可定制的办法运用 List ,这样我就可以完结删去缩进、分隔线,乃至更改整个列表的布景等操作? 现在,我总是去找 LazyVStack 来代替。

A:有多种润饰器可以完结这个功用:listRowSeparator, listRowInsets。不支撑整个列表填充,请对此提出反应。

在 SwiftUI 4 中,可以运用 .scrollContentBackground(.hidden) 躲藏列表的默认布景

searchable

Q:是否有办法在.searchable() 润饰器中以编程办法设置搜索字段的焦点?

A:你可以运用 dismissSearch 环境特点以编程办法撤销搜索字段。现在还没有 API 可以程序化地将焦点转至搜索字段。

TextField 内容验证

Q:怎样完结一个只承受数字的 SwiftUI TextField,小数是答应的。

A:向文本字段供给 FormatStyle 以完结主动将文本转换为各种数字。可是,此转换仅在文本字段完结修正时才会发生,而且不会阻挠输入非数字字符。现在 SwiftUI 没有 API 可以限制用户在字段中输入的字符。

很期望苹果可以继续扩展依据 FormatStyle 的处理计划,让其可以实时对输入内容进行校验。阅览 SwiftUI TextField 进阶 —— 格局与校验 一文了解其他的验证手法,以及怎样经过 onChange 完结近乎实时地限制输入字符的办法。

将布景扩展到安全区域

Q:假如我有一个自定义的容器类型,可以承受一个顶部和底部的视图,是否有办法让 API 的调用者将所供给的视图的布景扩展到安全区域内,一同将内容( 如文本或按钮 )保存在安全区域内?

A:你可以测验运用 safeAreaInset(edge: .top) { … } 或 safeAreaInset(edge: .bottom) { … } 润饰器来放置你的顶部和底部视图。然后让顶部/底部视图疏忽安全区域。我不确定这是否能满意你的用例,但值得一试。

在 background 润饰器中,可以经过 ignoresSafeAreaEdges 参数设置是否疏忽安全区域。这个技巧关于处于屏幕的顶部或底部的视图十分有用。详情请参阅 推文 。

动画转场

Q:为什么下面的代码没有显现动画转场。

struct ContentView: View {
    @State var isPresented = false
    var body: some View {
        VStack {
            Button("Toggle") {
                isPresented.toggle()
            }
            if isPresented {
                Text("Hello world!")
                    .transition(.move(edge: .top).animation(.default))
            }
        }
    }
}

A:测验将动画润饰器移到 transition 参数之外。

struct ContentView: View {
    @State var isPresented = false
    var body: some View {
        VStack {
            Button("Toggle") {
                withAnimation {
                    isPresented.toggle()
                }
            }
            if isPresented {
                Text("Hello world!")
                    .transition(.move(edge: .top))
                    .animation(.default, value: isPresented)
            }
        }
    }
}

在上面苹果工程师给出的修正代码中。.animation(.default, value: isPresented) 是多余的。转场的动画事件是经过 withAnimation 来显式增加的。关于相似的状况,也可以不运用显式动画驱动( 不运用 withAnimation ),只需将 .animation(.default, value: isPresented) 移动到 VStack 之外即可。阅览 SwiftUI 的动画机制 一文,了解更多有关动画的内容。

在 NavigationSplitView 的边栏中运用 LazyVStack

Q:iOS 16 的新 NavigationSplitView 当前只与主( master )列中的 List 一同作业。这意味着咱们不能运用 LazyVStack,或任何其他将挑选与具体视图绑定的自定义视图。有扩展这个功用的计划吗?

A:在 iOS 16.1 中,你可以在侧边栏里放一个。navigationDestination,这样侧边栏里的 NavigationLink 就会代替具体栏的根视图。

NavigationSplitView {
    LazyVStack {
        NavigationLink("link", value: 213)
    }
    .navigationDestination(for: Int.self) { i in
        Text("The value is \(value)")
    }
} detail: {
    Text("Click an item")
}

这是一个相当重要的改进!处理了之前的一大惋惜。如此一来,边栏视图的款式自由度取得了极大的提高。

软弃用

Q:最近,我留意到新的 @ViewBuilder 函数在曾经的版别中是不可用的,弃用信息提示我运用新的办法代替老办法,这是 SwiftUI 的 API 规划缺点仍是我错过了什么?

 @available(iOS, introduced: 13.0, deprecated: 100000.0, message: "Use `overlay(alignment:content:)` instead.")
 @inlinable public func overlay<Overlay>(_ overlay: Overlay, alignment: Alignment = .center) -> some View 

A:100000.0 的 deprecated 版别是 Swift 结构作者的一种沟通办法,即一个 API 不应该在新项目中运用,但在现有项目中继续运用也无妨。这种 “软弃用” 的 API 不会在代码主动补全中供给,而且一般处在文档中单独的一个部分。但编译器不会对现有的运用宣布正告。由于这些运用并不有害,咱们不期望开发者由于运用了新的编译器版别而处理一堆的正告。

macOS API

Q:关于运转 Monterey 的 Mac,能否怎样在 SwiftUI 中完结下面需求的主张:

  1. 翻开一个窗口
  2. 在该窗口中初始化数据
  3. 找到一切翻开的窗口
  4. 确定一个窗口是否翻开
  5. 从不在该窗口的视图中封闭一个窗口

A:我想说的是,假如可以,将 macOS Ventura 作为方针平台会对其间的一些操作更有协助。特别是,咱们在 WindowGroup 上增加了新的 OpenWindowAction 和新的初始化办法,这将一同满意 1 和 2 。假如您无法做到这一点,则可以运用 URL 和 handleExternalEvents 来仿照其间的一些行为,但它的局限性要大得多。关于其他点,现在没有适宜的 API 。

连锁动画

Q:在 SwiftUI 中,怎样完结连锁动画?例如,我想先给一个视图做动画,当动画完结后立即发动另一个动画。

A:不幸的是,现在不可能完结连锁动画。依据你的问题,你可以运用 animation.delay(…) 将动画的后半部分延迟到前半部分完结之后。假如你能将你的用例的细节反应给咱们,咱们将十分感激。

SwiftUI 当前缺乏动画完结后的回调机制。在动画不杂乱的状况下,可以经过创立一个符合 Animatable 协议的 ViewModifier 来同步查询动画的进程。详情请参阅 推文、代码 。

Too complex to type check

Q:我在 iOS 14 SwiftUI 中遇到一个问题,我企图有条件地显现 3 个符合 Shape 协议的方针中的一个。其间 2 个是自定义形状( 基本上是圆角矩形,只要两个角是圆的 ),其间一个是矩形。编译器抛出一个错误,说它花了太多时刻来检查视图的类型。

A:是的,不幸的是,像这样的大型构造器表达式有时会让 Swift 编译器难以处理。遇到这种错误的处理办法是把表达式拆成更小的子表达式,特别是假如这些小的子表达式被赋予了明确的类型。

当视图的结构过于杂乱时,除了难以阅览外,还会呈现无法运用代码主动补全以及上文提到的无法编译( too complex to type check )的状况。将视图的功用分散到函数、更小的视图结构以及视图润饰器当中是很好的处理办法。

Text 与 TextField 在修正方法下的切换

Q:在 editMode 的文档中主张,在非修正方法下,可以挑选将 Text 视图换成 TextField 。然而,两个内容相同的视图之间的沟通并不能使视图顺利地产生动画,由于两者的文本也被动画化了。我正在运用仅禁用 TextField 的代替办法,但有没有办法引导动画以运用文档中的办法?

A:处理办法:保存 TextField ,但当它不能被修正时,有条件地设置 disabled(true),当它可以修正时运用 disabled(false) 。

设置正确的转场方法,可以防止非必要的闪烁或动画。

struct ContentView: View {
    var body: some View{
        VStack {
            EditButton()
            List{
                Cell()
            }
        }
    }
}
struct Cell:View {
    @State var text = "Hello"
    @Environment(\.editMode) var editMode
    var body: some View{
        ZStack {
            if editMode?.wrappedValue == .active {
                TextField("",text: $text).transition(.identity)
            } else {
                Text(text).transition(.identity)
            }
        }
    }
}

别离代码

Q:我留意到我的视图代码变大了,但原因并非来自实际的视图内容,而是由于 sheet、toolbar 等润饰器中的代码形成的。我当前设法在一个标示 @ToolbarContentBuilder 的函数中单独提取 toolbar 的内容,是否有好的办法来提取掉大量的 shee 和 alert 中的代码。

A:你可以经过创立自定义 ViewModifier 来封装其间的一些代码。另外,sheet 和 alert 的内容都选用了 ViewBuilders,所以你可以以相似于处理 toolbar 内容的办法将其提取到函数或核算特点中。

Q&A ( 集锦 – 简体中文 )

下文中的问题来自开发者与苹果工程师在【 集锦 – 简体中文 】频道进行的中文评论( 没有呈现在英文 SwiftUI 频道中 )。我直接对其进行了复制粘贴。

加载 Core Data 图片

Q:我的 CoreData 内运用 BinaryData with extern storage 存储图片。然后用 SwiftUI Image 来加载,data 还挺大的,当多个图一同加载,会卡顿和内存占用,请问这种状况下怎样改善

A:首要尽量确保选用异步加载的办法加载和创立图片,比如 SwiftUI 中的 AsyncImage 就可以从 URL 中异步加载图片,也可以依据需求完结自己的异步加载器完结异步加载。关于内存占用问题,首要尽量只在内存中保存需求显现的图片,关于预先加载的图片也适度,主张参看 WWDC18 的 Image and Graphic Best Practices , 有许多图片内存优化上的很好的主张。

异步 + 缩率图。关于可能形成卡顿的图片数据,抛弃从保管方针的图片联系中直接获取的办法。在 Cell 视图中,经过创立 request 从私有上下文中提取数据并转换成图片。另外,可以考虑为原始图片创立缩略图,进一步提高显现的功率。

TextField 中文输入的问题

Q:请问 SwiftUI 的 TextField 在中文输入时,会在字母挑选阶段就直接上屏,形成输入内容错误的问题是已知问题吗?会在 16.1 RC 修正吗?

A:咱们没能在 iOS 16.0.3 上重现你说的问题,你是否可以供给相关的代码段便利咱们重现问题和查询?假如经过 Feedback Assistant 提交过此问题,请告诉咱们 Feedback ID。

这是一个在多个版别中都呈现过的古怪问题。在 SwiftUI 前期版别中,当在 iOS 中运用体系中文输入法时,很容易触发这种状况。但后期逐渐得到了修正。近期,在聊天室中我也看到了相似的评论( 我本人尚未在 iOS 16 上遇到 )。贴一个临时的处理计划。

Ask Apple 2022 与 SwiftUI 有关的问答(下)

翻滚速度

Q:有好的办法在 ListScrollView 滑动时监听滑动的 velocity 值么?截止 SwiftUI 现在的版别,可以经过以下步骤获取到滑动的距离:

  1. 自定义 struct, 让它完结 PreferenceKey 协议,其自定义结构体,是需求搜集的 gemmetry data (视图坐标信息)
  2. 调用 transformAnchorPreference(key:_, value:_, transform:_) or preference(key:_,value:_) 来在 SwiftUI 更新视图时搜集坐标信息
  3. 调用 onPreferenceChange(:_,perform:_) 来获取搜集的坐标信息可是这样的完结办法,无法获取到 velocity

A:请问你需求这个速度值做什么用处?由于一般状况下并不需求这个值,假如是要检测翻滚掉帧,可以在 Xcode Organizer 里检查,或许用 MetricKit 生成报告,开发环境也可以运用 Instruments 。所以更想知道你需求这个速度值有什么特定的用处。可以测验在获取位置改变的一同记录时刻改变来核算速度。不过假如是涉及到用户交互,主张衡量一下用户对速度的灵敏程度和交互效果本身,是否可以用更便捷的办法完结。

在 SwiftUI 中,有一个从第一版开端就存在但尚未公开的纯 SwiftUI 完结的翻滚容器 —— _ScrollView 。该翻滚容器供给了不少规范 ScrollView 无法供给的 API 接口,例如对手势的加强操控、容器内视图的位移、反弹操控等。但这个翻滚有两大问题,1、是一个未公开的半成品,有可能会被从 SwiftUI 结构中移除;2、不支撑懒加载,即便和 Lazy 视图一同运用也会一次性加载悉数的视图。更多内容可以检查一个对其进行二次包装的 SolidScroll 库。

总结

我疏忽掉了没有取得定论的问题。期望上述的收拾可以对你有所协助。

欢迎经过 Twitter、 Discord 频道 或博客的留言板与我进行沟通。

我正以聊天室、Twitter、博客留言等评论为灵感,从中选取有代表性的问题和技巧制作成 Tips ,发布在 Twitter 上。每周也会对当周博客上的新文章以及在 Twitter 上发布的 Tips 进行汇总,并经过邮件列表的方法发送给订阅者。

订阅下方的 邮件列表,可以及时取得每周的 Tips 汇总。

原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】