You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI:设备旋转时维持滚动位置及滚动控制问题

解决SwiftUI ScrollView旋转滚动位置丢失、初始定位及滚动跳转问题

问题梳理

我用SwiftUI做了一个带ForEach的ScrollView,遍历Verse数组,每个条目是包含阿拉伯语、英语文本的VStack:

  • readingMode=false时,显示英语文本,阿拉伯语文本居右;
  • readingMode=true时,隐藏英语文本,阿拉伯语文本居中。

现在遇到三个问题:

  1. 设备旋转后,滚动位置直接丢失;
  2. 视图刚加载时,没法直接定位到指定的初始章节;
  3. 用滚轮选择器选章节后,ScrollView没法同步跳转到对应位置。

之前试过用.onChange监听设备旋转+.scrollPosition存位置,效果不稳定;用ScrollViewReader+scrollTo实现定位,但会随机跳错位置。


完整解决方案代码

下面是修正后的代码,一次性解决三个问题:

import SwiftUI

struct Verse: Identifiable {
    let id: UUID = UUID()
    let arabic: String
    let english: String
    let chapterNumber: Int // 用于滚轮选择器的章节编号
}

struct VerseScrollView: View {
    let verses: [Verse]
    @State private var readingMode = false
    @State private var scrollID: Verse.ID?
    @State private var savedScrollID: Verse.ID?
    @State private var selectedChapter = 1 // 滚轮选择器选中的章节
    
    // 获取指定章节的第一个Verse ID
    private func getFirstVerseID(for chapter: Int) -> Verse.ID? {
        verses.first(where: { $0.chapterNumber == chapter })?.id
    }
    
    var body: some View {
        VStack {
            // 顶部控制区:阅读模式切换 + 章节滚轮选择器
            HStack {
                Toggle("纯阿拉伯语模式", isOn: $readingMode)
                    .onChange(of: readingMode) { _, _ in
                        // 切换阅读模式后,恢复之前的滚动位置
                        if let id = savedScrollID {
                            scrollID = id
                        }
                    }
                
                Spacer()
                
                Picker("选择章节", selection: $selectedChapter) {
                    ForEach(1...10, id: \.self) { chapter in
                        Text("第\(chapter)章").tag(chapter)
                    }
                }
                .pickerStyle(.wheel)
                .frame(width: 150)
                .onChange(of: selectedChapter) { _, newChapter in
                    scrollID = getFirstVerseID(for: newChapter)
                }
            }
            .padding()
            
            // 核心滚动视图
            ScrollViewReader { proxy in
                ScrollView {
                    Text("Some Header")
                        .font(.title)
                        .padding()
                    
                    LazyVStack {
                        ForEach(verses) { verse in
                            VStack {
                                HStack {
                                    if !readingMode {
                                        Spacer()
                                    }
                                    
                                    Text(verse.arabic)
                                        .multilineTextAlignment(readingMode ? .center : .trailing)
                                        .font(.title)
                                }
                                
                                if !readingMode {
                                    HStack {
                                        Text(verse.english)
                                            .multilineTextAlignment(.leading)
                                            .font(.body)
                                        Spacer()
                                    }
                                    .padding(.top, 4)
                                }
                                
                                Divider()
                            }
                            .padding(.horizontal)
                            .id(verse.id)
                        }
                    }
                }
                // 监听当前滚动位置,同步到scrollID
                .scrollPosition(id: $scrollID)
                // 处理设备旋转:旋转前保存位置,旋转后恢复
                .onChange(of: UIDevice.current.orientation) { newOrientation in
                    if newOrientation.isFlat { return } // 忽略平放状态
                    
                    // 旋转开始时保存当前滚动ID
                    if newOrientation.isPortrait || newOrientation.isLandscape {
                        savedScrollID = scrollID
                    }
                    
                    // 旋转完成后恢复位置
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                        if let id = savedScrollID {
                            withAnimation {
                                proxy.scrollTo(id, anchor: .top)
                            }
                        }
                    }
                }
                // 初始加载时定位到指定章节
                .onAppear {
                    scrollID = getFirstVerseID(for: selectedChapter)
                    DispatchQueue.main.async {
                        if let id = scrollID {
                            proxy.scrollTo(id, anchor: .top)
                        }
                    }
                }
                // 当scrollID变化时触发滚动
                .onChange(of: scrollID) { newID in
                    guard let id = newID else { return }
                    withAnimation {
                        proxy.scrollTo(id, anchor: .top)
                    }
                }
            }
        }
    }
}

关键解决要点

1. 设备旋转维持滚动位置

  • 用@State private var savedScrollID在旋转前记录当前滚动到的Verse.ID;
  • 监听UIDevice.current.orientation变化,旋转完成后延迟调用scrollTo恢复位置(延迟是为了等待视图布局完成);
  • 切换readingMode时也会触发视图布局变化,所以在模式切换后也要恢复之前保存的滚动ID。

2. 初始滚动定位

  • 在onAppear中通过getFirstVerseID获取目标章节的第一个Verse的ID,然后调用scrollTo;
  • 用DispatchQueue.main.async延迟执行,避免视图还没加载完成导致scrollTo失效。

3. 滚轮选择器同步滚动

  • 把滚轮选择器的selectedChapter和scrollID绑定,选中章节变化时,自动获取对应章节的第一个VerseID并设置为scrollID;
  • 通过.scrollPosition(id: $scrollID)监听滚动位置的变化,反向同步到scrollID,确保滚轮选择器和滚动位置状态一致。

内容的提问来源于stack exchange,提问作者Ali

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 23:22:20