Categories
Combine iOS Swift

Async-await support for Combine’s sink and map

Async-await in Swift is getting more popular as time goes by, but Combine publishers do not have built-in support for it currently. In this blog post, we’ll see how to expand some of the existing publishers.

Async-await supported sink

One case where I have encountered this is when I have wanted to call an async function in sink. Although I could wrap the call with Task within the sink subscriber, it gets unnecessary long if I need to do it in many places. Instead, we can just do it once and add an async-await supported sink subscriber.

extension Publisher where Self.Failure == Never {
func sink(receiveValue: @escaping ((Self.Output) async -> Void)) -> AnyCancellable {
sink { value in
Task {
await receiveValue(value)
}
}
}
}
// Allows writing sink without Task
$imageURL
.compactMap({ $0 })
.sink { [weak self] url in
await self?.processImageURL(url)
}
.store(in: &cancellables)
view raw ViewModel.swift hosted with ❤ by GitHub

Async-await supported map

The Combine framework has map and tryMap for supporting throwing functions, but is lacking something like tryAwaitMap for async throwing functions. Combine has a publisher named Future which supports performing asynchronous work and publishing a value. We can use this to wrap a Task with asynchronous work. Another publisher in Combine is flatMap what is used for turning one kind of publisher to a new kind of publisher. Therefore, we can combine these to turn a downstream publisher to a new publisher of type Future. The first tryAwaitMap below is for a case where the downstream publisher emits errors, and the second one is for the case where the downstream does not emit errors. We need to handle these separately since we need to tell Combine how error types are handled (non-throwing publisher has failure type set to Never).

extension Publisher {
public func tryAwaitMap<T>(_ transform: @escaping (Self.Output) async throws -> T) -> Publishers.FlatMap<Future<T, Error>, Self> {
flatMap { value in
Future { promise in
Task {
do {
let result = try await transform(value)
promise(.success(result))
}
catch {
promise(.failure(error))
}
}
}
}
}
public func tryAwaitMap<T>(_ transform: @escaping (Self.Output) async throws -> T) -> Publishers.FlatMap<Future<T, Error>, Publishers.SetFailureType<Self, Error>> {
// The same implementation but the returned publisher transforms failures with SetFailureType.
}
}
// Case 1: throwing downstream publisher
$imageURL
.tryMap({ try Self.validateURL($0) })
.tryAwaitMap({ try await ImageProcessor.process($0) })
.map({ Image(uiImage: $0) })
.sink(receiveCompletion: { print("completion: \($0)") },
receiveValue: { print($0) })
.store(in: &cancellables)
// Case 2: non-throwing downstream publisher
$imageURL
.compactMap({ $0 })
.tryAwaitMap({ try await ImageProcessor.process($0) })
.map({ Image(uiImage: $0) })
.sink(receiveCompletion: { print("completion: \($0)") },
receiveValue: { print($0) })
.store(in: &cancellables)
view raw ViewModel.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
Swift

Adding prefixMap for expensive operations in Swift

Swift Foundation contains a lot of useful functions on collection types. In the context of this blog post we are interested in map(_:) and prefix(while:). Map is used for transforming collection elements and prefix(while:) for getting a sequence containing initial elements until the predicate is true. In some cases the predicate used in the prefix(while:) can be expensive, or we just want to combine the information in prefix and map functions. One of such examples is when we use NSRegularExpression. More specifically, let’s take an example of processing a list of strings while the regular expression has matches and then extracting a range from the string. A concrete example could be parsing Fastlane’s Fastfile for visualization purposes.

Fastlane is used a lot in the iOS community for automating development related tasks. Lanes are added to a Fastfile where every individual lane has a name and optionally a description.

desc 'Build and upload app store build'
desc 'Captures screenshots, builds the app, uploads to app store, and posts a message to slack'
lane :appstore do
capture_screenshots
build_app
upload_to_app_store
slack
end
view raw Fastfile hosted with ❤ by GitHub

If we would like to extract the lane name and description from the Fastfile then we can use regular expressions. The flow could be something like this: firstly, we can read the Fastfile contents, divide the file into lines and match lines with regular expressions. Second step is finding lines which contain a lane keyword. Then we could loop over preceding lines and collect lines which contain description. All in all, the logic for getting description could look like this:

let descExpression = try! NSRegularExpression(pattern: "^\\s*desc [\"']{1}([^\"]*)[\"']{1}", options: [])
//
let description = (0..<lineIndex)
.reversed()
.prefixMap({ index -> String? in
// Get a line
let line = lines[index]
// Use regular expression
guard let match = descExpression.firstMatch(in: line) else { return nil }
// Regular expression contains a capture group, therefore looking for 2 ranges
guard match.numberOfRanges >= 2 else { return nil }
// When there are matches then extract description
guard let range = Range(match.range(at: 1), in: line) else { return nil }
return String(line[range])
})
.reversed()
.joined(separator: "\n")

Now when we have seen a case where prefixMap can be useful, it is time to look into how it is implemented. The transform passed into the prefixMap function can return nil and the nil value means that the looping should be stopped and all the transformed elements should be returned. And yes, the implementation is pretty straight-forward.

// Swift Foundation
func prefix(while predicate: (Self.Element) throws -> Bool) rethrows -> Self.SubSequence
func map<T>(_ transform: (Self.Element) throws -> T) rethrows -> [T]
// prefixMap (new)
extension Collection {
@inlinable func prefixMap<T>(_ transform: (Self.Element) throws -> T?) rethrows -> [T] {
var result = [T]()
for element in self {
if let transformedElement = try transform(element) {
result.append(transformedElement)
}
else {
break
}
}
return result
}
}

Summary

Foundation types can be extended with new functions easily. Although we could use prefix(while:) first followed with a map(_:) but sometimes we’ll just need to combine functionalities into a single function.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Project

PrefixMapPlayground (Xcode 12.4)