RxSwift in Aktion - Schreiben einer reaktiven Anwendung

Wenn Sie den neuesten Trends glauben, dann gewinnt FRP an Dynamik und wird nicht aufhören. Vor nicht allzu langer Zeit bin ich auf ein Projekt gestoßen , das FRP - ReactiveX und dessen Implementierung für Swift - RxSwift gewidmet ist . Es gab bereits einen kleinen Artikel über Habré , der für ein erstes Verständnis von RxSwift hilfreich sein wird. Ich würde dieses Thema gerne weiterentwickeln, also, wenn Sie interessiert sind, begrüßen Sie unter Katze!

Rasende Probleme fangen an


Und das ist es wirklich. Das Schwierigste, mit dem ich zu kämpfen hatte, war eine völlig andere Konstruktion des Programmcodes. Mit meiner Erfahrung in der imperativen Programmierung war es schwierig, auf eine neue Art und Weise neu aufzubauen. Aber mein Instinkt sagte mir, dass es sich lohnte, sich darum zu kümmern. Ich brauchte 2 Wochen in Panik , um die Essenz von ReactiveX zu verstehen, und ich bereue die aufgewendete Zeit nicht. Aus diesem Grund möchte ich Sie sofort warnen - der Artikel setzt ein Verständnis der Bedingungen von ReactiveX voraus, z. B. Observable, Subscriber usw.

Also fangen wir an. Wir werden einen einfachen Leser unserer Pinnwand mit Facebook schreiben. Dafür brauchen wir RxSwift , ObjectMapper für die Datenzuordnung, Facebook iOS das SDK und MBProgressHUDLast anzeigen. Wir erstellen ein Projekt in Xcode, verbinden die oben genannten Bibliotheken damit (ich verwende CocoaPods ), konfigurieren den Link mit Facebook gemäß den Anweisungen und fahren mit dem Codieren fort.

Anmeldebildschirm


Wir werden das Rad nicht neu erfinden, wir platzieren einfach den vorgefertigten Button von Facebook in der Mitte des Bildschirms - FBSDKLoginButton:

let loginButton = FBSDKLoginButton()
loginButton.center = self.view.center
loginButton.readPermissions = ["user_posts"]
loginButton.delegate = self

Vergessen Sie nicht, den FBSDKLoginButtonDelegate-Delegaten für die Anmeldeschaltfläche hinzuzufügen und die Delegatenmethoden zu implementieren:

// MARK: Facebook Delegate Methods
func loginButton(loginButton: FBSDKLoginButton!, didCompleteWithResult result: FBSDKLoginManagerLoginResult!, error: NSError!) {
        if ((error) != nil) {
            // Process error
            let alert = UIAlertController(title: "Ошибка", message: error.localizedDescription, preferredStyle: .Alert)
            alert.addAction(UIAlertAction(title: "OK", style: .Cancel, handler: nil))
            self.presentViewController(alert, animated: true, completion: nil)
        }
        else if result.isCancelled {
            let alert = UIAlertController(title: "Ошибка", message: "Result is cancelled", preferredStyle: .Alert)
            alert.addAction(UIAlertAction(title: "OK", style: .Cancel, handler: nil))
            self.presentViewController(alert, animated: true, completion: nil)
        }
        else {
            let storyboard = UIStoryboard(name: "Main", bundle: nil)
            let vc = storyboard.instantiateViewControllerWithIdentifier("navController") as! UINavigationController
            self.presentViewController(vc, animated: true, completion: nil)
        }
    }
    func loginButtonDidLogOut(loginButton: FBSDKLoginButton!) {
        print("User Logged Out")
    }

Hier ist alles ganz einfach: Wenn der Anmeldefehler oder der Benutzer auf dem Facebook-Autorisierungsbildschirm auf die Schaltfläche „Abbrechen“ geklickt hat, wird eine entsprechende Meldung in Form einer Warnung angezeigt, und wenn alles in Ordnung ist, wird diese an den nächsten Bildschirm mit der Nachrichtenliste gesendet. Ich habe die Abmeldefunktion nicht berührt. Wie wir sehen können, ist bisher alles trivial genug und es ist nicht von Reaktivität die Rede. Hier gibt es auch einen heiklen Punkt: Stecken Sie nicht in jede Lücke, um sich an das KISS- Prinzip zu erinnern .

