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
        }
    }
}
相关推荐
斯班奇的好朋友阿法法1 小时前
鸿蒙 vs iOS vs 微信小程序:开发平台全面对比
ios·微信小程序·harmonyos
@大迁世界15 小时前
14个你现在必须关闭的 iOS 26 设置,不然手机很快被它榨干
macos·ios·智能手机·objective-c·cocoa
四眼蒙面侠1 天前
深入 SwiftWork(第 0 篇):用 SwiftUI 构建一个 Agent 可视化工作台
swift·openagentsdk
YJlio1 天前
10.2.8 以其他账户运行服务(Running services in alternate accounts):为什么“把服务切到某个用户账号下运行”,本质上是在改变服务的整个安全上下文?
python·安全·ios·机器人·django·iphone·7-zip
pop_xiaoli1 天前
【iOS】KVC与KVO
笔记·macos·ios·objective-c·cocoa
90后的晨仔1 天前
《swiftUI进阶 第10章:现代状态管理(iOS 17+)》
ios
sakiko_2 天前
UIKit学习笔记4-使用UITableView制作滚动视图
笔记·学习·ios·swift·uikit
小锋学长生活大爆炸2 天前
【开源软件】这次iPhone也是用上Claw了 | PhoneClaw
ios·开源软件·iphone·claw
SameX2 天前
独立开发一个把走过的路变成 km² 的 App,聊聊 25m 网格和后台 GPS 的坑
ios
XD7429716362 天前
科技早报晚报|2026年4月30日:Agent 安全壳、浏览器 iOS 测试台与可穿戴数据 API,今天更值得看的 3 个技术机会
科技·ios·开源项目·科技新闻·开发者工具