iOS 实现微信读书的仿真翻页

先看效果

仿真翻页效果:

普通翻页效果:

实现方案

iOS 中实现翻页效果比较简单,直接使用系统提供的 UIPageViewController 即可做到。

UIPageViewController 是 UIKit 中的分页控制器,它允许用户通过横向或纵向滑动手势在多个页面(ViewController)之间切换,主要配置的两个属性如下:

1)UIPageViewControllerTransitionStyle

  • .pageCurl:仿真翻页
  • .scroll:类似 UIScrollView 自然滑动

2)UIPageViewControllerNavigationOrientation

  • .horizontal:左右翻页
  • .vertical:上下翻页

以仿真翻页配置为例子:

Swift 复制代码
class BookReaderViewController: UIViewController {

    // 模拟书籍数据
    private let bookPages = [
        "第一章:Swift 的起源\n\nSwift 是一种由 Apple 开发的强大且直观的编程语言...",
        "第二章:UIKit 基础\n\nUIKit 提供了构建 iOS 应用程序所需的关键对象...",
        "第三章:动画艺术\n\n核心动画 (Core Animation) 是 iOS 界面流畅的关键...",
        "第四章:高级翻页\n\nUIPageViewController 是实现仿真翻页的神器...",
        "终章:未来展望\n\n随着 SwiftUI 的普及,声明式 UI 正在改变世界..."
    ]

    private var pageViewController: UIPageViewController!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupPageViewController()
    }

    private func setupPageViewController() {
        // 关键设置:transitionStyle = .pageCurl (仿真翻页效果)
        // navigationOrientation = .horizontal (水平翻页)
        pageViewController = UIPageViewController(transitionStyle: .pageCurl,
                                                  navigationOrientation: .horizontal,
                                                  options: nil)

        pageViewController.dataSource = self
        pageViewController.delegate = self

        // 设置初始页面
        if let firstPage = getViewController(at: 0) {
            pageViewController.setViewControllers([firstPage], direction: .forward, animated: false, completion: nil)
        }

        // 将 PageVC 添加到当前 VC
        addChild(pageViewController)
        view.addSubview(pageViewController.view)
        pageViewController.view.frame = view.bounds
        pageViewController.didMove(toParent: self)

        // 解决仿真翻页背面颜色问题 (让背面也是纸张色,而不是默认的半透明或白色)
        // 注意:这是一个比较 Hack 的方法,更完美的做法是自定义背面的 Layer
        pageViewController.view.backgroundColor = UIColor(red: 248/255, green: 241/255, blue: 227/255, alpha: 1.0)
    }

    // 辅助方法:根据索引获取 VC
    private func getViewController(at index: Int) -> BookPageViewController? {
        guard index >= 0 && index < bookPages.count else { return nil }
        return BookPageViewController(index: index, totalPage: bookPages.count, content: bookPages[index])
    }
}

// MARK: - 3. DataSource 实现 (核心逻辑)
extension BookReaderViewController: UIPageViewControllerDataSource {

    // 获取"上一页"
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard let currentVC = viewController as? BookPageViewController else { return nil }
        let previousIndex = currentVC.pageIndex - 1
        return getViewController(at: previousIndex)
    }

    // 获取"下一页"
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard let currentVC = viewController as? BookPageViewController else { return nil }
        let nextIndex = currentVC.pageIndex + 1
        return getViewController(at: nextIndex)
    }
}

// MARK: - 4. Delegate (可选,用于处理翻页后的状态)
extension BookReaderViewController: UIPageViewControllerDelegate {
    // 这里可以处理 spineLocation,例如横屏时显示双页
    func pageViewController(_ pageViewController: UIPageViewController, spineLocationFor orientation: UIInterfaceOrientation) -> UIPageViewController.SpineLocation {
        // 手机竖屏通常是单页 (.min)
        return .min
    }
}

如上不到百行的代码即可实现仿真翻页效果,手势在 UIPageViewController 中会自动处理,外部不用感知。

