首页 iOS.& Swift Books RXSWIFT:SWIFT的无功

4
可观察到&实践中的主题 由Marin Todorov撰写

此时在本书中,您了解可观察到的观察和不同类型的科目,您已经学习了如何在Swift Playground中创建和实验它们。

然而,它可能有点具有挑战性,以便在日常开发情况下看到可观察到的实际使用,例如将UI绑定到数据模型,或者呈现新的控制器并从中返回输出。

有点不确定如何将这些新获得的技能应用于现实世界。在本书中,您将通过理论章节,如第2章,“观察到”和第3章“主题”,以及实际的逐步章节 - 就像这一个!

在里面 ”… 在实践中“章节,您将在完整的应用程序上工作。启动器Xcode项目将包括所有非RX代码。您的任务将是使用新获取的无功技巧添加rxSwift框架并添加其他功能。

这并不意味着说你不会沿途学习几件新事物 - Au对比度!

在本章中,您将使用RXSWIFT和您的新可观察到的超级管来创建一个应用程序,让用户创建漂亮的照片拼贴 - 反应方式。

入门

为本章打开Starter项目: combinestagram.。卷起你的舌头需要几点试图说出这个名字,不是吗?这可能不是最具市场性的名称,但它会这样做。

安装所有豆荚和打开 combinestagram.xcworkspace.。有关如何执行此操作的详细信息,请参阅第1章“Hello RxSwift”。

选择 资产/ main.storyboard. 你会看到你将带来的应用程序的界面:

在第一个屏幕中,用户可以看到当前的照片拼贴,并有按钮来清除当前照片列表,或将成品拼贴保存到磁盘。另外,当用户抽头时 + 右上方的按钮,它们将被带到故事板中的第二个视图控制器,在那里他们将在相机卷中看到照片列表。用户可以通过在缩略图上敲击拼贴的照片。

视图控制器和故事板已经有线了,你也可以偷看 UiImage + Collage.swift. 要查看实际的拼贴画如何放在一起。

在本章中,您将专注于将您的新技能练习。是时候开始了!

在视图控制器中使用主题/继电器

You’ll start by adding a BehaviorRelay<[UIImage]> property to the controller class and store the selected photos in its value. As you learned in Chapter 3, “Subjects”, the BehaviorRelay class works much like you’re used to with plain variables: you can manually change their value property any time you want. You will start with this simple example and later move on to subjects and custom observables.

打开 mainViewController.swift. and add the following inside the body of MainViewController:

private let bag = DisposeBag()
private let images = BehaviorRelay<[UIImage]>(value: [])

Since no other class will use those two constants, you define them as private. Encapsulation FTW!

Dispose Bag由View Controller拥有。只要查看控制器发布所有可观察的订阅都将被置于:

这使RX订阅内存管理非常简单:只需抛出包中的订阅,它们将与View Controller的Deallocation一起进行。

但是,这不会发生此特定视图控制器,因为它是根视图控制器,并且在应用程序退出之前未释放。在故事板中的另一章中,您将看到聪明的处置妥协的妥协卸货机制。

首先,您的应用程序将始终根据同一照片构建拼贴。不用担心;这是来自巴塞罗那国家边的漂亮照片,已经包含在App的资产目录中。每次用户抽头 +, you will add that same photo, one more time, to images.

Find actionAdd() and add the following to it:

let newImages = images.value
  + [UIImage(named: "IMG_1907.jpg")!]
images.accept(newImages)

First, you get the latest collection of images emitted by the relay fetching it via its value property and then you append one more image to it. Don’t mind the force-unwrapping after the UIImage initialization, we’re keeping things simple by skipping error handling for this chapter.

Next, you use the relay’s accept(_) to emit the updated set of images to any observers subscribed to the relay.

The initial value of the images relay is an empty array, and every time the user taps the + button, the observable sequence produced by images emits a new .next event with the new array as an element.

