6 opérateurs de moissonneuses-batteuses Swift à connaître

La traduction de l'article a été préparée en prévision du début du cours avancé "Développeur iOS".








Dans cet article, nous examinerons six opérateurs Combine utiles. Nous allons le faire avec des exemples, en expérimentant chacun dans le Xcode Playground.



Le code source est disponible à la fin de l'article.



Eh bien, sans plus tarder, commençons.



1. préparer



Ce groupe de déclarations nous permet de préfixer (littéralement «ajouter») des événements, des valeurs ou d'autres éditeurs à notre éditeur d'origine:



import Foundation
import Combine

var subscriptions = Set<AnyCancellable>()

func prependOutputExample() {
    let stringPublisher = ["World!"].publisher
    
    stringPublisher
        .prepend("Hello")
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}


Résultat: Helloet World! sont affichés dans un ordre séquentiel:







ajoutons maintenant un autre éditeur du même type:



func prependPublisherExample() {
    let subject = PassthroughSubject<String, Never>()
    let stringPublisher = ["Break things!"].publisher
    
    stringPublisher
        .prepend(subject)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
    
    subject.send("Run code")
    subject.send(completion: .finished)
}


Le résultat est similaire au précédent (notez que nous devons envoyer un événement .finishedau sujet pour que l'opérateur .prependfonctionne):







2. ajouter



L'opérateur .append(littéralement "ajouter à la fin") fonctionne de la même manière .prepend, mais dans ce cas, nous ajoutons des valeurs à l'éditeur d'origine:



func appendOutputExample() {
    let stringPublisher = ["Hello"].publisher
    
    stringPublisher
        .append("World!")
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}


En conséquence, nous voyons Helloet World! sortie sur la console:







similaire à ce que nous avons utilisé précédemment .prependpour ajouter un autre Publishera, nous avons également cette option pour l'opérateur .append:







3. commutateurToLatest



Un opérateur plus complexe .switchToLatestnous permet de combiner une série d'éditeurs en un seul flux d'événements:



func switchToLatestExample() {
    let stringSubject1 = PassthroughSubject<String, Never>()
    let stringSubject2 = PassthroughSubject<String, Never>()
    let stringSubject3 = PassthroughSubject<String, Never>()
    
    let subjects = PassthroughSubject<PassthroughSubject<String, Never>, Never>()
    
    subjects
        .switchToLatest()
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
    
    subjects.send(stringSubject1)
    
    stringSubject1.send("A")
    
    subjects.send(stringSubject2)
    
    stringSubject1.send("B") // 
    
    stringSubject2.send("C")
    stringSubject2.send("D")
    
    subjects.send(stringSubject3)
    
    stringSubject2.send("E") // 
    stringSubject2.send("F") // 
    
    stringSubject3.send("G")
    
    stringSubject3.send(completion: .finished)
}


Voici ce qui se passe dans le code:



  • Nous créons trois objets PassthroughSubjectauxquels nous enverrons des valeurs.
  • Nous créons un objet principal PassthroughSubjectqui distribue d'autres objets PassthroughSubject.
  • Nous expédions stringSubject1au sujet principal.
  • stringSubject1 obtient la valeur A.
  • Nous stringSubject2envoyons au sujet principal, en supprimant automatiquement les événements stringSubject1.
  • De même, nous lui envoyons des valeurs stringSubject2, nous y connectons stringSubject3et lui envoyons un événement d'achèvement.


Le résultat est sortie A, C, Det G:







Pour simplifier, la fonction isAvailableretourne une valeur aléatoire Boolaprès un certain délai.



func switchToLatestExample2() {
    func isAvailable(query: String) -> Future<Bool, Never> {
        return Future { promise in
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                promise(.success(Bool.random()))
            }
        }
    }
    
    let searchSubject = PassthroughSubject<String, Never>()
    
    searchSubject
        .print("subject")
        .map { isAvailable(query: $0) }
        .print("search")
        .switchToLatest()
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
    
    searchSubject.send("Query 1")
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        searchSubject.send( "Query 2")
    }
}


