iOS开发有什么好用的图片浏览器?

年更博主终于推出新版本,JXPhotoBrowser v4.0 全面重构焕新!

JXPhotoBrowser 是一个轻量级、可定制的 iOS 图片/视频浏览器,实现 iOS 系统相册的交互体验。支持缩放、拖拽关闭、自定义转场动画等特性,架构清晰,易于集成和扩展。同时支持 UIKitSwiftUI 两种调用方式(SwiftUI 通过桥接层集成,详见 Demo-SwiftUI 示例工程)。

首页列表 图片浏览 下拉关闭

核心设计

  • 零数据模型依赖:框架不定义任何数据模型,业务方完全使用自己的数据结构,通过 delegate 配置 Cell 内容。
  • 图片加载完全开放:框架不内置图片加载逻辑,业务方可自由选择 Kingfisher、SDWebImage 或其他任意图片加载方案。
  • 极简 Cell 协议JXPhotoBrowserCellProtocol 仅包含 browsertransitionImageView 两个属性,将浏览器与具体 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 :附加视图组件协议,定义了 setupreloadDatadidChangedPageIndex 三个方法,用于页码指示器、关闭按钮等附加 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 构建框架。

隐私清单(Privacy Manifest)

本框架已包含 PrivacyInfo.xcprivacy 隐私清单文件,符合 Apple 自 2024 年春季起对第三方 SDK 的隐私清单要求。

JXPhotoBrowser 不追踪用户、不收集任何数据、不使用任何 Required Reason API,隐私清单中所有字段均为空声明。通过 CocoaPods、SPM 或 Carthage 集成时,隐私清单会自动包含在框架中,无需额外配置。

系统要求

  • iOS 12.0+
  • Swift 5.4+

安装

CocoaPods

在你的 Podfile 中添加:

ruby 复制代码
pod 'JXPhotoBrowser', '~> 4.0.1'

注意 :Xcode 15 起默认开启了 User Script SandboxingENABLE_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 中:

  1. 选择 File > Add Package Dependencies...
  2. 输入仓库地址:https://github.com/JiongXing/PhotoBrowser
  3. 选择版本规则后点击 Add Package

或在 Package.swift 中添加依赖:

swift 复制代码
dependencies: [
    .package(url: "https://github.com/JiongXing/PhotoBrowser", from: "4.0.1")
]

Carthage

在你的 Cartfile 中添加:

arduino 复制代码
github "JiongXing/PhotoBrowser"

然后运行:

bash 复制代码
carthage update --use-xcframeworks --platform iOS

构建完成后,将 Carthage/Build/JXPhotoBrowser.xcframework 拖入 Xcode 工程的 Frameworks, Libraries, and Embedded Content 中,并设置为 Embed & Sign

手动安装

Sources 目录下的所有文件拖入你的工程中。

快速开始

基础用法

swift 复制代码
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 协议,提供数据和转场支持:

swift 复制代码
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 示例工程演示了完整的集成方案。

核心思路

  1. 网格和设置面板 使用纯 SwiftUI 实现(LazyVGridPickerAsyncImage 等)
  2. 全屏图片浏览器 通过桥接层调用 JXPhotoBrowserViewController
  3. 创建一个 Presenter 类实现 JXPhotoBrowserDelegate,获取当前 UIViewController 后调用 browser.present(from:)

桥接层示例

swift 复制代码
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 中调用

swift 复制代码
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)
                    }
            }
        }
    }
}

注意JXPhotoBrowserViewControllerdelegateweak 引用,必须在 SwiftUI 侧用 @State 持有 Presenter 实例,否则它会在创建后立即被释放。

关于 Zoom 转场

Demo-SwiftUI 示例工程未演示 Zoom 转场动画,默认使用 Fade 转场。

原因 :Zoom 转场依赖 thumbnailViewAt delegate 方法返回列表中缩略图的 UIView 引用,框架通过该引用计算动画起止位置并构建临时动画视图。而 SwiftUI 的 AsyncImage 等原生视图无法直接提供底层 UIView 引用。

如需自行实现 :可将缩略图从 AsyncImage 替换为 UIViewRepresentable 包裹的 UIImageView,从而获取真实的 UIView 引用,再通过 thumbnailViewAtsetThumbnailHidden 两个 delegate 方法提供给框架即可。具体的 Zoom 转场接入方式可参考 Demo-UIKit 示例工程。

JXImageCell 加载指示器

JXImageCell 内置了一个 UIActivityIndicatorView 加载指示器,默认不启用。适用于 Banner 等嵌入式场景下展示图片加载状态。

启用加载指示器

swift 复制代码
let cell = browser.dequeueReusableCell(withReuseIdentifier: JXImageCell.reuseIdentifier, for: indexPath) as! JXImageCell

// 启用加载指示器
cell.isLoadingIndicatorEnabled = true
cell.startLoading()

