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

时间:2019-12-11 23:33:45

标签: swiftui tabview

ScrollView内使用TabView时,我注意到当我在选项卡之间来回移动时,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)
        }
    }
}

3 个答案:

答案 0 :(得分:2)

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

两个原因:

  1. 当您离开标签页时,SwiftUI会完全处理您的View。这就是为什么滚动位置丢失的原因。与UIKit不同的是,UIKit中没有一堆屏幕外的UIViewController。
  2. 没有与ScrollView等效的UIScrollView.contentOffset。这意味着您无法将滚动状态保存在某处,并在用户返回到选项卡时将其恢复。

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

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

UIHostingController

当我想防止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) } } 每次用户短暂跳开时完全重新加载时,我就使用了这种技术。

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

答案 1 :(得分:1)

此答案是对@arsenius答案中链接的解决方案@oivvio的补充,该解决方案涉及如何使用由UITabController填充的UIHostingController

链接中的答案有一个问题:如果子视图具有外部SwiftUI依赖关系,则不会更新这些子视图。对于大多数子视图仅具有内部状态的情况,这很好。但是,如果您像我一样,是一个享誉全球的Redux系统的React开发人员,则会遇到麻烦。

为了解决此问题,关键是每次调用rootView时为每个UIHostingController更新updateUIViewController。我的代码还避免了创建不必要的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的比较,因为根据苹果公司关于视图更新的描述,我觉得同样的检查将在框架级别进行。我可能错了。

使用它非常简单。以下是我从当前正在执行的模拟项目中获取的部分代码:


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

答案 2 :(得分:0)

2020年11月更新

re.sub('\s+', ' ', word) 现在将在选项卡之间移动时保持选项卡中的滚动位置,因此不再需要原始问题答案中的方法。