To permit the user to clear the current selection, scroll up and add the following to actionClear():

images.accept([])

With few lines of code in this chapter section, you neatly handled the user input. You can now move on to observing images and displaying the result on screen.

将照片添加到拼贴

Now that you have images wired up, you can observe for changes and update the collage preview accordingly.

In viewDidLoad(), create the following subscription to images. Even though its a relay, you can subscribe to it directly, since its conforms to ObservableType, much like Observable itself does:

images
  .subscribe(onNext: { [weak imagePreview] photos in
    guard let preview = imagePreview else { return }

    preview.image = photos.collage(size: preview.frame.size)
  })
  .disposed(by: bag)

You subscribe for .next events emitted by images. For every event, you create a collage with the helper method collage(images:size:) provided for arrays of type UIImage. Finally, you add this subscription to the view controller’s dispose bag.

In this chapter, you are going to subscribe to your observables in viewDidLoad(). Later in the book, you will look into extracting these into separate classes and, in the last chapter, structure them into an MVVM architecture. You now have your collage UI together; the user can update images by tapping the + 酒吧项目(或 清除)然后依次更新UI。

运行应用程序并试一试!如果您添加照片四次,您的拼贴将如下所示:

哇,这很容易!

当然,该应用程序现在有点无聊,但别担心 - 你将增加一下从相机滚动中选择照片的能力。

驾驶复杂的视图控制器UI

当您使用当前应用程序时,您会发现UI可能有点聪明地改善用户体验。例如:

  • 你可以禁用 清除 如果尚未选择任何照片,或者在用户刚刚清除选择的情况下,则按钮。

  • 同样,没有必要 如果没有选择任何照片,则会启用按钮。

  • 你也可以禁用 对于奇数照片,因为它会在拼贴上留下一个空点。

  • 将单个拼贴画的照片数量限制为六个,因为更多照片看起来有点奇怪。

  • 最后,如果视图控制器标题反映了当前选择,则会很好。

如果您拍摄了一下以上的清单,请稍等一下,您肯定会看到这些修改可能是非常麻烦的,以实现非反应性的方式。

Thankfully, with RxSwift you simply subscribe to images one more time and update the UI from a single place in your code.

Add this subscription inside viewDidLoad():

images
  .subscribe(onNext: { [weak self] photos in
      self?.updateUI(photos: photos)
  })
  .disposed(by: bag)

Every time there’s a change to the photo selection, you call updateUI(photos:). You don’t have that method just yet, so add it anywhere inside the class body:

private func updateUI(photos: [UIImage]) {
  buttonSave.isEnabled = photos.count > 0 && photos.count % 2 == 0
  buttonClear.isEnabled = photos.count > 0
  itemAdd.isEnabled = photos.count < 6
  title= photos.count > 0 ? "\(photos.count) photos" : "Collage"
}

在上面的代码中,根据上面的规则集更新完整的UI。所有逻辑都在一个地方,易于阅读。再次运行应用程序,您将在您与UI播放时查看所有规则:

到目前为止,您可能开始在应用于您的iOS应用程序时看到RX的真正优势。如果您查看本章中写的所有代码,您将看到只有几条简单的行驱动整个UI!

通过主题与其他视图控制器交谈

In this section of the chapter, you will connect the 相片ViewController class to the main view controller in order to let the user select arbitrary photos from their Camera Roll. That will result in 远的 更有趣的拼贴!

First, you need to push 相片ViewController to the navigation stack. Open mainViewController.swift. and find actionAdd(). Comment out the existing code and add this code in its place:

let photosViewController = storyboard!.instantiateViewController(
  withIdentifier: "相片ViewController") as! PhotosViewController

navigationController!.pushViewController(photosViewController, animated: true)

Above, you instantiate 相片ViewController from the project’s storyboard and push it onto the navigation stack. Run the app and tap + 看相机卷。

您第一次执行此操作,您需要授予对照片库的访问权限:

一旦你挖掘 好的 您将看到照片控制器的样子。实际照片可能会在您的设备上有所不同,您可能需要在授予访问权限之后再次回复。

第二次,您应该看到iPhone模拟器中包含的示例照片。

如果您正在使用已建立的可可模式构建应用程序,则您的下一步将是添加委托协议,以便照片控制器可以向主控制器交谈(即非反应方式):

然而,随着rxswift,您有一种普遍的谈话方式 任何 two classes — an Observable! There is no need to define a special protocol, because an Observable can deliver any kind of message to any one or more interested parties — the observers.

从所选照片中创建可观察到的

You’ll next add a subject to 相片ViewController that emits a .next event each time the user taps a photo from the Camera Roll.Open 相片ViewController.Swift. 并在顶部附近添加以下内容:

import RxSwift

You’d like to add a PublishSubject to expose the selected photos, but you don’t want the subject publicly accessible, as that would allow other classes to call onNext(_) and make the subject emit values. You might want to do that elsewhere, but not in this case.

Add the following properties to 相片ViewController:

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

Here, you define both a private PublishSubject that will emit the selected photos and a public property named selectedPhotos that exposes the subject’s observable.

订阅此属性是主控制器如何观察照片序列,而不能够干扰它。

相片ViewController 已经包含从相机滚动中读取照片的代码,并在集合视图中显示它们。您需要做的就是添加代码以在Compoutt View Cell上的用户抽头时发出所选照片。

Scroll down to collectionView(_:didSelectItemAt:). The code inside fetches the selected image and flashes the collection cell to give the user a bit of a visual feedback.

Next, imageManager.requestImage(...) gets the selected photo and gives you image and info parameters to work with in its completion closure. In that closure, you’d like to emit a .next event from selectedPhotosSubject.

Inside the closure, just after the guard statement, add:

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

You use the info dictionary to check if the image is the thumbnail or the full version of the asset. imageManager.requestImage(...) will call that closure once for each size. In the event you receive the full-size image, you call onNext(_) on your subject and provide it with the full photo.

这就是将可观察的序列从一个视图控制器曝光到另一个。不需要委托协议或那种其他任何其他神经人。

作为奖励,一旦删除协议,控制器关系变得非常简单:

观察所选照片的​​序列

你的下一项任务是返回 mainViewController.swift. 并添加代码以完成上面的架构的最后一部分:即,观察所选照片序列。

Find actionAdd() and add the following just before the line where you push the controller onto the navigation stack:

photosViewController.selectedPhotos
  .subscribe(
    onNext: { [weak self] newImage in

    },
    onDisposed: {
      print("Completed photo selection")
    }
  )
  .disposed(by: bag)

Before you push the controller, you subscribe for events on its selectedPhotos observable. You are interested in two events: .next, which means the user has tapped a photo, and also when the subscription is disposed. You’ll see why you need that in a moment.

Insert the following code inside the onNext closure to get everything working. It’s the same code you had before, but this time it adds the photo from Camera Roll:

guard let images = self?.images else { return }
images.accept(images.value + [newImage])

运行应用程序,从相机滚动中选择几张照片,然后返回到结果。凉爽的!

处置订阅 - 审查

代码看似正常工作,但尝试以下内容:将几张照片添加到拼贴,返回主屏幕并检查控制台。

Do you see a message saying, “Completed photo selection”? You added a print to your last subscription’s onDispose closure, but it never gets called! That means the subscription is never disposed and never frees its memory!

怎么会这样?您订阅了可观察的序列并将其扔进主屏幕的处置袋中。此订阅(如前一章节中所讨论的)将在释放袋对象或通过错误或已完成的事件完成时何时处理。

Since you neither destroy the main view controller to release its bag property, nor complete the photos sequence, your subscription just hangs around for the lifetime of the app!

To give your observers some closure, you could emit a .completed event when that controller disappears from the screen. This would notify all observers that the subscription has completed to help with automatic disposal.

打开 相片ViewController.Swift. and add a call to your subject’s onComplete() method in the controller’s viewWillDisappear(_:):

selectedPhotosSubject.onCompleted()

完美的!现在,您已经准备好了本章的最后一部分:采取普通的旧无聊功能并将其转换为超级令人敬畏和幻想的反应课程。

创建自定义可观察到

So far, you’ve tried BehaviorRelay, PublishSubject, and an Observable. To wrap up, you’ll create your own custom Observable and turn a plain old callback API into a reactive class. You’ll use the 相片 framework to save the photo collage — and since you’re already an RxSwift veteran, you are going to do it the reactive way!

You could add a reactive extension on PHPhotoLibrary itself, but to keep things simple, in this chapter you will create a new custom class named PhotoWriter:

Creating an Observable to save a photo is easy: If the image is successfully written to disk you will emit its asset ID and a .completed event, or otherwise an .error event.

包装现有的API

打开 课程/ photowriter.swift. - 此文件包含几个定义以使您入门。

首先,一如既往地添加rxSwift框架的导入:

import RxSwift

Then, add a new static method to PhotoWriter, which will create the observable you will give back to code that wants to save photos:

static func save(_ image: UIImage) -> Observable<String> {
  return Observable.create { observer in
	
  }
}

save(_:) will return an Observable<String>, because, after saving the photo, you will emit a single element: the unique local identifier of the created asset.

Observable.create(_) creates a new Observable, and you need to add all the meaty logic inside that last closure.

Add the following to the Observable.create(_) parameter closure:

var savedAssetId: String?
PHPhotoLibrary.shared().performChanges({

}, completionHandler: { success, error in

})

In the first closure parameter of performChanges(_:completionHandler:), you will create a photo asset out of the provided image; in the second one, you will emit either the asset ID or an .error event.

在第一个关闭内添加:

let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
savedAssetId = request.placeholderForCreatedAsset?.localIdentifier

You create a new photo asset by using PHAssetChangeRequest.creationRequestForAsset(from:) and store its identifier in savedAssetId. Next insert into completionHandler closure:

DispatchQueue.main.async {
  if success, let id = savedAssetId {
    observer.onNext(id)
    observer.onCompleted()
  } else {
    observer.onError(error ?? Errors.couldNotSavePhoto)
  }
}

If you got a success response back and savedAssetId contains a valid asset ID, you emit a .next event and a .completed event. In case of an error, you emit either a custom or the default error.

因此,您可以完成可观察的序列逻辑。

Xcode should already be warning you that you miss a return statement. As a last step, you need to return a Disposable out of that outer closure so add one final line to Observable.create({}):

return Disposables.create()

That wraps up the class nicely. The complete save() method should look like this:

static func save(_ image: UIImage) -> Observable<String> {
  return Observable.create({ observer in
    var savedAssetId: String?
    PHPhotoLibrary.shared().performChanges({
      let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
      savedAssetId = request.placeholderForCreatedAsset?.localIdentifier
    }, completionHandler: { success, error in
      DispatchQueue.main.async {
        if success, let id = savedAssetId {
          observer.onNext(id)
          observer.onCompleted()
        } else {
          observer.onError(error ?? Errors.couldNotSavePhoto)
        }
      }
    })
    return Disposables.create()
  })
}

If you’ve been paying attention, you might be asking yourself, “Why do we need an Observable that emits just a single .next event?”

Take a moment to reflect on what you’ve learned in the previous chapters. For example, you can create an Observable by using any of the following:

  • Observable.never():创建一个永不发出任何元素的可观察序列。
  • Observable.just(_:): Emits one element and a .completed event.
  • Observable.empty(): Emits no elements followed by a .completed event.
  • Observable.error(_): Emits no elements and a single .error event.

As you see, observables can produce any combination of zero or more .next events, possibly terminated by either a .completed or an .error.

