Castie!

正态分布, 优劣伴生

北冥有鱼,其名为鲲(kūn)。鲲之大,不知其几千里也;化而为鸟,其名为鹏。鹏之背,不知其几千里也;怒而飞,其翼若垂天之云。是鸟也,海运则将徙于南冥。南冥者,天池也。


北海若曰:“井鼃不可以语于海者,拘于虚也;夏虫不可以语于冰者,笃于时也;曲士不可以语于道者,束于教也。今尔出于崖涘,观于大海,乃知尔丑,尔将可与语大理矣。

RxSwift 大神们都在看的响应式

上个系列的架构设计, 从MVC -> MVVM -> MVP -> Router -> Downgradable, 我们实现了通过分层将各个组件进行解耦并动态的升降级页面, 这一系列准备说说如何通过RxSwift进行响应式编程更好的和架构设计进行协同. 代码见:github

这系列所述案例是通过Ray家的RxSwift一书进行笔记, 在学习本篇之前, 请确保熟练掌握Swift3基础语法并具备中级iOS开发能力. 对于RxSwift的概念不是很清晰的同学可以参照本文代码中的playground事先熟悉, 不理解的请先查阅概要, 以便更好的了解本文.

首先我们需要通过cocapods进行引入RxSwift:

# Uncomment the next line to define a global platform for your project
platform :ios, '10.0'

target 'AlbumPuzzle' do
    # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
    use_frameworks!
    
    # Pods for AlbumPuzzle
    pod 'RxSwift',    '~> 3.2'
    pod 'RxCocoa',    '~> 3.2'
    
end

我们需要添加一些必要的控件, 避免代码过多, 我们就通过StoryBoard来进行View层的布局, 第一个页面中我们需要四个控件: 展示图片, 添加按钮, 清除按钮, 保存按钮. 第二个页面就是个简单的CollectoinViewController.

class ViewController: UIViewController {
    
    @IBOutlet weak var imagePreview: UIImageView!
    @IBOutlet weak var buttonClear: UIButton!
    @IBOutlet weak var buttonSave: UIButton!
    @IBOutlet weak var itemAdd: UIBarButtonItem!
    
    fileprivate let bag = DisposeBag()
    fileprivate let images = Variable<[UIImage]>([])
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupObservable()
    }
}

extension ViewController {
    
    fileprivate func setupObservable() {
        
        images.asObservable().subscribe(onNext: { [weak self] images in
            guard let preview = self?.imagePreview else { return }
            preview.image = UIImage.collage(images: images, size: preview.frame.size)
        }).addDisposableTo(bag)
        
        images.asObservable().subscribe(onNext: { [weak self] images in
            self?.updateUI(images: images)
        }).addDisposableTo(bag)
    }
    
    fileprivate func updateUI(images: [UIImage]) {
        buttonSave.isEnabled = images.count > 0 && images.count % 2 == 0
        buttonClear.isEnabled = images.count > 0
        itemAdd.isEnabled = images.count < 6
        title = images.count > 0 ? "\(images.count) photos" : "Collage"
    }
}


extension ViewController {
    
    @IBAction func actionAdd() {
        images.value.append(UIImage(named: "Castie!")!)
    }
    
    @IBAction func actionClear() {
        images.value = []
    }
    
    @IBAction func actionSave() {
        
    }
}

我们进入ViewController, 创建两个私有变量, bag, 是一个DisposeBag实例, images是一个Variable的泛型 对于DisposeBag和Variable不了解的同学请事先查阅playground中的内容 , 通过extension的语法 Swift的装饰模式 进行代码分区, 通过对images添加订阅监听images数组发生的变化, 并对按钮点击进行操作. 为了实现更好的效果, 需要引入UIImage+Collage分类.

这样我们就简单的实现了响应式编程, 当订阅者的值被改变的时候, 就会更新操作. 就和KVO的监听效果相似. 由于iOS10开始对于访问用户信息的机制有所更新, 本例中需要访问用户相册, 所以在info.plist进行如下配置: Privacy - Photo Library Usage Description, We need access to your device’s photo library to allow you to select photos for your collage.

进行配置完成, 我们就通过Photos系统框架来进行对系统相册中的图片进行获取, 当然你也可以使用UIImagePickerController, 预知如何使用请点击!

