草庐IT

支持SwiftUI!Swift版图片&视频浏览器-JFHeroBrowser上线啦

JerryFans 2023-03-28 原文

前言

20220719_112821.GIF

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

Usage

首先初始化配置

如上面所说,在Appdelegate didFinish处自行接入HeroNetworkImageProvider。实现func downloadImage(with imgUrl: String, complete: Complete<UIImage>?)

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<UIImage>?) {
        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<UIImage>?) {
        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()
}

然后定义你需要浏览的ViewModule

目前支持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混搭也是可以的。

另外支持参数:

  • pageControlType (默认pageControl或者数字1/5类似)
  • heroView (也就是你要放大缩放那个imageView,如果不填就不会有缩放的效果,就是一个alpha渐变)
  • heroBrowserDidLongPressHandle (长按回调,可以做些保存图片、分享等动作)
  • imageDidChangeHandle (切换图片后,上个页面的imageView也要切换,才可以dissmiss回到相应位置,如果不设置就是alpha效果)
  • enableBlurEffect 是否开启毛玻璃背景,默认开启,false就是黑色背景。
 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
                })
            ]
        }

浏览单个视频

效果:

20220719_112821.GIF

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
                })
            ]
        }

SwiftUI的支持

一开始的想法是通过官方的UIViewController转换成SwiftUI的写法,但实现中发现不少问题,特别是转场效果无从下手。如果要纯SwiftUI代码实现,看来只能使用SwiftUI布局的方式重写,期待之后有空可以做个尝试。但是实际上,HeroBrowser是通过modal的方式进场的,我们直接获取rootViewController直接跳转亦可,但是就是缺少缩放动画,使用了默认的alpha转场,代码如下。本demo也提交到github了,有需要可以查阅。

20220719_111436.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..<origins.count, id:\.self) { index in
                        ImageCell(index: index).frame(height: columns.count == 1 ? 300 : 150).onTapGesture {
                            var list: [HeroBrowserViewModule] = []
                            for i in 0..<origins.count {
                                list.append(HeroBrowserNetworkImageViewModule(thumbailImgUrl: thumbs[i], originImgUrl: origins[i]))
                            }
                            myAppRootVC?.hero.browserPhoto(viewModules: list, initIndex: index)
                        }
                    }
                }


附件地址