// 图片加载完成后停止
cell.imageView.kf.setImage(with: imageURL) { [weak cell] _ in
    cell?.stopLoading()
}

自定义样式

通过 loadingIndicator 属性可直接定制指示器的外观:

swift 复制代码
cell.loadingIndicator.style = .large       // 指示器尺寸
cell.loadingIndicator.color = .systemBlue  // 指示器颜色

自定义 Cell

框架支持两种方式创建自定义 Cell:

方式一:继承 JXZoomImageCell(推荐)

继承 JXZoomImageCell 可自动获得缩放、转场、手势等功能。以 Demo 中的 VideoPlayerCell 为例,它继承 JXZoomImageCell 并添加了视频播放能力:

swift 复制代码
class VideoPlayerCell: JXZoomImageCell {
    static let videoReuseIdentifier = "VideoPlayerCell"
    
    private var player: AVPlayer?
    private var playerLayer: AVPlayerLayer?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        // 自定义初始化:添加 loading 指示器等
    }
    
    /// 配置视频资源
    func configure(videoURL: URL, coverImage: UIImage? = nil) {
        imageView.image = coverImage
        // 创建播放器并开始播放...
    }
    
    /// 重写单击手势:暂停视频或关闭浏览器
    override func handleSingleTap(_ gesture: UITapGestureRecognizer) {
        if isPlaying {
            pauseVideo()
        } else {
            browser?.dismissSelf()
        }
    }
}

方式二:实现协议(完全自定义)

直接实现 JXPhotoBrowserCellProtocol 协议,获得完全的自由度:

swift 复制代码
class StandaloneCell: UICollectionViewCell, JXPhotoBrowserCellProtocol {
    static let reuseIdentifier = "StandaloneCell"
    
    // 必须实现:弱引用浏览器(避免循环引用)
    weak var browser: JXPhotoBrowserViewController?
    
    // 可选实现:用于 Zoom 转场动画,返回 nil 则使用 Fade 动画
    var transitionImageView: UIImageView? { imageView }
    
    let imageView = UIImageView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        // 自定义初始化
    }
    
    // 可选实现:下拉关闭交互状态变化时调用
    // isInteracting 为 true 表示用户正在下拉(图片缩小跟随手指),false 表示交互结束(回弹恢复)
    // 适用于在拖拽关闭过程中暂停视频、隐藏附加 UI 等场景
    func photoBrowserDismissInteractionDidChange(isInteracting: Bool) {
        // 例如:下拉时暂停视频播放
    }
}

注册和使用自定义 Cell

swift 复制代码
let browser = JXPhotoBrowserViewController()

// 注册自定义 Cell(必须在设置 delegate 之前)
browser.register(VideoPlayerCell.self, forReuseIdentifier: VideoPlayerCell.videoReuseIdentifier)

browser.delegate = self
browser.present(from: self)

// 在 delegate 中使用
func photoBrowser(_ browser: JXPhotoBrowserViewController, cellForItemAt index: Int, at indexPath: IndexPath) -> JXPhotoBrowserAnyCell {
    let cell = browser.dequeueReusableCell(withReuseIdentifier: VideoPlayerCell.videoReuseIdentifier, for: indexPath) as! VideoPlayerCell
    cell.configure(videoURL: url, coverImage: thumbnail)
    return cell
}

Overlay 组件

框架提供了通用的 Overlay 组件机制,用于在浏览器上层叠加附加 UI(如页码指示器、关闭按钮、标题栏等)。默认不装载任何 Overlay,业务方按需装载

使用内置页码指示器

框架内置了 JXPageIndicatorOverlay(基于 UIPageControl),一行代码即可装载:

swift 复制代码
let browser = JXPhotoBrowserViewController()
browser.addOverlay(JXPageIndicatorOverlay())

支持自定义位置和样式:

swift 复制代码
let indicator = JXPageIndicatorOverlay()
indicator.position = .bottom(padding: 20)  // 位置:底部距离 20pt(也支持 .top)
indicator.hidesForSinglePage = true         // 仅一页时自动隐藏
indicator.pageControl.currentPageIndicatorTintColor = .white
indicator.pageControl.pageIndicatorTintColor = .lightGray
browser.addOverlay(indicator)

自定义 Overlay

实现 JXPhotoBrowserOverlay 协议即可创建自定义组件:

swift 复制代码
class CloseButtonOverlay: UIView, JXPhotoBrowserOverlay {
    
    func setup(with browser: JXPhotoBrowserViewController) {
        // 在此完成布局(如添加约束)
    }
    
    func reloadData(numberOfItems: Int, pageIndex: Int) {
        // 数据或布局变化时更新
    }
    
    func didChangedPageIndex(_ index: Int) {
        // 页码变化时更新
    }
}

// 装载
browser.addOverlay(CloseButtonOverlay())