Nachrichtenbildschirm


Wir werden eine Funktion schreiben, um eine Liste von Nachrichten von einer Facebook-Pinnwand zu erhalten, deren Rückgabetyp Observable ist:

func getFeeds() -> Observable {
        return Observable.create { observer in
            let parameters = ["fields": ""]
            let friendsRequest = FBSDKGraphRequest.init(graphPath: "me/feed", parameters: parameters, HTTPMethod: "GET")
            friendsRequest.startWithCompletionHandler { (connection, result, error) -> Void in
                if error != nil {
                    observer.on(.Error(error!))
                } else {
                    let getFeedsResponse = Mapper().map(result)!
                    observer.on(.Next(getFeedsResponse))
                    observer.on(.Completed)
                }
            }
            return AnonymousDisposable {
            }
       }
}

Was passiert in diesem Code? Eine Netzwerkanforderung FBSDKGraphRequest wird generiert, um die "me / feed" -Nachrichten zu empfangen. Anschließend erteilen wir einen Befehl, um die Anforderung auszuführen und den Status im Beendigungsblock zu überwachen. Im Fehlerfall geben wir es an das Observable weiter, im Erfolgsfall übertragen wir die empfangenen Daten an das Observable.

Anmerkung: Ich übergebe eine Variable an FBSDKGraphRequest
let parameters = ["fields": ""]
mit einem leeren Parametersatz. Dies ist notwendig, damit Facebook nicht weint, sondern Warnungen in den Protokollen anzeigt, dass das Feld in den Parametern Pflichtfeld ist. Im Prinzip funktioniert alles ohne diesen Parameter, aber ich bin so müde.

Lassen Sie uns ein wenig vom Schreiben einer Anwendung zurücktreten und über die Datenzuordnung sprechen. Ich löse dieses Problem mit ObjectMapper. Mit ObjectMapper können Sie dies recht schnell und einfach durchführen:

class GetFeedsResponse: Mappable {
    var data = [Feed]()
    var paging: Paging!
    required init?(_ map: Map){
    }
    // Mappable
    func mapping(map: Map) {
        data <- map["data"]
        paging <- map["paging"]
    }
}
class Feed: Mappable {
    var createdTime: String!
    var id: String!
    var story: String?
    var message: String?
    required init?(_ map: Map){
    }
    // Mappable
    func mapping(map: Map) {
        createdTime <- map["created_time"]
        id <- map["id"]
        story <- map["story"]
        message <- map["message"]
    }
}
class Paging: Mappable {
    var next: String!
    var previous: String!
    required init?(_ map: Map){
    }
    // Mappable
    func mapping(map: Map) {
        next <- map["next"]
        previous <- map["previous"]
    }
}

Ich schlage vor, sofort eine Netzwerkanfrage zu schreiben, um detaillierte Informationen über die Neuigkeiten zu erhalten:

func getFeedInfo(feedId: String) -> Observable {
        return Observable.create { observer in
            let parameters = ["fields" : "id,admin_creator,application,call_to_action,caption,created_time,description,feed_targeting,from,icon,is_hidden,is_published,link,message,message_tags,name,object_id,picture,place,privacy,properties,shares,source,status_type,story,story_tags,targeting,to,type,updated_time,with_tags"]
            let friendsRequest = FBSDKGraphRequest.init(graphPath: "" + feedId, parameters: parameters, HTTPMethod: "GET")
            friendsRequest.startWithCompletionHandler { (connection, result, error) -> Void in
                if error != nil {
                    observer.on(.Error(error!))
                } else {
                    print(result)
                    let getFeedInfoResponse = Mapper().map(result)!
                    observer.on(.Next(getFeedInfoResponse))
                    observer.on(.Completed)
                }
            }
            return AnonymousDisposable {
            }
        }
    }

