漫画翻页效果编写(一)—— GPT 整理思路

我需要实现一个漫画的翻页效果,只在横屏下实现。

交互速览

  1. 页面效果

页面默认显示两张图片,两张图片组成一个新的 View ,这个 View 等比放大缩小,占满整个屏幕。而当第一张图片宽度大于高度时,页面只显示一张"宽图片"。

  1. 窄宽页面并存

当窄宽页面并存时,为了保证阅读的连续性,这里选择同时显示两张页面,适当透明化宽图片,当用户滑动时降低窄页面的透明度。

我想这样可以让阅读布局相对稳定,保证阅读行为的联系性。

  1. 换章节效果

换章节有两个情况。

第一种情况是前一个章节的"最后一页"刚好为一整张宽图片或者两张正常图片。那么继续翻页后,直接显示下一章节,并以 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 衔接的。

我整理了我需要了解的事情:

  1. ViewController 是如何实现编写一个页面的(不依靠 Storyboard)
  2. ViewController 编写的页面如何在 SwiftUI 中使用。
  3. 如何编写一个 UIPageViewController 视图,并在 SwiftUI 中使用,同时接受 SwiftUI 传递过来的 View 作为 Page 的内容。
  4. 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中创建一个页面

  1. 初始化ViewController: 使用UIViewController的子类,覆盖其生命周期方法。
  2. 设置视图元素: 使用UIKit的组件(如UILabel, UIButton等)。
  3. 布局: 使用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创建的页面的基础步骤:

  1. 创建遵循UIViewControllerRepresentable的Swift结构体
  2. 实现makeUIViewController(context:)方法 :这里创建并返回你的UIViewController
  3. 实现updateUIViewController(_:context:)方法 :这里更新UIViewController的状态。

下面是一个简单的例子,展示了如何在SwiftUI应用中嵌入一个显示"Hello, World!"的UILabelUIViewController

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协议,并实现了必要的方法来创建和更新MyViewControllerContentView则用该结构体来显示UIKit视图控制器的内容。

这样,你就可以在SwiftUI应用中无缝地使用由UIViewController编写的页面了。


Representable 的意思是可以代表的,所以 UIViewControllerRepresentable 应该就是 VC 在 SwiftUI 中的代表。

按理来说应该可以在 UIViewControllerRepresentable 中传入一些参数,通过桥接传入到 VC 中作用。

3. 如何编写一个 UIPageViewController 视图,并在 SwiftUI 中使用,同时接受 SwiftUI 传递过来的 View 作为 Page 的内容。

GPT回答:

要在SwiftUI中使用UIPageViewController,并且接受从SwiftUI传递过来的视图作为页面内容,你可以使用UIViewControllerRepresentable来实现。这里是一个简单示例:

  1. 首先,创建一个SwiftUI View用作页面内容
swift 复制代码
struct PageContent: View {
    var text: String
    var body: some View {
        Text(text)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.gray)
    }
}
  1. 其次,创建一个遵循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视图。

  1. 最后,使用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

  1. 框架: 属于 UIKit,需要与 SwiftUI 进行桥接。
  2. 自定义性: 提供高度的自定义能力,如转场动画、数据源等。
  3. 复杂性: 相对复杂,需要实现多个协议方法。
  4. 性能: 通常用于需要高度动态和交互的分页视图,因为它支持懒加载。

SwiftUI 的 TabView

  1. 框架: 属于 SwiftUI,与其紧密集成。
  2. 自定义性: 相对较低,主要用于创建标签式的多页面界面。
  3. 复杂性: 使用简单,只需几行代码。
  4. 性能: 适用于简单的静态页面,不支持页面的懒加载。

SwiftUIX 的 PaginationView

  1. 框架: 是一个第三方库,专为 SwiftUI 设计。
  2. 自定义性: 中等,提供了比原生 TabView 更多的自定义选项。
  3. 复杂性: 相对简单,但需要安装额外的依赖。
  4. 性能 : 设计更接近UIPageViewController,可以处理更动态的内容。

总结:

  • 如果你需要高度自定义的分页视图,并且不介意进行一些额外的设置,UIPageViewController是一个好选择。
  • 如果你正在构建一个纯 SwiftUI 应用,并且需要简单快速地创建多页面界面,TabView会是更好的选项。
  • 如果你想在SwiftUI中实现类似UIPageViewController的更多功能,而又不想回到UIKit,可以考虑使用PaginationView

根据你的具体需求和技术栈(特别是因为你已经有一定的SwiftUI经验),你可能会发现其中一种方法更适合你。


SwiftUI 的 TabView 我已经尝试过,首先是性能较差,更关键的是无法监听到翻页完成事件,而我需要在用户翻到最后一页时预先加载下一页的内容。

如果有想看使用 TabView 实现翻页效果的漫画 APP ,可以去查看这个开源项目:

oolxg/Hanami: Manga reading app for iOS/iPadOS written with SwiftUI and Composable Architecture (github.com)

翻页到下一章节时会有明显的跳动,虽然可以实现功能,但体验并不够完美。

SwiftUIX 的 PaginationView 没有可用监听接口,效果依然不理想的话。

所以还是要回到分析 Aidoku 项目上,自己写一个控件。

分析 Aidoku 项目

这个比较复杂,下一篇文章《漫画翻页效果编写(二)------分析 Aidoku 项目》继续

相关推荐
用户092 小时前
如何避免写垃圾代码:iOS开发篇
ios·swiftui·swift
HarderCoder16 小时前
iOS 知识积累第一弹:从 struct 到 APP 生命周期的全景复盘
ios
叽哥1 天前
Flutter Riverpod上手指南
android·flutter·ios
用户092 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan2 天前
iOS26适配指南之UIColor
ios·swift
权咚3 天前
阿权的开发经验小集
git·ios·xcode
用户093 天前
TipKit与CloudKit同步完全指南
ios·swift
法的空间3 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918413 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
00后程序员张3 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview