10
votes

Comment maintenir la position de défilement dans un SwiftUI TabView

En utilisant un ScrollView dans un TabView j'ai remarqué que le ScrollView ne conserve pas sa position de défilement lorsque je passe d'un onglet à l'autre. Comment modifier 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)
        }
    }
}


1 commentaires

Vous trouverez peut-être utile ma personnalisation de ScrollView dans Comment faire défiler automatiquement une liste SwiftUI?


3 Réponses :


9
votes

Malheureusement, cela n'est tout simplement pas possible avec les composants intégrés étant donné les limitations actuelles de SwiftUI (iOS 13.x / Xcode 11.x).

Deux raisons:

  1. SwiftUI supprime complètement votre vue lorsque vous quittez l'onglet. C'est pourquoi votre position de défilement est perdue. Ce n'est pas comme UIKit où vous avez un tas de UIViewControllers hors écran.
  2. Il n'y a pas d'équivalent ScrollView de UIScrollView.contentOffset . Cela signifie que vous ne pouvez pas enregistrer votre état de défilement quelque part et le restaurer lorsque l'utilisateur revient à l'onglet.

Votre itinéraire le plus simple sera probablement d'utiliser un UITabBarController rempli de UIHostingController . De cette façon, vous ne perdrez pas l'état de chaque onglet lorsque les utilisateurs se déplacent entre eux.

Sinon, vous pouvez créer un conteneur d'onglets personnalisé et modifier vous-même l'opacité de chaque onglet (par opposition à les inclure conditionnellement dans la hiérarchie, ce qui cause actuellement votre problème).

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

J'ai utilisé cette technique lorsque je voulais empêcher un WKWebView de se recharger complètement à chaque fois qu'un utilisateur s'éloignait brièvement.

Je recommanderais la première technique . Surtout s'il s'agit de la navigation principale de votre application.


1 commentaires

Merci! J'ai fini par utiliser votre première option, qui est plus détaillée dans cette réponse à une question similaire: stackoverflow.com/a/58164937/98374 Compte tenu de votre réponse, j'ai pu la localiser



1
votes

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 d' UIHostingController .

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. Cela convient à la plupart des cas où les vues 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 global, vous aurez des problèmes.

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

Attention : le code ne prend pas en charge une liste de vues par onglets dynamiques. Pour prendre en charge cela correctement, nous aimerions identifier chaque vue d'onglet enfant et faire un tableau de comparaison pour les ajouter, les ordonner 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 :

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

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 cadre, selon la description d'Apple sur la façon dont les vues sont mises à jour. J'ai peut être tort.

L'utiliser est vraiment simple. Ce qui suit est un code partiel que j'ai récupéré dans un projet simulé que je suis en train de faire:

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

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

MODIFICATIONS:

  • Il s'avère que .environmentObject peut être placé au niveau supérieur.


0 commentaires

0
votes

Mise à jour de novembre 2020

TabView conservera désormais la position de défilement dans les onglets lors du déplacement entre les onglets, de sorte que les approches dans les réponses à ma question d'origine ne sont plus nécessaires.


0 commentaires