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.