Wie wir sehen können, habe ich eine Reihe von Feldern in der Parametervariable übergeben - dies sind alle Felder, die in der Dokumentation enthalten waren. Ich werde sie nicht alle sortieren, nur einen Teil. Hier ist die Datenzuordnung:

class GetFeedInfoResponse: Mappable {
    var createdTime: String!
    var from: IdName!
    var id: String!
    var isHidden: Bool!
    var isPublished: Bool!
    var message: String?
    var name: String?
    var statusType: String?
    var story: String?
    var to = [IdName]()
    var type: String!
    var updatedTime: String!
    required init?(_ map: Map){
    }
    // Mappable
    func mapping(map: Map) {
        createdTime <- map["created_time"]
        from <- map["from"]
        id <- map["from"]
        isHidden <- map["is_hidden"]
        isPublished <- map["is_published"]
        message <- map["message"]
        name <- map["name"]
        statusType <- map["status_type"]
        story <- map["story"]
        // It necessary that Facebook API have a bad structure
        // buffer%varname% is a temporary variable
        var bufferTo = NSDictionary()
        bufferTo <- map["to"]
        if let bufferData = bufferTo["data"] as? NSArray {
            for bufferDataElement in bufferData {
                let bufferToElement = Mapper().map(bufferDataElement)
                to.append(bufferToElement!)
            }
        }
        type <- map["type"]
        updatedTime <- map["updated_time"]
    }
}
class IdName: Mappable {
    var id: String!
    var name: String!
    required init?(_ map: Map){
    }
    // Mappable
    func mapping(map: Map) {
        id <- map["id"]
        name <- map["name"]
    }
}

Wie Sie sehen, auch hier eine Fliege in der Salbe. Wenn ich zum Beispiel ein JSON-Objekt "to" analysiere, das ein einzelnes Feld "data" enthält, das wiederum ein JSON-Array ist, musste ich wie folgt vorgehen:

var bufferTo = NSDictionary()
        bufferTo <- map["to"]
        if let bufferData = bufferTo["data"] as? NSArray {
            for bufferDataElement in bufferData {
                let bufferToElement = Mapper().map(bufferDataElement)
                to.append(bufferToElement!)
            }
        }

Grundsätzlich könnte ich eine Datei mit einem "Daten" -Feld erstellen und das Objekt dort stillschweigend abbilden, aber es erschien mir albern, eine neue Datei zu erstellen, um ein Feld abzubilden. Wenn einer von Ihnen eine elegantere Lösung für dieses Problem hat - ich werde mich über die Freuden betrinken und würde mich freuen, davon zu erfahren.

Kommen wir zu unseren Schafen zurück. Wir sind mit Facebook in Kontakt getreten, haben eine Liste mit Neuigkeiten in Form eines Observable erhalten, jetzt müssen wir diese Daten anzeigen. Zu diesem Zweck verwende ich das MVVM- Muster. Meine kostenlose Interpretation dieser Vorlage unter Berücksichtigung der Verwendung von ReactiveX klingt folgendermaßen: Das ViewModel verfügt über eine Observable, die Ereignisse generiert, und View abonniert diese Ereignisse und verarbeitet sie. Das heißt, das ViewModel hängt nicht davon ab, wer es verfolgt. Wenn es Abonnenten gibt, generiert das Observable Daten. Wenn es keine Abonnenten gibt, generiert das Observable nichts. (Diese Aussage gilt für „kaltes“ Observable, weil „heißes“ Observable Daten generieren). Schreiben Sie ein ViewModel für den Nachrichtenbildschirm:

class FeedsViewModel {
    let feedsObservable: Observable<[Feed]>
    let clickObservable: Observable
    // If some process in progress
    let indicator: Observable
    init(input: (
        UITableView
        ),
        dependency: (
        API: APIManager,
        wireframe: Wireframe
        )
        ) {
            let API = dependency.API
            let wireframe = dependency.wireframe
            let indicator = ViewIndicator()
            self.indicator = indicator.asObservable()
            feedsObservable = API.getFeeds()
                .trackView(indicator)
                .map { getFeedResponse in
                    return getFeedResponse.data
                }
                .catchError { error in
                    return wireframe.promptFor(String(error), cancelAction: "OK", actions: [])
                        .map { _ in
                            return error
                        }
                        .flatMap { error in
                            return Observable.error(error)
                    }
                }
                .shareReplay(1)
            clickObservable = input
                .rx_modelSelected(Feed)
                .flatMap { feed  in
                    return API.getFeedInfo(feed.id).trackView(indicator)
                }
                .catchError { error in
                    return wireframe.promptFor(String(error), cancelAction: "OK", actions: [])
                        .map { _ in
                            return error
                        }
                        .flatMap { error in
                            return Observable.error(error)
                    }
                }
                // If error when click uitableview - set retry() if you want to click cell again
                .retry()
                .shareReplay(1)
    }
}

Analysieren wir den Code. Im Eingabefeld übergeben wir die Tabelle von View, im Abhängigkeitsfeld die API-Klasse und Wireframe. Es gibt 3 Variablen in der Klasse: feedsObservable gibt eine Observable mit einer Liste von Nachrichten zurück, clickObservable ist ein Handler zum Klicken auf eine Tabellenzelle und indicator ist eine boolesche Variable, die festlegt, ob der Ladeindikator angezeigt werden soll. Der Code enthält gleichzeitig 2 kuriose Klassen - Wireframe und ViewIndicator. Lassen Sie uns näher darauf eingehen. Ein Wireframe ist nichts anderes als eine reaktive Implementierung von Alarmen. Ich habe diese Implementierung aus Beispielen im RxSwift-Repository entnommen. ViewIndicator verfolgt, und die trackView-Funktion dieser Klasse wird ausgeführt, solange mindestens eine Sequenz in der Kette ausgeführt wird. Daher ist trackView zur Anzeige des Ladeindikators praktisch.

Lassen Sie uns auf die Logik unseres Observable eingehen. Das erste - feedsObservable - empfängt eine Antwort von Facebook, dann wird im Kartenblock die Nachrichtenliste abgerufen und zurückgegeben, dann gibt es eine Fehlerbehandlung und trackView, um die Last anzuzeigen. Der zweite Befehl - clickObservable - überwacht den Klick auf eine Tabellenzelle und ruft anschließend eine Netzwerkanforderung auf, um detaillierte Informationen zu den Nachrichten abzurufen. Super, wenn das Modell fertig ist, gehe direkt zu Ansicht.

Ich werde sofort den View-Code geben, danach werden wir ihn mit Ihnen analysieren:

class FeedsViewController: UIViewController, UITableViewDelegate {
    @IBOutlet weak var feedsTableView: UITableView!
    var disposeBag = DisposeBag()
    override func viewDidLoad() {
        super.viewDidLoad()
        let viewModel = FeedsViewModel(
            input:feedsTableView,
            dependency: (
                API: APIManager.sharedAPI,
                wireframe: DefaultWireframe.sharedInstance
            )
        )
        let progress = MBProgressHUD()
        progress.mode = MBProgressHUDMode.Indeterminate
        progress.labelText = "Загрузка данных..."
        progress.dimBackground = true
        viewModel.indicator
            .bindTo(progress.rx_mbprogresshud_animating)
            .addDisposableTo(self.disposeBag)
        feedsTableView.rx_setDelegate(self)
        viewModel.feedsObservable
            .bindTo(feedsTableView.rx_itemsWithCellFactory) { tableView, row, feed in
                let cell = tableView.dequeueReusableCellWithIdentifier("feedTableViewCell") as! FeedTableViewCell
                cell.feedCreatedTime.text = NSDate().convertFacebookTime(feed.createdTime)
                if let story = feed.story {
                    cell.feedInfo.text = story
                } else if let message = feed.message {
                    cell.feedInfo.text = message
                }
                cell.layoutMargins = UIEdgeInsetsZero
                return cell
            }
            .addDisposableTo(disposeBag)
        viewModel.clickObservable
            .subscribeNext { feed in
                let storyboard = UIStoryboard(name: "Main", bundle: nil)
                let feedInfoViewController = storyboard.instantiateViewControllerWithIdentifier("feedInfoViewController") as! FeedInfoViewController
                feedInfoViewController.feedInfo = feed
                self.navigationController?.pushViewController(feedInfoViewController, animated: true)
            }
            .addDisposableTo(disposeBag)
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    // Deselect tableView row after click
    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
    }
}

