【问题标题】:Is there any way to make a paged ScrollView in SwiftUI?有没有办法在 SwiftUI 中制作分页的 ScrollView?
【发布时间】:2019-07-04 19:08:04
【问题描述】:

我一直在查看每个测试版的文档,但还没有看到制作传统分页 ScrollView 的方法。我对 AppKit 不熟悉,所以我想知道这在 SwiftUI 中是否不存在,因为它主要是一个 UIKit 构造。无论如何,有没有人有这方面的例子,或者任何人都可以告诉我这绝对不可能,所以我可以停止寻找并自己滚动?

【问题讨论】:

标签: uiscrollview swiftui


【解决方案1】:

您现在可以使用 TabView 并将 .tabViewStyle 设置为 PageTabViewStyle()

TabView {
            View1()
            View2()
            View3()
        }
        .tabViewStyle(PageTabViewStyle())

【讨论】:

    【解决方案2】:

    从 Beta 3 开始,没有用于分页的原生 SwiftUI API。我已提交反馈并建议您也这样做。他们将ScrollView API 从 Beta 2 更改为 Beta 3,看到进一步的更新我不会感到惊讶。

    现在可以包装UIScrollView 以提供此功能。不幸的是,您必须将 UIScrollView 包装在 UIViewController 中,并进一步将其包装在 UIViewControllerRepresentable 中以支持 SwiftUI 内容。

    Gist here

    class UIScrollViewViewController: UIViewController {
    
        lazy var scrollView: UIScrollView = {
            let v = UIScrollView()
            v.isPagingEnabled = true
            return v
        }()
    
        var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.view.addSubview(self.scrollView)
            self.pinEdges(of: self.scrollView, to: self.view)
    
            self.hostingController.willMove(toParent: self)
            self.scrollView.addSubview(self.hostingController.view)
            self.pinEdges(of: self.hostingController.view, to: self.scrollView)
            self.hostingController.didMove(toParent: self)
    
        }
    
        func pinEdges(of viewA: UIView, to viewB: UIView) {
            viewA.translatesAutoresizingMaskIntoConstraints = false
            viewB.addConstraints([
                viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
                viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
                viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
                viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
            ])
        }
    
    }
    
    struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
    
        var content: () -> Content
    
        init(@ViewBuilder content: @escaping () -> Content) {
            self.content = content
        }
    
        func makeUIViewController(context: Context) -> UIScrollViewViewController {
            let vc = UIScrollViewViewController()
            vc.hostingController.rootView = AnyView(self.content())
            return vc
        }
    
        func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
            viewController.hostingController.rootView = AnyView(self.content())
        }
    }
    

    然后使用它:

        var body: some View {
            GeometryReader { proxy in
                UIScrollViewWrapper {
                    VStack {
                        ForEach(0..<1000) { _ in
                            Text("Hello world")
                        }
                    }
                    .frame(width: proxy.size.width) // This ensures the content uses the available width, otherwise it will be pinned to the left
                }
            }
        }
    

    【讨论】:

    • NavigationLinks 似乎在自定义包装器中不起作用。知道该怎么做吗?
    • 优雅且可扩展的解决方案。我喜欢!
    • 是否可以滚动到某个位置?我试过 scrollView.contentOffset = ... 但它不起作用。如何实现滚动视图滚动到某个位置?
    【解决方案3】:

    Apple 的官方教程以此为例。我发现它很容易理解并且适合我的情况。我真的建议您检查一下并尝试了解如何与 UIKit 交互。由于 SwiftUI 还很年轻,所以目前不会涵盖 UIKit 中的所有功能。与 UIKit 的接口应该可以满足大部分需求。

    https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit

    【讨论】:

    • 该教程的问题在于它只涵盖了使用页面视图控制器的非常基本的功能。当您开始添加添加或删除页面等功能时,教程中提供的代码很快就会过时。
    【解决方案4】:

    不确定这是否对您的问题有帮助,但目前,当 Apple 致力于在 SwiftUI 中添加分页视图时,我编写了一个 utility library,它在使用隐藏在引擎盖下的 UIPageViewController 时给您一种 SwiftUI 的感觉离开。

    你可以这样使用它:

    Pages {
        Text("Page 1")
        Text("Page 2")
        Text("Page 3")
        Text("Page 4")
    }
    

    或者,如果您的应用程序中有一个模型列表,您可以像这样使用它:

    struct Car {
        var model: String
    }
    
    let cars = [Car(model: "Ford"), Car(model: "Ferrari")]
    
    ModelPages(cars) { index, car in
        Text("The \(index) car is a \(car.model)")
            .padding(50)
            .foregroundColor(.white)
            .background(Color.blue)
            .cornerRadius(10)
    }
    

    【讨论】:

    • 很棒的图书馆!
    • currentPage 绑定对您有用吗?如果我将绑定到状态变量的绑定传递给 Pages 视图,它会一直工作,直到我尝试访问此状态变量(例如,将其显示为 Text("Page \(currentPage)") 的一部分),之后我不能再滑动超过一页。不确定这是一个 SwiftUI 错误还是我们不应该这样使用的东西,但我在尝试以 SwiftUIy 方式包装 UIPageViewController 时遇到了同样的问题。
    • 不过,您的数组自定义函数构建器很棒!整天都想弄清楚如何做到这一点:D
    【解决方案5】:

    您可以使用.onAppear() 简单地跟踪状态以加载您的下一页。

    struct YourListView : View {
    
        @ObservedObject var viewModel = YourViewModel()
    
        let numPerPage = 50
    
        var body: some View {
            NavigationView {
                List(viewModel.items) { item in
                    NavigationLink(destination: DetailView(item: item)) {
                        ItemRow(item: item)
                        .onAppear {
                            if self.shouldLoadNextPage(currentItem: item) {
                                self.viewModel.fetchItems(limitPerPage: self.numPerPage)
                            }
                        }
                    }
                }
                .navigationBarTitle(Text("Items"))
                .onAppear {
                    guard self.viewModel.items.isEmpty else { return }
                    self.viewModel.fetchItems(limitPerPage: self.numPerPage)
                }
            }
        }
    
        private func shouldLoadNextPage(currentItem item: Item) -> Bool {
            let currentIndex = self.viewModel.items.firstIndex(where: { $0.id == item.id } )
            let lastIndex = self.viewModel.items.count - 1
            let offset = 5 //Load next page when 5 from bottom, adjust to meet needs
            return currentIndex == lastIndex - offset
        }
    }
    
    class YourViewModel: ObservableObject {
        @Published private(set) items = [Item]()
        // add whatever tracking you need for your paged API like next/previous and count
        private(set) var fetching = false
        private(set) var next: String?
        private(set) var count = 0
    
    
        func fetchItems(limitPerPage: Int = 30, completion: (([Item]?) -> Void)? = nil) {
            // Do your stuff here based on the API rules for paging like determining the URL etc...
            if items.count == 0 || items.count < count {
                let urlString = next ?? "https://somePagedAPI?limit=/(limitPerPage)"
                fetchNextItems(url: urlString, completion: completion)
            } else {
                completion?(pokemon)
            }
    
        }
    
        private func fetchNextItems(url: String, completion: (([Item]?) -> Void)?) {
            guard !fetching else { return }
            fetching = true
            Networking.fetchItems(url: url) { [weak self] (result) in
                DispatchQueue.main.async { [weak self] in
                    self?.fetching = false
                    switch result {
                    case .success(let response):
                        if let count = response.count {
                            self?.count = count
                        }
                        if let newItems = response.results {
                            self?.items += newItems
                        }
                        self?.next = response.next
                    case .failure(let error):
                        // Error state tracking not implemented but would go here...
                        os_log("Error fetching data: %@", error.localizedDescription)
                    }
                }
            }
        }
    }
    

    修改以适应您正在调用的任何 API,并根据您的应用架构处理错误。

    【讨论】:

      【解决方案6】:

      结帐SwiftUIPager。这是一个建立在 SwiftUI 原生组件之上的寻呼机:

      【讨论】:

      • 您可以在不预先填充页面的情况下使用它吗?我必须显示大量页面并且无法提前填充。当寻呼机移动到该索引时,我需要能够填充页面。
      • 嗨@nick,是的,你应该可以。用一些元素预填充它并使用 onPageChanged 追加更多
      • 谢谢费尔南多!我的应用程序有一个包含 1190 页的阅读视图。用户可以打开多个阅读视图,因此您很可能会有多个视图,每个视图将填充 1190 页。那不是非常低效吗?我想知道系统将如何管理所有内存等...您对此有何看法?
      • 嗨@nick,框架只会加载足够的阅读视图来填充 3 个屏幕的空间。之后,它会按需请求更多页面
      【解决方案7】:

      如果您想利用TabView 的新PageTabViewStyle,但需要垂直分页滚动视图,则可以使用.rotationEffect() 等效果修饰符。

      使用这种方法,我编写了一个名为 VerticalTabView ?library,只需将现有的 TabView 更改为 VTabView 即可将 TabView 垂直。

      【讨论】:

        【解决方案8】:

        你可以使用这样的自定义修饰符:

        struct ScrollViewPagingModifier: ViewModifier {
            func body(content: Content) -> some View {
                content
                    .onAppear {
                        UIScrollView.appearance().isPagingEnabled = true
                    }
                    .onDisappear {
                        UIScrollView.appearance().isPagingEnabled = false
                    }
            }
        }
        
        extension ScrollView {
            func isPagingEnabled() -> some View {
                modifier(ScrollViewPagingModifier())
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-12-16
          • 2021-07-29
          • 1970-01-01
          • 1970-01-01
          • 2019-12-01
          • 2023-03-15
          相关资源
          最近更新 更多