web-dev-qa-db-fra.com

Comment maintenir la position du défilement dans une SwiftUI TabView

Utilisation d'un ScrollView à l'intérieur d'un TabView J'ai remarqué que ScrollView ne conserve pas sa position de défilement lorsque je recule d'un onglet à l'autre. Comment changer l'exemple ci-dessous pour que ScrollView conserve sa position de défilement?

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

Cette réponse est publiée en complément de la solution @oivvio liée dans la réponse de @arsenius, sur la façon d'utiliser un UITabController rempli de UIHostingControllers.

La réponse dans le lien a un problème: si les vues enfants ont des dépendances SwiftUI externes, ces enfants ne seront pas mis à jour. C'est très bien pour la plupart des cas où les vues des enfants n'ont que des états internes. Cependant, si vous êtes comme moi, un développeur React qui bénéficie d'un système Redux mondial, vous aurez des ennuis.

Afin de résoudre le problème, la clé consiste à mettre à jour rootView pour chaque UIHostingController à chaque fois que updateUIViewController est appelé. Mon code évite également de créer UIView ou UIViewControllers inutiles: ils ne sont pas si chers à créer si vous ne les ajoutez pas à la hiérarchie des vues, mais quand même, moins vous gaspillez, mieux c'est.

Avertissement: le code ne prend pas en charge une liste de vue d'onglet dynamique. Pour prendre en charge cela correctement, nous aimerions identifier chaque vue d'onglet enfant et faire un diff tableau pour les ajouter, les classer ou les supprimer correctement. Cela peut être fait en principe mais va au-delà de mes besoins.

Nous avons d'abord besoin d'un TabItem. C'est ainsi que le contrôleur récupère toutes les informations, sans créer de 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())
    }
}

Nous avons alors le contrôleur:

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

Les contrôleurs enfants sont initialisés dans makeUIViewController. Chaque fois que updateUIViewController est appelé, nous mettons à jour la vue racine de chaque contrôleur enfant. Je n'ai pas fait de comparaison pour rootView car je pense que la même vérification serait effectuée au niveau du framework, selon la description d'Apple à propos de comment les vues sont mises à jour. J'ai peut être tort.

Pour l'utiliser, c'est vraiment simple. Le code ci-dessous est partiel que j'ai récupéré d'un projet de simulation que je fais actuellement:


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

L'état est élevé au niveau racine sous la forme Model et il contient des aides à la mutation. Dans le CardContent, vous pouvez accéder à l'état et aux assistants via un EnvironmentObject. La mise à jour serait effectuée dans l'objet Model, propagé à ContentView, avec notre XNTabView notifié et chacun de ses UIHostController mis à jour.

MODIFICATIONS:

  • Il se trouve que .environmentObject peut être placé au niveau supérieur.
0
Minsheng Liu