Categories
iOS Swift

Running tasks in parallel with async-await in Swift

Async-await in Swift supports scheduling and running multiple tasks in parallel. One of the benefits is that we can schedule all the async operations at once without worrying about any thread explosions. Thread explosion could have happened with DispatchQueue APIs if our queue is concurrently performing, and we would add a lot of work items to it. The structured concurrency on the other hand makes sure this does not happen by only running a limit amount of tasks at the same time.

Let’s take an example where we have a list of filenames, and we would like to load images for these filenames. Loading is async and might also throw an error as well. Here is an example how to use the TaskGroup:

@MainActor final class ViewModel: ObservableObject {
let imageNames: [String]
init(imageNames: [String]) {
self.imageNames = imageNames
}
func load() {
Task {
let store = ImageStore()
let images = try await withThrowingTaskGroup(of: UIImage.self, body: { group in
imageNames.forEach { imageName in
group.addTask {
try await store.loadImage(named: imageName)
}
}
return try await group.reduce(into: [UIImage](), { $0.append($1) })
})
self.images = images
}
}
@Published var images = [UIImage]()
}
struct ImageStore {
func loadImage(named name: String) async throws -> UIImage {
return …
}
}
view raw ViewModel.swift hosted with ❤ by GitHub

In our view model, we have a load function which creates a task on the main actor. On the main actor because the view model has a @MainActor annotation. The Swift runtime makes sure that all the functions and properties in the view model always run on the main thread. This also means that the line let store runs on the main thread as well because the created task belongs to the main actor. If a task belongs to an actor, it will run on the actor’s executor. Moreover, all the code except the child task’s closure containing loadImage runs on the main thread. This is because our ImageStore does not use any actors. If ImageStore had @MainActor annotation, then everything would run on the main thread and using task group would not make any sense. If we remove the @MainActor from the view model, then we can see that let store starts running on a background thread along with all the other code in the load function. That is a case of unstructured concurrency. Therefore, it is important to think about if code has tied to any actors or not. Creating a task does not mean it will run on a background thread.

But going back to the TaskGroup. Task groups are created with withThrowingTaskGroup or when dealing with non-throwing tasks then withTaskGroup function. This function creates a task group where we can add tasks which run independently. For getting results back from the group, we can use AsyncSequence protocol functions. In this simple example, we just want to collect results and return them. Async sequence has reduce function which we can use exactly for that.

To summarize what we achieved in the code snippet above. We had a list of filenames which we transformed into a list of UIImages by running the transformation concurrently using a task group. In addition, we used MainActor for making sure UI updates always happen on 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

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.

Categories
Xcode

Looking into Xcode 14 beta 1

It is a WWDC week and the first day is over. I am eager to see what Xcode 14 is about to bring and therefore let’s dive into Xcode’s release notes where I pick out some of the changes which caught my interest.

Bitcode is deprecated

Turns out that bitcode is deprecated. In the release notes we can read that bitcode for iOS, tvOS, and watchOS is turned off by default. In addition, bitcode uploads are going to be rejected when using Xcode 14. That is the one I did not expect to change, but it did.

Single 1024*1024 app icon

Whoever has dealt with app icons are going to be happy to hear that in Xcode 14 we can just use a single image for the app icon. Of course, the option is going to be there for using separate images for each of the sizes who need it.

Better parallelism in build system

Release notes mention that Xcode 14 can build targets in parallel with their Swift target dependencies. Sounds like we can hope for faster builds.

Recommended deployment targets

There are new build settings like RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET etc. Not sure yet what are the default values, since I did not find them in Xcode beta 1. But still, interesting addition.

Shell script sandboxing

There is a new build setting ENABLE_USER_SCRIPT_SANDBOXING for turning on sandboxing in shell script build phases.

LLDB swift-healthcheck

It is a frustrating when using a debugger and then any expression is resolving. Seems like there is a way to investigate these issues and possibly solve them with the new swift-healthcheck command. I am gonna definitely check it out.

Interface builder is not forgotten

