☰
PokedexGo 源码解析:用 UISplitViewController 打造 iPhone/iPad 双平台的宝可梦图鉴应用
2026/10/14 2:45:30 网站建设 项目流程
  • 示例工程

【免费下载链接】Swift-30-Projects

30 mini Swift Apps for self-study

项目地址:https://gitcode.com/gh_mirrors/sw/Swift-30-Projects
点击查看免费下载

导读

PokedexGo 是 Swift-30-Projects 仓库中的第 7 个实战项目(位于Project 07 - PokedexGo/),核心是一套展示宝可梦列表与详情信息的 iOS 应用,其技术主线是通过 UISplitViewController 让同一套代码同时适配 iPhone 与 iPad 两种平台。阅读本文后,你将掌握 UISplitViewController 的 Storyboard 搭建、Master-Detail 委托通信、基于 RxSwift 的搜索框响应式处理,以及 NotificationCenter + KVO 驱动的异步图片加载与本地缓存等一整套可复用的移动端架构方案。

![PokedexGo 应用运行界面,展示了左侧宝可梦列表与右侧详情信息的分栏布局](https://raw.gitcode.com/gh_mirrors/sw/Swift-30-Projects/raw/70fa254298352558220fc250c268b1b9e5128321/Project 07 - PokedexGo/PokedexGo.gif?utm_source=gitcode_repo_files)

项目定位:一个 UISplitViewController 的完整演示

README(Project 07 - PokedexGo/README.md)对项目的定位非常明确:PokedexGo 是一个用 Swift 编写的 iOS 应用,用来展示宝可梦及其详细信息,本质上是演示如何用 UISplitViewController 在 iPhone 和 iPad 两个平台上实现应用。

值得说明的是,这个仓库中的 PokedexGo 并不只是一个"能跑"的 Demo,它还围绕主表-详情架构补齐了多个工程化细节:

  • 通过Master-Detail 委托协议解耦列表页与详情页;
  • 引入RxSwift / RxCocoa做搜索框的响应式输入处理(见 Podfile);
  • 基于NotificationCenter + KVO + 单例实现列表图片的异步加载、占位图与本地磁盘缓存。

整套代码分布在Project 07 - PokedexGo/PokedexGo/目录下,配合Main.storyboard完成界面装配,下面逐层拆解。

第一步:在 Storyboard 中搭建 UISplitViewController

UISplitViewController的容器结构直接定义在 Main.storyboard 中,它是整个应用的根视图控制器(initialViewController="tr7-Ww-CTY")。从 Storyboard 的场景关系可以看出标准的两栏结构:

  • master 栏:UISplitViewController→UINavigationController→MasterViewController(UITableViewController),即宝可梦列表;
  • detail 栏:UISplitViewController→ 另一个UINavigationController→DetailViewController,即宝可梦详情。

Storyboard 中通过两条relationshipsegue 声明主从关系:

<splitViewController id="tr7-Ww-CTY" sceneMemberID="viewController"> <connections> <segue destination="q4c-ha-VgO" kind="relationship" relationship="masterViewController" .../> <segue destination="SLy-nv-FBG" kind="relationship" relationship="detailViewController" .../> </connections> </splitViewController>

也就是说,布局本身完全不需要代码参与——UISplitViewController会根据设备类型与屏幕宽度自动决定展示形态:在 iPad 上左右分栏并排显示,在 iPhone 上则以"压入(push)"的方式在导航栈中呈现详情页。这正是"一份 Storyboard、双平台自适应"的核心机制。

AppDelegate 中的装配与首屏数据

虽然容器在 Storyboard 里搭好了,但两个控制器之间的数据连接需要在启动阶段完成。AppDelegate.swift 的application(_:didFinishLaunchingWithOptions:)做了四件事:

let splitViewController = self.window!.rootViewController as! UISplitViewController let leftNavController = splitViewController.viewControllers.first as! UINavigationController let masterViewController = leftNavController.topViewController as! MasterViewController let rightNavController = splitViewController.viewControllers.last as! UINavigationController let detailViewController = rightNavController.topViewController as! DetailViewController let firstPokemon = masterViewController.pokemons.first detailViewController.pokemon = firstPokemon masterViewController.delegate = detailViewController detailViewController.navigationItem.leftItemsSupplementBackButton = true detailViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem

这里的关键点有三个:

  1. 默认展示第一只宝可梦:把masterViewController.pokemons.first赋给详情页,保证启动时右侧(或压入后的详情页)不是空白;
  2. 建立委托链:masterViewController.delegate = detailViewController,让列表的选择事件能直通详情页;
  3. 补上系统分栏切换按钮:displayModeButtonItem是 UISplitViewController 自动生成的控制按钮,配合leftItemsSupplementBackButton = true,在 iPhone 横屏或 iPad 上可以手动收起/展开侧栏。

数据模型:Pokemon 与 PokeType

宝可梦数据模型定义在 Pokemon.swift 中,包含六个字段:

class Pokemon: NSObject { let name: String // 名称(中文,如"妙蛙种子") let id: Int // 图鉴编号 let detailInfo: String // 描述文字 let type: [PokeType] // 属性 let weak: [PokeType] // 弱点属性 let pokeImgUrl: String // 图片远程地址 }

属性通过PokeType枚举建模,覆盖了从normal、fire、water、electric、grass到dark、steel、fairy在内的 18 种宝可梦属性,这为后续扩展"属性克制"之类的玩法预留了类型安全的基础。

实际数据固化在 PokemonConstants.swift 中,是一个全局的pokemons数组,内置了编号 001~015 的 15 只宝可梦,例如:

Pokemon(name: "妙蛙种子", id: 1, detailInfo: "妙蛙种子经常在阳光下酣睡。它背上有个种子,通过吸收阳光渐渐长大。", type: [PokeType.grass, PokeType.poison], weak: [PokeType.fire, PokeType.flying, PokeType.ice, PokeType.psychic], pokeImgUrl: "http://assets.pokemon.com/assets/cms2/img/pokedex/full/001.png"),

注意图片地址是http://明文协议,因此 Info.plist 中设置了NSAppTransportSecurity→NSAllowsArbitraryLoads = true来放行非 HTTPS 请求,这是项目能够正常显示网络图片的前提条件之一。

列表侧:MasterViewController 与 RxSwift 搜索

MasterViewController(MasterViewController.swift)继承自UITableViewController,承担两个职责:展示宝可梦列表、对外转发选中事件。

数据流

列表数据来自LibraryAPI.sharedInstance.getPokemons(),同时维护一份filteredPokemons用于搜索过滤,viewDidLoad时两者等价。tableView的 cell 复用标识为"Cell",行高固定 140,每行渲染MasterTableViewCell(MasterTableViewCell.swift),cell 内部通过awakeFromNib(id:name:pokeImageUrl:)注入数据。

RxSwift 响应式搜索

搜索框的输入处理是本项目引入 Rx 框架的典型场景,代码片段如下:

searchBar .rx.text .throttle(0.5, scheduler: MainScheduler.instance) .subscribe( onNext: { [unowned self] query in if query?.characters.count == 0 { self.filteredPokemons = self.pokemons } else { self.filteredPokemons = self.pokemons.filter{ $0.name.hasPrefix(query!) } } self.tableView.reloadData() }) .addDisposableTo(disposeBag)

这段代码的工程含义包括:

  • rx.text把UISearchBar的文本变化转成事件流,无需手写UISearchBarDelegate;
  • throttle(0.5, scheduler: MainScheduler.instance)做 500ms 节流,用户连续输入时只在停顿后触发一次过滤,避免频繁刷新表格;
  • 过滤逻辑使用name.hasPrefix(query!)(前缀匹配),空输入则恢复完整列表;
  • disposeBag统一管理订阅生命周期,防止内存泄漏(这套代码属于 Swift 3 早期写法,addDisposableTo在后来的 RxSwift 版本中更名为disposed(by:),Podfile 锁定版本为RxSwift ~> 3.0.0、RxCocoa ~> 3.0.0)。

选中事件的转发

行选中时,控制器把模型通过委托协议抛给详情页:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let pokemon = self.filteredPokemons[(indexPath as NSIndexPath).row] delegate?.pokemonSelected(pokemon) if let detailViewController = self.delegate as? DetailViewController { splitViewController?.showDetailViewController(detailViewController.navigationController!, sender: nil) } }

这里showDetailViewController是 iOS 8 之后推荐的 API:在 iPad 上它更新右侧栏内容,在 iPhone 上则执行 push 导航,两个平台共用同一行代码。

桥接协议:PokemonSelectionDelegate

Master 与 Detail 之间的契约定义在 MasterViewController.swift 文件顶部,是一个典型的"一页持协议、另一页实现"模式:

protocol PokemonSelectionDelegate: class { func pokemonSelected(_ newPokemon: Pokemon) }

MasterViewController持有weak var delegate: PokemonSelectionDelegate?,而DetailViewController在 DetailViewController.swift 的扩展中实现了该协议:

extension DetailViewController: PokemonSelectionDelegate { func pokemonSelected(_ newPokemon: Pokemon) { pokemon = newPokemon } }

赋值动作会触发pokemon属性的didSet观察器,从而自动刷新 UI。这种"控制器依赖协议而非具体类型"的做法,让列表侧完全不知道详情页的内部实现,后续替换详情控制器时列表侧代码零改动。

详情侧:DetailViewController 的刷新机制

DetailViewController界面包含三个 IBOutlet:nameIDLabel(名称 + 编号)、pokeImageView(图片)、pokeInfoLabel(描述),对应 Storyboard 中的布局约束(图片为正方形、文本位于图片右侧多行展示)。

它的核心是pokemon属性与refreshUI():

var pokemon: Pokemon! { didSet (newPokemon) { self.refreshUI() } } func refreshUI() { nameIDLabel?.text = pokemon.name + (pokemon.id < 10 ? " #00\(pokemon.id)" : pokemon.id < 100 ? " #0\(pokemon.id)" : " #\(pokemon.id)") pokeImageView?.image = LibraryAPI.sharedInstance.downloadImg(pokemon.pokeImgUrl) pokeInfoLabel?.text = pokemon.detailInfo self.title = pokemon.name }

编号格式化为三位数(#001、#015),viewDidLoad与每次数据更新都会调用refreshUI(),保证启动首屏与切换条目时 UI 一致。这里直接调用了downloadImg同步下载,属于演示用途的简化实现,真正的异步化处理发生在列表侧的 MasterTableViewCell 流程中(见下节)。

图片的异步加载与磁盘缓存

列表 cell 的图片加载是 PokedexGo 里最有工程含量的一段,涉及三条协作链路:单例 LibraryAPI → NotificationCenter 广播 → KVO 监听收尾。

LibraryAPI:门面式单例

LibraryAPI.swift 使用static let sharedInstance提供全局唯一入口,内部持有一个PersistencyManager,并在初始化时注册downloadImageNotification通知观察者:

NotificationCenter.default.addObserver(self, selector:#selector(LibraryAPI.downloadImage(_:)), name: NSNotification.Name(rawValue: downloadImageNotification), object: nil)

MasterTableViewCell:发起下载请求

MasterTableViewCell.swift 的流程是:

  1. 先用UIImage(named: "default_img")显示占位图(对应Assets.xcassets/default_img.imageset/);
  2. 在图片中央叠加一个白色大号UIActivityIndicatorView并开始转圈;
  3. 通过addObserver(_:forKeyPath:options:context:)监听pokeImageView.image的 KVO 变化;
  4. 立即post一条携带pokeImageView与pokeImageUrl的通知,把下载任务抛给 LibraryAPI。
fileprivate func setupNotification(_ pokeImageUrl: String) { NotificationCenter.default.post(name: Notification.Name(rawValue: downloadImageNotification), object: self, userInfo: ["pokeImageView": pokeImageView, "pokeImageUrl": pokeImageUrl]) }

LibraryAPI 接收通知:缓存优先 + 后台下载

LibraryAPI.downloadImage(_:)收到通知后,先尝试从本地读取(persistencyManager.getImage),命中则直接设置图片;未命中则在全局后台队列执行下载,回到主队列更新 imageView,并交给PersistencyManager落盘:

DispatchQueue.global().async { let downloadedImage = self.downloadImg(pokeImageUrl as String) DispatchQueue.main.async { imageViewUnWrapped.image = downloadedImage self.persistencyManager.saveImage(downloadedImage, filename: URL(string: pokeImageUrl)!.lastPathComponent) } }

PersistencyManager:写入 Documents

PersistencyManager.swift 是纯粹的磁盘 I/O 层:saveImage用UIImagePNGRepresentation转成 PNG 数据,以 URL 末尾文件名写入NSHomeDirectory()/Documents/(原子写入);getImage按相同路径读回,读取失败返回nil,从而触发上述的后台下载兜底逻辑。

KVO 收尾:隐藏加载指示器

当 imageView 的image属性被赋值后,cell 的observeValue(forKeyPath:...)收到回调,调用indicator.stopAnimating()隐藏菊花。cell 在deinit中移除观察者,避免 KVO 泄漏。

这套"通知派发 + 后台下载 + 磁盘缓存 + KVO 结束动画"的流程,与LibraryAPI单例一起构成了一个轻量级、不依赖第三方图片库的异步图片加载方案,非常适合作为理解 iOS 线程调度与内存管理的教学案例。

依赖与运行环境

项目通过 CocoaPods 管理依赖,Podfile 内容如下:

platform :ios, '9.0' target 'PokedexGo' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks use_frameworks! pod 'RxSwift', '~> 3.0.0' pod 'RxCocoa', '~> 3.0.0' end

结合 Podfile.lock(RxSwift 3.0.1、RxCocoa 3.0.1、CocoaPods 1.2.0.beta.1)以及源码中的addDisposableTo、UIImagePNGRepresentation等 API 可以确认:当前仓库代码面向 iOS 9 + Swift 3 时代编写,若要运行,建议使用支持该时期的 Xcode 版本并先执行pod install,之后打开PokedexGo.xcworkspace构建运行。需要联网获取宝可梦图片,因此 Info.plist 已开启 ATS 任意加载权限。

小结:PokedexGo 提供的可复用架构

从工程复用的角度,PokedexGo 至少示范了四类可以迁移到其他 App 的模式:

  1. UISplitViewController 自适应布局:Storyboard 声明式搭建 +showDetailViewController,一份代码同时覆盖 iPhone / iPad;
  2. Master-Detail 委托协议:用PokemonSelectionDelegate解耦列表与详情,数据方向单一、替换成本低;
  3. RxSwift 响应式搜索:rx.text+throttle的组合,免去手写 delegate 与节流样板代码;
  4. 异步图片加载管线:NotificationCenter 派发 + 全局队列下载 + Documents 磁盘缓存 + KVO 驱动 UI 收尾,不依赖任何第三方图片框架。

如果你正在设计一个"左侧列表、右侧详情"的双平台应用,或者想系统理解 UISplitViewController 与响应式编程在真实项目中的协作方式,PokedexGo 是一份短小但五脏俱全的参考实现。

  • 示例工程

【免费下载链接】Swift-30-Projects

30 mini Swift Apps for self-study

项目地址:https://gitcode.com/gh_mirrors/sw/Swift-30-Projects
点击查看免费下载

相关推荐

上一篇:网盘直链下载助手完整指南:告别限速,实现高速下载自由
下一篇:怎样实现网盘高速下载:9大主流平台直链解析全攻略

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询