有没有办法在 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?的主要内容,如果未能解决你的问题,请参考以下文章