和 UITableView 使用类似,需要通过 UIPageViewControllerDataSource 来提供上一页和下一页的数据源,通过 UIPageViewControllerDelegate 来感知翻页时机。

UIPageViewControllerDelegate 主要提供三个时机:

1)willTransitionTo:当用户开始滑动翻页的时候触发,系统已经准备好目标页,通过该回调来告诉你将要显示哪个页面(pendingViewControllers)

Swift 复制代码
/// pendingViewControllers: 将要显示的页面
func pageViewController(_ pageViewController: UIPageViewController,
                        willTransitionTo pendingViewControllers: [UIViewController])

2)didFinishAnimating:当用户的翻页动画结束时回调

Swift 复制代码
/// finished: 动画是否完成
/// previousViewControllers: 原来显示的ViewController
/// completed: 最终是否翻页成功;比如滑一半又拖回去,不会真正翻页
func pageViewController(_ pageViewController: UIPageViewController,
                        didFinishAnimating finished: Bool,
                        previousViewControllers: [UIViewController],
                        transitionCompleted completed: Bool)

3)spineLocationFor:控制仿真书本翻页时 "书脊" 的位置,只在 UIPageViewControllerTransitionStyle 为 pageCurl 时有效

Swift 复制代码
/// SpineLocation:
/// - none: 没书脊
/// - min: 书脊在左边(单页模式)
/// - mid: 书脊在中间(双页模式)
/// - max: 书脊在右边(单页模式)
func pageViewController(_ pageViewController: UIPageViewController,
                        spineLocationFor orientation: UIInterfaceOrientation)
-> UIPageViewController.SpineLocation

如果要配置普通翻页效果,只需要修改 UIPageViewController 的配置即可:

Swift 复制代码
// Options: 设置页面之间的间距 (微信读书一般有 10-20pt 的间距)
let options: [UIPageViewController.OptionsKey: Any] = [
    .interPageSpacing: 20
]

// 核心修改 1: transitionStyle 改为 .scroll
pageViewController = UIPageViewController(transitionStyle: .scroll,
                                          navigationOrientation: .horizontal,
                                          options: options)

另外翻页手势通常和系统的侧滑返回手势有冲突,可以手动禁用手势来解决;类似微信读书一样,在导航栏出现时才开启侧滑返回手势,否则禁用侧滑返回:

TypeScript 复制代码
private func updateGesture() {
    if isNaviBarHidden { // 导航栏隐藏:禁用侧滑,开启翻页手势
        navigationController?.interactivePopGestureRecognizer?.isEnabled = false
        for gesture in pageViewController.gestureRecognizers {
            gesture.isEnabled = true
        }
    } else { // 导航栏显示:开启侧滑,禁用翻页手势
        navigationController?.interactivePopGestureRecognizer?.isEnabled = true
        for gesture in pageViewController.gestureRecognizers {
            gesture.isEnabled = false
        }
    }
}
相关推荐
CocoaKier3 分钟前
如何安装旧版的MacOS系统?
macos·ios·mac
2501_9151063228 分钟前
iOS开发之Xcode常用调试技巧总结
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
sakiko_2 小时前
使用SPM(Xcode27), 并解决Package.swift不存在问题
swift
美狐美颜SDK开放平台2 小时前
直播APP平台搭建需要哪些技术?视频美颜SDK只是其中一环
android·ios·音视频·直播美颜sdk·视频美颜sdk
小π军4 小时前
安卓、iOS、鸿蒙三端的理解
android·ios·harmonyos
终端安全笔记5 小时前
声明式之后,怎么证明更新策略真的生效了
ios·iphone
茶底世界之下6 小时前
瀑布的白色需要压住吗?用 Awaking 检查高光调整的边界
ios
TheLittleBoy6 小时前
iOS 27支持的设备列表
macos·ios·cocoa·ios 27
智能相对论6 小时前
STEPX Neo:AI终端的“初代iPhone时刻”
人工智能·ios·iphone
茶底世界之下21 小时前
灯塔照片怎么裁竖幅?用 Awaking 留住塔顶,收紧两侧环境
ios