web-dev-qa-db-ja.com

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

この回答は、UITabControllerで満たされたUIHostingControllerの使用方法について、@ arseniusからの回答にリンクされているソリューション@oivvioの補足として投稿されています。

リンクの答えには1つの問題があります。子ビューに外部SwiftUI依存関係がある場合、それらの子は更新されません。これは、子ビューに内部状態しかないほとんどの場合に問題ありません。ただし、私と同じように、グローバルなReduxシステムを楽しんでいるReact開発者である場合は、問題が発生します。

この問題を解決するには、rootViewが呼び出されるたびにUIHostingControllerごとにupdateUIViewControllerを更新することが重要です。私のコードは、不要なUIViewまたはUIViewControllersの作成も回避しています。これらをビュー階層に追加しない場合、作成するのにそれほどコストはかかりませんが、それでも無駄を少なくすればするほど効果的です。

警告:コードは動的タブビューリストをサポートしていません。これを正しくサポートするために、各子タブビューを識別し、配列のdiffを実行して、それらを正しく追加、順序付け、または削除します。それは原則として可能ですが、私の必要を超えています。

最初に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が呼び出されるたびに、各子コントローラーのルートビューを更新します。 Appleの説明によると、同じチェックがフレームワークレベルで行われると思うので、私はしない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を最上位に配置できます。
0
Minsheng Liu