美文网首页
16.11Todo IV - 进一步理解Subject的实际应用

16.11Todo IV - 进一步理解Subject的实际应用

作者: CDLOG | 来源:发表于2019-07-17 18:08 被阅读0次

在了解了常用过滤型operator的基本概念和用法之后,在这段视频里,我们给之前的ToDoDemo添加一个功能,以此进一步了解这些operators在App开发中的用法。大家可以在这里下载项目的初始模板,在这里下载项目的完成版本。

我们要做什么?

在开始之前,先来看下要完成的功能:

filter-in-practice
  • 允许用户给Todo添加一个图片备忘;
  • 允许用户从照片库中直接选择图片,并自动合成到一张图片中;

基本上,大的功能就这两部分,看起来不太复杂,但其中包含了诸多会用到过滤型operator的实现细节,我们将会一一看到。

对项目模板做了哪些修改?

在继续之前,先来看下基于上一次完成的版本,我们对模板进行了哪些修改。

对storyboard和view controller的修改

filter-in-practice

首先是storyboard,在TodoDetailViewController对应的UI上,底部添加了一个UIButton,稍后,我们会把合成之后的图片设置为这个按钮的背景图片,用于显示用户选择的结果。点击这个button,会跳转到图片选择UI,为此:

  • 我们给这个按钮添加了一个segue;
  • TodoDetailViewController中添加了两个属性:一个@IBOutlet表示按钮,一个UIImage表示合成后的图片;

TodoDetailViewController

class TodoDetailViewController: UITableViewController {
    // ...
    fileprivate var todoCollage: UIImage?
    @IBOutlet weak var memoCollageBtn: UIButton!
    // ...
}

  • TodoDetailViewController.viewDidLoad方法中,添加了初始化图片备忘按钮背景的代码:
override func viewDidLoad() {
    // ...

    if todoItem.pictureMemoFilename != "" {
        let url = getDocumentsDir().appendingPathComponent(
            todoItem.pictureMemoFilename)

        if let data = try? Data(contentsOf: url) {
            self.memoCollageBtn.setBackgroundImage(
                UIImage(data: data),
                for: .normal)

            self.memoCollageBtn.setTitle("", for: .normal)
        }
    }

    // ...
}

  • TodoDetailViewController中添加了以下方法,其中:resetMemoBtn / setMemoBtn用于重置和设置按钮的默认样式;savePictureMemos用于保存为用户合成的图片并返回图片的名称;setMemoSectionHeaderText用于在用户选择完图片后,提示对应的图片个数。稍后,我们会逐步实现这些方法;