import UIKit
import Photos
import RxSwift

private let reuseIdentifier = "Cell"

class AlbumViewController: UICollectionViewController {
    
    let bag = DisposeBag()
    var selectedImages: Observable<UIImage> {
        return selectedImagesSubject.asObservable()
    }
    
    fileprivate let selectedImagesSubject = PublishSubject<UIImage>()
    fileprivate lazy var images = AlbumViewController.loadImages()
    fileprivate lazy var imageManager = PHCachingImageManager()
    fileprivate lazy var thumbnailSize: CGSize = {
        let cellSize = (self.collectionViewLayout as! UICollectionViewFlowLayout).itemSize
        return CGSize(width: cellSize.width * UIScreen.main.scale,
                      height: cellSize.height * UIScreen.main.scale)
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
    }
}

extension AlbumViewController {
    
    static func loadImages() -> PHFetchResult<PHAsset> {
        let allImagesOptions = PHFetchOptions()
        allImagesOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
        return PHAsset.fetchAssets(with: allImagesOptions)
    }
}

extension AlbumViewController {
    
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return images.count
    }
    
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let asset = images.object(at: indexPath.item)
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
        imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: { image, _ in
            cell.layer.contents = image?.cgImage
        })
        return cell
    }
    
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
        let asset = images.object(at: indexPath.item)

        if let cell = collectionView.cellForItem(at: indexPath) {
            cell.alpha = 0
            UIView.animate(withDuration: 0.5, animations: {
                cell.alpha = 1
            })
        }

        imageManager.requestImage(for: asset, targetSize: view.frame.size, contentMode: .aspectFill, options: nil, resultHandler: { [weak self] image, info in
            guard let image = image, let info = info else { return }
            
            if let isThumbnail = info[PHImageResultIsDegradedKey as NSString] as? Bool, !isThumbnail {
                self?.selectedImagesSubject.onNext(image)
            }
        })
    }
}

这里简要的说下Photos这个系统框架, Photos framework是iOS8苹果提供的新的图片框架,能直接获取图片和视频,包括iCloud上面的图库。使用这个框架可以获取assets来展示和回放,编辑图片或者视频内容,或者使用系统相册、时刻、和分享到iCloud的相册来进行相关操作.

我们这里用到的有PHCachingImageManager, PHFetchResult, PHFetchOptions, PHAsset

PHCachingImageManager是PHImageManager的子类,如果相册中有大量的图片,而你的需求是要快速的获取这些图片的缩略图和大图数据用于展示,这个时候可以用PHCachingImageManager来实现,它具有缓存机制可以快速获取一个图册的缩略图,或者在后台请求全尺寸图片以便于快速展示。

/**
 *  会多次调用'resultHandler',第一次调用,返回的图片清晰度较低,
 *  当高清晰度图片可以获取时,会再次调用,如果高质量的图片在缓存汇中,只调用一次'resultHandler'。
 *  这个方法默认是异步的,当从后台线程调用这个方法时,需要将options的'synchronous'设为YES.
 *
 *  @param asset         保存图片信息的asset
 *  @param targetSize    返回的图片尺寸
 *  @param contentMode   返回的图片显示模式
 *  @param options       image request option
 *  @param resultHandler 返回的内容
 *
 *  @return 请求标示,用于取消请求*/
    open func requestImage(for asset: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode, options: PHImageRequestOptions?, resultHandler: @escaping (UIImage?, [AnyHashable : Any]?) -> Swift.Void) -> PHImageRequestID

PHFetchResult是有序的photo实体对象的容器,包含通过给定的检索条件返回的asset,相册,一个相册类型中的所有相册列表(例如,smart album类型下的所有相册,它是有序的),在PHAsset,PHCollection,PHAssetcollection,PHCollectionList这几个类中都包含有相应的类方法包含对应信息的PHFetchRequest对象,例如:

    open class func fetchAssets(with options: PHFetchOptions?) -> PHFetchResult<PHAsset>

PHFetchOptions: 当你使用没方法获取PHAsset(图片和视频),PHCollection(相册类型),PHAssetCollection(相册)的实体(相当于包含多个数据的模型)使用PHFetchOptions对象指定操作,例如获取的实体的排列属性等。

