Ask Apple为开发人员提供了与WWDC以外的苹果工程师直接沟通的机会。本文总结了与Swiftui有关的一些问答,并添加了一些个人见解。这是文章的第二部分。
问答
表格与列表
Q:这可能是一个非常愚蠢的问题,但是我总是对形式和列表感到困惑。它们之间有什么区别,我什么时候应该使用表单,什么时候应该使用列表?谢谢!
a:形式是将许多相关控件分组在一起的一种方式。尽管形式和列表在iOS上看起来相似,但是如果您查看MacOS,您会发现它们之间有很多差异。与MACOS上的列表相比,许多形式的控件具有不同的外观和行为。与表单不同,列表对编辑模式具有内置支持。因此,如果您创建一个视图以显示可滚动内容并可能执行选择操作,则使用iOS和MACOS上的列表提供最佳体验。如果您需要渲染许多相关控件,则使用表格将在iOS和MacOS上提供最佳的默认体验。
除了Swiftui的早期版本,表单,列表,Lazystack和LazyGrid的性能和子视图生命周期非常相似。 Swiftui 4.0的表格在Ventura上发生了很大变化,现在与iOS样式更相似,同时也更适合MacOS。
在可访问性模式下隐藏图像
问:图像(装饰性:)和可访问性的可访问性之间是否有区别?
a:没有区别,两种方法都可以用来适当隐藏图像,以免可访问性技术检测到它们!
可访问性隐藏率支持任何符合视图协议的元素,并且可以动态调整其隐藏状态。
表上下文菜单
Q:如果我在表中添加上下文菜单,如何确定哪个行触发菜单的显示(而不选择行)?
?a:使用contextMenu(forSelectionType :)在桌子外。
在上一篇文章的第一个问题中,我们介绍了ContextMenu(forSelectionType :)的用法。与常用的contextMenu不同,contextMenu(forSelectionType :)用于整个列表或表(不是单个单元格)。阅读Creating Tables in SwiftUI with Table以了解表的特定用法。
视图的性能优化
问:在处理复杂的用户接口时,在视图中控制更新范围的最佳实践是什么?在更复杂的UI中,由于视图的快速更新速度,性能(至少在MacOS上)迅速下降。
a:有不同的策略。
-
可观察的视图是使视图或查看层次结构无效的单元(触发重新组件)。您可以使用符合可观察的对象协议的不同对象来拆分无效的范围。
-
有时候,获得一些手动控制并直接发布对objectwillchange的更改而不依赖@publed是有用的。
-
添加一个中间视图,该视图仅提取您需要的属性并依赖于Swiftui的平等检查以提前终止无效的计算。
苹果工程师给出的答案与How to Avoid Repeating SwiftUI View Updates文章中的许多建议一致。视图的性能优化是一项系统的工程任务,并且有了对其操作机制,注射原理和更新时间安排的全面了解,可以更好地开发更有针对性的解决方案。
快速搜索数组元素
Q:为什么没有简单的方法将表的选定行映射到提供表内容的数组元素?似乎唯一的方法是在数组中搜索匹配的ID值,这对于大表格可能会降低。
a:使用数组索引存储选择是脆弱的:如果阵列突变,则选择将变得不同步。 Swift Collections有一个有序的命令,可能对您有所帮助。
这正是Swift Identified Collections项目的目的。 Swift确定的集合是基于OrderedDictionary的钥匙类数组。唯一的要求是元素必须符合可识别的协议。
struct Todo: Identifiable {
var description = ""
let id: UUID
var isComplete = false
}
class TodosViewModel: ObservableObject {
@Published var todos: IdentifiedArrayOf<Todo> = []
...
}
// You can operate on elements similar to a dictionary to locate them quickly, and it is not easy to cause exceptions in ForEach when updating IdentifiedArray.
todos[id:id] = newTodo
自定义布局
问:实现自定义布局时,处理非常小或非常大的空间的边缘案例有多重要?
a:像许多事情一样,这个问题的答案取决于您的用例(无论该答案多么不满意)。如果您的容器需要零和无限的可用空间,并且您需要确定最小和最大尺寸,则至少应考虑这些情况。此外,如果您要实现可以在各种情况下使用的通用布局,则应考虑它。但是,如果您自己只使用它并且条件可控,那么不处理这些情况也是合理的。
创建一个通用布局,该布局认为所有情况(例如VSTACK,HSTACK)是一项相当困难的任务。即使开发人员无法实施这样的布局容器,他们也应该清楚地了解定义各种尺寸要求。有关SwiftUI layout-sizing的文章介绍了几种建议的尺寸模式。
如何减轻主线程的负担
问:如何避免将所有操作放在主线程上?任何标记为@Publed的变量都应在主线程上修改,因此应使用@MainActor。但是,任何接触该属性的代码都会受到影响。是否有推荐的标准模式或方法可以改善这种模式?
a:一般而言,您确实需要与主线程上的UI框架进行交互。使用参考类型时,这一点尤其重要,因为您必须确保始终对其进行序列化读数。实际上,我们有一个很棒的WWDC presentation,详细介绍了并发和Swiftui,特别提到了使用可观察的案例。通常,性能瓶颈并不是围绕 @Plubling属性写作。我建议的方法是在主线程外进行任何昂贵或阻止的工作,然后在需要写入ObservableObject上的属性时才跳回主线程。
@State是线程安全,@stateObject自动标记包装值(一种符合可观察的视点协议的参考类型)为@mainactor。
自定义布局
Q:我经常想根据列表中的最长或最短文本来安排各种小部件。鉴于动态文本的大小在应用程序运行时可能会发生变化,因此测量给定字体的文本大小的最佳方法是什么?
a:嗨!我们的新布局协议支持此功能。任何自定义布局的完整实现都比我可以在这篇文章中快速概述的时间更长,但是总体想法是,您可以创建一个布局来查询其儿童的理想大小并相应地对其进行分类。然后,您可以使用垂直或水平堆栈布局将其组合起来,以便您必须自己完成所有实施工作。
Jane在Auto Layout based on width上的视频与此问题非常相关。阅读The SwiftUI Layout Protocol以学习如何创建自定义布局。
创建底部对齐的滚动视图
问:如何实现与底部保持一致的滚动视图,MacOS的性能会差吗?我尝试了旋转滚动视图和内部每个单元格的常见解决方案,以获得所需的倒置列表,该列表在iOS上效果很好。但是,在MacOS上,它使CPU使用率保持在100%。
a:您的最佳选择是使用scrollview和scrollViewReader,然后滚动到onappear上的底部视图或新内容时。我不建议尝试旋转滚动视图。
*Swiftcord的代码显示了如何在Swiftui中实现倒置列表。阅读*Demystifying SwiftUI List Responsiveness: Best Practices for Large Datasets*文章以了解Apple工程师的推荐方法。在这两种方法中,如果您有大量数据,我更喜欢第一种方法,因为它可以进行按需数据获取。*
自定义列表
Q:有没有一种方法可以完全自定义的方式使用列表,以便我可以删除凹痕,分离器,甚至更改整个列表的背景?目前,我一直在寻找懒惰的替代品。
a:有几个可以实现此功能的修饰符:listrowseparator,listrowinsets。不支持整个列表填充,请提供有关此的反馈。
在Swiftui 4中,您可以使用.scrollContentBackground(.hidden)隐藏列表的默认背景。
可搜索
Q:是否有一种方法可以编程地将重点设置为.searchable()修饰符中的搜索字段?
a:您可以使用解雇环境属性来编程删除搜索字段。当前,没有API可以编程地将重点设置在搜索字段上。
TextField内容验证
问:如何实现仅接受数字的Swiftui Textfield,包括小数?
a:向文本字段提供格式,以自动将文本转换为各种数字。但是,此转换仅在文本字段完成编辑时才发生,并且不会阻止非数字字符输入。当前,SwiftUI没有API来限制用户可以输入字段的字符。
我真的希望苹果继续扩展基于格式的解决方案以实现实时内容验证。阅读本文``SwiftUI TextField Advanced — Format and Validation学习,以了解其他验证方法以及如何使用Onchange几乎实时限制输入字符。
将背景扩展到安全区域
Q:如果我有一个可以使用顶部和底部视图的自定义容器类型,那么API呼叫者是否可以将提供视图的背景扩展到安全区域,同时保持内容(例如文本或文本或按钮)在安全区域内?
a:您可以尝试使用SafeAreainset(Edge:.top){â}或SafeAreainset(Edge:.bottom){â}修饰符以放置您的顶部和底部视图。然后使顶部/底部视图忽略安全区域。我不确定这是否会符合您的用例,但值得一试。
在背景修改器中,您可以设置ignoressFafeareAdges参数,以确定是否忽略安全区域。此技巧对于屏幕顶部或底部的视图很有用。有关更多详细信息,请参见tweet。
动画过渡
问:为什么在下面的代码中显示动画过渡?
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))
}
}
}
}
尝试将动画装饰器移到过渡参数外。
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)
}
}
}
}
在上面的Apple工程师提供的修改代码中。过渡动画事件是通过击打明确添加的。对于类似的情况,您还可以避免使用显式动画驱动程序(无需使用抗击),只需通过移动。阅读文章âUnderstanding SwiftUI’s Animation Mechanism有关动画的更多信息。
在NavigationsPlitView的侧边栏中使用LazyvStack
Q:当前,iOS 16中的新NavigationsPlitView仅与主列中的列表一起使用。这意味着我们不能使用lazyvstack或任何其他将选择与详细视图绑定的自定义视图。有什么计划扩展此功能吗?
a:在iOS 16.1中,您可以在侧边栏中放置导航截面,以便侧边栏中的导航链接将替换详细列的根视图。
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
不推荐使用的Swift框架的100000.0版本是API作者传达不应在新项目中使用API的一种方式,但是在现有项目中继续使用它是可以接受的。这些软弃用API不包括在代码自动完成中,通常在文档中单独列出。但是,编译器不会为现有用途发出警告。由于这些用途无害,因此我们不希望开发人员使用新的编译器版本来处理很多警告。
API MACOS
Q:对于运行蒙特雷的Mac,在Swiftui中达到以下要求的建议是什么:
-
打开一个窗口
-
初始化窗口中的数据
-
找到所有打开的Windows
-
确定窗口是否打开
-
从窗口中不在窗口中的视图上关闭窗口
A:我想说的是,如果可能的话,针对Macos Ventura对其中一些操作将更有帮助。具体来说,我们在窗口组上添加了新的OpenWindowAction和新的初始化方法,该方法将同时满足1和2。如果您可以做到这一点,则可以使用URL和处理Externalevents来模仿某些行为,但它具有更多的限制。目前没有适合其他点的API。
链动画
问:如何在Swiftui中获得链动画?例如,我想先动画一个视图,然后在第一个动画完成时立即启动另一个动画。
a:不幸的是,目前无法实现链动画。基于您的问题,您可以使用Animation.delay(â)将动画的第二部分延迟到第一部分完成。如果您可以提供有关用例的更多详细信息,我们将不胜感激。
Swiftui目前在动画完成后缺乏回调机制。在简单的情况下,您可以通过创建符合动画协议的视图模型来观察动画的进度。有关更多信息,请参阅tweet和code。
太复杂了,无法输入检查
Q:我在iOS 14 swiftui中遇到了一个问题。我试图有条件地显示符合形状协议的三个对象之一。其中两个是自定义形状(基本上是圆形的矩形,只有两个角圆),一个是矩形。编译器引发了一个错误,指出检查视图的类型花了太长时间。
a:是的,不幸的是,像这样的大型构造函数表达式有时可能很难处理。遇到此错误的解决方案是将表达式分解为较小的子表达,尤其是在给出这些较小的子表达式的明确类型的情况下。
当视图的结构太复杂时,可能很难读取,并且在某些情况下,代码自动完成无法使用,并且上述无法编译(太复杂而无法键入)查看)。一个好的解决方案是将视图的功能分散到函数,较小的视图结构和查看修饰符中。
在编辑模式下在文本和Textfield之间切换
Q:在“ EditMode文档”中,建议可以在非编辑模式下将文本视图替换为Textfield。但是,两个视图之间具有相同内容的交换并不能顺利进行动画,因为两者的文本也是动画的。我正在使用仅禁用TextField的替代方法,但是有没有一种方法来指导动画在文档中使用该方法?
a:解决方案:保留文本字段,但在无法编辑时无法编辑和禁用(false)时有条件设置禁用(true)。
设置正确的过渡样式可以避免不必要的闪烁或动画。
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:我注意到我的视图代码越来越大,不是由于实际视图内容,而是由于装饰器(例如纸牌和工具栏)中的代码所致。我目前正在找出一种将工具栏内容提取到用@ToolBarcontentBuilder注释的函数中的方法。是否有一个很好的方法可以在大量纸张和警报中提取代码?
a:您可以通过创建自定义视图模型来封装一些代码。此外,表和警报的内容都使用ViewBuilders,因此您可以以类似的方式将其提取到函数或计算属性中。
问答(汇编简化中文)
以下问题来自开发人员和苹果工程师之间简化的中文频道的讨论(在英语swiftui频道中没有出现)。
加载核心数据图像
Q:我使用二进制数据存储图像,并在我的Coredata中使用外部存储。然后,我使用SwiftUI图像加载图像。数据的大小很大,当同时加载多个图像时,它会导致滞后和高内存使用情况。我该如何改善这种情况?
a:首先,尝试使其异步加载和创建图像。例如,在Swiftui中,您可以使用异步图像从URL异步加载图像,也可以根据需要实现自己的异步加载器。关于内存使用情况,尝试仅保留需要显示在内存中的图像,并正确处理预加载的图像。您可以参考WWDC18的Image and Graphic Best Practices,以获取有关图像内存优化的许多好建议。
异步加载 +缩略图。对于可能导致滞后的图像,请避免直接从托管对象的图像关系中访问它们。在“单元格”视图中,创建一个请求,以从私有上下文中提取数据并将其转换为图像。此外,考虑为原始图像创建缩略图以进一步提高显示效率。阅读Memory Optimization Journey for a SwiftUI + Core Data App以了解有关内存优化方法的更多信息。
在Textfield中的中文输入问题
Q:我可以问一下,在输入中文时,Swiftui的Textfield会在字母选择阶段直接输入所选字母,引起输入错误,这是否是一个已知的问题?它将在16.1 rc?
中固定a:我们无法复制您在iOS 16.0.3上提到的问题。您可以提供相关的代码段来帮助我们复制和调查问题吗?如果您已经通过反馈助理提交了此问题,请让我们知道反馈ID。
这是一个奇怪的问题,已经出现在多个版本中。在Swiftui的早期版本中,当在iOS中使用中文输入方法时,很容易触发这种情况。但是稍后逐渐修复。最近,我还在聊天室看到了类似的讨论(我本人在iOS 16上没有遇到过)。这是一个临时解决方案。
滚动速度
Q:在滚动列表或滚动浏览时,是否有一种很好的方法来聆听速度值?从当前版本的Swiftui开始,您可以通过以下方式获得滚动的距离:
-
自定义结构以实现Preferection Koodent,其中自定义结构负责收集几何数据(查看坐标信息)。
-
调用transform canceanchorPreference(key:,value:,transform:)或偏好(键:,value:_)收集坐标信息时SwiftUi更新查看。
-
调用OnPreferenceChange(:,perform:)以获取收集的坐标信息。但是,此实现不允许获得速度。
a:我可以问一下您需要这个速度值什么?通常,此值不是必需的,如果您需要在滚动过程中检查掉落的帧,则可以在Xcode组织者中查看它或使用Metrickit生成报告。您也可以在开发环境中使用仪器。因此,我很好奇您需要该速度值的特定目的。您可以尝试记录时间变化,同时获得位置更改以计算速度。但是,如果涉及用户互动,我建议考虑用户对速度和交互作用本身的敏感性,以及是否有一种更方便的方法来实现这一目标。
在Swiftui中,自第一个版本以来就存在一个滚动容器,但尚未公开。该滚动容器提供了许多API接口,标准卷轴无法提供的许多API接口,例如增强对手势的控制,容器内视图的位移以及弹跳控制。但是,此卷轴有两个主要问题:1。它是一个未发表的原型,可以从SwiftUI框架中删除; 2.它不支持懒惰加载,即使与懒惰视图一起使用,它也会一次加载所有视图。有关更多信息,您可以查看SolidScroll库,该库将其包装了两次。阅读How to Determine if ScrollView is Scrolling in SwiftUI以了解确定滚动状态的几种方法。
概括:
我忽略了没有得出结论的问题。
我希望本文对您有帮助。也欢迎您通过Twitter,Discord channel或my blog的留言板与我交流。