Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >优化在 SwiftUI List 中显示大数据集的响应效率

优化在 SwiftUI List 中显示大数据集的响应效率

作者头像
东坡肘子
发布于 2022-07-28 05:07:13
发布于 2022-07-28 05:07:13
10K00
代码可运行
举报
运行总次数:0
代码可运行

访问我的博客 www.fatbobman.com[1] 可以获得更好的阅读体验

拥有优秀的交互效果和手感,是很多 iOS 开发者长久以来坚守的原则。同样一段代码,在不同数据量级下的响应表现可能会有云泥之别。本文将通过一个优化列表视图的案例,展现在 SwiftUI 中查找问题、解决问题的思路,其中也会对 SwiftUI 视图的显式标识、@FetchRequest 的动态设置、List 的运作机制等内容有所涉及。本文的范例需运行在 iOS 15 及以上系统,技术特性也以 SwiftUI 3.0 为基础。

首先创建一个假设性的需求:

  • 一个可以展示数万条记录的视图
  • 从上个视图进入该视图时不应有明显延迟
  • 可以一键到达数据的顶部或底部且没有响应延迟

响应迟钝的列表视图

通常会考虑采用如下的步骤以实现上面的要求:

  • 创建数据集
  • 通过 List 展示数据集
  • 用 ScrollViewReader 对 List 进行包裹
  • 给 List 中的 item 添加 id 标识,用于定位
  • 通过 scrollTo 滚动到指定的位置(顶部或底部)

下面的代码便是按照此思路来实现的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                // 通过一个 NavigationView 进入列表视图
                NavigationLink("包含 40000 条数据的列表视图", destination: ListEachRowHasID())
            }
        }
    }
}

struct ListEachRowHasID: View {
    // 数据通过 CoreData 创建。创建了 40000 条演示数据。Item 的结构非常简单,记录容量很小。
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default
    )
    private var items: FetchedResults<Item>

    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                HStack {
                    Button("Top") {
                        withAnimation {
                            // 滚动到列表最上面的记录
                            proxy.scrollTo(items.first?.objectID, anchor: .center)
                        }
                    }.buttonStyle(.bordered)
                    Button("Bottom") {
                        withAnimation {
                            // 滚动到列表最下面的记录
                            proxy.scrollTo(items.last?.objectID)
                        }
                    }.buttonStyle(.bordered)
                }
                List {
                    ForEach(items) { item in
                        ItemRow(item: item)
                            // 给每行记录视图设置标识
                            .id(item.objectID)
                    }
                }
            }
        }
    }
}

struct ItemRow: View {
    let item: Item
    var body: some View {
        Text(item.timestamp, format: .dateTime)
            .frame(minHeight: 40)
    }
}
// 满足 ForEach 的 Identifiable 需求
extension Item: Identifiable {}

本文中的 全部源代码可以在此处获取[2]

在只拥有数百条记录的情况下,上面的代码运行的效果非常良好,但在创建了 40000 条演示数据后,该视图的响应状况如下:

id_delay_demo_2022-04-23 12.22.44.2022-04-23 12_29_07

进入视图的时候有明显的卡顿(1 秒多钟),进入后列表滚动流畅且可无延迟的响应滚动到列表底部或顶部的指令。

找寻问题原因

或许有人会认为,毕竟数据量较大,进入列表视图有一定的延迟是正常的。但即使在 SwiftUI 的效能并非十分优秀的今天,我们仍然可以做到以更小的卡顿进入一个数倍于当面数据量的列表视图。

考虑到当前的卡顿出现在进入视图的时刻,我们可以将查找问题的关注点集中在如下几个方面:

  • Core Data 的性能( IO 或 惰值填充 )
  • 列表视图的初始化或 body 求值
  • List 的效能

Core Data 的性能

@FetchRequest 是 NSFetchedResultsController 的 SwiftUI 包装。它会根据指定的 NSFetchReqeust ,自动响应数据的变化并刷新视图。上面的代码对应的 NSFetchRequest 如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default
    )
private var items: FetchedResults<Item>

// 等效的 NSFetchRequest
extension Item {
    static var fetchRequest:NSFetchRequest<Item> {
        let fetchRequest = NSFetchRequest<Item>(entityName: "Item")
        fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)]
        return fetchRequest
    }
}

