iOS下类似的图片浏览器不管是OC版本还是Swift版本目前已经开源了不少。但是作为一个六七年的老iOS开发者,以及自己曾经积累了的不少社交App经验,还是忍不住基于自己的想法以及目前项目中类似的组件重新撸了一个。毫无疑问,此次开源的JFHeroBrowser,首选语言是Swift(完全Swift不包含任何OC代码),偏向更Swifty的方式-面向协议处理数据模型,还有Swift进阶枚举用法,命名空间等,如果你想深入学习Swift,我相信本组件会让你有不同的体验,另外由于楼主同时也在开发Flutter,编码方式上也是更"响应式"。而且与大多数三方库内置ImageCache(大多是SDWebImage)不同,本组件,不包含内置的ImageCache,但是如果您集成了本项目作为图片浏览,网络图这块,您需要自行实现HeroNetworkImageProvider协议,可以使用Kingfisher或SDWebImage抑或是你项目中自行设计的图片缓存,完美解决组件耦合问题,具体使用参考下面用法。另外本组件支持多种资源格式,如本地图(UIImage),网络图(url),data(二进制),视频(url),甚至你自行实现ImageVM也可以接入你想要的资源。话不多说,我们来看具体使用方式。
cocoaPods:
pod 'JFHeroBrowser', '1.3.2'
github:
https://github.com/JerryFans/JFHeroBrowser
如上面所说,在Appdelegate didFinish处自行接入HeroNetworkImageProvider。实现func downloadImage(with imgUrl: String, complete: Complete?)
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
JFHeroBrowserGlobalConfig.default.networkImageProvider = HeroNetworkImageProvider.shared
// JFHeroBrowserGlobalConfig.default.networkImageProvider = SDWebImageNetworkImageProvider.shared
return true
}
Kingfisher参考
extension HeroNetworkImageProvider: NetworkImageProvider {
func downloadImage(with imgUrl: String, complete: Complete?) {
KingfisherManager.shared.retrieveImage(with: URL(string: imgUrl)!, options: nil) { receiveSize, totalSize in
guard totalSize > 0 else { return }
let progress:CGFloat = CGFloat(CGFloat(receiveSize) / CGFloat(totalSize))
complete?(.progress(progress))
} downloadTaskUpdated: { task in
} completionHandler: { result in
switch result {
case .success(let loadingImageResult):
complete?(.success(loadingImageResult.image))
break
case .failure(let error):
complete?(.failed(error))
break
}
}
}
}
class HeroNetworkImageProvider: NSObject {
@objc static let shared = HeroNetworkImageProvider()
}
SDWebImage参考
extension SDWebImageNetworkImageProvider: NetworkImageProvider {
func downloadImage(with imgUrl: String, complete: Complete?) {
SDWebImageManager.shared.loadImage(with: URL(string: imgUrl)) { receiveSize, totalSize, url in
guard totalSize > 0 else { return }
let progress:CGFloat = CGFloat(CGFloat(receiveSize) / CGFloat(totalSize))
complete?(.progress(progress))
} completed: { image, data, error, _, isfinished, url in
if let error = error {
complete?(.failed(error))
} else if let image = image {
complete?(.success(image))
} else {
complete?(.failed(nil))
}
}
}
}
class SDWebImageNetworkImageProvider: NSObject {
@objc static let shared = SDWebImageNetworkImageProvider()
}
目前支持HeroBrowserNetworkImageViewModule、HeroBrowserDataImageViewModule、HeroBrowserLocalImageViewModule、HeroBrowserVideoViewModule四种类型ViewModule。理论上还可以定义AssetImageViewModule(支持从相册浏览图片),在我另外一个未开源的相册组件里面使用了,所以ViewModule的扩展非常方便使用者去扩展各种各样的场景,而且单一场景,由于某些特定场景比较不场景,我这只提供几种常用的场景。
几种ViewModule代码示例
//视频
let vm1 = HeroBrowserVideoViewModule(thumbailImgUrl: "http://image.jerryfans.com/bf.jpg", fileUrlPath: path, provider: HeroNetworkImageProvider.shared, autoPlay: false)
list.append(vm1)
//本地图(UIImage)
list.append(HeroBrowserLocalImageViewModule(image: img))
//data图 (file Image支持转二进制,或者flutter的Uin8List)
list.append(HeroBrowserDataImageViewModule(data: imageSource[i]))
//网络图 也是最常用场景
list.append(HeroBrowserNetworkImageViewModule(thumbailImgUrl: thumbs[i], originImgUrl: origins[i]))
self是当前控制器,写了一个hero的命名空间,viewModules就是上面一个个定义的viewModule示例,支持视频或者不同图片VM混搭也是可以的。
另外支持参数:
self.hero.browserPhoto(viewModules: list, initIndex: indexPath.item) {
[
.pageControlType(.pageControl),
.heroView(cell.imageView),
.heroBrowserDidLongPressHandle({ [weak self] heroBrowser,vm in
self?.longPressHandle(vm: vm)
}),
.imageDidChangeHandle({ [weak self] imageIndex in
guard let self = self else { return nil }
guard let cell = self.collectionView.cellForItem(at: IndexPath(item: imageIndex, section: 0)) as? NetworkImageCollectionViewCell else { return nil }
let rect = cell.convert(cell.imageView.frame, to: self.view)
if self.view.frame.contains(rect) {
return cell.imageView
}
return nil
})
]
}
效果:
let vm = HeroBrowserVideoViewModule(thumbailImgUrl: "http://image.jerryfans.com/w_720_h_1280_d_41_89fd26217dc299a442363581deb75b90_iOS_0.jpg", videoUrl: "http://image.jerryfans.com/w_720_h_1280_d_41_2508b8aa06a2e30d2857f9bcbdfd1de0_iOS.mp4", provider: HeroNetworkImageProvider.shared, autoPlay: true)
self.hero.browserVideo(viewModule: vm)
lazy var list: [HeroBrowserViewModuleBaseProtocol] = {
var list: [HeroBrowserViewModuleBaseProtocol] = []
let vm = HeroBrowserVideoViewModule(thumbailImgUrl: "http://image.jerryfans.com/w_720_h_1280_d_41_89fd26217dc299a442363581deb75b90_iOS_0.jpg", videoUrl: "http://image.jerryfans.com/w_720_h_1280_d_41_2508b8aa06a2e30d2857f9bcbdfd1de0_iOS.mp4", provider: HeroNetworkImageProvider.shared, autoPlay: true)
list.append(vm)
list.append(HeroBrowserLocalImageViewModule(image: UIImage(named: "template-1")!))
if let path = Bundle.main.path(forResource: "bf.MOV", ofType: nil) {
let vm1 = HeroBrowserVideoViewModule(thumbailImgUrl: "http://image.jerryfans.com/bf.jpg", fileUrlPath: path, provider: HeroNetworkImageProvider.shared, autoPlay: false)
list.append(vm1)
}
return list
}()
self.hero.browserMultiSoures(viewModules: self.list, initIndex: 1) {
[
.enableBlurEffect(false),
.heroView(button.imageView),
.imageDidChangeHandle({ [weak self] imageIndex in
guard let self = self else { return nil }
guard let btn = self.view.viewWithTag(imageIndex) as? UIButton else { return nil }
return btn.imageView
})
]
}
一开始的想法是通过官方的UIViewController转换成SwiftUI的写法,但实现中发现不少问题,特别是转场效果无从下手。如果要纯SwiftUI代码实现,看来只能使用SwiftUI布局的方式重写,期待之后有空可以做个尝试。但是实际上,HeroBrowser是通过modal的方式进场的,我们直接获取rootViewController直接跳转亦可,但是就是缺少缩放动画,使用了默认的alpha转场,代码如下。本demo也提交到github了,有需要可以查阅。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qMG2KusP-1658281692470)(http://image.jerryfans.com/swiftui_example.gif)]
配置初始化代码
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
JFHeroBrowserGlobalConfig.default.networkImageProvider = HeroNetworkImageProvider.shared
return true
}
}
@main
struct SwiftUIExampleApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
//获取顶层vc
let keyWindow = UIApplication.shared.connectedScenes
.map({ $0 as? UIWindowScene })
.compactMap({ $0 })
.first?.windows.first
let myAppRootVC : UIViewController? = keyWindow?.rootViewController
//从一个图片 GridView 跳转浏览
LazyVGrid(columns: columns) {
ForEach(1..