如何在SwiftUI TabView中保持滚动位置

15

TabView中使用一个ScrollView后,我注意到当我在选项卡之间来回切换时,ScrollView无法保持其滚动位置。如何更改下面的示例以使ScrollView保持其滚动位置?

import SwiftUI

struct HomeView: View {
    var body: some View {
        ScrollView {
            VStack {
                Text("Line 1")
                Text("Line 2")
                Text("Line 3")
                Text("Line 4")
                Text("Line 5")
                Text("Line 6")
                Text("Line 7")
                Text("Line 8")
                Text("Line 9")
                Text("Line 10")
            }
        }.font(.system(size: 80, weight: .bold))
    }
}

struct ContentView: View {
    @State private var selection = 0

    var body: some View {
        TabView(selection: $selection) {
            HomeView()
                .tabItem {
                    Image(systemName: "house")
                }.tag(0)

            Text("Out")
                .tabItem {
                    Image(systemName: "cloud")
                }.tag(1)
        }
    }
}

你可能会发现我的自定义ScrollView如何使SwiftUI列表自动滚动?中非常有用。 - Asperi
3个回答

9

很遗憾,基于当前 SwiftUI (iOS 13.x/Xcode 11.x) 的限制,使用内置组件无法实现该功能。

原因如下:

  1. 当您切换标签时,SwiftUI 会完全销毁您的视图。这就是为什么您的滚动位置会丢失。这与 UIKit 不同,后者有一堆屏幕外的 UIViewControllers。
  2. ScrollView 没有 UIScrollView.contentOffset 的等效物。这意味着您无法在用户返回标签时将滚动状态保存在某个地方并恢复它。

最简单的方法可能是使用由 UIHostingController 填充的 UITabBarController。这样,当用户在选项卡之间移动时,每个选项卡的状态不会丢失。

否则,您可以创建自定义选项卡容器,并自己修改每个选项卡的不透明度(而不是将它们条件性地包含在层次结构中,这是当前导致问题的原因)。

var body: some View {
  ZStack {
    self.tab1Content.opacity(self.currentTab == .tab1 ? 1.0 : 0.0)
    self.tab2Content.opacity(self.currentTab == .tab2 ? 1.0 : 0.0)
    self.tab3Content.opacity(self.currentTab == .tab3 ? 1.0 : 0.0)
  }
}

当我想让用户短暂切换时,我使用了这种技术来避免WKWebView的完全重新加载。

我会推荐第一种技术,特别是如果这是你的应用程序的主导航。


谢谢!我最终使用了你的第一个选项,这个答案对于一个类似的问题进行了更详细的解释:https://dev59.com/Z1MH5IYBdhLWcg3w30wJ#58164937 在你的回答的帮助下,我找到了它。 - oivvio

4

2020年11月更新

TabView现在在切换标签页时将保持滚动位置,因此不再需要使用原问题答案中的方法。


TabView在通过编程方式更改selection绑定时确实会保持滚动位置,但是当使用滑动手势在选项卡之间切换时,如果有4个或更多的选项卡,滚动位置最终会被重置。我已经报告了这个错误,并创建了这个示例测试项目来重现这个问题。 - undefined

1
这个答案是对@oivvio在@arsenius的答案中提供的关于如何使用填充了UIHostingController的UITabController的解决方案的补充。
链接中的答案有一个问题:如果子视图有外部的SwiftUI依赖,那些子视图将不会被更新。这对于大多数情况下子视图只有内部状态的情况来说是可以接受的。然而,如果你像我一样是一个喜欢全局Redux系统的React开发者,那么你就会遇到麻烦。
为了解决这个问题,关键是每次调用updateUIViewController时更新每个UIHostingController的rootView。我的代码还避免创建不必要的UIView或UIViewControllers:如果您不将它们添加到视图层次结构中,它们并不那么昂贵,但是仍然越少浪费越好。
警告:该代码不支持动态选项卡视图列表。为了正确支持它,我们需要识别每个子选项卡视图,并进行数组差异以正确添加、排序或删除它们。原则上可以做到,但超出了我的需求。