extension TodoDetailViewController {
    fileprivate func resetMemoBtn() { // ... }
    fileprivate func setMemoBtn(bkImage: UIImage) { // ... }
    fileprivate func savePictureMemos() -> String { //... }
    func setMemoSectionHederText() { // ... }
}

PhotoCollectionViewController

另外,为了显示照片库中的所有图片,我们添加了一个PhotoCollectionViewController,其中实现了用UICollectionView显示照片,并通过一个蓝色的对勾显示选中图片的效果。

添加的helper方法

在项目的Helper group中,新添加了两个文件:

  • PhotoCell.swift,其中定义了PhotoCollectionViewController中使用的collection cell;
  • UIImage+Collage.swift,其中,为UIImage添加了一个extension,包含了用于缩放图片(scale)、合成用户选中的图片(collage)以及根据图片内容比较两个图片是否相等(isEqual (lhs: rhs:))的方法;

对model的修改

TodoItem中,新添加了一个属性pictureMemoFilename,表示每一个todo对应的图片名,默认是空字符串。并对TodoItem中对应的init以及序列化方法进行了修改:

class TodoItem: NSObject, NSCoding {
    // ...
    var pictureMemoFilename: String = ""
    // ...
}

了解了以上修改之后,我们就可以实现对应的功能了。第一个要完成的,就是合成用户选择的图片,并把它显示出来。

用Rx的方式实现图片的选取与合成

这个功能实现的起点,在PhotoCollectionViewController,我们需要通知TodoDetailViewController用户选择的每一张图片,进而在TodoDetailViewController里,完成图片的合成、显示以及保存。这个过程用一张图来表示,就是这样的:

filter-in-practice

PhotoCollectionViewController

首先,来完成PhotoCollectionViewController的部分,在它的定义里,添加下面的代码:

class PhotoCollectionViewController: UICollectionViewController {
    // ...

    fileprivate let selectedPhotosSubject = PublishSubject<UIImage>()
    var selectedPhotos: Observable<UIImage> {
        return selectedPhotosSubject.asObservable()
    }
    let bag = DisposeBag()

    // ...
}

同样,我们用了之前视频里介绍的一个技巧,为了避免来自外部“伪造”的图片选中事件,我们让selectedPhotosSubject的访问级别是fileprivate,然后,把它Observable的一面,用另外一个属性暴露给选中图片事件的订阅者。

其次,在UICollectionView单元格被选中的处理方法里,添加下面的代码:

override func collectionView(_ collectionView: UICollectionView,
                             didSelectItemAt indexPath: IndexPath) {
    // 1\. Get photo object
    let asset = photos.object(at: indexPath.item)
    // 2\. Flip the checked status
    if let cell = collectionView.cellForItem(at: indexPath) as? PhotoCell {
        cell.selected()
    }

    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? Bool,
               !isThumbnail {
                // 3\. Trigger event if the image is not an icloud
                // thumbnail.
                self?.selectedPhotosSubject.onNext(image)
            }
        })
}

其中,前半部分获取图片对象、反选单元格的代码都很简单,重点其实只有requestImagerequestHandler参数,在这里,只要图片库中的图片不是iCloud中的缩略图,我们就把它作为selectedPhotosSubject.next事件发送。

最后,在PhotoCollectionViewController.viewWillDisappear方法里,我们给selectedPhotosSubject发送完成事件,表示结束:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    selectedPhotosSubject.onCompleted()
}

这样,PhotoCollectionViewController的部分就完成了。接下来,我们完成TodoDetailViewController的部分。

TodoDetailViewController

首先,给TodoDetailViewController添加一个Variable,它的值类型是[UIImage],用来保存用户选中的所有图片:

class TodoDetailViewController: UITableViewController {
    fileprivate let images = Variable<[UIImage]>([])
    // ...
}

其次,在TodoDetailViewController.prepare(segue:sender:)方法里,我们先获取之前添加的selectedPhotos

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let photoCollectionViewController =
        segue.destination as! PhotoCollectionViewController
    images.value.removeAll()
    resetMemoBtn()

    let selectedPhotos = photoCollectionViewController.selectedPhotos
    // ...
}

第三,我们从selectedPhotos中订阅到用户选择的所有图片:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    // ...
    _ = selectedPhotos.subscribe(onNext: { image in
            self.images.value.append(image)
        }, onDisposed: {
            print("Finished choose photo memos.")
        })
    // ...
}

第四,让image是一个Observable,我们订阅它的值并更新UI。在TodoDetailViewController.viewDidLoad方法里,订阅这个Variable

override func viewDidLoad(){
    // ...

    images.asObservable().subscribe(onNext: {
        [weak self] images in
        guard let `self` = self else {
            return
        }
        guard !images.isEmpty else {
            self.resetMemoBtn()
            return
        }

        /// 1\. Merge photos
        self.todoCollage = UIImage.collage(images: images,
            in: self.memoCollageBtn.frame.size)

        /// 2\. Set the merged photo as the button background
        self.setMemoBtn(bkImage: self.todoCollage ?? UIImage())
    }).addDisposableTo(bag)

    // if ...
}

订阅后的处理逻辑很简单,就像代码注释中说明的那样,分成两个步骤:

  1. 合成所有用户选择的图片;
  2. 把合成后的图片设置为按钮的背景;

要注意的是,这段订阅的代码一定要放在viewDidLoad中初始化UI的代码前面,否则,当用户打开编辑Todo的时候,由于此时还未选择任何图片,订阅代码会重置memoCollageBtn的状态,我们就看不到之前合成的图片了。

最后,在处理Done按钮的IBOutlet方法里,我们保存合成后的图片,并结束todoSubject序列。

@IBAction func done() {
    todoItem.name = todoName.text!
    todoItem.isFinished = isFinished.isOn
    todoItem.pictureMemoFilename = savePictureMemos()

    todoSubject.onNext(todoItem)
    todoSubject.onCompleted()
    dismiss(animated: true, completion: nil)
}

What's next?

至此,保存图片备忘的基本功能就实现了,但是,我们还没有在合成图片的时候,动态的修改对应的section header text,这关系到一个值得思考的问题:同一个Observable可以订阅多次么?下一节,我们就来讨论这个话题,并实现修改section header text的功能。

相关文章

网友评论

      本文标题:16.11Todo IV - 进一步理解Subject的实际应用

      本文链接:https://www.haomeiwen.com/subject/jipydqtx.html