别让刷新把人送回顶部:用 AI + SwiftUI scrollPosition 记住阅读位置

别让刷新把人送回顶部:用 AI + SwiftUI scrollPosition 记住阅读位置

这一技教你让 AI 用 SwiftUI scrollPosition(id:) 绑定稳定条目 ID,在列表刷新、插入或删除数据后保留用户的阅读位置,并用 Xcode 独立验收回退规则和连续刷新边界。

刷新列表后,用户还应该停在原来的位置

一个阅读列表刚打开时没问题,用户滑到中间,点了刷新,页面却回到顶部。用户只好重新找刚才看到的内容;如果刷新同时插入了新条目,这次寻找甚至不一定能靠记忆完成。
今天这招解决的是「数据更新后,用户不应该丢掉当前阅读位置」:让 AI 用 SwiftUI 的 scrollPosition(id:) 记录当前滚动目标,并让每一行使用稳定的业务 ID。Apple 将 scrollPosition 定义为描述滚动视图语义位置的类型,scrollPosition(id:anchor:) 则把一个绑定连接到滚动视图,让绑定在滚动时更新。1 2
本文按 iOS 17.0 及以上的 SwiftUI API 写。如果项目的 Deployment Target 低于 iOS 17,先让 AI 检查兼容方案,不要因为当前模拟器能编译就直接提高最低版本。

先定清楚「刷新后保留什么」

scrollPosition 记录的是某个滚动目标的 ID,不是屏幕上的像素坐标。所以第一步不是改代码,而是确定列表里的哪一项代表用户正在看的位置。
产品场景当前项的身份当前项被删除时怎么办
资讯流文章的服务端 ID停在删除项后面第一项,找不到时回到列表顶部
最近项目项目的稳定 ID停在项目列表中距离原位置最近的项目
消息列表会话或消息的稳定 ID显示一条已读提示,再回到最新消息
今天用「资讯流」举例。先写下四条规则:
  • 每篇文章的 ID 来自数据本身,不使用数组下标。
  • 刷新可以插入新文章,但当前文章仍存在时,阅读位置尽量跟着它。
  • 当前文章已经被删除时,必须有明确的回退项,不能让页面停在一个不存在的 ID 上。
  • 文章排序、分页和加载失败提示保持原样,不因为加入滚动位置而重写。
这和上一期的 ScrollViewReader 用法不一样。提交表单失败时,ScrollViewReader 适合主动跳到一个错误字段;今天的场景是持续记录列表当前落在哪个条目,供刷新后继续判断应该停在哪里。

把产品规则交给 AI

把列表 View、数据模型、项目的最低 iOS 版本和上面的规则一起交给 AI。可以直接使用下面这段指令,再替换页面名和数据类型:
请只改造「资讯流」的滚动位置保持能力,不要重写数据请求、分页、排序、文章卡片和点击导航。
当前产品规则:用户刷新后,如果当前文章仍在新数据中,尽量继续停在这篇文章附近;如果当前文章被删除,回退到删除项后面的第一篇文章,找不到时回到第一篇。新插入的文章不能让用户无故回到顶部。
请使用 SwiftUI iOS 17.0 及以上的 scrollPosition(id:)scrollTargetLayout()。列表行必须使用文章自身的稳定 ID,不要用数组下标当 ID。先解释哪些视图 modifier 会移动,哪些状态会新增,再给出最小改动代码。
刷新完成后,请保留旧的当前文章 ID,检查它是否仍存在,再决定继续使用旧 ID 还是使用回退 ID。不要用固定延迟、手写 offset 或 GeometryReader 模拟这件事。最后列出 iPhone 窄屏、大字号、插入新文章和当前文章被删除时的验收步骤。
这段指令里最重要的是「当前项被删除怎么办」。如果不先定回退规则,AI 往往只会把一个可选 ID 绑到滚动视图上,却没有处理数据更新后目标消失的情况。

看懂 AI 应该改成什么