The platforms State of the Union mentioned Swift and SwiftUI being the future of building apps. But on the other hand, there are multiple changes happening in Interface builder as well. More options and more supported views.

Swift packages and localization

At my work, we have struggled with Swift packages which contain localization. The only way for importing and exporting localizations was having Xcode project only for that. Happy to see that xcodebuild -importLocalizations and -exportLocalizations work with Swift packages.

Better reloading for SwiftUI previews

Many items about improving SwiftUI previews and reloading them when making edits. Sounds like there is going to be less need to trigger render preview manually. That is really nice.

Xcode Server is deprecated

As Xcode Cloud is not any more in beta, Apple is removing Xcode Server from Xcode 14.

That is a quick overview of things coming with Xcode 14. There is so much more. Feel free to dig into Xcode 14 release notes yourself as well.

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 Xcode

CollectionOfOne in Swift

The Swift standard library includes a peculiar struct CollectionOfOne. It just represents a single value as a collection. Since it represents only a single value, then it is more efficient than creating an Array with a single element, which involves in allocating a buffer. Also, all the collection functions require inspecting the buffer whereas CollectionOfOne can hard-code many of these, like count which is always one. It can make a difference in performance critical code, but most of the time it does not make a real difference if we create a CollectionOfOne instance or an Array with one element.

let first = ["a", "b", "c"]
let second = CollectionOfOne("d")
print(first + second)
// ["a", "b", "c", "d"]
An example of appending a single element to an array.

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

LibraryContentProvider in Xcode

LibraryContentProvider protocol in Xcode is a way for adding custom views and view modifiers to the Xcode library, which enables drag and dropping them to the SwiftUI preview. Xcode automatically looks for types implementing the protocol and then adds items to the library. It takes so little code that it makes sense to do it if you are using the library for building views.

As an example, we’ll add a custom SwiftUI view SubtitledButton to the library.

struct SubtitledButton: View {
let title: LocalizedStringKey
let subtitle: LocalizedStringKey
let action: () -> Void
var body: some View {
Button(action: action, label: {
VStack(spacing: 4) {
Text(title)
Text(subtitle)
.font(.footnote)
}
})
}
}

The only thing we need to do is creating a new type which conforms to LibraryContentProvider and then returning the button as a LibraryItem.

struct LibraryContent: LibraryContentProvider {
var views: [LibraryItem] {
return [
LibraryItem(SubtitledButton(title: "Title",
subtitle: "Subtitle",
action: {}),
title: "SubtitledButton",
category: .control)
]
}
}

After adding the new type, Xcode updates the library automatically.

Xcode library view with SubtitledButton showing up.
Xcode library with SubtitledButton.

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

Linking a Swift package only in debug builds

One of the cases where we would like to build debug and release configurations differently is when we want to enable some extra features. A concrete example could be a debug view which has a list of commands or displays additional information. In this blog post, we are going to take a look at a Xcode project which only links a local Swift package in debug builds.

The project setup

In the example project, we have an iOS app and a local Swift package “DebugFeatures”. The Swift package description also contains swiftSettings which defines a DEBUG compilation condition when the package is built with debug configuration. This is just an extra measure to make sure we do not compile any of the code in this package accidentally in release configuration.

.target(
name: "DebugFeatures",
swiftSettings: [
.define("DEBUG", .when(configuration: .debug))
]
),
view raw Package.swift hosted with ❤ by GitHub

Since it is a compilation condition, then we should also wrap our code in the package with if/endif DEFINE.

#if DEBUG
import SwiftUI
public struct DebugView: View {
public init() {}
public var body: some View {
Text("Debug View")
}
}
#endif
view raw DebugView.swift hosted with ❤ by GitHub

Linking the package only in debug builds

The next step is to change app project settings and make sure we do not link the package in release builds. The simplest way for this is to first add the package as the app dependency in “Frameworks, Libraries, and Embedded Content”.

App target's libraries containing DebugFeatures package.
App target’s libraries.

Then we’ll open build settings and look for “Excluded Source File Names” and configure release builds to ignore “DebugFeatures*”.

Build settings configured to ignore the package in release builds.
Build settings configured to ignore the package in release builds.

