结构拆解

首要需求定位到漫画阅览的那一个 ViewController。得益于 Aidoku 良好的命名办法和代码结构,定位这个并不困难,这儿不多说。

找到了对应的 VC ReaderViewController 后,需求逐渐剖析代码的结构。项目作者将一切布局相关的代码都放到了 configure() 中,在这个办法中找到子 View 的增加办法,并定位到要害的漫画显现 VC。漫画显现 VC 有两种状况,ReaderPagedViewControllerReaderWebtoonViewController,这儿需求挑选一个进行检查,所以我将这两个 VC 的代码交给 AI 进行检查,并让它剖析其效果。

GPT 无法阅览这么多的上下文,所以我这儿挑选了另一个 LLM 模型,也便是月之暗面的 KimiChat。相较于 GPT 的 32K 数据,月之暗面支持 200K ,满足运用了。

依据 AI 给出的回答,ReaderPagedViewController 常见的翻页办法,ReaderWebtoonViewController 是网络漫画也便是条漫常用的上下滑动办法。这儿挑选前者进行持续研讨。

按照相同的思路,我持续深挖页面的结构,终究结论如下:

漫画翻页效果编写(二)——剖析 Aidoku 项目

虚线代表二选一。

在 ReaderPageVC 和 ReaderDoublePageVC 这一层会有一些差异,ReaderPageVC 能够直接组成上一层的界面,也能够经过 ReaderDoublePageVC 将两个 ReaderPageVC 包装后再组成上一层。

接下来需求做的事情,则是剖析每个 VC 详细的完结办法,方便后续调整为适合自己运用的办法。

View 和 VC 文件剖析

按照从下往上的顺序,逐个剖析代码完结。

1. ReaderPageView

从 configure() 来看,这个文件内含一个 Loading 和 一张图片,也便是用来加载详细页面的图片的。

需求先要搞清楚这个 View 是怎么加载图片,也便是看上层调用这个文件中的哪个办法。在ZoomableScrollView中没有找到,所以在更上一层的 ReaderPageVC 里找。终究定位到 setPage 这个办法,接下来就从这个办法出发研讨图片的加载逻辑。

...
func setPage(_ page: Page, sourceId: String? = nil) async -> Bool {
    if sourceId != nil {
        self.sourceId = sourceId
    }
    if let urlString = page.imageURL, let url = URL(string: urlString) {
        return await setPageImage(url: url, sourceId: self.sourceId)
    } else if let base64 = page.base64 {
        return await setPageImage(base64: base64, key: page.hashValue)
    } else {
        return false
    }
}
...

接下来运用流程图展现图片的加载逻辑。

漫画翻页效果编写(二)——剖析 Aidoku 项目

经过 setPage,传入一个 Page 目标,依据 page 中的 imageURL 或 base64 数据,调用 setPageImage 办法。

setPageImage 需求运用 ImageRequest 请求图片,可是先判别 imageTask 是否存在了使命。imageTask 是 View 里的一个特点,用来保存当时的使命。

假如存在

​ 假如是正在运转中,调用 completion

​ 假如是已经完结

​ 可是 imageView 为空,那么把使命再建议(猜想是再建议一次,由于使命是 complete 可是图没加载)

​ 假如不为空,那么直接返回成功

​ 假如是已取消,和 imageView 为空是一样的,从头建议。

假如不存在,需求初始化这个 request 然后赋值 imageTask 开端使命。以 url 类型的 page 目标为例,一个 imageTask 包括两部分: url 请求和 processors 预处理器,urlRequest 好像是从一个单例模式的 SourceManager 中经过传入的 url 快速初始化的,而 processors 则是依据设置中的开关决议是否启用,对图画进行紧缩和边缘多余部分裁剪。

总归 setPage 这个办法完结后,能够确保 ReaderPageView 中的 imageView 有内容或者有待运转的 imageTask 内容来加载图片。


我期望检查一下这个 View 的效果来验证我的剖析,可是又不期望独自开一个项目,所以我运用了 UIViewRepresentable 协议在 SwiftUI 中显现这个 View。

我先暂时注释掉其他的办法防止不必要的过错,并新增了一个 setFakePageImage() 办法用来初始化一张占位用的图片。

然后运用

struct ReaderPageViewWrapper: UIViewRepresentable{
    func makeUIView(context: Context) -> ReaderPageView {
        let readerPageView = ReaderPageView()
        readerPageView.setFakePageImage() // 初始化图片
        return readerPageView
    }
    func updateUIView(_ uiView: ReaderPageView, context: Context) {
        uiView.fixImageSize() // 依据需求调整图片尺寸
    }
}

这样就能够在 SwiftUI 中运用 ReaderPageViewWrapper 检查这个 View。后面也会用到这个办法来检查其他 View 和 ViewController 的效果。

2. ZoomableScrollView

从完结的效果上来看,承继了 UIKit 中的 UIScrollView ,完结了类似相册应用中一样的双击放大拖动检查细节的交互

