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

UIKit navigation with SwiftUI views

Recently I was asked a question about creating an app which has SwiftUI views but no navigation logic in it. Instead, UIKit controls how views are presented. It is a fair question since SwiftUI views have navigation support, but not everything is there if we need to support previous iOS versions as well, or we have a case of an app which have both UIKit and SwiftUI views. Therefore, let’s take a look at on one approach, how to handle navigation on the UIKit side but still use SwiftUI views.

UIHostingController presenting SwiftUI view

SwiftUI views are presented in UIKit views with UIHostingController which just takes in the SwiftUI view. UIHostingController is a UIViewController subclass, therefore it can be used like any other view controller in the view hierarchy. For getting things started, let’s configure SceneDelegate to use an object named FlowCoordinator which will handle navigation logic and then ask it to present a simple SwiftUI view.

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private lazy var flowController = FlowCoordinator(window: window!)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: windowScene)
flowController.showRootView()
window?.makeKeyAndVisible()
}
}
final class FlowCoordinator {
private let window: UIWindow
init(window: UIWindow) {
self.window = window
}
func showRootView() {
let swiftUIView = ContentView()
let hostingView = UIHostingController(rootView: swiftUIView)
window.rootViewController = UINavigationController(rootViewController: hostingView)
}
}
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.green)
}
}
A simple iOS app showing a green view which is rendered with SwiftUI but presented with UIKit
A simple app showing a green view which is rendered with SwiftUI but presented with UIKit.

Inserting the FlowCoordinator into SwiftUI view

The next step is that we want to allow SwiftUI view to control what is presented on the screen. For example, let’s add a button to the SwiftUI view which should present a sheet with another SwiftUI view. The button action needs to be able to talk to the flow coordinator, which controls what is presented on the screen. One way to insert the FlowCoordinator into SwiftUI environment is by conforming to ObservableObject and using the environmentObject() view modifier. Alternative is using EnvironmentValues and defining a key. For more information please check Injecting dependencies using environment values and keys in SwiftUI.