// 相当于
@FetchRequest(fetchRequest: Item.fetchRequest, animation: .default)
var items:FetchedResults<Item>

此时 fetchRequest 的 returnsObjectsAsFaults 为默认值 false (托管对象为惰值状态),fetchBatchSize 没有设置 (会将所有数据加载到持久化存储的行缓冲区)。

通过使用 Instruments 得知,即便使用当前没有进行优化的 fetchRequest , 从数据库中将 40000 条记录加载到持久化存储的行缓冲所用的时间也只有 11ms 左右。

image-20220423145552324

另外,通过下面的代码也可以看到仅有 10 余个托管对象( 显示屏幕高度所需的数据 )进行了惰值化填充:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func info() -> some View {
    let faultCount = items.filter { $0.isFault }.count
    return VStack {
        Text("item's count: \(items.count)")
        Text("fault item's count : \(faultCount)")
    }
}

image-20220425075620588

因此可以排除卡顿是由于 Core Data 的原因所导致的。

列表视图的初始化和 body 求值

如果对 SwiftUI 的 NavigationView 有一定了解的话,应该知道 SwiftUI 会对 NavigationLink 的目标视图进行预实例化(但不会对 body 求值)。也就是当显示主界面菜单时,列表视图已经完成了实例的创建(可以通过在 ListEachRowHasID 的构造函数中添加打印命令得以证明),因此也不应是实例化列表视图导致的延迟。

通过检查 ListEachRowHasID 的 body 的求值消耗时间,也没有发现任何的效率问题。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    var body: some View {
        let start = Date()
        ScrollViewReader { proxy in
            VStack {
     ....
            }
        }
        let _ = print(Date().timeIntervalSince(start))
    }
// 0.0004889965057373047

目前已经可以基本排除性能问题来源于 IO、数据库、列表视图实例化等因素,那么有极大的可能源自 SwiftUI 的内部处理机制。

List 的效能

List 作为 SwiftUI 对 UITableView ( NSTableView )的封装,大多数情况下它的性能都比较令人满意。在 SwiftUI 视图的生命周期研究[3] 一文中,我对 List 如何对子视图的显示进行优化做了一定的介绍。按照正常的逻辑,当进入列表视图 ListEachRowHasID 后 List 只应该实例化十几个 ItemRow 子视图 ( 按屏幕的显示需要 ),即便使用 scrollTo 滚动到列表底部,List 也会对滚动过程进行显示优化,滚动过程中至多实例化 100 多个 ItemRow 。

我们对 ItemRow 进行一定的修改以验证上述假设:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct ItemRow:View{
    static var count = 0
    let item:Item
    init(item:Item){
        self.item = item
        Self.count += 1
        print(Self.count)
    }
    var body: some View{
//        let _ = print("get body value")
        Text(item.timestamp, format: .dateTime)
            .frame(minHeight:40)
    }
}

重新运行,再次进入列表视图,我们竟然得到了如下的结果:

itemRow_count_2022-04-23_16.39.41.2022-04-23 16_40_53

List 将所有数据的 itemRow 都进行了实例化,一共 40000 个。这与之前仅会实例化 10 - 20 个子视图的预测真是大相径庭。是什么影响了 List 对视图的优化逻辑?

在进一步排除掉 ScrollViewReader 的影响后,所有的迹象都表明用于给 scrollTo 定位的 id 修饰符可能是导致延迟的罪魁祸首。

在将 .id(item.objectID) 注释掉后,进入列表视图的卡顿立刻消失了,List 对子视图的实例化数量也完全同我们最初的预测一致。

itemRow_withoutID_2022_04_23.2022-04-23 17_01_05

现在摆在我们面前有两个问题:

  • 为什么使用了 id 修饰符的视图会提前实例化呢?
  • 不使用 .id(item.objectID) ,我们还有什么方法为列表两端定位?

id 修饰符与视图的显式标识

想搞清楚为什么使用了 id 修饰符的视图会提前实例化,我们首先需要了解 id 修饰符的作用。