Aidoku 的作者也是从 Github 获得的代码,所以这个 View 解耦性很好ZoomableScrollView的上层ReaderPageViewController也是经过直接调用ReaderPageView的 setPage 加载漫画图片,ZoomableScrollView仅仅独自的一个东西层。

3. ReaderInfoPageView

用于显现章节之间的过渡页面。

漫画翻页效果编写(二)——剖析 Aidoku 项目

源代码,是一个很纯粹的布局文件,经过 init 直接写布局,并经过一个 updateLabelText() 办法更新文字,不像 ReaderPageView 中还有加载图片的逻辑。

class ReaderInfoPageView: UIView {
    ...(省掉)...
    let noChapterLabel = UILabel()
    let stackView = UIStackView()
    let topChapterLabel = UILabel()
    let topChapterTitleLabel = UILabel()
    let bottomChapterLabel = UILabel()
    let bottomChapterTitleLabel = UILabel()
    init(type: ReaderInfoPageType, currentChapter: Chapter? = nil) {
        self.type = type
        self.currentChapter = currentChapter
        super.init(frame: .zero)
        backgroundColor = .systemBackground
        stackView.distribution = .equalSpacing
        stackView.axis = .vertical
        stackView.spacing = 14
        stackView.translatesAutoresizingMaskIntoConstraints = false
        ...(省掉)...
        updateLabelText()
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func updateLabelText() {
        guard let currentChapter = currentChapter else { return }
        if let previousChapter = previousChapter {
            topChapterLabel.text = NSLocalizedString("PREVIOUS_COLON", comment: "")
            ...(省掉)...
            noChapterLabel.isHidden = false
        }
    }
}

并且全部都运用了 UIKit 中的 UIStackView (能够了解成 VStack 和 HStack 等的前身),所以应该能够直接转为 SwiftUI 的布局。

4. ReaderPageViewController

页面由三部分构成,除了ReaderPageViewZoomableScrollView 是东西 View) 和ReaderInfoPageView这两个依据条件显现的 View ,还有默认隐藏的 reload button。经过外部传入的 type 特点操控 View 的显现条件。

这个 VC 起到了承上启下的效果。

首要,之前的三个文件都是独自的 View,需求一个 VC 包装成页面;

其次,操控这一页应该显现详细的漫画页仍是章节过渡页。

从逻辑上来说,能够将ReaderPageViewController看成是漫画书中的一个最小的单元,权且称之为”漫画页“。

5. ReaderDoublePageViewController

双页版别的ReaderPageVC,不多剖析。

6. ReaderPagedViewController

将”漫画页“们调集在一起,开端显现可翻页的”漫画书“。

其内部核心是一个UIPageViewController,然后经过 delegate 和 dataSource 协议向这个UIPageViewController供给数据。

至于UIPageViewController中的数据是怎么加载的,和ReaderPageView一样的剖析思路,看一下上层是怎么让ReaderPagedVC加载的数据。

ReaderViewController这层中,它调用了ReaderPagedVC中的 setChapter 办法,传入了 chapter 目标和 startPage 数值。

在 setChapter 办法中,经过运用 Task 办法,在后台履行 loadChapter 办法。

loadChapter 办法中会调用 VC 内 viewModel 的 loadPages 办法加载页面,同时经过代理向上层传递页面数量。

加载完结后调用 loadPageControllers 将 viewModel 中的页面都初始化为 viewController,也便是下一层的ReaderPageVC,这也是UIPageViewController显现的内容。

然后运用 move 办法设置 VC 内部UIPageViewController翻到对应的页面。

运用流程图表明如下:

漫画翻页效果编写(二)——剖析 Aidoku 项目

持续看 loadPageControllers 办法。

运用一个数组 pageViewControllers 保存初始化的 pageVC。

初始化了 previousChapter ,也便是上一章节。当然这儿需求经过代理的办法在上一层完结,由于 ReaderPagedViewController 一开端只接收了来自上层的一个章节信息,并不清楚章节前后的其他章节,这些信息需求从上一章节的一个 ChapterList 中获取。

接下来便是向 pageViewControllers 中填充数据了,按顺序来,应该先增加上一章节的最后一页。不过里边的内容还没有初始化。

然后是前一章节和这一章节的过渡页,过渡页的内容比较简单,所以直接初始化了一切信息。

然后便是章节一切的页面,运用一个 for 循环增加了对应数量的 pageVC,同样的也没有初始化。

压轴的是与下一章节之间的 Info 信息页,也直接初始化了。

大轴是下一章节的第一张图,作为预览信息,同样的没有初始化。

到这一步位置,一切的 PageVC 都初始化完结了,能够说壳子已经有了,还差往里边填内容。

填内容的行为,则发生在 UIPageViewController 的 delegate 中,在 didFinishAnimating 和 willTransitionTo ,也便是翻页动画完毕后,和开端滑动到另一页时会调用。这一部分的内容需求独自开一个部分讲,这儿咱们先将当时的逻辑进行一下整理。