首先,我们需要一个 TabItem。这样做是为了使控制器能够获取所有信息,而不创建任何 UITabBarItem

struct XNTabItem: View {
    let title: String
    let image: UIImage?
    let body: AnyView

    public init<Content: View>(title: String, image: UIImage?, @ViewBuilder content: () -> Content) {
        self.title = title
        self.image = image
        self.body = AnyView(content())
    }
}

我们接下来有控制器:

struct XNTabView: UIViewControllerRepresentable {
    let tabItems: [XNTabItem]

    func makeUIViewController(context: UIViewControllerRepresentableContext<XNTabView>) -> UITabBarController {
        let rootController = UITabBarController()
        rootController.viewControllers = tabItems.map {
            let host = UIHostingController(rootView: $0.body)
            host.tabBarItem = UITabBarItem(title: $0.title, image: $0.image, selectedImage: $0.image)
            return host
        }
        return rootController
    }

    func updateUIViewController(_ rootController: UITabBarController, context: UIViewControllerRepresentableContext<XNTabView>) {
        let children = rootController.viewControllers as! [UIHostingController<AnyView>]
        for (newTab, host) in zip(self.tabItems, children) {
            host.rootView = newTab.body
            if host.tabBarItem.title != host.tabBarItem.title {
                host.tabBarItem.title = host.tabBarItem.title
            }
            if host.tabBarItem.image != host.tabBarItem.image {
                host.tabBarItem.image = host.tabBarItem.image
            }
        }
    }
}

子控制器在makeUIViewController中初始化。每当调用updateUIViewController时,我们更新每个子控制器的根视图。我没有对rootView进行比较,因为我认为在框架级别上会执行相同的检查,根据苹果关于如何更新视图的描述。但我可能是错误的。

使用它非常简单。下面是我从一个模拟项目中获取的部分代码:


class Model: ObservableObject {
    @Published var allHouseInfo = HouseInfo.samples

    public func flipFavorite(for id: Int) {
        if let index = (allHouseInfo.firstIndex { $0.id == id }) {
            allHouseInfo[index].isFavorite.toggle()
        }
    }
}

struct FavoritesView: View {
    let favorites: [HouseInfo]

    var body: some View {
        if favorites.count > 0 {
            return AnyView(ScrollView {
                ForEach(favorites) {
                    CardContent(info: $0)
                }
            })
        } else {
            return AnyView(Text("No Favorites"))
        }
    }
}

struct ContentView: View {
    static let housingTabImage = UIImage(systemName: "house.fill")
    static let favoritesTabImage = UIImage(systemName: "heart.fill")

    @ObservedObject var model = Model()

    var favorites: [HouseInfo] {
        get { model.allHouseInfo.filter { $0.isFavorite } }
    }

    var body: some View {
        XNTabView(tabItems: [
            XNTabItem(title: "Housing", image: Self.housingTabImage) {
                NavigationView {
                    ScrollView {
                        ForEach(model.allHouseInfo) {
                            CardView(info: $0)
                                .padding(.vertical, 8)
                                .padding(.horizontal, 16)
                        }
                    }.navigationBarTitle("Housing")
                }
            },
            XNTabItem(title: "Favorites", image: Self.favoritesTabImage) {
                NavigationView {
                    FavoritesView(favorites: favorites).navigationBarTitle("Favorites")
                }
            }
        ]).environmentObject(model)
    }
}

状态被提升到根级别作为Model,并携带变异辅助工具。在CardContent中,您可以通过EnvironmentObject访问状态和辅助工具。更新将在Model对象中完成,传播到ContentView,通知我们的XNTabView并更新其每个UIHostController。
编辑:
  • 结果发现.environmentObject可以放置在顶层。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接