To verify this change, we can make a release build with shift+command+i (Product -> Build For -> Profiling which builds release configuration). If we check the latest build log with command+9 and clicking on the top most build item, scrolling to app target’s linker step, we can see that Xcode did not link “DebugFeatures”. Exactly what we wanted to achieve.

Output of the linker step of the app target showing that DebugeFeatures package was not linked.
Output of the linker step of the app target.

Summary

This is one way how to link some package only for debug builds. Although it sounds a bit unexpected that “Excluded Source File Names” also removes the package from the linking phase, but I am happy it does since it means only changing one build setting to get it working like this.

Example project

DebugOnlySwiftPackage (Xcode 13.3.1)

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 UIKit

Building a list view with collection view in UIKit

UICollectionViewCompositionalLayout was an important change to how we create collection view layouts. In iOS14 Apple added a new static function to this class which creates a layout object for list views. Meaning, it is very easy to create a list views which look like a table view we are familiar with. The static list() function takes a configuration object UICollectionLayoutListConfiguration which allows further to configure the appearance of the header view. For example, supplementary header views are enabled here. In this blog post, we’ll create a list view with a collection view and use cell registration and diffable data source APIs.

List view created with diffable data source and collection view.

Generating data for the list view

Firstly, we’ll generate some data types which we want to display in the list view. The aim is to represent each Palette type with one section, and each PaletteColor is a row in the section.

struct Palette: Hashable {
let name: String
let colors: [PaletteColor]
// other properties
static let fancy = Palette(name: "Fancy", colors: [
PaletteColor(name: "Red", color: .systemRed),
PaletteColor(name: "Blue", color: .systemBlue),
PaletteColor(name: "Cyan", color: .systemCyan),
PaletteColor(name: "Mint", color: .systemMint),
PaletteColor(name: "Pink", color: .systemPink),
PaletteColor(name: "Teal", color: .systemTeal),
PaletteColor(name: "Green", color: .systemGreen),
PaletteColor(name: "Brown", color: .systemBrown)
])
static let secondary = Palette(name: "Secondary", colors: [
PaletteColor(name: "Label", color: .secondaryLabel),
PaletteColor(name: "Fill", color: .secondarySystemFill)
])
}
struct PaletteColor: Hashable {
let name: String
let color: UIColor
// other properties
}
view raw ListView.swift hosted with ❤ by GitHub

Configuring a collection view instance

We’ll create the collection view instance with a layout object which is configured to display lists. We go for insetGrouped appearance and turn on header views.

final class ListViewController: UIViewController {
private func makeCollectionView() -> UICollectionView {
var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
configuration.headerMode = .supplementary
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = .systemBackground
view.translatesAutoresizingMaskIntoConstraints = false
return view
}
private lazy var collectionView = makeCollectionView()
override func loadView() {
title = "Palettes"
view = UIView(frame: .zero)
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
viewModel.reloadContent(in: dataSource)
}
}
view raw ListView.swift hosted with ❤ by GitHub

Configuring collection view data source and cell registration

The UICollectionViewDiffableDataSource is used for managing the data and also provides cells on demand. We’ll need to define section and item types when creating the data source. With the data we want to display, we’ll use String as a section type and PaletteColor as the item type when the section is just the name of the Palette’s name. In addition, we’ll use the cell and supplementary view registration APIs which keep the logic of creating different cells with a model object in the same place, which I find it to be really nice. Cells and supplementary views we’ll configure using the content configuration APIs which describe the data the cell or supplementary view displays. For list views, there is a specialized UIListContentConfiguration type which supports a variety of appearances. In many cases we do not need any custom cell classes at all since UIListContentConfiguration and UICollectionViewListCell takes care of it for us.

final class ListViewController: UIViewController {
let viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func makeDataSource() -> UICollectionViewDiffableDataSource<String, PaletteColor> {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, PaletteColor> { [viewModel] cell, indexPath, paletteColor in
var configutation = UIListContentConfiguration.cell()
configutation.image = viewModel.cellImage(for: paletteColor)
configutation.text = viewModel.cellTitle(for: paletteColor)
cell.contentConfiguration = configutation
}
let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [viewModel] supplementaryView, elementKind, indexPath in
var configutation = UIListContentConfiguration.groupedHeader()
configutation.text = viewModel.headerTitle(in: indexPath.section)
supplementaryView.contentConfiguration = configutation
}
let dataSource = UICollectionViewDiffableDataSource<String, PaletteColor>(collectionView: collectionView, cellProvider: { collectionView, indexPath, paletteColor in
collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: paletteColor)
})
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
}
return dataSource
}
private lazy var dataSource = makeDataSource()
}
view raw ListView.swift hosted with ❤ by GitHub

Applying a snapshot

The final missing piece is creating a snapshot and applying it to the collection view data source which then tells the collection view what to render. If we would generate a new snapshot with slightly different data then the collection view only renders the changes between snapshots. No need to do this manually on our own.

extension ListViewController {
@MainActor final class ViewModel {
let palettes: [Palette]
init(palettes: [Palette]) {
self.palettes = palettes
}
func reloadContent(in dataSource: UICollectionViewDiffableDataSource<String, PaletteColor>) {
var snapshot = NSDiffableDataSourceSnapshot<String, PaletteColor>()
snapshot.appendSections(palettes.map(\.name))
palettes.forEach({ palette in
snapshot.appendItems(palette.colors, toSection: palette.name)
})
dataSource.apply(snapshot)
}
}
}
view raw ViewModel.swift hosted with ❤ by GitHub

Summary

Diffable data sources with new cell registration APIs make a huge difference in how we implement collection views. Although it might take a bit of time to see how all the new APIs work together, I do not want to go back. Please check the example project for full code.

Example project

UIKitExampleDiffableListView (Xcode 13.3.1)

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

Is existential any a performance problem in Swift?

Swift 5.6 was released in March with Xcode 13.3 and among other changes it introduces a new keyword – any. The Swift evolution proposal for it is SE-0335: existential any. Existential types are used for storing an any kind of value which conforms to a specific protocol. Therefore, whenever we use protocol for defining a property type or function argument, then we are encountering existential types. In Swift 5.6, using the any keyword is optional, but it is expected to be required in Swift 6.

protocol Computable {
func compute() -> Int
}
struct DataCore {
let dataProcessors: Computable
}
// becomes
struct Core {
let dataProcessor: any Computable
}

The evolution proposal’s motivation paragraph mentions that “Existential types in Swift have significant limitations and performance implications.” (SE-0335) and “Existential types are also significantly more expensive than using concrete types.” (SE-0335). Which can lead to thinking that one should try to avoid existential types, meaning protocols, because of performance implications. One might even ask if we should always try to use generics over protocols?

// Should I use this?
struct Core {
let dataProcessor: any Computable
}
// or should I inject dependencies using generics?
struct Core2<T: Computable> {
let dataProcessor: T
}

My answer is to continue using protocols as we have so far. Yes, the protocol based approach needs a tiny bit more CPU cycles as Swift needs to do dynamic dispatch and look up the exact type which conforms to the protocol in runtime, but dynamic dispatch is nothing new. Objective-C was all about dynamic dispatch, and we rarely needed to think about it, only when dealing with performance critical code it could have showed up. Therefore, when reading about existential any it is possible that we get a feeling like we should, because of performance, replace everything, what is possible, with generics. But that should not be a case. Performance becomes relevant only in areas where we need to call a loooot of functions in a very short time. As a silly micro benchmark, I set up a code which created a million structs which require more than 3 word buffer (makes Swift to use heap memory) and then calling a function on these values. I was seeing that 2 ms of CPU time was spent in “__swift_project_boxed_opaque_existential_1”. If I then compared it with generics based implementation, then it was 3 ms faster (7 ms vs 4 ms). But to reiterate, a million values were involved. In summary, there is no need to go and refactor old code which uses existential any and replace it with generics only because of generics being faster. Both have their own use-cases.

A snapshot of the generics based implementation in Time Profiler.
A snapshot of the existential any based implementation in Time Profiler.

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.