我是SwiftUI的新手。我有三个视图,想要将它们放在一个PageView中。我希望通过滑动移动每个视图,就像PageView一样,并且我希望小点指示我在哪个视图中。
iOS 15引入了一种新的TabViewStyle
: CarouselTabViewStyle
(仅限于watchOS)。
此外,我们现在可以更轻松地设置样式:
.tabViewStyle(.page)
在SwiftUI 2 / iOS 14中,现在有了UIPageViewController
的本地相应等价物。
要创建一个分页视图,请将.tabViewStyle
修饰符添加到TabView
并传递PageTabViewStyle
。
@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
TabView {
FirstView()
SecondView()
ThirdView()
}
.tabViewStyle(PageTabViewStyle())
}
}
}
您还可以控制分页按钮的显示方式:
// hide paging dots
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
您可以在此链接中找到更详细的说明:
TabView {
Group {
FirstView()
SecondView()
ThirdView()
}
.rotationEffect(Angle(degrees: -90))
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.rotationEffect(Angle(degrees: 90))
如果你厌倦了每次都要传递tabViewStyle
,你可以创建自己的 PageView
:
注意: 在iOS 14.0中,TabView选择方式不同,这就是为什么我使用了两个Binding
属性:selectionInternal
和selectionExternal
。但是,在iOS 14.3中,似乎只需要一个Binding
就可以工作了。然而,您仍然可以从修订历史记录中访问原始代码。
struct PageView<SelectionValue, Content>: View where SelectionValue: Hashable, Content: View {
@Binding private var selection: SelectionValue
private let indexDisplayMode: PageTabViewStyle.IndexDisplayMode
private let indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode
private let content: () -> Content
init(
selection: Binding<SelectionValue>,
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode = .automatic,
@ViewBuilder content: @escaping () -> Content
) {
self._selection = selection
self.indexDisplayMode = indexDisplayMode
self.indexBackgroundDisplayMode = indexBackgroundDisplayMode
self.content = content
}
var body: some View {
TabView(selection: $selection) {
content()
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: indexDisplayMode))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: indexBackgroundDisplayMode))
}
}
extension PageView where SelectionValue == Int {
init(
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode = .automatic,
@ViewBuilder content: @escaping () -> Content
) {
self._selection = .constant(0)
self.indexDisplayMode = indexDisplayMode
self.indexBackgroundDisplayMode = indexBackgroundDisplayMode
self.content = content
}
}
现在您有一个默认的 PageView
:
PageView {
FirstView()
SecondView()
ThirdView()
}
可以定制:
PageView(indexDisplayMode: .always, indexBackgroundDisplayMode: .always) { ... }
或提供一个选择
:
struct ContentView: View {
@State var selection = 1
var body: some View {
VStack {
Text("Selection: \(selection)")
PageView(selection: $selection, indexBackgroundDisplayMode: .always) {
ForEach(0 ..< 3, id: \.self) {
Text("Page \($0)")
.tag($0)
}
}
}
}
}
struct PageControl: UIViewRepresentable {
var numberOfPages: Int
@Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIPageControl {
let control = UIPageControl()
control.numberOfPages = numberOfPages
control.pageIndicatorTintColor = UIColor.lightGray
control.currentPageIndicatorTintColor = UIColor.darkGray
control.addTarget(
context.coordinator,
action: #selector(Coordinator.updateCurrentPage(sender:)),
for: .valueChanged)
return control
}
func updateUIView(_ uiView: UIPageControl, context: Context) {
uiView.currentPage = currentPage
}
class Coordinator: NSObject {
var control: PageControl
init(_ control: PageControl) {
self.control = control
}
@objc
func updateCurrentPage(sender: UIPageControl) {
control.currentPage = sender.currentPage
}
}
}
您的页面浏览次数
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
@State var currentPage = 0
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
ZStack(alignment: .bottom) {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
}
}
}
您的页面视图控制器
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
@Binding var currentPage: Int
@State private var previousPage = 0
init(controllers: [UIViewController],
currentPage: Binding<Int>)
{
self.controllers = controllers
self._currentPage = currentPage
self.previousPage = currentPage.wrappedValue
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
guard !controllers.isEmpty else {
return
}
let direction: UIPageViewController.NavigationDirection = previousPage < currentPage ? .forward : .reverse
context.coordinator.parent = self
pageViewController.setViewControllers(
[controllers[currentPage]], direction: direction, animated: true) { _ in {
previousPage = currentPage
}
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return parent.controllers.first
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController) {
parent.currentPage = index
}
}
}
}
假设您有一个视图,如下所示:
struct CardView: View {
var album: Album
var body: some View {
URLImage(URL(string: album.albumArtWork)!)
.resizable()
.aspectRatio(3 / 2, contentMode: .fit)
}
}
您可以像这样在您的主要SwiftUI视图中使用此组件。PageView(vM.Albums.map { CardView(album: $0) }).frame(height: 250)
if
语句,例如 if currentPage == 1 {...}
,会导致分页出现问题。 我在这里创建了一个问题:https://stackoverflow.com/questions/59448446/conditional-sibling-of-a-pageview-uipageviewcontroller-breaks-paging - 任何帮助将不胜感激,谢谢! - taber警告:以下答案使用了不公开的SwiftUI方法(如果您知道查找位置,仍然可以访问它们)。但是,它们没有得到适当的文档支持,可能不稳定。风险自负。
浏览SwiftUI文件时,我偶然发现了_PagingView
,它似乎自iOS 13起可用:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _PagingView<Views> : SwiftUI.View where Views : Swift.RandomAccessCollection, Views.Element : SwiftUI.View, Views.Index : Swift.Hashable
这个视图有两个初始化器:
public init(config: SwiftUI._PagingViewConfig = _PagingViewConfig(), page: SwiftUI.Binding<Views.Index>? = nil, views: Views)
public init(direction: SwiftUI._PagingViewConfig.Direction, page: SwiftUI.Binding<Views.Index>? = nil, views: Views)
我们还拥有_PagingViewConfig
:
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _PagingViewConfig : Swift.Equatable {
public enum Direction {
case vertical
case horizontal
public static func == (a: SwiftUI._PagingViewConfig.Direction, b: SwiftUI._PagingViewConfig.Direction) -> Swift.Bool
public var hashValue: Swift.Int {
get
}
public func hash(into hasher: inout Swift.Hasher)
}
public var direction: SwiftUI._PagingViewConfig.Direction
public var size: CoreGraphics.CGFloat?
public var margin: CoreGraphics.CGFloat
public var spacing: CoreGraphics.CGFloat
public var constrainedDeceleration: Swift.Bool
public init(direction: SwiftUI._PagingViewConfig.Direction = .horizontal, size: CoreGraphics.CGFloat? = nil, margin: CoreGraphics.CGFloat = 0, spacing: CoreGraphics.CGFloat = 0, constrainedDeceleration: Swift.Bool = true)
public static func == (a: SwiftUI._PagingViewConfig, b: SwiftUI._PagingViewConfig) -> Swift.Bool
}
_PagingView
:_PagingView(direction: .horizontal, views: [
AnyView(Color.red),
AnyView(Text("Hello world")),
AnyView(Rectangle().frame(width: 100, height: 100))
])
这里是另一个更加定制化的示例:
struct ContentView: View {
@State private var selection = 1
var body: some View {
_PagingView(
config: _PagingViewConfig(
direction: .vertical,
size: nil,
margin: 10,
spacing: 10,
constrainedDeceleration: false
),
page: $selection,
views: [
AnyView(Color.red),
AnyView(Text("Hello world")),
AnyView(Rectangle().frame(width: 100, height: 100))
]
)
}
}
Swift 5
在SwiftUI中实现页面视图非常简单,我们只需要使用一个带有页面样式的TabView
,非常非常容易。我喜欢它。
struct OnBoarding: View {
var body: some View {
TabView {
Page(text:"Page 1")
Page(text:"Page 2")
}
.tabViewStyle(.page(indexDisplayMode: .always))
.ignoresSafeArea()
}
}
针对目标iOS 14及以上版本的应用程序,建议考虑@pawello2222提出的答案。我已在两个应用程序中尝试过它,它非常有效,而且代码很少。
我将所提出的概念封装在一个结构体中,可以向其提供视图以及项目列表和视图构建器。它可以在此处找到:这里。代码如下:
@available(iOS 14.0, *)
public struct MultiPageView: View {
public init<PageType: View>(
pages: [PageType],
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
currentPageIndex: Binding<Int>) {
self.pages = pages.map { AnyView($0) }
self.indexDisplayMode = indexDisplayMode
self.currentPageIndex = currentPageIndex
}
public init<Model, ViewType: View>(
items: [Model],
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
currentPageIndex: Binding<Int>,
pageBuilder: (Model) -> ViewType) {
self.pages = items.map { AnyView(pageBuilder($0)) }
self.indexDisplayMode = indexDisplayMode
self.currentPageIndex = currentPageIndex
}
private let pages: [AnyView]
private let indexDisplayMode: PageTabViewStyle.IndexDisplayMode
private var currentPageIndex: Binding<Int>
public var body: some View {
TabView(selection: currentPageIndex) {
ForEach(Array(pages.enumerated()), id: \.offset) {
$0.element.tag($0.offset)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: indexDisplayMode))
}
}
.any()
只是一个自定义视图扩展,可将任何视图转换为 AnyView
。 - Daniel Saidi首先添加该包:https://github.com/xmartlabs/PagerTabStripView 然后
import SwiftUI
import PagerTabStripView
struct MyPagerView: View {
var body: some View {
PagerTabStripView() {
FirstView()
.frame(width: UIScreen.main.bounds.width)
.pagerTabItem {
TitleNavBarItem(title: "ACCOUNT", systomIcon: "character.bubble.fill")
}
ContentView()
.frame(width: UIScreen.main.bounds.width)
.pagerTabItem {
TitleNavBarItem(title: "PROFILE", systomIcon: "person.circle.fill")
}
NewsAPIView()
.frame(width: UIScreen.main.bounds.width)
.pagerTabItem {
TitleNavBarItem(title: "PASSWORD", systomIcon: "lock.fill")
}
}
.pagerTabStripViewStyle(.barButton(indicatorBarHeight: 4, indicatorBarColor: .black, tabItemSpacing: 0, tabItemHeight: 90))
}
}
struct TitleNavBarItem: View {
let title: String
let systomIcon: String
var body: some View {
VStack {
Image(systemName: systomIcon)
.foregroundColor( .white)
.font(.title)
Text( title)
.font(.system(size: 22))
.bold()
.foregroundColor(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.orange)
}
}
最简单的方法是通过iPages来实现。
import SwiftUI
import iPages
struct ContentView: View {
@State var currentPage = 0
var body: some View {
iPages(currentPage: $currentPage) {
Text("")
Color.pink
}
}
}
UICollectionView
。这里有一个很棒的教程:https://www.youtube.com/watch?v=a5yjOMLBfSc - Bartosz KunatUIPageViewController
提供了SwiftUI包装器,详见PaginatedViewsContent.swift。 - Yonat