In the particular case of PhotoWriter, you are only interested in one event since the save operation completes just once. You use .next + .completed for successful writes, and .error if a particular write failed.

如果你尖叫着,你会得到一个很大的奖励点 “但是单身怎么样?” about now. Indeed, what about 单身的?

实践中的rxswift特征

In Chapter 2, “Observables,” you had the chance to learn about RxSwift traits: specialized variations of the Observable implementation that are very handy in certain cases.

In this chapter, you’re going to do a quick review and use some of the traits in the Combinestagram project! Let’s start with 单身的.

单身的

As you know from Chapter 2, 单身的 is an Observable specialization. It represents a sequence, which can emit just once either a .success(Value) event or an .error. Under the hood, a .success is just .next + .completed pair.

This kind of trait is useful in situations such as saving a file, downloading a file, loading data from disk or basically any asynchronous operation that yields a value. You can categorize two distinct use-cases of 单身的:

  1. For wrapping operations that emit exactly one element upon success, just as PhotoWriter.save(_) earlier in this chapter.

    You can directly create a 单身的 instead of an Observable. In fact you will update the save(_) method in PhotoWriter to create a 单身的 in one of this chapter’s challenges.

  2. 为了更好地表达您打算从序列中消耗一个元素,并确保序列是否发出多个元素,订阅将出错。

    To achieve this, you can subscribe to any observable and use .asSingle() to convert it to a 单身的. You’ll try this just after you’ve finished reading through this section.

可能是

可能是 is quite similar to 单身的 with the only difference that the observable 可能 成功完成后不会发出价值。

If we keep to the photograph-related examples imagine this use-case for 可能是, your app is storing photos in its own custom photo album. You persist the album identifier in UserDefaults and use that ID each time to “open” the album and write a photo inside. You would design a open(albumId:) -> Maybe<String> method to handle the following situations:

  • In case the album with the given ID still exists, just emit a .completed event.
  • In case the user has deleted the album in the meanwhile, create a new album and emit a .next event with the new ID so you can persist it in UserDefaults.
  • In case something is wrong and you can’t access the Photos library at all, emit an .error event.

Just like other traits, you can achieve the same functionality with using a “vanilla” Observable, but 可能是 gives more context both to you as you’re writing your code and to the programmers coming to alter the code later on.

Just as with 单身的, you can either create a 可能是 directly by using 可能是.create({ ... }) or by converting any observable sequence via .asMaybe().

合适的

The final trait to cover is 合适的. This variation of Observable allows only for a single .completed or .error event to be emitted before the subscription is disposed of.

You can convert an observable sequence to a completable by using the ignoreElements() operator, in which case all next events will be ignored, with only a completed or error event emitted, just as required for a 合适的.

You can also create a completable sequence by using 合适的.create { ... } with code very similar to that you’d use to create other observables or traits.

You might notice that 合适的 simply doesn’t allow for emitting any values and wonder why would you need a sequence like that. You’d be surprised at the number of use-cases wherein you only need to know whether an async operation succeeded or not.

Let’s look at an example before going back to combinestagram.. Let’s say your app auto-saves the document while the user is working on it. You’d like to asynchronously save the document in a background queue and, when completed, show a small notification or an alert box onscreen if the operation fails.

Let’s say you wrapped the saving logic into a function saveDocument() -> Completable. This is how easy it is then to express the rest of the logic:

saveDocument()
  .andThen(Observable.from(createMessage))
  .subscribe(onNext: { message in
    message.display()
  }, onError: {e in
    alert(e.localizedDescription)
  })

The andThen operator allows you to chain more completables or observables upon a success event and subscribe for the final result. In case any of them emits an error, your code will fall through to the final onError closure.

I’ll assume you’re delighted to hear that you will get to use 合适的 in two chapters later in the book. And now back to combinestagram. and the problem at hand!

订阅您的自定义可观察