最小结构可以接近下面这样,FeedItemFeedRowreloadItems() 换成你项目里已有的类型和加载函数:
struct FeedView: View {
    @State private var visibleItemID: FeedItem.ID?
    @State private var items: [FeedItem] = []

var body: some View {
        ScrollView {
            LazyVStack(spacing: 12) {
                ForEach(items) { item in
                    FeedRow(item: item)
                        .id(item.id)
                }
            }
            .scrollTargetLayout()
        }
        .scrollPosition(id: $visibleItemID)
        .refreshable {
            let oldID = visibleItemID
            let newItems = await reloadItems()
            items = newItems
            visibleItemID = fallbackID(keeping: oldID, in: newItems)
        }
    }
}
这里不用背代码,盯住五个位置就够了:
  1. visibleItemID 存的是文章 ID,不是第几行,也不是屏幕坐标。
  2. ForEach 里的每一行要有稳定身份。模型已经遵守 Identifiable 时,通常沿用它的 id;不要让 AI 改成 ForEach(items.indices) 只为快速通过编译。
  3. .scrollTargetLayout() 放在承载重复文章行的 LazyVStack 上。Apple 文档说明,这个 modifier 会把外层布局配置成滚动目标布局。3
  4. .scrollPosition(id: $visibleItemID) 放在 ScrollView 上,绑定才有机会随着滚动更新,也能由状态反向指定目标。
  5. 刷新完成后先更新数据,再检查旧 ID 是否仍存在。fallbackID 是产品规则的代码化,不要让 AI 用 newItems.first 作为所有场景的默认答案。
如果 AI 只加了 .scrollPosition(id:),却没有 .scrollTargetLayout(),让它解释滚动目标是怎样被识别的。如果它用数组下标保存位置,也要退回修改,因为插入或删除一条数据后,同一个下标已经不再代表同一篇文章。

用 Xcode 验收「没跳位」

先给列表准备固定假数据,文章标题要有长有短,数据顺序也要能在刷新前后变化。不要只用五条完全相同的占位文本,否则很难判断你看到的是同一篇文章,还是恰好回到了相似的位置。
按下面顺序验收:
  1. 打开资讯流,滑到中间,记住当前文章的标题或首句。
  2. 触发刷新,确认顶部可以出现新文章,但当前文章仍在可见区域附近,页面没有直接回到顶部。
  3. 在当前文章之前插入两条假数据,再刷新一次,确认位置跟着文章 ID,而不是跟着原来的行号。
  4. 删除当前文章,再刷新,确认页面落到产品规则指定的后继文章;如果后继文章不存在,确认回退到第一篇。
  5. 刷新请求失败时,确认旧列表和当前滚动位置仍然保留,错误提示不会清空内容。
  6. 在 iPhone 窄屏和 iPad 横屏上重复前面的步骤,确认行高、分页和加载提示没有因为绑定滚动位置而变化。
  7. 打开大字号,检查长标题、多行摘要和图片加载中的文章,确认用户仍能识别当前文章。
  8. 连续快速刷新两次,确认最后一次数据与位置状态没有被前一次结果覆盖。这个问题属于数据请求管理,要单独验收,不能只看滚动 modifier。
人工检查 AI 的差异说明时,重点看三件事:它有没有把数据 ID 换成数组下标,是否在当前项消失后留下了无效 ID,以及是否为了「看起来稳定」偷偷加入定时滚动或固定延迟。后两种写法可能在演示里有效,遇到真实网络速度和数据变化就会变脆。

这招的边界

scrollPosition(id:) 给的是语义位置,不是「永远保持屏幕像素不动」的承诺。刷新后如果当前条目高度发生变化、当前条目被删除,或者列表完全换成另一组数据,页面应该落在哪里,仍然需要产品规则和回退逻辑。
它也不负责处理异步请求的先后关系。比如用户连续触发两次刷新,旧请求晚于新请求返回,仍可能覆盖列表数据;这要让 AI 另外检查请求取消、版本号或并发策略。滚动位置绑定解决的是「当前看哪一项」,不是所有刷新竞态。
项目最低版本低于 iOS 17 时,先记录旧系统方案、提高最低版本的成本,以及不支持语义滚动位置时的体验差异,再决定是否合入。不要把模拟器里一次成功的刷新当成完整验收。

PM 今天可以独立完成的清单

  • 找一个刷新后经常回到顶部的真实列表。
  • 写下列表项的稳定 ID,以及当前项被删除时的回退规则。
  • 把现有 View、数据模型、最低 iOS 版本和产品规则一起交给 AI。
  • 检查每一行使用的是业务 ID,不是数组下标。
  • 检查 .scrollTargetLayout() 是否挂在承载重复行的布局容器上。
  • 检查 .scrollPosition(id:) 是否绑定到了 ScrollView 的状态。
  • 用插入、删除、失败和连续刷新假数据验收位置是否符合规则。
  • 在 iPhone 窄屏、iPad 横屏和大字号下重复测试。
  • 如果项目低于 iOS 17,先记录兼容方案和取舍,再决定是否合入。
先改一个资讯流页面。验收标准很具体:刷新可以改变数据,但不应该把用户刚看到的那篇文章凭空送回列表顶部。

Contenido relacionado

  • Inicia sesión para comentar.
More from this channel