有关支持SwiftUI!Swift版图片&视频浏览器-JFHeroBrowser上线啦的更多相关文章

  1. ruby - 在 Ruby 中用键盘诅咒数组浏览 - 2

    我正在尝试在Ruby中制作一个cli应用程序,它接受一个给定的数组,然后将其显示为一个列表,我可以使用箭头键浏览它。我觉得我已经在Ruby中看到一个库已经这样做了,但我记不起它的名字了。我正在尝试对soundcloud2000中的代码进行逆向工程做类似的事情,但他的代码与SoundcloudAPI的使用紧密耦合。我知道cursesgem,我正在考虑更抽象的东西。广告有没有人见过可以做到这一点的库或一些概念证明的Ruby代码可以做到这一点? 最佳答案 我不知道这是否是您正在寻找的,但也许您可以使用我的想法。由于我没有关于您要完成的工作

  2. ruby-on-rails - 浏览 Ruby 源代码 - 2

    我的主要目标是能够完全理解我正在使用的库/gem。我尝试在Github上从头到尾阅读源代码,但这真的很难。我认为更有趣、更温和的踏脚石就是在使用时阅读每个库/gem方法的源代码。例如,我想知道RubyonRails中的redirect_to方法是如何工作的:如何查找redirect_to方法的源代码?我知道在pry中我可以执行类似show-methodmethod的操作,但我如何才能对Rails框架中的方法执行此操作?您对我如何更好地理解Gem及其API有什么建议吗?仅仅阅读源代码似乎真的很难,尤其是对于框架。谢谢! 最佳答案 Ru

  3. 动漫制作技巧如何制作动漫视频 - 2

    动漫制作技巧是很多新人想了解的问题,今天小编就来解答与大家分享一下动漫制作流程,为了帮助有兴趣的同学理解,大多数人会选择动漫培训机构,那么今天小编就带大家来看看动漫制作要掌握哪些技巧?一、动漫作品首先完成草图设计和原型制作。设计草图要有目的、有对象、有步骤、要形象、要简单、符合实际。设计图要一致性,以保证制作的顺利进行。二、原型制作是根据设计图纸和制作材料,可以是手绘也可以是3d软件创建。在此步骤中,要注意的问题是色彩和平面布局。三、动漫制作制作完成后,加工成型。完成不同的表现形式后,就要对设计稿进行加工处理,使加工的难易度降低,并得到一些基本准确的概念,以便于后续的大样、准确的尺寸制定。四、

  4. python ffmpeg 使用 pyav 转换 一组图像 到 视频 - 2

    2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p

  5. TimeSformer:抛弃CNN的Transformer视频理解框架 - 2

    Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图

  6. ruby - 强制浏览器下载文件而不是打开文件 - 2

    我要下载http://foobar.com/song.mp3作为song.mp3,而不是让Chrome在其native中打开它浏览器中的播放器。我怎样才能做到这一点? 最佳答案 您只需要确保发送这些header:Content-Disposition:attachment;filename=song.mp3;Content-Type:application/octet-streamContent-Transfer-Encoding:binarysend_file方法为您完成:get'/:file'do|file|file=File.

  7. ruby - 如何使用 readline 支持重新安装 ruby​​? - 2

    我已经按照https://github.com/wayneeseguin/rvm#installation上的说明通过RVM安装了Ruby.有关信息,我有所有文件(readline-5.2.tar.gz、readline-6.2.tar.gz、ruby-1.9.3-p327.tar.bz2、rubygems-1.8.24.tgz、wayneeseguin-rvm-stable.tgz和yaml-0.1.4.tar.gz)在~/.rvm/archives目录中,我不想在任何目录中重新下载它们方式。当我这样做时:sudo/usr/bin/apt-getinstallbuild-essent

  8. ruby - 404 未找到,但可以从网络浏览器正常访问 - 2

    我在这方面尝试了很多URL,在我遇到这个特定的之前,它们似乎都很好:require'rubygems'require'nokogiri'require'open-uri'doc=Nokogiri::HTML(open("http://www.moxyst.com/fashion/men-clothing/underwear.html"))putsdoc这是结果:/Users/macbookair/.rvm/rubies/ruby-2.0.0-p481/lib/ruby/2.0.0/open-uri.rb:353:in`open_http':404NotFound(OpenURI::HT

  9. ruby-on-rails - "undefined method ` stub_request '"访问 RSpec 支持文件中的方法时 - 2

    我的Ruby-on-Rails项目中有以下文件结构,用于规范:/spec/msd/serviceservice_spec.rb/support/my_modulerequests_stubs.rb我的request_stubs.rb有:moduleMyModule::RequestsStubsmodule_functiondeflist_clientsurl="dummysite.com/clients"stub_request(:get,url).to_return(status:200,body:"clientsbody")endend在我的service_spec.rb我有:re

  10. ruby - 如何在 watir 测试套件结束时关闭浏览器? - 2

    使用ruby​​的watir测试网络应用程序时,浏览器最后会保持打开状态。网上的一些建议是,要进行真正的单元测试,您应该在每次测试时(在拆卸调用中)打开和关闭浏览器,但这很慢而且毫无意义。或者他们做这样的事情:defself.suites=superdefs.afterClass#Closebrowserenddefs.run(*args)superafterClassendsend但这会导致摘要输出不再显示(诸如“100次测试、100次断言、0次失败、0次错误”之类的内容仍应显示)。我怎样才能让ruby​​或watir在我的测试结束时关闭浏览器? 最佳答案

随机推荐