先确保能了解上述的逻辑,一开端由上层调用 setChapter 激活,然后初度初始化界面。

还有其他状况下,也会调用 setChapter ,翻页到前一章节/后一章节,或者是上层直接修改当时阅览的章节,就会更改 ReaderPagedViewController 中的 chapter ,此刻需求从头调用 setChapter 来加载内容。

经过切换章节履行的代码和上层调用 setChapter 的基本差不多。不过在 loadPageControllers 中增加了一些代码,用来复用之前初始化的一些页面。

var firstPageController: ReaderPageViewController?
var lastPageController: ReaderPageViewController?
var nextChapterPreviewController: ReaderPageViewController?
var previousChapterPreviewController: ReaderPageViewController?
if chapter == previousChapter {
    lastPageController = pageViewControllers.first
    nextChapterPreviewController = pageViewControllers[2]
} else if chapter == nextChapter {
    firstPageController = pageViewControllers.last
    previousChapterPreviewController = pageViewControllers[pageViewControllers.count - 3]
}

假如新章节是前一章节,那么之前的 firstPC 会作为新的 nextCPC,preCPC 会作为新的 lastPC;假如新章节是后一章节,那么之前的 lastPC 会作为新的 preCPC ,nextCPC 会作为新的 firstPC。

漫画翻页效果编写(二)——剖析 Aidoku 项目

经过这种办法,在初始化前一章节和后一章节时,防止了不必要内容的二次加载,节省了资源。

ReaderPagedViewController 中的主要逻辑便是这些,接下来就要剖析 Delegate 和 DataSource 是怎么操控页面的内容显现了。

Delegate 和 DataSource

Delegate

Delegate 监听了在翻页动画开端前和动画加载完结后这两个事情,并履行了相应的代码。

动画开端前调用的办法是:

func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController])

它会对 pendingViewControllers ,也便是翻页后显现的 PageVC ,调用 setPage 加载内容。

动画加载后调用的办法是:

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool)

细分后有三种状况,

第一种是惯例翻页,翻页动画完结后会加载当时页面前一张到后 x 张的图片,x 即期望的漫画预加载数量,在设置中能够设置;

第二种是翻到了过渡页,此刻判别是否有对应的前/后一章节,假如有的话,调用 ViewModel 的预加载行为,并对前/后一章节的预览 PageVC ,也便是 preCPC/nextCPC,调用 setPage 加载内容;

第三种是翻到了preCPC/nextCPC,预览页,此刻调用 loadPreviousChapter 或 loadNextChapter 办法。这两个办法会将当时的数据源直接替换成新章节的数据源,并修改页码。

这三种状况其实说白了便是当翻页到第二页或倒数第二页时预加载接近章节的数据,并在翻到第一页或倒数第一页时正式替换数据。这样做来确保能够在页面之间无缝切换。

我将第一个办法,也便是 willTransitionTo 注释掉,发现也能正常阅览漫画,好像说明只靠第二个办法也能正常加载漫画,可能第一个办法是双保险。

DataSource

DataSource 中,依据 pageViewControllers 数组中的 VC 决议每一次翻页的内容。

首要需求供给数据源,需求将本身的 datasource 设置为一个契合 UIPageViewControllerDataSource 协议的类,这个类中需求完结两个办法:

func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?

PageView 中每一页都是由一个 ViewController 组成的,而上述两个办法的效果便是依据当时的 ViewController 推测出前/后一个 ViewController。

办法内部完结了两件事,首要是依据设置的阅览方向决议前后的页面;其次是依据是否是双页阅览决议页面是 ReaderPageVC 仍是 ReaderDoubleVC。

后记

Aidoku 的整体结构很明晰,这让代码阅览起来没有什么太大压力,仅仅量比较大。

结构拆解相对来说是最要害的,了解了结构之后才可能更明晰的对代码中数据的流转有一个直观的认知。而 AI 在决议剖析的方向上起到了很大帮助。如在ReaderViewController中,测验决议下一步应该剖析ReaderPagedViewController仍是ReaderWebtoonViewController时,运用 AI 快速协助阅览并了解代码,能够防止在不必要的当地浪费时间。

这儿强烈推荐月之暗面的 LLM ,能够在 KimiChat 试用。尽管逻辑才能还比不上 GPT-4(片面感触强于 3.5),但上下文长度上,市面上的大语言模型无出其右。

而在详细的代码逻辑中,也获得了不少的有用代码部分:可缩放的东西 View、单双页灵活切换逻辑、前后章节预加载、已加载资源复用,都是后续能够用到的。

接下来要做的,便是测验在 SwiftUI 中,复刻这个漫画阅览控件了。

我测验过直接运用 UIViewControllerRepresentable 调用 ReaderViewController, 不过 VC 的逻辑层和体现层耦合的很强,很难只把几个 VC 复制到自己的项目中。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。