有没有办法在 SwiftUI 中制作分页的 ScrollView?

Posted

技术标签:

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

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

【问题讨论】:

不确定是否有任何直接的方法可以做到这一点,但您可以创建您的 customView 并实现。如果有帮助,请参考此链接。 ***.com/questions/56827148/… 【参考方案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())
    

【讨论】:

以上是关于有没有办法在 SwiftUI 中制作分页的 ScrollView?的主要内容,如果未能解决你的问题,请参考以下文章

Elasticsearch聚合后将聚合结果进行分页的解决办法

有没有办法使用 ApiPlatform 分页来获取总项目?

带有 TabView、SwiftUI 的水平分页滚动视图

thinkPHP分页的制作

thinkPHP分页的制作

有没有控制打印机分页的HTML标记