Is there any way to make a paged ScrollView in SwiftUI?

前端 未结 7 1313
轻奢々
轻奢々 2021-02-03 15:02

I\'ve been looking through the docs with each beta but haven\'t seen a way to make a traditional paged ScrollView. I\'m not familiar with AppKit so I am wondering if this doesn\

相关标签:
7条回答
  • Not sure if this helps your question but for the time being while Apple is working on adding a Paging View in SwiftUI I've written a utility library that gives you a SwiftUI feel while using a UIPageViewController under the hood tucked away.

    You can use it like this:

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

    Or if you have a list of models in your application you can use it like this:

    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)
    }
    
    0 讨论(0)
  • 2021-02-03 15:11

    If you would like to exploit the new PageTabViewStyle of TabView, but you need a vertical paged scroll view, you can make use of effect modifiers like .rotationEffect().

    Using this method I wrote a library called VerticalTabView

    0 讨论(0)
  • 2021-02-03 15:15

    You can now use a TabView and set the .tabViewStyle to PageTabViewStyle()

    TabView {
                View1()
                View2()
                View3()
            }
            .tabViewStyle(PageTabViewStyle())
    
    0 讨论(0)
  • 2021-02-03 15:20

    As of Beta 3 there is no native SwiftUI API for paging. I've filed feedback and recommend you do the same. They changed the ScrollView API from Beta 2 to Beta 3 and I wouldn't be surprised to see a further update.

    It is possible to wrap a UIScrollView in order to provide this functionality now. Unfortunately, you must wrap the UIScrollView in a UIViewController, which is further wrapped in UIViewControllerRepresentable in order to support SwiftUI content.

    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())
        }
    }
    

    And then to use it:

        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
                }
            }
        }
    
    0 讨论(0)
  • 2021-02-03 15:27

    You can simply track state using .onAppear() to load your next page.

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

    Modify to fit whatever API you are calling and handle errors based on your app architecture.

    0 讨论(0)
  • 2021-02-03 15:30

    Checkout SwiftUIPager. It's a pager built on top of SwiftUI native components:

    0 讨论(0)
提交回复
热议问题