2017-06-02 101 views
1

我知道有很多的代碼在這裏的SO問題,但它是我能在那一刻做了最好的......你可以複製/粘貼代碼爲一啓動RX-操場上看到的問題。爲什麼完成事件會阻止這個測試通過?

在第89行有一段註釋碼let creds = Observable.just(credentials)//.concat(Observable.never())。如果我刪除//並允許concat,代碼將通過它的測試。如果creds被允許發送完成事件,任何人都可以提供一個線索,說明爲什麼這段代碼不能通過測試?

import Foundation 
import RxSwift 
import RxCocoa 
import UIKit 

typealias Credentials = (email: String, password: String) 

struct User { 
    let id: String 
    let properties: [Property] 
} 

struct Property { 
    let id: String 
    let name: String 
} 

struct LoginParams { 
    let touchIDPossible: Bool 
} 

class LoginScreen { 
    var attemptLogin: Observable<Credentials> { 
     assert(_attemptLogin == nil) 
     _attemptLogin = PublishSubject() 
     return _attemptLogin! 
    } 

    var _attemptLogin: PublishSubject<(email: String, password: String)>? 
} 

class DashboardScreen { 
    func display(property: Observable<Property?>) { 
     property.subscribe(onNext: { [unowned self] in 
      self._property = $0 
     }).disposed(by: bag) 
    } 

    var _property: Property? 
    let bag = DisposeBag() 
} 

class Interface { 
    func login(params: LoginParams) -> Observable<LoginScreen> { 
     assert(_login == nil) 
     _login = PublishSubject() 
     return _login! 
    } 

    func dashboard() -> Observable<DashboardScreen> { 
     assert(_dashboard == nil) 
     _dashboard = PublishSubject() 
     return _dashboard! 
    } 

    var _login: PublishSubject<LoginScreen>? 
    var _dashboard: PublishSubject<DashboardScreen>? 
    let bag = DisposeBag() 
} 

class Server { 
    func user(credentials: Credentials) -> Observable<User> { 
     assert(_user == nil) 
     _user = PublishSubject() 
     return _user! 
    } 

    func property(id: String) -> Observable<Property> { 
     assert(_property == nil) 
     _property = PublishSubject() 
     return _property! 
    } 

    var _user: PublishSubject<User>? 
    var _property: PublishSubject<Property>? 
} 

class Coordinator { 

    init(interface: Interface, server: Server) { 
     self.interface = interface 
     self.server = server 
    } 

    func start() { 
     let credentials = (email: "foo", password: "bar") 

     // remove the `//` and the test will pass. Why does it fail when `creds` completes? 
     let creds = Observable.just(credentials)//.concat(Observable.never()) 

     let autoUser = creds.flatMap { 
      self.server.user(credentials: $0) 
       .materialize() 
       .filter { !$0.isCompleted } 
      }.shareReplayLatestWhileConnected() 

     let login = autoUser.filter { $0.error != nil } 
      .flatMap { _ in self.interface.login(params: LoginParams(touchIDPossible: false)) } 

     let attempt = login.flatMap { $0.attemptLogin } 
      .shareReplayLatestWhileConnected() 

     let user = attempt.flatMap { 
      self.server.user(credentials: $0) 
       .materialize() 
       .filter { !$0.isCompleted } 
      }.shareReplayLatestWhileConnected() 

     let propertyID = Observable.merge(autoUser, user).map { $0.element } 
      .filter { $0 != nil }.map { $0! } 
      .map { $0.properties.sorted(by: { $0.name < $1.name }).map({ $0.id }).first } 

     let property = propertyID.filter { $0 != nil }.map { $0! } 
      .flatMap { self.server.property(id: $0) 
       .map { Optional.some($0) } 
       .catchErrorJustReturn(nil) 
      }.debug("property").shareReplayLatestWhileConnected() 

     let dashboard = property.flatMap { _ in self.interface.dashboard() } 

     dashboard.map { $0.display(property: property) } 
      .subscribe() 
      .disposed(by: bag) 
    } 

    let interface: Interface 
    let server: Server 
    let bag = DisposeBag() 
} 