Erstellen Sie zunächst ein viewModel. Als nächstes müssen Sie einen Ladeanzeiger erstellen und ihn irgendwie mit ViewIndicator verbinden. Dazu müssen wir Extension für MBProgressHUD schreiben:

extension MBProgressHUD {
    /**
     Bindable sink for MBProgressHUD show/hide methods.
     */
    public var rx_mbprogresshud_animating: AnyObserver {
        return AnyObserver {event in
            MainScheduler.ensureExecutingOnScheduler()
            switch (event) {
            case .Next(let value):
                if value {
                    let loadingNotification = MBProgressHUD.showHUDAddedTo(UIApplication.sharedApplication().keyWindow?.subviews.last, animated: true)
                    loadingNotification.mode = self.mode
                    loadingNotification.labelText = self.labelText
                    loadingNotification.dimBackground = self.dimBackground
                } else {
                    MBProgressHUD.hideHUDForView(UIApplication.sharedApplication().keyWindow?.subviews.last, animated: true)
                }
            case .Error(let error):
                let error = "Binding error to UI: \(error)"
                #if DEBUG
                    rxFatalError(error)
                #else
                    print(error)
                #endif
            case .Completed:
                break
            }
        }
    }
}

Wenn MBProgressHUD einen bestimmten Wert enthält, wird ein Indikator angezeigt. Wenn keine Werte angegeben sind, blenden Sie diese aus. Jetzt müssen wir die Bindung zwischen unserem MBProgressHUD und ViewIndicator konfigurieren. Das geht so:

viewModel.indicator
            .bindTo(progress.rx_mbprogresshud_animating)
            .addDisposableTo(self.disposeBag)

Danach konfigurieren wir die Bindung zwischen dem UITableView und dem Empfangen von Daten sowie zwischen dem Klicken auf das UITableView-Element und dem Wechsel zu einem neuen Bildschirm. Ich habe auch einen Bildschirm mit detaillierten Informationen über den Beitrag ohne "Reaktivität" erstellt:

override func viewDidLoad() {
        super.viewDidLoad()
        var feedDetail = "From: " + feedInfo.from.name
        if feedInfo.to.count > 0 {
            feedDetail += "\nTo: "
            for to in feedInfo.to {
                feedDetail += to.name + "\n"
            }
        }
        if let date = feedInfo.createdTime {
            feedDetail += "\nDate: " + NSDate().convertFacebookTime(date)
        }
        if let story = feedInfo.story {
            feedDetail += "\nStory: " + story
        }
        if let message = feedInfo.message {
            feedDetail += "\nMessage: " + message
        }
        if let name = feedInfo.name {
            feedDetail += "\nName: " + name
        }
        if let type = feedInfo.type {
            feedDetail += "\nType: " + type
        }
        if let updatedTime = feedInfo.updatedTime {
            feedDetail += "\nupdatedTime: " + NSDate().convertFacebookTime(updatedTime)
        }
        feedTextView.text = feedDetail
        self.navigationController?.navigationBar.tintColor = UIColor.whiteColor()
    }

Das ist alles für mich. Der Quellcode des Projekts auf Github kann unter diesem Link heruntergeladen werden . Wenn Sie meinen Artikel gefallen hat, bin ich platzen vor Glück mit Vergnügen wird auch weiterhin über RxSwift schreiben und versuchen , ihr Potenzial in einer nicht-triviale Aufgabe zu entdecken.

Jetzt auch beliebt: