【问题标题】:How to maintain scroll position in a SwiftUI TabView如何在 SwiftUI TabView 中保持滚动位置
【发布时间】:2020-04-05 07:39: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)
        }
    }
}

【问题讨论】:

标签: swiftui tabview


【解决方案1】:

不幸的是,鉴于 SwiftUI (iOS 13.x/Xcode 11.x) 的当前限制,内置组件根本无法做到这一点。

两个原因:

  1. 当您离开选项卡时,SwiftUI 会完全释放您的视图。这就是您的滚动位置丢失的原因。它不像 UIKit,你有一堆屏幕外的 UIViewController。
  2. 没有ScrollView 等效于UIScrollView.contentOffset。这意味着您无法将滚动状态保存在某处并在用户返回选项卡时恢复它。

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

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

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)
  }
}

当我想在每次用户短暂地使用 Tab 键时防止 WKWebView 完全重新加载时,我使用了这种技术。

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

【讨论】:

  • 谢谢!我最终使用了您的第一个选项,在这个类似问题的答案中更加充实:stackoverflow.com/a/58164937/98374 鉴于您的回答,我能够找到它
【解决方案2】:

2020 年 11 月更新

TabView 现在在标签之间移动时将保持标签中的滚动位置,因此不再需要我原来问题的答案中的方法。

【讨论】:

    【解决方案3】:

    此答案作为对@arsenius 答案中链接的解决方案@oivvio 的补充发布,关于如何使用填充有UIHostingControllers 的UITabController

    链接中的答案有一个问题:如果子视图具有外部 SwiftUI 依赖项,则这些子视图将不会更新。对于大多数子视图只有内部状态的情况,这很好。但是,如果你像我一样,一个喜欢全球 Redux 系统的 React 开发者,那你就麻烦了。

    为了解决这个问题,关键是每次调用updateUIViewController时,为每个UIHostingController更新rootView。我的代码还避免了创建不必要的 UIViewUIViewControllers:如果您不将它们添加到视图层次结构中,创建它们的成本并不高,但是,浪费越少越好。

    警告:代码不支持动态标签视图列表。为了正确支持这一点,我们希望识别每个子选项卡视图并进行数组差异以正确添加、排序或删除它们。原则上可以这样做,但超出了我的需要。

    我们首先需要一个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 进行比较,因为根据Apple 关于如何更新视图的描述,我觉得会在框架级别进行相同的检查。我可能错了。

    使用起来非常简单。以下是我从我目前正在做的一个模拟项目中获取的部分代码:

    
    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可以放在顶层。

    【讨论】:

      猜你喜欢
      • 2010-10-03
      • 2011-11-28
      • 1970-01-01
      • 1970-01-01
      • 2013-04-16
      • 1970-01-01
      • 2011-03-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多