do { 
    let interface = Interface() 
    let server = Server() 
    let coordinator = Coordinator(interface: interface, server: server) 

    coordinator.start() 

    assert(server._user != nil) 

    let simpleProperty = Property(id: "bar", name: "tampa") 
    let user = User(id: "foo", properties: [simpleProperty]) 
    server._user?.onNext(user) 
    server._user?.onCompleted() 
    server._user = nil 

    assert(interface._login == nil) 

    assert(server._property != nil) 

    let property = Property(id: "bar", name: "tampa") 
    server._property!.onNext(property) 
    server._property!.onCompleted() 
    server._property = nil 

    assert(interface._dashboard != nil) 

    let dashboard = DashboardScreen() 
    interface._dashboard?.onNext(dashboard) 
    interface._dashboard?.onCompleted() 

    assert(dashboard._property != nil) 
    print("test passed") 
} 

這裏是代碼的輸出作爲其高於:

2017-06-01 22:22:42.534: property -> subscribed 
2017-06-01 22:22:42.552: property -> Event next(Optional(__lldb_expr_134.Property(id: "bar", name: "tampa"))) 
2017-06-01 22:22:42.557: property -> Event completed 
2017-06-01 22:22:42.557: property -> isDisposed 
2017-06-01 22:22:42.559: property -> subscribed 
assertion failed: file MyPlayground.playground, line 159 

爲什麼property被訂閱後已經設置?

這裏是輸出,如果你刪除\\

2017-06-01 22:23:51.540: property -> subscribed 
2017-06-01 22:23:51.553: property -> Event next(Optional(__lldb_expr_136.Property(id: "bar", name: "tampa"))) 
test passed 
+0

既然我們沒有你的行號,你至少可以給代碼添加一個關於哪個斷言失敗的註釋(159行)? – ctietze

+0

對不起,這是最後一次失敗。 –

回答

1

我本來建議保持dashboard繞了DisposeBag這樣,當start()完成時,參考不走太早。 OP已經更新了代碼,因此這裏有一個更新的答案。


當你添加更多的調試信息:

let dashboard = property.debug("prop in") 
    .flatMap { _ in self.interface.dashboard().debug("dash in") } 
    .debug("dash out") 

日誌將揭示該財產提前完成,之後即右內側序列已訂閱(「破折號 - >訂閱」 ):

2017-06-03 08:33:27.442: property -> Event next(Optional(Property(id: "bar", name: "tampa"))) 
2017-06-03 08:33:27.442: prop in -> Event next(Optional(Property(id: "bar", name: "tampa"))) 
2017-06-03 08:33:27.449: dash in -> subscribed 
2017-06-03 08:33:27.452: property -> Event completed 
2017-06-03 08:33:27.452: property -> isDisposed 
2017-06-03 08:33:27.452: prop in -> Event completed 
2017-06-03 08:33:27.452: prop in -> isDisposed 
2017-06-03 08:33:27.456: dash in -> Event next(DashboardScreen) 
2017-06-03 08:33:27.456: dash out -> Event next(DashboardScreen) 

如果.concat(.never()),完成事件不會觸發,並且不會干擾過程。

的問題是你的測試代碼編寫勢在必行。你start()的過程,然後發佈更改。但是如果你把各種onNext事件異步地放到主隊列上,整個事情就會更快崩潰。您的協調員的設計讀取類似於聲明性代碼,但實際上它們的使用方式類似於一個很有說服力的順序代碼路徑。

補救措施是要求及時性。PublishSubjects沒有歷史記錄;如果您使用的是BehaviorSubjects重播他們的最新值,而不是,你可以設置所有的改變調用start()之前,它會工作。我假設您使用PublishSubject s,因爲您首先打電話start()打開管道,並希望通過它一個接一個地推送更改。問題是,你的管道是以不等待你推動所有事情的方式製造的。可以這麼說,輸入閥獨立關閉。

呀,這個比喻是不是在人類歷史:)

所以選擇最好的真的是:

  1. 讓所有的協調工作的一大Observable.combineLatest使整個改造序列ISN直到每個序列都有發言權,
  2. 使用緩衝/重放主題並提前設置它們,
  3. 用一個從未完成的鹼基序列替換.just(其完成)保持管道暢通;你可以將其設置爲Observable<Observable<Credentials>>,其中外部序列保持活動狀態,內部序列使用Observable.just - 儘管我懷疑你的生產代碼將依賴於這個小細節。
+0

添加DisposeBag沒有幫助,斷言仍然觸發。對於我不瞭解的完成事件有一些特殊之處,或者RxSwift中存在錯誤。 –