上个系列的架构设计, 从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:
🌟 源码 请点这里🌟 »> 喜欢的朋友请点喜欢 »> 下载源码的同学请送下小星星 »> 有闲钱的壕们可以进行打赏 »> 小弟会尽快推出更好的文章和大家分享 »> 你的激励就是我的动力!!