final class FlowCoordinator: ObservableObject {}
let swiftUIView = ContentView()
.environmentObject(self) // self is FlowCoordinator
struct ContentView: View {
@EnvironmentObject var flowController: FlowCoordinator
var body: some View {
VStack(spacing: 8) {
Text("Root view")
Button("Present Sheet", action: flowController.showDetailView)

Presenting a sheet

The sheet presentation code goes into the FlowCoordinator and as an example we show a DetailView which has a button for dismissing itself. Yet again, SwiftUI view just passes the handling to the FlowCoordinator.

final class FlowCoordinator: ObservableObject {
// …
func showDetailView() {
let detailView = DetailView()
.environmentObject(self)
let viewController = UIHostingController(rootView: detailView)
window.rootViewController?.present(viewController, animated: true, completion: nil)
}
func closeDetailView() {
// Needs to be more sophisticated later when there are more views
window.rootViewController?.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
struct DetailView: View {
@EnvironmentObject var flowController: FlowCoordinator
var body: some View {
VStack(spacing: 8) {
Text("Detail view content")
Button("Dismiss", action: flowController.closeDetailView)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.orange)
}
}
A simple iOS app showing an orange sheet which is rendered with SwiftUI but presented with UIKit
A simple app showing an orange sheet which is rendered with SwiftUI but presented with UIKit.

Summary

We created a simple sample app which uses UIKit navigation logic but renders views with SwiftUI views. This kind of setup could be useful for apps which mix UIKit and SwiftUI. But I believe that even in case of that we could still use SwiftUI navigation in sub-flows but could keep using this approach for handling root view navigation.

Example Project

UIKitNavigationWithSwiftUIViews (Xcode 13.2.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 SwiftUI UIKit

Using a multi component picker in a SwiftUI form

SwiftUI has a Picker view available with multiple different styles. One example of when it falls short is when we want to use a multi component picker with wheel style. One way how to try to achieve this is using a HStack with two Picker views, but it does not work very well, especially when trying to show it inside a Form view. So what else we can do? If something can’t be done in SwiftUI then we can use UIKit instead.

In my case, I wanted to create a picker which allows picking a date duration. It would have one wheel for selecting a number and the other wheel for selecting either days, weeks or months.

Screenshot of a SwiftUI form with a two component wheel picker where left wheel selects a number and right wheel selects days, weeks or months.

Firstly, let’s create a tiny struct which is going to hold the state of this picker. It needs to store a numeric value and the unit: days, weeks, months. Let’s name it as DateDuration. Since we want to iterate over the DateDuration.Unit, we’ll conform it to CaseIterable protocol.

struct DateDuration {
let value: Int
let unit: Unit
enum Unit: String, CaseIterable {
case days, weeks, months
}
}

UIPickerView in UIKit can do everything we want, therefore we’ll need to wrap it into a SwiftUI view. This can be done by creating a new type which conforms to UIViewRepresentable protocol. Also, we need a binding which holds the value of the current selection: when the user changes it, the binding communicates the changes back and vice-versa. Additionally, we’ll add properties for configuring values and units. UIPickerView us created and configured in the makeUIView(context:) function. UIPickerView is driven by a data source and a delegate, which means we require a coordinator object as well. Coordinator is part of the UIViewRepresentable protocol.

struct DateDurationPicker: UIViewRepresentable {
let selection: Binding<DateDuration>
let values: [Int]
let units: [DateDuration.Unit]
func makeUIView(context: Context) -> UIPickerView {
let pickerView = UIPickerView(frame: .zero)
pickerView.translatesAutoresizingMaskIntoConstraints = false
pickerView.delegate = context.coordinator
pickerView.dataSource = context.coordinator
return pickerView
}
// …
}

Coordinator is created in the makeCoordinator() function. It is going to do most of the work by providing data to the UIPickerView and handling the current selection. Therefore, we’ll store the selection binding, values, and units in the Coordinator class as well.

struct DateDurationPicker: UIViewRepresentable {
// …
func makeCoordinator() -> Coordinator {
return Coordinator(selection: selection, values: values, units: units)
}
final class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
let selection: Binding<DateDuration>
let values: [Int]
let units: [DateDuration.Unit]
init(selection: Binding<DateDuration>, values: [Int], units: [DateDuration.Unit]) {
self.selection = selection
self.values = values
self.units = units
}
// …
}
}

The last missing piece is implementing UIPickerViewDataSource and UIPickerViewDelegate methods in the Coordinator class. This is pretty straight-forward to do. We’ll need to display two components where the first component is the list of values and the second component is the unit: days, weeks, months. When the user selects a new value, we’ll change the DateDuration value of the binding.

func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 2
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return component == 0 ? values.count : units.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
return "\(values[row])"
}
else {
return units[row].rawValue
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
let valueIndex = pickerView.selectedRow(inComponent: 0)
let unitIndex = pickerView.selectedRow(inComponent: 1)
selection.wrappedValue = DateDuration(value: values[valueIndex], unit: units[unitIndex])
}

Finally, let’s hook it up in an example view.

struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
Form {
Section {
// …
DateDurationPicker(
selection: $viewModel.selection,
values: Array(1..<100),
units: DateDuration.Unit.allCases
)
// …
}
}
.navigationTitle("Reminders")
}
}
}
extension ContentView {
final class ViewModel: ObservableObject {
@Published var selection = DateDuration(value: 1, unit: .days)
// …
}
}

Example Project

SwiftUIDateDurationPicker (GitHub, Xcode 13.2.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

Using XCTExpectFailure in XCTests

XCTExpectFailure is a function in XCTest framework which tackles the problem of managing failing tests. Test which are broken, but not ready to be fixed. An example use-case is when refactoring code which causes some tests to fail but since the whole refactoring is far for complete we might want to hold off fixing tests now because everything can change yet again. This is when we can use XCTExpectFailure function to mark the entire test or just a code block in a test as an expected failure. Every time we run tests, the test code runs but if it fails, the expected failure message is added to the test result. In addition, this failure does not fail the entire suit. This is important when there is a CI pipeline set up which requires that there are no failing tests. In reality the test is failing, but we have marked it to be OK for now. Something to keep in mind is that XCTest framework also has a XCTSkip functions. The main difference between XCTExpectFailure and XCTSkip is that the latter stops the test code execution immediately, where the former always runs the test. Therefore, XCTExpectFailure allows us to see when the test is passing again. Might be that after a lot of refactorings, the code is in a shape again where it produces expected output.

The simplest usage of XCTExpectFailure takes no arguments. If an error is thrown inside the test then the test is marked as an expected failure, but if the test does not throw any errors then XCTExpectFailure fails the test. This is due to the fact that XCTExpectFailure is strict by default strict. The strict mode means that a failure must be happening in the test or otherwise XCTExpectFailure fails the tests. When taking that refactoring example again, then that is useful for removing XCTExpectFailure calls as soon as the test starts passing. If non-strict mode is used, then the test is marked as passed if no failures are happening, although XCTExpectFailure is used within that test. An example use-case is that we’ll clean up all XCTExpectFailures at the end of the refactoring cycle when all the tests are passing again. In addition to the strict flag, there is also enabled flag which can be used for disabling the XCTExpectFailure based on environment or any other reason on run-time. Another thing what we can configure is what kind of failure is observed by passing in an issue matcher closure, where we can inspect the issue and decide if it should be considered as an expected failure or not in fine detail. An example could be that there is a very specific error thrown which we want to consider as expected failure, but all the other failures should lead to failing the test itself.

Different usages of the XCTExpectFailure are shown below.

XCTExpectFailure("Will be fixed in ticket XXX")
XCTExpectFailure("Will be fixed in ticket XXX", strict: false)
XCTExpectFailure("Will be fixed in ticket XXX", enabled: true, strict: false) {
    XCTAssertEqual(validateData(), false)
}
XCTExpectFailure("Will be fixed in ticket XXX", enabled: false, strict: false, failingBlock: {
    XCTAssertEqual(validateData(), false)
}, issueMatcher: { issue in
    issue.type == .assertionFailure
})

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

Getting started with UIButton.Configuration on iOS

UIButton.Configuration API was introduced in WWDC’21 with a goal of making it easier to create buttons with many styles, sizes, colours etc. Configurations API comes with functions for creating common button configurations: filled(), bordered(), borderedProminent(), borderedTinted(), borderless(), plain(), gray(), tinted(). These configurations can be further customized. One benefit of the API is that it will take care of different states of the button like highlighted, disabled, hovered and so on and applies different styling based on the state. Most of the time that is all we needed, but additionally we can also adjust styling for different states as well through the configurationUpdateHandler().

The basic usage of the API involves in creating a UIButton.Configuration object and passing it to UIButton init function of setting it to UIButton’s configuration property. The button will then apply the configuration. In WWDC session it was emphasized that applying a new configuration is cheap and optimized behind the scenes therefore we should never compare the new configuration with the one currently applied on the button.

var configuration = UIButton.Configuration.filled() // or .plain() or something else
configuration.baseBackgroundColor = .systemCyan
configuration.buttonSize = .large
configuration.cornerStyle = .dynamic
configuration.showsActivityIndicator = false
configuration.image = UIImage(systemName: "pencil")
configuration.imagePlacement = .trailing
configuration.imagePadding = 6
configuration.title = "Title"
configuration.titleAlignment = .leading
configuration.titlePadding = 10
configuration.subtitle = "Subtitle"
// and more
button.configuration = configuration
// or
let button2 = UIButton(configuration: configuration, primaryAction: action)
Example of button configurations when the tint colour is set to red.

For getting a better overview on how default configurations change based on the properties, we set on UIButton.Configuration type, I built a simple preview app which takes configurations: filled(), bordered(), borderedProminent(), borderedTinted(), borderless(), plain(), gray(), tinted() and applies some modifications on these. The results are shown below.

private func configurations(forStyleIndex index: Int) -> [UIButton.Configuration] {
var configuration1 = UIButton.Configuration.filled()
configuration1.title = "Filled"
var configuration2 = UIButton.Configuration.tinted()
configuration2.title = "Tinted"
var configuration3 = UIButton.Configuration.gray()
configuration3.title = "Gray"
var configuration4 = UIButton.Configuration.plain()
configuration4.title = "Plain"
var configuration5 = UIButton.Configuration.bordered()
configuration5.title = "Bordered"
var configuration6 = UIButton.Configuration.borderedProminent()
configuration6.title = "Bordered Prominent"
var configuration7 = UIButton.Configuration.borderedTinted()
configuration7.title = "Bordered Tinted"
var configuration8 = UIButton.Configuration.borderless()
configuration8.title = "Borderless"
let all: [UIButton.Configuration] = [configuration1, configuration2, configuration3, configuration4, configuration5, configuration6, configuration7, configuration8]
.map({ original in
var configuration = original
switch index {
case 0:
break
case 1:
configuration.subtitle = "Subtitle"
case 2:
configuration.image = UIImage(systemName: "pencil")
case 3:
configuration.buttonSize = .large
case 4:
configuration.image = UIImage(systemName: "pencil")
configuration.imagePlacement = .trailing
case 5:
configuration.baseBackgroundColor = .systemBlue
case 6:
configuration.buttonSize = .mini
case 7:
configuration.showsActivityIndicator = true
case 8:
configuration.baseBackgroundColor = .systemCyan
configuration.buttonSize = .large
configuration.cornerStyle = .dynamic
configuration.showsActivityIndicator = false
configuration.image = UIImage(systemName: "pencil")
configuration.imagePlacement = .trailing
configuration.imagePadding = 6
configuration.titleAlignment = .leading
configuration.titlePadding = 10
configuration.subtitle = "Subtitle"
default:
fatalError("Implement")
}
return configuration
})
return all
}
view raw Buttons.swift hosted with ❤ by GitHub

Example Project

UIButtonConfigurationPreview (Xcode 13)

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

Tab view in SwiftUI

TabView is a container view which enables navigating between multiple flows by selecting one of the items on the tab bar. Tapping on a tab item replaces the visible view with a view associated with the item. Tab view is set up by creating views which have tab items attached. Tab items are created with the tabItem() view modifier, which supports setting a text and an image. In addition, there is a badge() view modifier if we would like to show a badge on top of the item.

Tab view also supports selection handling. Selection handling is needed when we need to programmatically control which tab is selected. For that, we’ll need to choose a type which represents the selection. The only requirement is that the type is Hashable. Therefore, we can use an enum with raw values and have a clear and readable representation of tabs. Next, all the views managed the tab view need to have a tag set with one of the enum cases. Then we can create a binding with the selection type and pass it into TabView and SwiftUI will select the tab view item which has a tag equal to the selection. Just to reiterate that we can use any other type for representing the selection, as long as it conforms to Hashable. Could be just integers as well.

struct ContentView: View {
@State private var selection: Tab = .schedule
enum Tab: Int {
case conversations, schedule, settings
}
var body: some View {
TabView(selection: $selection) {
ConversationListView()
.badge(1)
.tabItem {
Image(systemName: "bubble.left.and.bubble.right.fill")
Text("Messages")
}
.tag(Tab.conversations)
ScheduleView()
.tabItem {
Image(systemName: "clock.fill")
Text("Schedule")
}
.tag(Tab.schedule)
SettingsView()
.tabItem {
Image(systemName: "gearshape.fill")
Text("Settings")
}
.tag(Tab.settings)
}
}
}
view raw TabView.swift hosted with ❤ by GitHub
iOSsimulator with an app which shows tab view with 3 items: messages with a badge showing 1, schedule, and settings.

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

Structuring platform specific code in SwiftUI

Xcode provides a cross-platform app template in SwiftUI which can be built for macOS and iOS. This template includes a Shared folder which is meant for code used both on macOS and iOS. Many SwiftUI types and functions work both on macOS and iOS but along with developing an app we’ll encounter cases where we need to specialize per platform. When I was building a multiplatform app then the very first thing I encountered was list styles. On macOS I wanted to use a different list style than on iOS (SidebarListStyle vs GroupedListStyle). Therefore, let’s take this as an example and see what are the ways how to handle this.

Compile time checks

The first way is to add compile time checks directly to the UI code. Swift provides #if os() for branching out code which should only be compiled on the specified platform. One downside of this approach is that it can make the code pretty long in the view body when we have multiple platform specific branches. In those cases, it might make sense to have separate functions which just contain the #if os() branch. But in the example below, we just have a listStyle view modifier, which is configured differently on macOS and iOS.

struct ContentView: View {
@State var items = [1, 2, 3]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text("\(item)")
}
}
#if os(macOS)
.listStyle(SidebarListStyle())
#else
.listStyle(GroupedListStyle())
#endif
}
}

Platform specific extensions

Another way for handling platform specific code is to create a separate files which are only included to platform targets. In this concrete case, those files contain a view extension and define a contentListStyle property which returns a list style. ContentView.swift file can access the property and when we are building for macOS then ContentView+Mac.swift is compiled and when building for iOS then ContentView+iOS.swift. I would recommend creating separate folders for platform specific code. For example, the project folders could be Shared, Mac, and iOS. Separate folders give a clear overview where the platform specific code is.

struct ContentView: View {
@State var items = [1, 2, 3]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text("\(item)")
}
}
.listStyle(contentListStyle)
}
}
extension ContentView {
var contentListStyle: GroupedListStyle {
return GroupedListStyle()
}
}
extension ContentView {
var contentListStyle: SidebarListStyle {
return SidebarListStyle()
}
}
Project structure in Xcode with Shared, Mac, and iOS folders for managing platform specific code.
Project structure with Shared, Mac, and iOS folders.

Summary

SwiftUI code can be shared most of the time between platforms. Sometimes we’ll need to configure the shared code per platform or use platform specific APIs. Hopefully those two approaches will give some flexibility on handling such code.

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.