我需要实现一个漫画的翻页效果,只在横屏下实现。
交互速览
- 页面效果
页面默认显示两张图片,两张图片组成一个新的 View ,这个 View 等比放大缩小,占满整个屏幕。而当第一张图片宽度大于高度时,页面只显示一张"宽图片"。
- 窄宽页面并存
当窄宽页面并存时,为了保证阅读的连续性,这里选择同时显示两张页面,适当透明化宽图片,当用户滑动时降低窄页面的透明度。
我想这样可以让阅读布局相对稳定,保证阅读行为的联系性。
- 换章节效果
换章节有两个情况。
第一种情况是前一个章节的"最后一页"刚好为一整张宽图片或者两张正常图片。那么继续翻页后,直接显示下一章节,并以 Toast 提示"Start Reading"+章节名。如果没有下个章节,则显示全黑界面+一行问题"无更多章节"
第二种情况,"最后一页"为半页,也就是只有一张正常图片。那么会使用一个长宽比为 2:3 的 View 来替代另外一半的位置进行显示,里面有一行文字"继续阅读下个章节";如果没有下一个章节,那么直接提示"无更多章节"。若有下个章节,继续翻页后,直接显示下一章节,不进行 Toast 提示。
开源代码参考
在 github 上搜索类似的代码,发现一个开源的项目 Aidoku,其中多漫画章节切换的部分和本需求很像。
Aidoku/Aidoku: Free and open source manga reader for iOS and iPadOS (github.com)
但是还存在一个问题,即该项目使用的是 ViewController 这种传统的方式实现的页面(也就是 SwiftUI 推出之前使用的页面布局编写方式),而且没有依赖 xib 布局文件,是纯代码实现的界面。所以在阅读理解它的源代码之前,需要先了解 ViewController 是如何和 SwiftUI 衔接的。
我整理了我需要了解的事情:
- ViewController 是如何实现编写一个页面的(不依靠 Storyboard)
- ViewController 编写的页面如何在 SwiftUI 中使用。
- 如何编写一个 UIPageViewController 视图,并在 SwiftUI 中使用,同时接受 SwiftUI 传递过来的 View 作为 Page 的内容。
- UIPageViewController 和 SwiftUI 中的 TabView ,SwiftUIX 中的 PaginationView 有什么区别
暂时就这些问题,可以先向 GPT 寻求答案了。
需要注意的是,OPENAI 官方是建议我们将内容分布地向 GPT 询问,"Give it time to 'think.'",所以在这里我分开向 GPT 询问。
1. ViewController 是如何实现编写一个页面的(不依靠 Storyboard)
GPT的回答:
在UIKit中,页面通常由UIViewController类来管理,而在SwiftUI中,这种任务由视图(Views)来执行。由于你对SwiftUI更为熟悉,我将简要比较两者的不同,并以代码示例展示如何在UIKit中纯代码实现一个页面。
在UIKit中创建一个页面
- 初始化ViewController: 使用UIViewController的子类,覆盖其生命周期方法。
- 设置视图元素: 使用UIKit的组件(如UILabel, UIButton等)。
- 布局: 使用AutoLayout或手动设置frame进行布局。
swift
import UIKit
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 设置背景色
view.backgroundColor = .white
// 创建一个标签
let myLabel = UILabel()
myLabel.text = "Hello, World!"
myLabel.translatesAutoresizingMaskIntoConstraints = false
// 添加标签到视图
view.addSubview(myLabel)
// 布局
NSLayoutConstraint.activate([
myLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
可以看到,如果不依赖 Storyboard 进行布局,VC 需要先将子元素使用view.addSubview(...)
添加到父视图中,然后使用"Constraint"定义元素的位置关系。
2. ViewController 编写的页面如何在 SwiftUI 中使用。
GPT的回答:
在SwiftUI中,如果你想使用由UIViewController
编写的页面,你可以使用UIViewControllerRepresentable
协议。这个协议允许SwiftUI与UIKit之间进行桥接。以下是使用UIViewControllerRepresentable
在SwiftUI中嵌入一个由UIViewController
创建的页面的基础步骤:
- 创建遵循
UIViewControllerRepresentable
的Swift结构体。 - 实现
makeUIViewController(context:)
方法 :这里创建并返回你的UIViewController
。 - 实现
updateUIViewController(_:context:)
方法 :这里更新UIViewController
的状态。
下面是一个简单的例子,展示了如何在SwiftUI应用中嵌入一个显示"Hello, World!"的UILabel
的UIViewController
。
swift
import SwiftUI
import UIKit
struct MyUIViewControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let viewController = MyViewController()
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
// 更新UIViewController(如果需要)
}
}
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let label = UILabel()
label.text = "Hello, World!"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
struct ContentView: View {
var body: some View {
MyUIViewControllerRepresentable()
}
}
在这个例子中,MyUIViewControllerRepresentable
结构体遵循了UIViewControllerRepresentable
协议,并实现了必要的方法来创建和更新MyViewController
。ContentView
则用该结构体来显示UIKit视图控制器的内容。
这样,你就可以在SwiftUI应用中无缝地使用由UIViewController
编写的页面了。
Representable 的意思是可以代表的,所以 UIViewControllerRepresentable 应该就是 VC 在 SwiftUI 中的代表。
按理来说应该可以在 UIViewControllerRepresentable 中传入一些参数,通过桥接传入到 VC 中作用。
3. 如何编写一个 UIPageViewController 视图,并在 SwiftUI 中使用,同时接受 SwiftUI 传递过来的 View 作为 Page 的内容。
GPT回答:
要在SwiftUI中使用UIPageViewController
,并且接受从SwiftUI传递过来的视图作为页面内容,你可以使用UIViewControllerRepresentable
来实现。这里是一个简单示例:
- 首先,创建一个SwiftUI View用作页面内容。
swift
struct PageContent: View {
var text: String
var body: some View {
Text(text)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
}
}
- 其次,创建一个遵循
UIViewControllerRepresentable
协议的结构体,并接受SwiftUI视图数组作为初始化参数。
swift
import SwiftUI
import UIKit
struct PageViewController<Content: View>: UIViewControllerRepresentable {
var controllers: [UIHostingController<Content>]
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal
)
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[0]], direction: .forward, animated: true, completion: nil
)
}
}
在这里,controllers
是一个UIHostingController<Content>
数组,用于包装传入的SwiftUI视图。
- 最后,使用
PageViewController
在你的SwiftUI视图中。
swift
struct ContentView: View {
var body: some View {
let view1 = PageContent(text: "Page 1")
let view2 = PageContent(text: "Page 2")
let view3 = PageContent(text: "Page 3")
let hostingControllers = [view1, view2, view3].map { UIHostingController(rootView: $0) }
return PageViewController(controllers: hostingControllers)
}
}
这样,你就可以在SwiftUI中使用一个基于UIPageViewController
的分页视图,并能从SwiftUI传递视图作为页面内容。注意,这个例子非常基础,只是为了展示基础的桥接过程。在实际应用中,你可能还需要添加更多的逻辑,比如分页指示器、滑动控制等。
这个回答错误其实很严重,也很明显,例如 UIPageViewController 没有实现 datasource 和 delegate 协议,使用 setViewControllers 只添加了一个 controller(不知道这样是否正确但实际效果表现为无法滑动页面)。
应该是逻辑的复杂程度已经探到了 GPT 的上限,多次生成后答案也没有改善。不过已经为我指明了大概得思路,即使用 UIHostingController 将 SwiftUI 中的 View 传入。
4. UIPageViewController 和 SwiftUI 中的 TabView ,SwiftUIX 中的 PaginationView 有什么区别
GPT回答:
这三者都可以用于创建多页面的用户界面,但它们之间有一些关键区别:
UIPageViewController
- 框架: 属于 UIKit,需要与 SwiftUI 进行桥接。
- 自定义性: 提供高度的自定义能力,如转场动画、数据源等。
- 复杂性: 相对复杂,需要实现多个协议方法。
- 性能: 通常用于需要高度动态和交互的分页视图,因为它支持懒加载。
SwiftUI 的 TabView
- 框架: 属于 SwiftUI,与其紧密集成。
- 自定义性: 相对较低,主要用于创建标签式的多页面界面。
- 复杂性: 使用简单,只需几行代码。
- 性能: 适用于简单的静态页面,不支持页面的懒加载。
SwiftUIX 的 PaginationView
- 框架: 是一个第三方库,专为 SwiftUI 设计。
- 自定义性: 中等,提供了比原生 TabView 更多的自定义选项。
- 复杂性: 相对简单,但需要安装额外的依赖。
- 性能 : 设计更接近
UIPageViewController
,可以处理更动态的内容。
总结:
- 如果你需要高度自定义的分页视图,并且不介意进行一些额外的设置,
UIPageViewController
是一个好选择。 - 如果你正在构建一个纯 SwiftUI 应用,并且需要简单快速地创建多页面界面,
TabView
会是更好的选项。 - 如果你想在SwiftUI中实现类似
UIPageViewController
的更多功能,而又不想回到UIKit,可以考虑使用PaginationView
。
根据你的具体需求和技术栈(特别是因为你已经有一定的SwiftUI经验),你可能会发现其中一种方法更适合你。
SwiftUI 的 TabView 我已经尝试过,首先是性能较差,更关键的是无法监听到翻页完成事件,而我需要在用户翻到最后一页时预先加载下一页的内容。
如果有想看使用 TabView 实现翻页效果的漫画 APP ,可以去查看这个开源项目:
翻页到下一章节时会有明显的跳动,虽然可以实现功能,但体验并不够完美。
SwiftUIX 的 PaginationView 没有可用监听接口,效果依然不理想的话。
所以还是要回到分析 Aidoku 项目上,自己写一个控件。
分析 Aidoku 项目
这个比较复杂,下一篇文章《漫画翻页效果编写(二)------分析 Aidoku 项目》继续