The current feature — saving a photo to the Photos library — falls under one of those special use-cases for which there is a special trait. Your PhotoWriter.save(_) observable emits just once (the new asset ID), or it errors out, and is therefore a great case for a 单身的.

Now for the sweetest part of all: making use of your custom-designed Observable and kicking serious butt along the way!

打开 mainViewController.swift. and add the following inside the actionSave() action method for the button:

guard let image = imagePreview.image else { return }

PhotoWriter.save(image)
  .asSingle()
  .subscribe(
    onSuccess: { [weak self] id in
      self?.showMessage("救d with id: \(id)")
      self?.actionClear()
    },
    onError: { [weak self] error in
      self?.showMessage("Error", description: error.localizedDescription)
    }
  )
  .disposed(by: bag)

Above you call PhotoWriter.save(image) to save the current collage. Then you convert the returned Observable to a 单身的, ensuring your subscription will get at most one element, and display a message when it succeeds or errors out. Additionally, you clear the current collage if the write operation was a success.

笔记: asSingle() ensures that you get at most one element by throwing an error if the source sequence emits more than one.

给应用程序最后一个胜利运行,建立一个漂亮的照片拼贴并将其保存到磁盘上。

别忘了检查你的 相片 app for the result!

有了这个,你已经完成了本书的第1节 - 祝贺!

你不是一个年轻的padawan了,而是一个经验丰富的rid jedi。但是,不要觉得刚刚拍摄黑暗的一面。您将获得战斗网络,线程切换和交易很快!

Before that, you must continue your training and learn about one of the most powerful aspects of RxSwift. In Section 2, “Operators and Best Practices,” operators will allow you to take your Observable superpowers to a whole new level!

挑战

Before you move on to the next section, there are two challenges waiting for you. You will once again create a custom Observable — but this time with a little twist.

挑战1:使用单一的逻辑是逻辑

You’ve probably noticed that you didn’t gain much by using .asSingle() when saving a photo to the Camera Roll. The observable sequence already emits at most one element!

Well, you are right about that, but the point was to provide a gentle introduction to .asSingle(). Now you can improve the code on your own in this very challenge.

打开 photowriter.swift. and change the return type of save(_) to 单身的<String>. Then replace Observable.create with 单身的.create.

This should clear most errors. There is one last thing to take care of: Observable.create receives an observer as parameter so you can emit multiple values and/or terminating events. 单身的.create receives as a parameter a closure, which you can use only once to emit either a .success(T) or .error(E) values.

Complete the conversion yourself and remember that the parameter is a closure not an observer object, so you call it like this: single(.success(id)).

挑战2:添加自定义可观察到的警报

打开 mainViewController.swift. and scroll towards the bottom of the file. Find the showMessage(_:description:) method that came with the starter project.

该方法显示警报屏幕上,并在用户点击时运行回调 关闭 button to dismiss the alert. That does sound quite similar to what you’ve already done for PHPhotoLibrary.performChanges(_), doesn’t it?

要完成此挑战,请执行以下内容:

  • 添加A.n extension method to UIViewController that presents an alert onscreen with a given title and message and returns an 合适的.
  • 添加A. 关闭 按钮允许用户关闭警报。
  • 订阅被解雇后解散警报控制器,以便您没有任何悬挂警报。

In the end, use the new completable to present the alert from within showMessage(_:description:).

一如既往地,如果您遇到麻烦,或者很奇怪地看到提供的解决方案,您可以查看已完成的项目和挑战代码 项目 本章文件夹。无论如何,你可以在那里偷看,但请先给它最好的镜头!

有一个技术问题?想报告一个错误吗? 您可以向官方书籍论坛中的书籍作者提出问题和报告错误 这里.

有反馈分享在线阅读体验吗? 如果您有关于UI,UX,突出显示或我们在线阅读器的其他功能的反馈,您可以将其发送到设计团队,其中表格如下所示:

© 2021 Razeware LLC