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)
}
}
}
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 UIHostingController
s.
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:
.environmentObject
peut être placé au niveau supérieur.