这里的操作很简单, 就是通过访问特定选项的PHFetchOptions得到PHFetchResult中的PHAsset并使用PHCachingImageManager请求系统相册得到相应的图片并通过寄宿图的方式展示到Cell上.

响应式编程的关键我们在Cell点击的函数中的以下代码:

 if let isThumbnail = info[PHImageResultIsDegradedKey as NSString] as? Bool, !isThumbnail {
       self?.selectedImagesSubject.onNext(image)
}

通过发布对象PublishSubject的onNext方法将数据发送给订阅者, 回到ViewController, 我们对部分代码进行更新:

    @IBAction func actionAdd() {
//        images.value.append(UIImage(named: "Castie!")!)
        
        let albumViewController = storyboard?.instantiateViewController(withIdentifier: "AlbumViewController") as! AlbumViewController
        
        albumViewController.selectedImages.subscribe(
            onNext: { [weak self] newImage in
                guard let images = self?.images else { return }
                images.value.append(newImage)
            },
            onDisposed: {
                print("completed photo selection")
            }
        ).addDisposableTo(albumViewController.bag)
        
        navigationController?.pushViewController(albumViewController, animated: true)
    }

这样我们就通过RxSwift实现了页面之间的响应式交互, 接下来我们来实现图片的保存操作:

class AlbumWriter: NSObject {
    
    typealias Callback = (NSError?)->Void
    
    private var callback: Callback
    private init(callback: @escaping Callback) {
        self.callback = callback
    }
    
    func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
        callback(error)
    }
    
    static func save(_ image: UIImage) -> Observable<Void> {
        return Observable.create({ observer in
            let writer = AlbumWriter(callback: { error in
                if let error = error {
                    observer.onError(error)
                } else {
                    observer.onCompleted()
                }
            })
            UIImageWriteToSavedPhotosAlbum(image, writer, #selector(AlbumWriter.image(_:didFinishSavingWithError:contextInfo:)), nil)
            return Disposables.create()
        })
    }
}

封装一个图片储存的AlbumWriter类, 通过Create方法来创建观察者.

@IBAction func actionSave() {
        
        guard let image = imagePreview.image else { return }
        
        AlbumWriter.save(image).subscribe(
            
            onError: { [weak self] error in
            self?.showMessage("Error", description: error.localizedDescription)
            
            }, onCompleted: { [weak self] in
                self?.showMessage("Saved")
                self?.actionClear()
            }
            
        ).addDisposableTo(bag)
    }

最后在Save操作的时候进行订阅就大功告成了!!

演示效果:

About:

点击下方链接跳转!!

🌟 源码 请点这里🌟 »> 喜欢的朋友请点喜欢 »> 下载源码的同学请送下小星星 »> 有闲钱的壕们可以进行打赏 »> 小弟会尽快推出更好的文章和大家分享 »> 你的激励就是我的动力!!

最近的文章

RxSwift 函数式过滤运算符实操

接着上一部分, 我们已经完成了函数式编程的简单实践, 也同时接触了Photos框架带来的自定义的便利, 主要熟悉了Variable, Subject 作为 Observable 进行订阅 subscribe 并通过处置包DisposeBag 处理的具体案例, 这一节我们来进一步完善相片拼图. 代码见:github本节所需的一些关于订阅信息过滤的基本知识已经更新到github代码中的playground文件中, 没有接触过响应式编程的同学请和之前一样先行在playground中了解概要以...…

移动开发继续阅读
更早的文章

Hybird 搭建客户端实时降级架构

Router真是一个非常好的一种设计模式, 能够做到超低的耦合, 作为系列的最后一节, 我们来统筹三端, 实现热修复架构, 再来说下这个架构的定义, 就是当Native页面业务逻辑出现Bug时, 又不能马上打包上线通过审核的情况下, 通过后台配置将页面降级为H5的页面, 等待修复完成上线后再回到Native的设计思想.参考链接: Hybird 搭建零耦合架构从MVC开始 Hybird 搭建后端Koa.js并过度到MVVM Hybird 搭建前端Vue.js并升级至MVP Hyb...…

移动开发继续阅读