Grâce à l'opérateur, .switchToLatestnous réalisons ce que nous voulons. Une seule valeur booléenne sera affichée:







4. fusionner (avec :)



Nous utilisons .merge(with:)pour combiner deux Publisherss comme si nous obtenions des valeurs à partir d'un seul:



func mergeWithExample() {
    let stringSubject1 = PassthroughSubject<String, Never>()
    let stringSubject2 = PassthroughSubject<String, Never>()
    
    stringSubject1
        .merge(with: stringSubject2)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
    
    stringSubject1.send("A")
    
    stringSubject2.send("B")
    
    stringSubject2.send("C")
    
    stringSubject1.send("D")
}


Le résultat est une séquence alternée d'éléments:







5.combineDernières



L'opérateur .combineLatestpublie un tuple contenant la dernière valeur de chaque éditeur.



Pour illustrer cela, considérons l'exemple réel suivant: nous avons un nom d'utilisateur, un mot de passe UITextFieldset un bouton Continuer. Nous voulons garder le bouton désactivé jusqu'à ce que le nom d'utilisateur soit d'au moins cinq caractères et que le mot de passe soit d'au moins huit caractères. Nous pouvons facilement y parvenir en utilisant l'opérateur .combineLatest:



func combineLatestExample() {
    let usernameTextField = CurrentValueSubject<String, Never>("")
    let passwordTextField = CurrentValueSubject<String, Never>("")
    
    let isButtonEnabled = CurrentValueSubject<Bool, Never>(false)
    
    usernameTextField
        .combineLatest(passwordTextField)
        .handleEvents(receiveOutput: { (username, password) in
            print("Username: \(username), password: \(password)")
            let isSatisfied = username.count >= 5 && password.count >= 8
            isButtonEnabled.send(isSatisfied)
        })
        .sink(receiveValue: { _ in })
        .store(in: &subscriptions)
    
    isButtonEnabled
        .sink { print("isButtonEnabled: \($0)") }
        .store(in: &subscriptions)
    
    usernameTextField.send("user")
    usernameTextField.send("user12")
    
    passwordTextField.send("12")
    passwordTextField.send("12345678")
}


Une fois usernameTextField et passwordTextFieldrecevoir user12, et en 12345678conséquence, la condition est satisfaite, et le bouton est activé:







6. zip



L'opérateur .zipfournit une paire de valeurs correspondantes de chaque éditeur. Disons que nous voulons déterminer si les deux éditeurs ont publié la même valeur Int:



func zipExample() {
    let intSubject1 = PassthroughSubject<Int, Never>()
    let intSubject2 = PassthroughSubject<Int, Never>()
    
    let foundIdenticalPairSubject = PassthroughSubject<Bool, Never>()
    
    intSubject1
        .zip(intSubject2)
        .handleEvents(receiveOutput: { (value1, value2) in
            print("value1: \(value1), value2: \(value2)")
            let isIdentical = value1 == value2
            foundIdenticalPairSubject.send(isIdentical)
        })
        .sink(receiveValue: { _ in })
        .store(in: &subscriptions)
    
    foundIdenticalPairSubject
        .sink(receiveValue: { print("is identical: \($0)") })
        .store(in: &subscriptions)
    
    intSubject1.send(0)
    intSubject1.send(1)
    
    intSubject2.send(4)
    
    intSubject1.send(6)
    intSubject2.send(1)
    intSubject2.send(7)
    
    intSubject2.send(9) //  ,       
}


Nous avons les valeurs correspondantes suivantes de intSubject1et intSubject2:



  • 0 et 4
  • 1 et 1
  • 6 et 7


Cette dernière valeur n'est 9pas affichée car la intSubject1valeur correspondante n'a pas encore été publiée:







Ressources



Le code source est disponible sur le Gist .



Conclusion



Intéressé par d'autres types d'opérateurs de moissonneuse-batteuse? N'hésitez pas à visiter mes autres articles:






All Articles