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) } } }
3 Réponses :
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:
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.
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
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:
.environmentObject
peut être placé au niveau supérieur. 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.
Vous trouverez peut-être utile ma personnalisation de
ScrollView
dans Comment faire défiler automatiquement une liste SwiftUI?