多个 Overlay 可同时装载,互不干扰:

swift 复制代码
browser.addOverlay(JXPageIndicatorOverlay())
browser.addOverlay(CloseButtonOverlay())

保存图片/视频到相册

框架本身不内置保存功能,业务方可自行实现。Demo 中演示了通过长按手势弹出 ActionSheet 保存媒体到系统相册的完整流程。

前提 :需要在 Info.plist 中配置 NSPhotoLibraryAddUsageDescription(写入相册权限描述)。

核心步骤

  1. 添加长按手势 :在自定义 Cell 中添加 UILongPressGestureRecognizer
  2. 弹出 ActionSheet :通过 browser 属性获取浏览器控制器来 present。
  3. 请求权限并保存 :使用 PHPhotoLibrary 请求权限,下载后写入相册。

示例:在自定义 Cell 中长按保存

以 Demo 中的 VideoPlayerCell 为例,继承 JXZoomImageCell 后添加长按保存能力:

swift 复制代码
import Photos

class VideoPlayerCell: JXZoomImageCell {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        // 添加长按手势
        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
        scrollView.addGestureRecognizer(longPress)
    }
    
    @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
        guard gesture.state == .began else { return }
        
        let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
        alert.addAction(UIAlertAction(title: "保存视频", style: .default) { [weak self] _ in
            self?.saveVideoToAlbum()
        })
        alert.addAction(UIAlertAction(title: "取消", style: .cancel))
        
        // 通过 browser 属性获取浏览器控制器来 present
        browser?.present(alert, animated: true)
    }
    
    private func saveVideoToAlbum() {
        guard let url = videoURL else { return }
        
        // 1. 请求相册写入权限
        PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
            guard status == .authorized || status == .limited else { return }
            
            // 2. 下载视频(远程 URL 需先下载到本地)
            URLSession.shared.downloadTask(with: url) { tempURL, _, _ in
                guard let tempURL else { return }
                
                // 3. 写入相册
                PHPhotoLibrary.shared().performChanges({
                    PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: tempURL)
                }) { success, error in
                    // 处理结果...
                }
            }.resume()
        }
    }
}

保存图片的流程类似,将下载部分替换为图片写入即可:

swift 复制代码
// 下载图片数据
URLSession.shared.dataTask(with: imageURL) { data, _, _ in
    guard let data, let image = UIImage(data: data) else { return }
    
    PHPhotoLibrary.shared().performChanges({
        PHAssetChangeRequest.creationRequestForAsset(from: image)
    }) { success, error in
        // 处理结果...
    }
}.resume()

常见问题 (FAQ)

Q: Zoom 转场动画时图片尺寸不对或有闪烁现象?

A : 这通常是因为打开浏览器时,目标 Cell 的 imageView 还没有设置图片,导致其 bounds 为 zero。

解决方案 :在 willDisplay 代理方法中,确保同步设置占位图。例如使用 Kingfisher 时:

swift 复制代码
func photoBrowser(_ browser: JXPhotoBrowserViewController, willDisplay cell: JXPhotoBrowserAnyCell, at index: Int) {
    guard let photoCell = cell as? JXZoomImageCell else { return }
    
    // 同步从缓存取出缩略图作为占位图
    let placeholder = ImageCache.default.retrieveImageInMemoryCache(forKey: thumbnailURL.absoluteString)
    photoCell.imageView.kf.setImage(with: imageURL, placeholder: placeholder) { [weak photoCell] _ in
        photoCell?.setNeedsLayout()
    }
}

这样可以确保转场动画开始时,Cell 已经有正确尺寸的图片,动画效果更加流畅。

项目开源地址

github.com/JiongXing/P...

相关推荐
mCell8 小时前
如何零成本搭建个人站点
前端·程序员·github
YJlio9 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
我怎么又饿了呀14 小时前
如何上传本地文件到github仓库中入门教学
github
Jingzhou14 小时前
基于 AKShare 金融数据做了一个轻量级的金融研究 Agent
后端·github
淘矿人17 小时前
[特殊字符] 别再手动写提示词了!Claude Skills 10分钟入门,效率暴涨200%,Token狂省78%
人工智能·vscode·python·pycharm·github·文心一言·ai编程
逛逛GitHub19 小时前
GitHub 上狂揽 1.3 万 Star!港大开源的轻量版 Clawdbot。
github
2501_9419820520 小时前
Go 开发实战:基于 RPA 接口的主动消息推送
ios·iphone
云雾J视界20 小时前
当Agent接管代码后:产品经理的终极价值,是定义AI无法感知的“恐惧”与“爽点”
github·openai·copilot·ai产品经理·碳基情绪·硅基概率
忆江南20 小时前
Swift 全面深入指南
ios
WebInfra21 小时前
模块联邦 2.0 稳定版发布:兼顾开发效率与极致性能
前端·javascript·github