PhotoBrowser
Lightweight, highly customizable iOS photo & video browser in Swift. Supports zoom transitions, drag-to-dismiss, pinch-to-zoom, infinite looping, custom cells and overlays. No third-party dependencies. Works with both UIKit and SwiftUI. 图片/视频媒体浏览器。
Install / Use
/learn @JiongXing/PhotoBrowserREADME
JXPhotoBrowser
JXPhotoBrowser 是一个轻量级、可定制的 iOS 图片/视频浏览器,实现 iOS 系统相册的交互体验。支持缩放、拖拽关闭、自定义转场动画等特性,架构清晰,易于集成和扩展。同时支持 UIKit 和 SwiftUI 两种调用方式(SwiftUI 通过桥接层集成,详见 Demo-SwiftUI 示例工程)。
详细技术方案与实现说明请参阅 TECHNICAL_SOLUTION.md。
| 首页列表 | 图片浏览 | 下拉关闭 |
| :---: | :---: | :---: |
|
|
|
|
核心设计
- 零数据模型依赖:框架不定义任何数据模型,业务方完全使用自己的数据结构,通过 delegate 配置 Cell 内容。
- 图片加载完全开放:框架不内置图片加载逻辑,业务方可自由选择 Kingfisher、SDWebImage 或其他任意图片加载方案。
- 极简 Cell 协议:
JXPhotoBrowserCellProtocol仅包含browser和transitionImageView两个属性,将浏览器与具体 Cell 实现解耦,既可以直接使用内置的JXZoomImageCell,也可以实现完全自定义的 Cell。 - 协议驱动的数据与 UI 解耦:
JXPhotoBrowserDelegate只关心数量、Cell 与转场,不强制统一的数据模型。
功能特性
- 多模式浏览:支持水平(Horizontal)和垂直(Vertical)两个方向的滚动浏览。
- 无限循环:支持无限循环滚动(Looping),无缝切换首尾图片。
- 手势交互:
- 双击缩放:仿系统相册支持双击切换缩放模式。
- 捏合缩放:支持双指捏合随意缩放(1.0x - 3.0x)。
- 拖拽关闭:支持下滑手势(Pan)交互式关闭,伴随图片缩小和背景渐变效果。为避免与放大后的内容滚动冲突,仅在图片回到最小缩放时触发。
- 转场动画:
- Fade:经典的渐隐渐现效果。
- Zoom:类似微信/系统相册的缩放转场效果,无缝衔接列表与大图。
- None:无动画直接显示。
- 浏览体验优化:基于
UICollectionView复用机制,内存占用低,滑动流畅。 - 自定义 Cell 支持:内置图片
JXZoomImageCell,也支持通过协议与注册机制接入完全自定义的 Cell(如视频播放 Cell)。 - Overlay 组件机制:支持按需装载附加 UI 组件(如页码指示器、关闭按钮等),默认不装载任何组件,零开销。内置
JXPageIndicatorOverlay页码指示器。
核心架构
- JXPhotoBrowserViewController:核心控制器,继承自
UIViewController。内部维护一个UICollectionView用于展示图片页面,负责处理全局配置(如滚动方向、循环模式)和手势交互(如下滑关闭)。 - JXZoomImageCell:可缩放图片展示单元,继承自
UICollectionViewCell并实现JXPhotoBrowserCellProtocol。内部使用UIScrollView实现缩放,负责单击、双击等交互。通过imageView属性供业务方设置图片。 - JXImageCell:轻量级图片展示 Cell,不支持缩放手势,适用于 Banner 等嵌入式场景。内置可选的加载指示器(默认不启用),支持样式定制。
- JXPhotoBrowserCellProtocol:极简 Cell 协议,仅需
browser(弱引用浏览器)和transitionImageView(转场视图)两个属性即可接入浏览器,另提供photoBrowserDismissInteractionDidChange可选方法响应下拉关闭交互,不强制依赖特定基类。 - JXPhotoBrowserDelegate:代理协议,负责提供总数、Cell 实例、生命周期回调(
willDisplay/didEndDisplaying)以及转场动画所需的缩略图视图等,不强制要求统一的数据模型。 - JXPhotoBrowserOverlay:附加视图组件协议,定义了
setup、reloadData、didChangedPageIndex三个方法,用于页码指示器、关闭按钮等附加 UI 的统一接入。 - JXPageIndicatorOverlay:内置页码指示器组件,基于
UIPageControl,支持自定义位置和样式,通过addOverlay按需装载。
依赖
- 框架本身依赖:
UIKit(核心),无任何第三方依赖。 - 图片加载:框架不内置图片加载逻辑,业务方可自由选择 Kingfisher、SDWebImage 或其他任意图片加载方案。
- 示例工程:
- Demo-UIKit:UIKit 示例,使用 CocoaPods 集成,依赖
Kingfisher加载图片,演示完整功能(图片浏览、视频播放、Banner 轮播等)。 - Demo-SwiftUI:SwiftUI 示例,使用 SPM 集成,演示如何通过桥接层在 SwiftUI 中使用 JXPhotoBrowser(媒体网格、设置面板、图片浏览)。
- Demo-Carthage:UIKit 示例,使用 Carthage 集成。首次使用需在
Demo-Carthage目录下执行carthage update --use-xcframeworks --platform iOS构建框架。
- Demo-UIKit:UIKit 示例,使用 CocoaPods 集成,依赖
隐私清单(Privacy Manifest)
本框架已包含 PrivacyInfo.xcprivacy 隐私清单文件,符合 Apple 自 2024 年春季起对第三方 SDK 的隐私清单要求。
JXPhotoBrowser 不追踪用户、不收集任何数据、不使用任何 Required Reason API,隐私清单中所有字段均为空声明。通过 CocoaPods、SPM 或 Carthage 集成时,隐私清单会自动包含在框架中,无需额外配置。
系统要求
- iOS 12.0+
- Swift 5.4+
安装
CocoaPods
在你的 Podfile 中添加:
pod 'JXPhotoBrowser', '~> 4.0.3'
注意:Xcode 15 起默认开启了 User Script Sandboxing(
ENABLE_USER_SCRIPT_SANDBOXING=YES),该沙盒机制会阻止 CocoaPods 的 Run Script 阶段(如[CP] Copy Pods Resources、[CP] Embed Pods Frameworks等)访问沙盒外的文件,导致编译失败。需要在编译 Target 的 Build Settings 中将ENABLE_USER_SCRIPT_SANDBOXING设置为NO:Target → Build Settings → Build Options → User Script Sandboxing → No
Swift Package Manager
在 Xcode 中:
- 选择 File > Add Package Dependencies...
- 输入仓库地址:
https://github.com/JiongXing/PhotoBrowser - 选择版本规则后点击 Add Package
或在 Package.swift 中添加依赖:
dependencies: [
.package(url: "https://github.com/JiongXing/PhotoBrowser", from: "4.0.3")
]
Carthage
在你的 Cartfile 中添加:
github "JiongXing/PhotoBrowser"
然后运行:
carthage update --use-xcframeworks --platform iOS
构建完成后,将 Carthage/Build/JXPhotoBrowser.xcframework 拖入 Xcode 工程的 Frameworks, Libraries, and Embedded Content 中,并设置为 Embed & Sign。
手动安装
将 Sources 目录下的所有文件拖入你的工程中。
快速开始
基础用法
import JXPhotoBrowser
// 1. 创建浏览器实例
let browser = JXPhotoBrowserViewController()
browser.delegate = self
browser.initialIndex = indexPath.item // 设置初始索引
// 2. 配置选项(可选)
browser.scrollDirection = .horizontal // 滚动方向
browser.transitionType = .zoom // 转场动画类型
browser.isLoopingEnabled = true // 是否开启无限循环
// 3. 展示
browser.present(from: self)
实现 Delegate
遵守 JXPhotoBrowserDelegate 协议,提供数据和转场支持:
import Kingfisher // 示例使用 Kingfisher,可替换为任意图片加载库
extension ViewController: JXPhotoBrowserDelegate {
// 1. 返回图片总数
func numberOfItems(in browser: JXPhotoBrowserViewController) -> Int {
return items.count
}
// 2. 提供用于展示的 Cell
func photoBrowser(_ browser: JXPhotoBrowserViewController, cellForItemAt index: Int, at indexPath: IndexPath) -> JXPhotoBrowserAnyCell {
let cell = browser.dequeueReusableCell(withReuseIdentifier: JXZoomImageCell.reuseIdentifier, for: indexPath) as! JXZoomImageCell
return cell
}
// 3. 当 Cell 将要显示时加载图片
func photoBrowser(_ browser: JXPhotoBrowserViewController, willDisplay cell: JXPhotoBrowserAnyCell, at index: Int) {
guard let photoCell = cell as? JXZoomImageCell else { return }
let item = items[index]
// 使用 Kingfisher 加载图片(可替换为 SDWebImage 或其他库)
let placeholder = ImageCache.default.retrieveImageInMemoryCache(forKey: item.thumbnailURL.absoluteString)
photoCell.imageView.kf.setImage(with: item.originalURL, placeholder: placeholder) { [weak photoCell] _ in
photoCell?.setNeedsLayout()
}
}
// 4. (可选) Cell 结束显示时清理资源(如取消加载、停止播放等)
func photoBrowser(_ browser: JXPhotoBrowserViewController, didEndDisplaying cell: JXPhotoBrowserAnyCell, at index: Int) {
// 可用于取消图片加载、停止视频播放等
}
// 5. (可选) 支持 Zoom 转场:提供列表中的缩略图视图
func photoBrowser(_ browser: JXPhotoBrowserViewController, thumbnailViewAt index: Int) -> UIView? {
let indexPath = IndexPath(item: index, section: 0)
guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return nil }
return cell.imageView
}
// 6. (可选) 控制缩略图显隐,避免 Zoom 转场时视觉重叠
func photoBrowser(_ browser: JXPhotoBrowserViewController, setThumbnailHidden hidden: Bool, at index: Int) {
let indexPath = IndexPath(item: index, section: 0)
if let cell = collectionView.cellForItem(at: indexPath) as? MyCell {
cell.imageView.isHidden = hidden
}
}
// 7. (可选) 自定义 Cell 尺寸,默认使用浏览器全屏尺寸
func photoBrowser(_ browser: JXPhotoBrowserViewController, sizeForItemAt index: Int) -> CGSize? {
return nil // 返回 nil 使用默认尺寸
}
}
在 SwiftUI 中使用
JXPhotoBrowser 是基于 UIKit 的框架,在 SwiftUI 项目中可通过桥接方式集成。Demo-SwiftUI 示例工程演示了完整的集成方案。
核心思路
- 网格和设置面板使用纯 SwiftUI 实现(
LazyVGrid、Picker、AsyncImage等) - 全屏图片浏览器通过桥接层调用
JXPhotoBrowserViewController - 创建一个 Presenter 类实现
JXPhotoBrowserDelegate,获取当前UIViewController后调用browser.present(from:)
桥接层示例
import JXPhotoBrowser
/// 封装 JXPhotoBrowserViewController 的创建、配置和呈现
final class PhotoBrowserPresenter: JXPhotoBrowserDelegate {
private let items: [MyMediaItem]
func present(initialIndex: Int) {
guard let viewController = topViewController() else { return }
let browser = JXPhotoBrowserViewController()
browser.delegate = self
browser.initialIndex = initialIndex
browser.transitionType = .fade
browser.addOverlay(JXPageIndicatorOverlay())
browser.present(from: viewController)
}
func numberOfItems(in browser: JXPhotoBrowserViewController) -> Int {
items.count
}
func photoBrowser(_ browser: JXPhotoBrowserViewController, cellForItemAt index: Int, at indexPath: IndexPath) -> JXPhotoBrowserAnyCell {
browser.dequeueReusableCell(withReuseIdentifier: JXZoomImageCell.reuseIdentifier, for: indexPath) as! JXZoomImageCell
}
func photoBrowser(_ browser: JXPhotoBrowserViewController, willDisplay cell: JXPhotoBrowserAnyCell, at index: Int) {
guard let photoCell = cell as? JXZoomImageCell else { return }
// 加载图片到 photoCell.imageView ...
}
}
在 SwiftUI View 中调用
struct ContentView: View {
// 持有 presenter(JXPhotoBrowserViewController.delegate 为 weak,需要外部强引用)
@State private var presenter: PhotoBrowserPresenter?
var body: some View {
LazyVGrid(columns: columns) {
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
AsyncImage(url: item.thumbnailURL)
.onTapGesture {
let p = PhotoBrowserPresenter(items: items)
presenter = p
p.present(initialIndex: index)
}
}
}
}
}
注意:
JXPhotoBrowserViewController的delegate是weak引用,必须在 SwiftUI 侧用@State持有 Presenter 实例,否则它会在创建后立即被释放。
关于 Zoom 转场
Demo-SwiftUI 示例工程未演示 Zoom 转场动画,默认使用 Fade 转场。
原因:Zoom 转场依赖 thumbnailViewAt delegate 方法返回列表中缩略图的 UIView 引用,框架通过该引用计算动画起止位置并构建临时动画视图。而 SwiftUI 的 AsyncImage 等原生视图无法直接提供底层 UIView 引用。
如需自行实现:可将缩略图从 AsyncImage 替换为 UIViewRepresentable 包裹的 UIImageView,从而获取真实的 UIView 引用,再通过 thumbnailViewAt 和 setThumbnailHidden 两个 delegate 方法提供给框架即可。具体的 Zoom 转场接