标识( Identity )是 SwiftUI 在程序的多次更新中识别相同或不同元素的手段,是 SwiftUI 理解你 app 的关键。标识为随时间推移而变化的视图值提供了一个坚固的锚,它应该是稳定且唯一的。

在 SwiftUI 应用代码中,绝大多数的视图标识都是通过结构性标识 (有关结构性标识的内容可以参阅 ViewBuilder 研究(下) —— 从模仿中学习[4])来实现的 —— 通过视图层次结构(视图树)中的视图类型和具体位置来区分视图。但在某些情况下,我们需要使用显式标识( Explicit identity )的方式来帮助 SwiftUI 辨认视图。

在 SwiftUI 中为视图设置显式标识目前有两种方式:

  • 在 ForEach 的构造方法中指定 由于 ForEach 中的视图数量是动态的且是在运行时生成的,因此需要在 ForEach 的构造方法中指定可用来标识子视图的 KeyPath 。在我们的当前的例子中,通过将 Item 声明为符合 Identifiable 协议,从而实现了在 ForEach 中进行了默认指定。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
extension Item: Identifiable {}
// NSManagedObject 是 NSObject 的子类。NSObject 为 Identifiable 提供了默认实现
ForEach(items) { item in ... }
// 相当于
ForEach(items, id:\.id) { item in ... }
  • 通过 id 修饰符指定 id 修饰符是另一个对视图进行显式标识的方式。通过它,开发者可以使用任何符合 Hashable 协议的值为视图设置显式标识。ScrollViewProxy 的 scrollTo 方法就是通过该值来找到对应的视图。另外如果 id 的标识值发生变化,SwiftUI 将丢弃原视图(生命周期终止及重置状态)并重新创建新的视图。

当仅通过 ForEach 来指定显示标识时,List 会对这些视图的显示进行优化,仅在需要显示时才会对其进行实例化。但一旦为这些子视图添加了 id 修饰符,这些视图将无法享受到 List 提供的优化能力 ( List 只会对 ForEach 中的内容进行优化)。

id 修饰符标识是通过 IDViewList 对显式标识视图进行跟踪、管理和缓存,它与 ForEach 的标识处理机制完全不同。使用了 id 修饰符相当于将这些视图从 ForEach 中拆分出来,因此丧失了优化条件。

总之,当前在数据量较大的情况下,应避免在 List 中对 ForEach 的子视图使用 id 修饰符

虽然我们已经找到了导致进入列表视图卡顿的原因,但如何在不影响效率的情况下通过 scrollTo 来实现到列表端点的滚动呢?

解决方案一

从 iOS 15 开始,SwiftUI 为 List 添加了更多的定制选项,尤其是解除了对列表行分割线设置的屏蔽且添加了官方的实现。我们可以通过在 ForEach 的外面分别为列表端点设置显式标识来解决使用 scrollTo 滚动到指定位置的问题。

对 ListEachRowHasID 进行如下修改:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct ListEachRowHasID: View {
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default
    )
    private var items: FetchedResults<Item>

    @FetchRequest(fetchRequest: Item.fetchRequest1, animation: .default)
    var items1:FetchedResults<Item>

    init(){
        print("init")
    }

    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                HStack {
                    Button("Top") {
                        withAnimation {
                            proxy.scrollTo("top", anchor: .center)
                        }
                    }.buttonStyle(.bordered)
                    Button("Bottom") {
                        withAnimation {
                            proxy.scrollTo("bottom")
                        }
                    }.buttonStyle(.bordered)
                }
                List {
                    // List 中不在 ForEach 中的视图不享受优化,无论显示与否都会提前实例化
                    TopCell()
                        .id("top")
                     // 隐藏两端视图的列表分割线
                        .listRowSeparator(.hidden)
                    ForEach(items) { item in
                        ItemRow(item: item)
                    }
                    BottomCell()
                        .id("bottom")
                        .listRowSeparator(.hidden)
                }
                // 设置最小行高,隐藏列表两端的视图
                .environment(\.defaultMinListRowHeight, 0)
            }
        }
    }
}

struct TopCell: View {
    init() { print("top cell init") } 
    var body: some View {
        Text("Top")
            .frame(width: 0, height: 0) // 隐藏两端视图
    }
}

