Categories
iOS Swift

TaskGroup error handling in Swift

Task groups in Swift are used for running n-number of child tasks and letting it handle things like cancellation or different priorities. Task groups are created with either withThrowingTaskGroup(of:returning:body:) or withTaskGroup(of:returning:body:). The latter is for cases when errors are not thrown. In this blog post, we will observe two cases of generating Data objects using a task group. In the first case, we want to stop the group as soon as an error has occurred and discard all the remaining work. The other case looks at ignoring any errors in child tasks and collect just collecting Data objects for tasks which were successful.

The example we are going to use simulates creating image data for multiple identifiers and then returning an array of Data objects. The actual image creating and processing is simulated with Task’s sleep function. Since task groups coordinate cancellation to all the child tasks, then the processor implementation also calls Task.checkCancellation() to react to cancellation and stopping as soon as possible for avoiding unnecessary work.

struct ImageProcessor {
static func process(identifier: Int) async throws -> Data {
// Read image data
try await Task.sleep(nanoseconds: UInt64(identifier) * UInt64(1e8))
try Task.checkCancellation()
// Simulate processing the data and transforming it
try await Task.sleep(nanoseconds: UInt64(1e8))
try Task.checkCancellation()
if identifier != 2 {
print("Success: \(identifier)")
return Data()
}
else {
print("Failing: \(identifier)")
throw ProcessingError.invalidData
}
}
enum ProcessingError: Error {
case invalidData
}
}

Now we have the processor created. Let’s see an example of calling this function from a task group. As soon as we detect an error in one of the child tasks, we would like to stop processing and return an error from the task group.

let imageDatas = try await withThrowingTaskGroup(of: Data.self, returning: [Data].self) { group in
imageIdentifiers.forEach { imageIdentifier in
group.addTask {
return try await ImageProcessor.process(identifier: imageIdentifier)
}
}
var results = [Data]()
for try await imageData in group {
results.append(imageData)
}
return results
}
view raw Case1.swift hosted with ❤ by GitHub

We loop over the imageIdentifiers array and create a child task for each of these. When child tasks are created and running, we wait for child tasks to finish by looping over the group and waiting each of the child task. If the child task throws an error, then in the for loop we re-throw the error which makes the task group to cancel all the remaining child tasks and then return the error to the caller. Since we loop over each of the task and wait until it finishes, then the group will throw an error of the first added failing task. Also, just to remind that cancellation needs to be handled explicitly by the child task’s implementation by calling Task.checkCancellation().

Great, but what if we would like to ignore errors in child tasks and just collect Data objects of all the successful tasks. This could be implemented with withTaskGroup function by specifying the child task’s return type optional and handling the error within the child task’s closure. If error is thrown, return nil, and later when looping over child tasks, ignore nil values with AsyncSequence’s compactMap().

let imageDatas = await withTaskGroup(of: Data?.self, returning: [Data].self) { group in
imageIdentifiers.forEach { imageIdentifier in
group.addTask {
do {
return try await ImageProcessor.process(identifier: imageIdentifier)
} catch {
return nil
}
}
}
var results = [Data]()
for await imageData in group.compactMap({ $0 }) {
results.append(imageData)
}
return results
}
view raw Case2.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
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
iOS Swift Swift Package

Most visited blog posts in 2022

Time to take a look at most visited blog posts in 2022 in two categories: most visited posts written in 2022 and most visited posts in overall.

Top 10 written in 2022

Top 10 overall

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
iOS Swift

Reading GraphQL queries from URLRequest in network tests

Back in 2019 I wrote about Testing networking code with custom URLProtocol on iOS. Recently, I was using the same approach for setting up network mocking for GraphQL requests, but then I stumbled on something unexpected. If we create an URLRequest which has httpBody set, then inside the custom URLProtocol implementation, httpBody method returns nil, and we need to access httpBodyStream instead. When dealing with GraphQL requests, the GraphQL query is part of the data set to httpBody. Reading the data from httpBody property would be straight-forward, then httpBodyStream on the other hand returns an InputStream and this requires a bit of code. InputStreams can only be read once, therefore while inspecting the URLRequest inside the URL protocol, we need to make sure not to read it twice. This is a nature of how input streams work.

In the snippet below, we can see a Data extension which adds a new initializer which takes in an InputStream. The implementation opens the stream, closes it when initializer is exited, reads data 512 bytes at the time. The read function of the InputStream returns number of read bytes if successful, negative value on error and 0 when buffer end has been reached which means that there is noting more to read.

extension Data {
init(inputStream: InputStream) throws {
inputStream.open()
defer { inputStream.close() }
self.init()
let bufferSize = 512
var readBuffer = [UInt8](repeating: 0, count: bufferSize)
while inputStream.hasBytesAvailable {
let readBytes = inputStream.read(&readBuffer, maxLength: bufferSize)
switch readBytes {
case 0:
break
case ..<0:
throw inputStream.streamError!
default:
append(readBuffer, count: readBytes)
}
}
}
}

Let’s see how to use this extension and reading a GraphQL query from the URLRequest. In the example below we can see that the example GraphQL data is set to a dictionary with 3 keys: query, operationName, and variables. Therefore, we need to first turn the InputStream into Data and then decoding the data to a model type and after that reading the query. Since we know the query, we can proceed with writing network tests where the mocked response depends on the query.

// Example data set to URLRequest httpBody
// {
// "query": "query HeroNameAndFriends($episode: Episode) { hero(episode: $episode) { name friends { name } } }",
// "operationName": "",
// "variables": { "episode": "JEDI" }
// }
struct Payload: Decodable {
let query: String
}
let outputData = try Data(inputStream: httpBodyStream)
let payload = try JSONDecoder().decode(Payload.self, from: outputData)
print(payload.query)
// Prints: query HeroNameAndFriends($episode: Episode) { hero(episode: $episode) { name friends { name } } }
view raw Usage.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
iOS Swift Swift Package

Setting up a build tool plugin for a Swift package

Swift package manager supports creating command and build plugins. In this blog post we’ll take a closer look at build plugins since these enable us to tap into the package build process and add extra steps like generating some code for the package. Command plugins, on the other hand, add extra commands which we can invoke from the command line or from Xcode since Xcode automatically exposes these commands in the UI. The example problem we are going to tackle is creating a build plugin which takes in a JSON and outputs some Swift code which in turn is used by the package. For making things more interesting, we’ll hook up a custom executable target which contains custom logic for generating that code. In many other cases, we could use existing tools like Sourcery and skip our own executable target.

Plugins run in a sandbox environment, so they can only write to a pluginWorkDirectory. So in our case we’ll write our generated code in that folder and tell the build command to include the generated file while building the target. This wasn’t immediately clear for me that this happens automatically if I set the generated file path as an output file of the build command. But let’s start with setting up a build plugin and then adding the executable target which the build plugin ends up calling and which generates the code.

We’ll name the build plugin as “ToomasKitPlugin” since the example package is named as “ToomasKit”. Plugins need to be defined in the Package.swift where we also let the Swift build system know which target runs the plugin. The final Package.swift looks like this which includes a library with its target and testing target, plugin, and executable target.

// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "ToomasKit",
platforms: [
.iOS(.v16)
],
products: [
.library(
name: "ToomasKit",
targets: ["ToomasKit"]
),
],
targets: [
.target(
name: "ToomasKit",
plugins: [
.plugin(name: "ToomasKitPlugin")
]
),
.executableTarget(
name: "CodeGenerator"
),
.plugin(
name: "ToomasKitPlugin",
capability: .buildTool(),
dependencies: ["CodeGenerator"]
),
.testTarget(
name: "ToomasKitTests",
dependencies: ["ToomasKit"]
),
]
)
view raw Package.swift hosted with ❤ by GitHub

Here we can see that the plugin goes under the targets section and since we are interested in creating build plugins the capability is set to buildTool. The other option is command plugin, which was mentioned before. Also, we add a plugin dependency since the plugin ends up calling an executable which we will create ourselves. Secondly, we need to add the plugin to the library target, since Swift build system needs to know which target runs this plugin. Time to create the build plugin. Plugins go, by default, to <PackageRoot>/Plugins folder and since our plugin is named as ToomasKitPlugin the full path is <PackageRoot>/Plugins/ToomasKitPlugin. Build tool plugins need to conform to BuildToolPlugin protocol, and we also need to annotate the struct with @main for letting Swift know which type is the entry point of the plugin. Build tool plugins need to implement a createBuildCommands function which returns a list of build commands to run. At the time of writing the post there are available pre-build and build commands where the former runs every single time we build and the latter only when there is a change in input or output files which the command refers to. In our case, the input file is a JSON file and the output is the generated Swift file. The JSON file is part of the package target, so we can get a path to the file using the target.directory API. The output can only be written to pluginWorkDirectory since plugins run in a sandbox. All the paths which are added to outputFiles get included with the build. Since we are generating Swift code the package will use, we add it to outputFiles. Now when we have the plugin configured, let’s take a look at the CodeGenerator implementation, which is an executable target the plugin runs.

import Foundation
import PackagePlugin
@main
struct ToomasKitPlugin: BuildToolPlugin {
func createBuildCommands(context: PackagePlugin.PluginContext, target: PackagePlugin.Target) async throws -> [PackagePlugin.Command] {
let inputJSON = target.directory.appending("Source.json")
let output = context.pluginWorkDirectory.appending("GeneratedEnum.swift")
return [
.buildCommand(displayName: "Generate Code",
executable: try context.tool(named: "CodeGenerator").path,
arguments: [inputJSON, output],
environment: [:],
inputFiles: [inputJSON],
outputFiles: [output])
]
}
}

The CodeGenerator executable target takes in two paths: input and output, where the input is the JSON file and output is the final generated Swift file. It will then decode the JSON, generate an enum based on the JSON content and writes it to the output file. This is just a simple example, but I would like to highlight the fact that using swift-argument-parser probably makes sense for a little bit more complicated executables.

@main
@available(macOS 13.0.0, *)
struct CodeGenerator {
static func main() async throws {
// Use swift-argument-parser or just CommandLine, here we just imply that 2 paths are passed in: input and output
guard CommandLine.arguments.count == 3 else {
throw CodeGeneratorError.invalidArguments
}
// arguments[0] is the path to this command line tool
let input = URL(filePath: CommandLine.arguments[1])
let output = URL(filePath: CommandLine.arguments[2])
let jsonData = try Data(contentsOf: input)
let enumFormat = try JSONDecoder().decode(JSONFormat.self, from: jsonData)
let code = """
enum \(enumFormat.name): CaseIterable {
\t\(enumFormat.cases.map({ "case \($0)" }).joined(separator: "\n\t"))
}
"""
guard let data = code.data(using: .utf8) else {
throw CodeGeneratorError.invalidData
}
try data.write(to: output, options: .atomic)
}
}
struct JSONFormat: Decodable {
let name: String
let cases: [String]
}
@available(macOS 13.00.0, *)
enum CodeGeneratorError: Error {
case invalidArguments
case invalidData
}

If we build the package, we can see that the plugin runs our CodeGenerator, the generated Swift file gets included with the package, and we can use the generated code in other files. Exactly what we wanted.

The full example project can be found here: SwiftExampleToomasKit.

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
iOS Swift Swift Package

Handling never finishing async functions in Swift package tests

Why does my CI never finish and post a message to the merge request? Logged in to CI and oh, my merge job had been running for 23 minutes already, although typically it finishes in 4 minutes. What was going on? Nothing else than on unit-test marked with async was still waiting for an async function to finish. So what can we to avoid this? Let’s first create a Swift package which will be demonstrating the issue.

struct ImageLoader {
func loadImage(for identifier: String) async throws -> UIImage {
// Delay for 100 seconds
try await Task.sleep(nanoseconds: UInt64(100 * 1e9))
return UIImage()
}
}

And a simple unit-test for the successful case.

final class ImageLoaderTests: XCTestCase {
func testLoadingImageSuccessfully() async throws {
let imageLoader = ImageLoader()
_ = try await imageLoader.loadImage(for: "identifier")
}
}

This test passes after 100 seconds, but clearly, we do not want to wait so long if something takes way too much time. Instead, we want to fail the test when it is still running after 5 seconds.

Exploring XCTestCase executionTimeAllowance

XCTestCase has a property called executionTimeAllowance what we can set. Ideally I would like to write something like executionTimeAllowance = 5 and Xcode would fail the test with a timeout failure after 5 seconds.

override func setUpWithError() throws {
executionTimeAllowance = 5 // gets rounded up to 60
}

But if we read the documentation, then it mentions that the value set to this property is rounded up to the nearest minute value. In addition, this value is not used if you do not enable it explicitly: “To use this setting, enable timeouts in your test plan or set the -test-timeouts-enabled option to YES when using xcodebuild.”. If we are working on a Swift package, then I am actually not sure how to set it in the Package.swift so that it gets set when running the test from Xcode or from a command line.

Custom test execution with XCTestExpectation

One way to avoid never finishing tests is to use good old XCTestExpectation. We can set up a method which runs the async work and then waits for the test expectation with a timeout. If a timeout occurs, the test fails. If the async function throws an error, we can capture it, fail the test with XCTFail.

final class ImageLoaderTests: XCTestCase {
func testLoadingImageSuccessfully() {
execute(withTimeout: 5) {
let imageLoader = ImageLoader()
_ = try await imageLoader.loadImage(for: "identifier")
}
}
}
extension XCTestCase {
func execute(withTimeout timeout: TimeInterval, file: StaticString = #filePath, line: UInt = #line, workItem: @escaping () async throws -> Void) {
let expectation = expectation(description: "wait for async function")
var workItemError: Error?
let captureError = { workItemError = $0 }
let task = Task {
do {
try await workItem()
}
catch {
captureError(error)
}
expectation.fulfill()
}
waitForExpectations(timeout: timeout) { _ in
if let error = workItemError {
XCTFail("\(error)", file: file, line: line)
}
task.cancel()
}
}
}

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
iOS Swift SwiftUI

Wrapping delegates for @MainActor consumers in Swift

Sometimes we need to handle delegates in a class which has the @MainActor annotation. Often it can be a view model where we expect that code runs on the main thread. Therefore, view models have the @MainActor annotation, since we want that their methods run on the main thread when interacting with other async code. In an example below, we’ll be looking into integrating a delegate based ImageBatchLoader class which calls delegate methods on a background thread. The end goal is to handle the delegate in a view model and making sure it runs on the main thread.

final class ImageBatchLoader {
weak var delegate: ImageBatchLoaderDelegate?
init(delegate: ImageBatchLoaderDelegate) {
self.delegate = delegate
}
func start() {
DispatchQueue.global().async {
self.delegate?.imageLoader(self, didLoadBatch: [UIImage()])
}
}
}
protocol ImageBatchLoaderDelegate: AnyObject {
func imageLoader(_ imageLoader: ImageBatchLoader, didLoadBatch batch: [UIImage])
}
An example ImageBatchLoader with stubbed out start method.

This is an example of a class which uses delegates and calls delegate methods from background threads. If we have a view model with @MainActor annotation, then we just can’t conform to that delegate since the delegate does not use any async-await support. Xcode would show a warning saying that the protocol is non-isolated. A protocol would be isolated if it would have, for example, @MainActor annotation as well for that protocol. Let’s say this is not possible and it is a third party code instead.

The solution I have personally settled with is creating a wrapper class which conforms to that delegate and then uses main thread bound closures to notify when any of the delegate callbacks happen.

final class ImageBatchLoaderHandler: ImageBatchLoaderDelegate {
var didLoadBatch: @MainActor ([UIImage]) -> Void = { _ in }
func imageLoader(_ imageLoader: ImageBatchLoader, didLoadBatch batch: [UIImage]) {
print("isMainThread", Thread.isMainThread, #function)
Task {
await didLoadBatch(batch)
}
}
}

Here we can see a class which conforms to the ImageBatchLoaderDelegate and provides a didLoadBatch closure which has an @MainActor annotation. Since we use @MainActor and tap into the async-await concurrency, then we need an async context as well, which the Task provides.

@MainActor final class ViewModel: ObservableObject {
private let imageLoader: ImageBatchLoader
private let imageLoaderHandler: ImageBatchLoaderHandler
init() {
imageLoaderHandler = ImageBatchLoaderHandler()
imageLoader = ImageBatchLoader(delegate: imageLoaderHandler)
imageLoaderHandler.didLoadBatch = handleBatch
imageLoader.start()
}
func handleBatch(_ batch: [UIImage]) {
print("isMainThread", Thread.isMainThread, #function)
// redacted
}
}
view raw ViewModel.swift hosted with ❤ by GitHub

Finally we have hooked up the image loader, its handler and also forwarding the didLoadBatch to a separate function which is part of the view model. With a little bit of code, we achieved what we wanted: listening to delegate callbacks and forwarding them to the view model on the main thread. If we ran the code we would see that the delegate callback runs on a background thread but the view model method runs on the main thread.

isMainThread false imageLoader(_:didLoadBatch:)
isMainThread true handleBatch(_:)

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
iOS Swift SwiftUI

Wrapping a long-running function with async-await in Swift

In this blog post, we are going to take a look at a case where we have a long-running synchronous function, and we want to wrap this with an async version. The aim is that the long-running function runs on a background thread instead and keeps the UI snappy. We’ll be using structured concurrency features. In the example code, we’ll also add print statements to see what thread the code is running on.

struct ImageProcessor {
func syncProcessData(_ imageData: Data) -> UIImage {
print(#function, "started", Thread.isMainThread, Thread.current)
Thread.sleep(forTimeInterval: 10) // simulates a blocking operation
print(#function, "finished", Thread.isMainThread, Thread.current)
return UIImage(systemName: "sun.max")!
}
}
A synchronous function blocking the current thread for 10 seconds.

The very first step is adding an async version, which is a matter of wrapping it with a Task.

struct ImageProcessor {
func process(_ imageData: Data) async -> UIImage {
print(#function, Thread.isMainThread, Thread.current)
let image = await Task {
syncProcessData(imageData)
}.value
print(#function, Thread.isMainThread, Thread.current)
return image
}
}
An async process function wrapping a synchronous long-running function.

What happens inside the function is that we created a new Task object which defines the work we want to run. Since we want to return an instance of UIImage as part of this function, we need to wait until the created task finishes. Therefore, we access the return value of the task using the value property which is an async property. Since it is async, we need to use the await keyword which tells the runtime that the function flow could be suspended here. As the process function is async then this function can only be called from an async context. For example, this function is called from another task. Then the task is added as a child task to the calling, parent, task. OK, so let’s use this code in a view model used by a SwiftUI view.

@MainActor
final class ViewModel: ObservableObject {
let imageProcessor = ImageProcessor()
@Published var currentImage: UIImage?
func applyEffectsToImage() {
print(#function, Thread.isMainThread, Thread.current)
Task {
print(#function, Thread.isMainThread, Thread.current)
let imageData = Data()
currentImage = await imageProcessor.process(imageData)
}
}
}
view raw ViewModel.swift hosted with ❤ by GitHub
A view model which triggers the asynchronous work.

Above is a view model which is used by a SwiftUI view. The applyEffectsToImage() function is called by a button and the published image is displayed by the view. This view model is a main actor which means that all the properties and functions will run on the main thread. Since we want to call an async process function, we need to create an async context. This is where the Task comes into the play again. If we do not have an async context and create a task then that task will run on the actor it was created on. In this case, it runs on the main thread. But if the task creates a child task then that task will run on a background thread. In the example above, the task’s closure runs on the main thread until the closure is suspended when calling an async process function.

If we take a look at which threads are used, then it looks like this:

applyEffectsToImage() true <_NSMainThread: 0x600000908300>{number = 1, name = main}
applyEffectsToImage() true <_NSMainThread: 0x600000908300>{number = 1, name = main}
process(_:) false <NSThread: 0x600000901040>{number = 4, name = (null)}
syncProcessData(_:) started false <NSThread: 0x600000979100>{number = 7, name = (null)}
syncProcessData(_:) finished false <NSThread: 0x600000979100>{number = 7, name = (null)}
process(_:) false <NSThread: 0x600000979100>{number = 7, name = (null)}

Here we can see that the task in the view model runs on the main actor and therefore on the main thread. Methods in the image processor run on the background threads instead – exactly what we wanted. The long-running function does not block the main thread.

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
iOS Swift SwiftUI Xcode

Getting started with mocking networking in UI-tests on iOS

It is important to have stable unit-tests and UI-tests since no-one wants to encounter failures in tests which happen non-deterministically. Many of the iOS apps rely on networking, and therefore the content depends on what is fetched from servers. The last thing what we want to see is that an API outage or instability affects UI-tests. This time we’ll take a look at how to mock networking in UI-tests which differs from mocking networking in unit-tests since the whole app is going to be running as is and is controlled by a separate UI-testing runner app. A while ago I also covered unit-testing part in Testing networking code with custom URLProtocol on iOS, please take a look at that post for more information since we’ll be using the same custom URL protocol approach.

The main difference between unit-tests and UI-tests is that with unit-tests Xcode injects the unit-testing code into the app and then runs tests. UI-testing on the other hand rely on a separate test runner app which uses accessibility APIs for driving the user-interface in the app. This means that our network mocking code needs to be bundled with the app when we build it. The approach we are going to use is setting up a separate “UITestingSupport” Swift package, which is included only in debug builds. This library contains mocked data and configures the custom URL protocol and handles any network requests. Please see Linking a Swift package only in debug builds for more information on how to only link a package in debug builds.

I’ll be using a sample app “UITestingNetworking” for demonstrating how to set it up. The app has an app target and a local Swift package with a name “UITestingSupport”.

Xcode project layout with UITestingSupport package.

The first piece of the “UITestingSupport” package is a custom URLProtocol. All it does is providing a way to return either error or URLResponse and Data for a URLRequest. It is a simplified protocol. In an actual app we would want to control which requests are handled by it and which are not. Either because it is way too difficult to mock all the network request in all the tests at first, or we might also want to have some tests using an actual data coming from servers.

final class UITestingURLProcotol: URLProtocol {
override class func canInit(with request: URLRequest) -> Bool {
return true // TODO: only return true for requests we have mocked data
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
struct ResponseData {
let response: URLResponse
let data: Data
}
static var responseProvider: ((URLRequest) -> Result<ResponseData, Error>)?
override func startLoading() {
guard let client else { fatalError() }
if let responseProvider = Self.responseProvider {
switch responseProvider(request) {
case .success(let responseData):
client.urlProtocol(self, didReceive: responseData.response, cacheStoragePolicy: .notAllowed)
client.urlProtocol(self, didLoad: responseData.data)
client.urlProtocolDidFinishLoading(self)
case .failure(let error):
client.urlProtocol(self, didFailWithError: error)
client.urlProtocolDidFinishLoading(self)
}
}
else {
let error = NSError(domain: "UITestingURLProcotol", code: -1)
client.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
}

The second piece of the library is a UITestingNetworkHandler class which the app code will call, and it configures the custom URLProtocol and starts providing responses based on the “responseProvider” callback.

public final class UITestingNetworkHandler {
public static func register() {
URLProtocol.registerClass(UITestingURLProcotol.self)
UITestingURLProcotol.responseProvider = { request in
guard let url = request.url else { fatalError() }
switch (url.host, url.path) {
case ("augmentedcode.io", "/api/example"):
let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let data = "MyMockedData".data(using: .utf8)!
return .success(UITestingURLProcotol.ResponseData(response: response, data: data))
default:
fatalError("Unhandled")
}
}
}
}
Simple example of providing response for a URLRequest.

The example above just handles one network request. For larger apps we probably want to have more component based implementation here since this file would otherwise grow a lot based on how many cases we want to handle. Another thing to note is that in some tests we want to mimic network request failures and in others successful requests but with different response data. This is not shown above, but can be implemented by providing the expected configuration flag through environment variables. XCUIApplication has a launchEnvironent property what we can set and then reading that value in the UITestingNetworkHandler with Process environment property. I’m thinking something like “MyAppAPIExampleResponseType” which equals to a number or some identifier.

The last piece is to call the register code when we are running the app in UI-tests.

@main
struct UITestingNetworkingApp: App {
var body: some Scene {
WindowGroup {
ContentView()
#if DEBUG
.onAppear(perform: {
guard CommandLine.arguments.contains("–uitesting") else { return }
UITestingNetworkHandler.register()
})
#endif
}
}
}
view raw App.swift hosted with ❤ by GitHub
Calling register only in debug builds and when a launch argument is set by the runner app.

And finally, an example UI-test which taps a button which in turn fetches some data from the network and then just displays the raw data in the app.

class UITestingNetworkingUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testExample() throws {
let app = XCUIApplication()
app.launchArguments = ["–uitesting"]
app.launch()
app.buttons["Load Data"].tap()
XCTAssertEqual(app.staticTexts.element.label, "MyMockedData")
}
}
An example UI-test which sets launch argument which enables network mocking in the app.

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.

Example Project

UITestingNetworking (GitHub)

Categories
iOS Xcode

Comparing build speeds of Xcode 13 and Xcode 14b2

Xcode 14 beta 2 came with WWDC’22 and one of the highlights of the upcoming Xcode is faster builds. What’s new in Xcode mentions that Xcode 14 can be up to 25% faster, mostly due to improved parallelism. For testing this out, I looked for a large open source iOS app. WordPress for iOS is available on GitHub, and it was pretty easy to set up the local development setup.

My 16″ MBP is from 2019 and has 2,3 GHz 8-Core Intel Core i9 with 32 GB 2667 MHz DDR4 RAM. Benchmarking involved in deleting derived data, letting the Mac idle a couple of minutes to cool a bit, and then building the project. Also, I switched between Xcodes after every build. Let’s look into results.

Xcode 13.4.1Xcode 14 beta 2
293314
288317
279318
Build times for WordPress for iOS project

Results are in, and I have to admit that this is something I did not expect to see. I was expecting to see Xcode 14 doing much better compared to Xcode 13, but it is actually the other way around. Probably this project has many ways to improve the build times and making it better to use the improved parallelism in Xcode 14, but at the moment it turned out to be around 10% slower instead. Fascinating.

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.