Make your codebase more reactive with ReactiveSwift and protocol extensions
Lately, I’ve been working on a fun side project involving the MailCore2 library. This tool allows you to work with emails protocols, such as IMAP and SMTP in my case. Mailcore2 is a cross platform library written in C++ with native bindings for the iOS, Android, Windows and Linux platforms. It’s super easy to use (and works with Carthage!) but as it’s often the case with 3rd party libraries: interfacing with ReactiveSwift is not pretty.
/// Fetch a message from the INBOX folder by its unique id
func fetchMessage(with uid: UInt32) -> SignalProducer<Item, TimelineError> {
return SignalProducer { sink, disposable in
let operation = session.fetchMessageByUIDOperation(withFolder: "INBOX", uid: uid)
operation?.start { error, messageContent in
// handle errors, decode message...
}
disposable.add {
operation?.cancel()
}
}
}
When ReactiveCocoa 3 was released, the API exposed a rac_dataWithRequest
extension on NSURLSession
to perform a HTTP request the “reactive” way: you would rely on a SignalProducer instead of the “normal” approach using a completion block.
let URLRequest = NSURLRequest(...)
let producer = NSURLSession.sharedSession().rac_dataWithRequest(URLRequest)
producer.startWithNext { data, URLResponse in
/// Handle error, decode data...
}
This project is using the latest version of ReactiveSwift that comes with a much more clever way of “Reactifying” your code: ReactiveExtensionsProvider. If you look at the code, conforming to this protocol adds a reactive
parameter on your object. Thanks to protocol extensions, this parameter returns your object wrapped in a Reactive struct
you can add methods to. In our case, we want the code that fetches a message to be made more reactive.
extension MCOIMAPFetchContentOperation: ReactiveExtensionsProvider {}
public enum MailCoreError: Error {
case internalError(Error)
case unknownError
}
extension Reactive where Base: MCOIMAPFetchContentOperation {
func content() -> SignalProducer<MCOMessageParser, MailCoreError> {
return SignalProducer { [base = self.base] sink, disposable in
disposable += {
base.cancel()
}
base.start { error, data in
if let error = error {
sink.send(error: .internalError(error))
} else if let data = data {
sink.send(value: MCOMessageParser(data: data))
sink.sendCompleted()
} else {
sink.send(error: .unknownError)
}
}
}
}
}
func fetchMessage(with uid: UInt32) -> SignalProducer<Message, MailCoreError> {
let operation = session.fetchMessageByUIDOperation(withFolder: "INBOX", uid: uid)
return operation!.reactive.content().map { /* map to Message */}
}
It’s not much more pretty but the code making this operation reactive is now isolated and easily testable. I, for one, am super happy with how the contributors have shapped ReactiveCocoa 5: it’s amazing to use.