struct BottomCell: View {
    init() { print("bottom cell init") }  // 仅两端的视图会被提前实例化,其他的视图仅在需要时进行实例化
    var body: some View {
        Text("Bottom")
            .frame(width: 0, height: 0)
    }
}

运行修改后的代码结果如下:

onlyTopAndBottomWithID_2022-04-23_18.58.53.2022-04-23 19_02_53

目前我们已经可以秒进列表视图,并实现了通过 scrollTo 滚动到指定的位置。

由于 id 修饰符并非惰性修饰符( Inert modifier ),因此我们无法在 ForEach 中仅为列表的头尾数据使用 id 修饰符。如果你尝试通过 if 语句的方式利用判断来添加 id 修饰符,将进一步劣化性能(可在 ViewBuilder 研究(下) —— 从模仿中学习[5])中找到原因 )。范例代码[6] 中也提供了这种实现方式,大家可以自行比对。

新的问题

细心的朋友应该可以注意到,运行解决方案一的代码后,在第一次点击 bottom 按钮时,大概率会出现延迟情况(并不会立即开始滚动)。

scrollToBottomDelay_2022-04-24_07.40.24.2022-04-24 07_42_06

从控制台的打印信息可以得知,通过 scrollTo 滚动到指定的位置,List 会对滚动过程进行优化。通过对视觉的欺骗,仅需实例化少量的子视图即可完成滚动动画(同最初的预计一致),从而提高效率。

由于整个的滚动过程中仅实例化并绘制了 100 多个子视图,对系统的压力并不大,因此在经过反复测试后,首次点击 bottom 按钮会延迟滚动的问题大概率为当前 ScrollViewProxy 的 Bug 所致。

解决方案二

在认识到 ScrollViewProxy 以及在 ForEach 中使用 id 修饰符两者的异常表现后,我们只能尝试通过调用底层的方式来获得更加完美的效果。

除非没有其他选择,否则我并不推荐大家对 UIKit ( AppKit ) 控件进行重新包装,应使用尽可能微小的侵入方式对 SwiftUI 的原生控件进行补充和完善。

我们将通过 SwiftUI-Introspect[7] 来实现在 List 中滚动到列表两端。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import Introspect
import SwiftUI
import UIKit

struct ListUsingUIKitToScroll: View {
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default
    )
    private var items: FetchedResults<Item>
    @State var tableView: UITableView?
    var body: some View {
        VStack {
            HStack {
                Button("Top") {
                    // 使用 UITableView 的 scrollToRow 替代 ScrollViewReader 的 scrollTo
                    self.tableView?.scrollToRow(at: IndexPath(item: 0, section: 0), at: .middle, animated: true)
                }.buttonStyle(.bordered)
                Button("Bottom") {
                    self.tableView?.scrollToRow(at: IndexPath(item: items.count - 1, section: 0), at: .bottom, animated: true)
                }.buttonStyle(.bordered)
            }
            List {
                // 无需使用 id 修饰符进行标识定位
                ForEach(items) { item in
                    ItemRow(item: item)
                }
            }
            .introspectTableView(customize: {
                // 获取 List 对应的 UITableView 实例
                self.tableView = $0
            })
        }
    }
}

至此我们已经实现了无延迟的进入列表视图,并在首次滚动到列表底部时也没有延迟。

scrollByUITableView_2022-04-23_19.44.26.2022-04-23 19_46_20

希望 SwiftUI 在之后的版本中能够改善上面的性能问题,这样就可以无需使用非原生方法也能达成好的效果。

范例代码还提供了使用 @SectionedFetchRequest 和 section 进行定位的例子。

生产中的处理方式

本文为了演示 id 修饰符在 ForEach 中的异常状况以及问题排查思路,创建了一个在生产环境中几乎不可能使用的范例。如果在正式开发中面对需要在 List 中使用大量数据的情况,我们或许可以考虑下述的几种解决思路( 以数据采用 Core Data 存储为例 ):

数据分页

将数据分割成若干页面是处理大数据集的常用方法,Core Data 对此也提供了足够的支持。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fetchRequest.fetchBatchSize = 50 
fetchRequest.returnsObjectsAsFaults = true // 如每页数据较少,可直接对其进行惰值填充,进一步提高效率
fetchRequest.fetchLimit = 50 // 每页所需数据量
fetchRequest.fetchOffset = 0 // 逐页变换  count * pageNumber

通过使用类似上面的代码,我们可以逐页获取到所需数据,极大地减轻了系统的负担。

升降序切换

对数据进行降序显示且仅允许使用者手工滚动列表。系统中的邮件、备忘录等应用均采用此种方式。

由于用户滚动列表的速度并不算快,所以对于 List 来说压力并不算大,系统将有足够的时间构建视图。

对于拥有复杂结构子视图(尺寸不一致、图文混排)的 List 来说,在数据量大的情况下,任何的大跨度滚动( 例如直接滚动到列表底部 )都会给 List 造成巨大的布局压力,有不小的滚动失败的概率。如果必须给用户提供直接访问两端数据的方式,动态切换 SortDescriptors 或许是更好的选择。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default
    )
private var items: FetchedResults<Item>

// 在视图中切换 SortDescriptors
$items.wrappedValue.sortDescriptors = [SortDescriptor(\Item.timestamp,order: .reverse)]

增量读取

通讯类软件(比如微信)往往采用初期仅显示部分最新数据,向上滚动后采用增量获取数据的方式来减轻系统压力。

  • 不使用 @FetchRequest 或 NSFetchResultController 等动态管理方式,用数组来持有数据
  • 通过设置 NSPredicate 、NSSortDescription 和 fetchRequest.fetchLimit获取若干最新数据,将数据逆向添加入数组
  • 在列表显示后率先移动到最底端(取消动画)
  • 通过 refreshable 调用下一批数据,并继续逆向添加入数组

用类似的思路,还可以实现向下增量读取或者两端增量读取。

总结

相较于 UIKit ,已经推出了 3 年的 SwiftUI 仍有很多的不足。但回首最初的版本,现在我们已经可以实现太多以前无法想象的功能。期盼 6 月的 WWDC 会带来更多的好消息。

希望本文能够对你有所帮助。同时也欢迎你通过Twitter[8]Discord频道[9]或下方的留言板与我进行交流。

参考资料

[1] www.fatbobman.com: https://www.fatbobman.com

[2] 全部源代码可以在此处获取: https://github.com/fatbobman/BlogCodes/tree/main/FetchRequestDemo

[3] SwiftUI 视图的生命周期研究: https://www.fatbobman.com/posts/swiftUILifeCycle/

[4] ViewBuilder 研究(下) —— 从模仿中学习: https://www.fatbobman.com/posts/viewBuilder2/

[7] SwiftUI-Introspect: https://github.com/siteline/SwiftUI-Introspect

[8] Twitter: https://twitter.com/fatbobman

[9] Discord频道: https://discord.gg/JuVeuXHcAc

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 肘子的Swift记事本 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
用 Table 在 SwiftUI 下创建表格
Table 是 SwiftUI 3.0 中为 macOS 平台提供的表格控件,开发者通过它可以快捷地创建可交互的多列表格。在 WWDC 2022 中,Table 被拓展到 iPadOS 平台,让其拥有了更大的施展空间。本文将介绍 Table 的用法、分析 Table 的特点以及如何在其他的平台上实现类似的功能。
东坡肘子
2022/07/28
4.4K0
用 Table 在 SwiftUI 下创建表格
只在视图 Body 中生存的变量
SwiftUI 通过调用视图实例的 body 属性来获取视图值。在 View 协议中,body 被属性包装器 @ViewBuilder 所标注,这意味着,通常我们只能在 body 中使用 ViewBuilder 认可的 Expression 来声明视图( 如果显式使用 return ,虽然可以避开 ViewBuilder 的限制,但因受只能返回一种类型的限制,影响视图的表达能力 )。
东坡肘子
2023/05/18
7500
只在视图 Body 中生存的变量
深入了解 SwiftUI 5 中 ScrollView 的新功能
为可滚动容器的内容或滚动指示器(Scroll Indicator)添加外边距(Margin)。
东坡肘子
2023/07/08
1.2K0
深入了解 SwiftUI 5 中 ScrollView 的新功能
SwiftUI + Core Data App 的内存占用优化之旅
尽管 SwiftUI 的惰性容器以及 Core Data 都有各自的内存占用优化机制,但随着应用视图内容的复杂( 图文混排 ),越来越多的开发者遇到了内存占用巨大甚至由此导致 App 崩溃的情况。本文将通过对一个演示 App 进行逐步内存优化的方式( 由原先显示 100 条数据要占用 1.6 GB 内存,优化至显示数百条数据仅需 200 多 MB 内存 ),让读者对 SwiftUI 视图的存续期、惰性视图中子视图的生命周期、托管对象的惰值特性以及持久化存储协调器的行缓存等内容有更多的了解。
东坡肘子
2023/03/08
2.6K0
SwiftUI + Core Data App 的内存占用优化之旅
SwiftUI 与 Core Data —— 安全地响应数据
保证应用不因 Core Data 的原因导致意外崩溃是对开发者的起码要求。本文将介绍可能在视图中产生严重错误的原因,如何避免,以及在保证视图对数据变化实时响应的前提下如何为使用者提供更好、更准确的信息。由于本文会涉及大量前文中介绍的技巧和方法,因此最好一并阅读。
东坡肘子
2022/12/16
3.5K0
SwiftUI 与 Core Data —— 安全地响应数据
SwiftUI 的动画机制
大多初学者都会在第一时间惊叹于 SwiftUI 轻松实现各种动画效果的能力,但经过一段时间的使用后,他们会发现 SwiftUI 的动画并非像表面上看起来那样容易驾驭。开发者经常需要面对:如何动、怎么动、什么能动、为什么不动、为什么这么动、如何不让它动等等困扰。对 SwiftUI 的动画处理逻辑了解的不够深入是造成上述困扰的主要原因。本文将尝试对 SwiftUI 的动画机制做以介绍,以帮助大家更好地学习、掌握 SwiftUI 的动画,制作出满意的交互效果。
东坡肘子
2022/07/28
15.1K0
SwiftUI 的动画机制
如何判断 ScrollView、List 是否正在滚动中
判断一个可滚动控件( ScrollView、List )是否处于滚动状态在某些场景下具有重要的作用。比如在 SwipeCell[3] 中,需要在可滚动组件开始滚动时,自动关闭已经打开的侧滑菜单。遗憾的是,SwiftUI 并没有提供这方面的 API 。本文将介绍几种在 SwiftUI 中获取当前滚动状态的方法,每种方法都有各自的优势和局限性。
东坡肘子
2022/12/16
4.1K0
如何判断 ScrollView、List 是否正在滚动中
掌握 Transaction,实现 SwiftUI 动画的精准控制
SwiftUI 因其简便的动画 API 与极低的动画设计门槛而广受欢迎。但是,随着应用程序复杂性的增加,开发者逐渐发现,尽管动画设计十分简单,但要实现精确细致的动画控制并非易事。同时,在 SwiftUI 的动画系统中,有关 Transaction 的解释很少,无论是官方资料还是第三方文章,都没有对其运作机制进行系统的阐述。
东坡肘子
2023/07/08
6740
掌握 Transaction,实现 SwiftUI 动画的精准控制
ObservableObject研究
我是在去年阅读王巍写的《SwiftUI 与 Combine 编程》才第一次接触到单一数据源这一概念的。
东坡肘子
2022/07/28
2.5K0
ObservableObject研究
SwiftUI-ScrollView进化史
ScrollView 即滚动视图,在 iOS 开发中扮演着非常重要的角色。但在 SwiftUI 的发展史上,ScrollView 一直处于“残废”的状态,直到 SwiftUI 6.0 才逐渐补齐短板。下面详细讲解 SwiftUI 中 ScrollView 的进化史。
YungFan
2025/02/10
1820
SwiftUI geometryGroup() 指南:从原理到实践
在 WWDC 2023 中,苹果为 SwiftUI 添加了一个新的修饰器:geometryGroup()。它可以解决一些之前无法处理或处理起来比较困难的动画异常。本文将介绍 geometryGroup() 的概念、用法,以及在低版本 SwiftUI 中,在不使用 geometryGroup() 的情况下如何处理异常。
东坡肘子
2023/11/30
4340
SwiftUI geometryGroup() 指南:从原理到实践
用 SwiftUI 的方式进行布局
最近时常有朋友反映,尽管 SwiftUI 的布局系统学习门槛很低,但当真正面对要求较高的设计需求时,好像又无从下手。SwiftUI 真的具备创建复杂用户界面的能力吗?本文将通过用多种手段完成同一需求的方式,展示 SwiftUI 布局系统的强大与灵活,并通过这些示例让开发者对 SwiftUI 的布局逻辑有更多的认识和理解。
东坡肘子
2023/03/08
5K2
用 SwiftUI 的方式进行布局
SwiftUI 布局 —— 尺寸( 下 )
在 上篇[3] 中,我们对 SwiftUI 布局过程中涉及的众多尺寸概念进行了说明。本篇中,我们将通过对视图修饰器 frame 和 offset 的仿制进一步加深对 SwiftUI 布局机制的理解,并通过一些示例展示在布局时需要注意的问题。
东坡肘子
2022/07/28
2.8K0
SwiftUI 布局 —— 尺寸( 下 )
如何在 Core Data 中使用 Derived 和 Transient 属性
使用过 Core Data 的开发者,一定会在编辑 Data Model 时看到过右侧的属性面板中的 Derived 和 Transient 两个属性。关于这两个属性的文档不多,大多的开发者并不清楚该如何使用或在何时使用该属性。文本将结合我的使用体验,对 Derived 和 Transient 两个属性的功能、用法、注意事项等内容作以介绍。
东坡肘子
2022/07/28
1.1K0
如何在 Core Data 中使用 Derived 和 Transient 属性
SwiftUI 新容器视图 API 深度解析:轻松构建自定义布局
自 SwiftUI 的第一个版本发布以来,它就拥有了几种容器视图。最常用的有 HStack、VStack、List 等。今年,Apple 引入了新的 API,使我们能够以全新的方式构建自定义容器视图。本周,我们将学习 SwiftUI 新的分解 API 的优势。
Swift社区
2024/09/30
3840
SwiftUI 新容器视图 API 深度解析:轻松构建自定义布局
WWDC 23 之后的 SwiftUI 有哪些新功能
WWDC 23 已经到来,SwiftUI 框架中有很多改变和新增的功能。在本文中将主要介绍 SwiftUI 中数据流、动画、ScrollView、搜索、新手势等功能的新变化。
Swift社区
2023/09/06
7030
WWDC 23 之后的 SwiftUI 有哪些新功能
SwiftUI 中掌握 ScrollView 的使用:滚动可见性
我们的滚动 API 中又有一个重要的新增功能:滚动可见性。现在,你可以获取可见标识符列表,或者快速检查并监控 ScrollView 内视图的可见性状态。本周,我们将学习如何使用新的 onScrollTargetVisibilityChange 和 onScrollVisibilityChange 视图修饰符。
Swift社区
2024/09/25
7020
SwiftUI 中掌握 ScrollView 的使用:滚动可见性
SwiftUI-ScrollView进化史
ScrollView 即滚动视图,在 iOS 开发中扮演着非常重要的角色。但在 SwiftUI 的发展史上,ScrollView 一直处于“残废”的状态,直到 SwiftUI 6.0 才逐渐补齐短板。下面详细讲解 SwiftUI 中 ScrollView 的进化史。
YungFan
2025/02/09
1960
如何使用 SwiftUI 中 ScrollView 的滚动偏移
WWDC 24 已经结束,我决定开始写一些关于 SwiftUI 框架即将推出的新特性的文章。今年,苹果继续填补空白,引入了对滚动位置更细粒度的控制。本周,我们将学习如何操作和读取滚动偏移。
Swift社区
2024/09/18
7740
如何使用 SwiftUI 中 ScrollView 的滚动偏移
掌握 SwiftUI 中的 ScrollView:滚动几何
本文探讨了如何使用 onScrollGeometryChange 视图修饰符有效地监控和管理滚动位置和几何。通过详细的代码示例和解释,你将学习如何利用这些工具创建动态和响应迅速的用户界面。
Swift社区
2024/09/21
5250
掌握 SwiftUI 中的 ScrollView:滚动几何
相关推荐
用 Table 在 SwiftUI 下创建表格
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验