Categories
Swift

Static DocC documentation for Swift packages

Swift 5.6 released with Xcode 13.3 implements evolution proposals extensible built tools (SE-0303) and its extension command plugins (SE-0332). This opens up plugins for Swift packages. Along with that, Apple released DocC command plugin for Swift packages, which supports generating static webpages containing the documentation of the package.

Swift-DocC plugin

Apple’s Swift-DocC plugin comes with pretty rich documentation which covers many aspects of the documentation generation process. Something to keep in mind still is that the generated website can’t just be opened with Safari like we might have been used to when using Jazzy. The plugin has a separate preview command if we want to open the documentation locally. That command starts a local web server which renders the site.

Getting started with Swift-DocC plugin

As an example, we’ll take my IndexedDataStore Swift package and see what are the steps to generate and preview the documentation. But before that, for local usage, I would like to highlight the fact that Xcode’s Product menu contains a “Build Documentation” command which generates documentation and adds it to the Developer Documentation window.

Xcode product menu with build documentation menu item.
Xcode product menu with build documentation menu item.
Xcode documentation viewer with locally built documentation.
Xcode documentation viewer with locally built documentation.


OK, back to generating HTML webpages ourselves. The very first thing we need to do is adding the docc plugin as a dependency to our Swift package. If we have done that, then we have access to new commands which the plugin defines.

dependencies: [
  .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],

Let’s take a look at the preview command at first which generates documentation, spins up a local web server which renders it.

swift package --disable-sandbox preview-documentation --target IndexedDataStore
Building for debugging...
Build complete! (0.13s)
Template: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/share/docc/render
========================================
Starting Local Preview Server
	 Address: http://localhost:8000/documentation/indexeddatastore
========================================
The generated documentation site.
The generated documentation site.

The other command is the one which generates the static documentation webpage, which we can then commit to GitHub and let the GitHub pages to render.

swift package \
    --allow-writing-to-directory ./docs \
    generate-documentation \
    --target IndexedDataStore \
    --disable-indexing \
    --output-path ./docs \
    --transform-for-static-hosting \
    --hosting-base-path IndexedDataStore

Since plugin commands run under a sandboxed environment, we’ll need to explicitly define which folder is writable with the --allow-writing-to-directory argument. The --disable-indexing argument disables generating index, which is used by Xcode or other IDEs. The --transform-for-static-hosting removes the need to have any routing rules on the web server. And finally, --hosting-base-path defines the base-path of the documentation. Meaning, if the GitHub repository name is IndexedDataStore then we should pass in IndexedDataStore. Otherwise, relative links in the generated webpage are incorrect. The full format of the URL when it is pushed to a branch and GitHub pages is configured to read from the pushed branch with relative path set to /docs is: https://<username>.github.io/<repository-name>/documentation/<target-name> .

GitHub pages configuration where the branch is set to docc-documentation and relative path to /docs.
GitHub pages configuration where the branch is set to docc-documentation and relative path to /docs.

For IndexedDataStore, it is https://laevandus.github.io/IndexedDataStore/documentation/indexeddatastore/. Note that the target name is in lowercase. The link won’t work since I switched GitHub pages back to the Jazzy documentation, which is auto-generated on merge.

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
Swift

A few examples of async await in Swift

Async await was added to Swift 5.5 and brought a new way how to write asynchronous code. Functions can be annotated with an async keyword, which enables them to be called with the await keyword. When an async function is being awaited, the execution of the current context is suspended and is resumed when the async function finishes. There are a lot more details in the SE-0296. In this blog post, we’ll take a look at a few examples of async await.

Wrapping completion handler based function

We can add a separate async functions for completion hander code. Which is nice since we do not reimplement everything we have using completion handlers. Take a look at a bit longer explanation in Adding async await to existing completion handler based code.

public func storeData(_ dataProvider: @escaping () -> Data?, identifier: Identifier = UUID().uuidString) async throws -> Identifier {
return try await withCheckedThrowingContinuation({ continuation in
self.storeData(dataProvider, identifier: identifier) { result in
continuation.resume(with: result)
}
})
}
Wrapping a completion handler function which uses the Result type.

Calling async function from non-async context

When using the await keyword then the current asynchronous context suspends until the awaited async function finishes. Suspension can only happen if we are in an asynchronous context. Therefore, a function without the async keyword can’t use await directly. Fortunately, we can create asynchronous contexts easily with a Task. One example of this is when we use MVVM in SwiftUI views and the view model has different methods we want to call when, for example, a user taps on a button

func refreshDocuments() {
Task(priority: .userInitiated) {
do {
self.documents = try await fetcher.fetchAllDocuments()
}
catch {
// Handle error
}
}
}
Creating a Task for enabling to use await on an async function.

Running tasks concurrently

Sometimes we have several independent tasks we need to complete. Let’s take an example when we want to preload messages for conversations. The code snippet below takes an array of conversations and then starts loading messages for each of the conversation. Since conversations are not related to each other, we can do this concurrently. This is what a TaskGroup enables us to do. We create a group and add tasks to it. Tasks in the group can run at the same time, which can be a time-saver.

func preloadMessages(for conversations: [Conversation]) async {
await withThrowingTaskGroup(of: Void.self) { taskGroup in
for conversation in conversations {
taskGroup.addTask {
try await self.preloadMessages(for: conversation)
}
}
}
}

Retrying a task with an exponential delay

This is especially related to networking code, where we might want to retry a couple of times before giving up and displaying an error. Additionally, we might want to wait before each request, and probably we want to wait a bit longer with each delay. Task has a static function detached(priority:operation:) so let’s create a similar retried() static function. In addition to priority and operation, we have arguments for defining how many times to retry and how much to delay, where the delay is increased exponentially with each retry. The first retry attempt is delayed by default 1 second, then the next 2 seconds, the third 4 seconds and so on. If the task happens to be cancelled while waiting, then the Task.sleep(nanoseconds:) throws CancellationError.

extension Task {
static func retried(times: Int, backoff: TimeInterval = 1.0, priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) -> Task where Failure == Error {
Task(priority: priority) {
for attempt in 0..<times {
do {
return try await operation()
}
catch {
let exponentialDelay = UInt64(backoff * pow(2.0, Double(attempt)) * 1_000_000_000)
try await Task<Never, Never>.sleep(nanoseconds: exponentialDelay)
continue
}
}
return try await operation()
}
}
}
Retrying a Task with an exponential delay.

self.documents = try await Task.retried(times: 3) {
try await self.fetcher.fetchAllDocuments() // returns [Documents]
}.value
Example of Task.retried() with accessing the returned value.

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

Creating a signed Swift package collection

Swift 5.5 brought us a new feature which allows creating Swift package collections (SE-0291). In this blog post we’ll go through required steps and create a package collection with Augmented Code packages. At the time of writing, there is only one package: IndexedDataStore.

Install Swift Package Collection Generator

First, we’ll need to install a tool which Apple created for package collections. It is called Swift Package Collection Generator. We’ll need to clone the repository, build it and then either using the built tools in their current location or also installing them to /usr/local/bin for easy access later on.


git clone https://github.com/apple/swift-package-collection-generator.git
cd swift-package-collection-generator
swift build --configuration release
install .build/release/package-collection-generate /usr/local/bin/package-collection-generate
install .build/release/package-collection-diff /usr/local/bin/package-collection-diff
install .build/release/package-collection-sign /usr/local/bin/package-collection-sign
install .build/release/package-collection-validate /usr/local/bin/package-collection-validate

Collection description in JSON

When the tool is installed, then the next step is to create a definition for the collection. Supported keys in that JSON file are available here: PackageCollectionsModel/Formats/v1.md.

{
  "name": "Augmented Code Collection",
  "overview": "Packages created by Augmented Code",
  "author": {
    "name": "Toomas Vahter"
  },
  "packages": [
    { "url": "https://github.com/laevandus/IndexedDataStore.git" }
  ]
}

Next step is to feed that JSON into the package-collection-generate tool which fetches additional metadata for these packages (–auth-token argument with GitHub personal access token must be used if the GitHub access is not already set up with SSH).

package-collection-generate input.json output.json --verbose --pretty-printed

The output.json looks like this:

{
  "formatVersion" : "1.0",
  "generatedAt" : "2022-01-08T07:05:44Z",
  "generatedBy" : {
    "name" : "Toomas Vahter"
  },
  "name" : "Augmented Code Collection",
  "overview" : "Packages created by Augmented Code",
  "packages" : [
    {
      "keywords" : [

      ],
      "license" : {
        "name" : "MIT",
        "url" : "https://raw.githubusercontent.com/laevandus/IndexedDataStore/main/LICENSE"
      },
      "readmeURL" : "https://raw.githubusercontent.com/laevandus/IndexedDataStore/main/README.md",
      "url" : "https://github.com/laevandus/IndexedDataStore.git",
      "versions" : [
        {
// … version descriptions which are pretty long

Signing the package collection

Before we go ahead and sign the package collection, we’ll need to prepare certificates. Open Keychain Access and then from the main menu Keychain Access > Certificate Assistant > Request Certificate from a Certificate Authority. Use your email and name and check the “Saved to disk” option. The next step is uploading the certificate request file to Apple. Uploading is done in Certificates, Identifiers & Profiles by tapping on the plus button and selecting Swift Package Collection Certificate. After clicking on the Continue button, we can upload the certificate request file we created with the Keychain Access. After that, we’ll download the certificate and double-clicking on the certificate file adds it to Keychain Access. Before we can sign the collection, the next step is exporting the private key from Keychain Access. Look for “Swift Package Collection” certificate, expand the item which reveals the private key, right-click on it and export it (set a password). Keychain Access creates .p12 file, which we’ll need to convert to .pem (set a password when asked). In the example below, I saved the exported private key to swift_package.p12.

openssl pkcs12 -nocerts -in swift_package.p12 -out swift_package.pem
openssl rsa -in swift_package.pem -out swift_package_rsa.pem

Now we have ready for signing the package collection as we have .cer and .pem files prepared.

package-collection-sign output.json output-signed.json swift_package_rsa.pem swift_package.cer

When the command is successful, we have an output-signed.json file, which we can share and add to Xcode.

Adding a new package collection

A new package collection can be added in Xcode by navigating to File > Add Packages sheet and clicking on the plus button and selecting Add Swift Package Collection. Xcode asks for a https URL of the collection. One option is to upload the signed collection json file to GitHub and then passing in an URL to the raw representation of the file. The URL to Augmented Code’s package collection is available here.

Package collection view in Xcode showing added Augmented Code Packages collection.
Package collection view in Xcode.

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

Most visited blog posts in 2021

Another year spent on writing blog posts. Let’s see which were the most read blog posts in 2021.

Top 10 written on any year

  1. Opening hyperlinks in UILabel on iOS (December 20, 2020)
  2. Using an image picker in SwiftUI (November 22, 2020)
  3. Using SwiftUI previews for UIKit views (June 7, 2020)
  4. Resizing UIImages with aspect fill on iOS (October 25, 2020)
  5. Alert and LocalizedError in SwiftUI (March 1, 2020)
  6. Adding custom attribute to NSAttributedString on iOS (November 10, 2019)
  7. Using CoreData with SwiftUI (January 19, 2020)
  8. Text input in UITableView (November 4, 2018)
  9. Adding SwiftLint to a project in Xcode (September 13, 2020)
  10. @StateObject and MVVM in SwiftUI (August 2, 2020)

Top 3 written in 2021

  1. Measurement, Unit, Dimension, and MeasurementFormatter on iOS (January 18, 2021)
  2. Running tests in Swift package with GitHub actions (April 26, 2021)
  3. Exploring AttributedString and custom attributes (June 21, 2021)
  4. Code coverage for Swift Packages with Fastlane (April 12, 2021)
  5. Sidebar layout on macOS in SwiftUI (September 13, 2021)

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

Sorting data with KeyPathComparator

KeyPathComparator was added to Foundation in iOS 15 and macOS 12. The KeyPathComparator is used by defining a key path which is used for fetching a value for comparison. Values are then compared with a SortComparator. In the simplest form we do not need to create the SortComparator ourselves and instead, ComparableComparator is created automatically. But if the value is String then String.StandardComparator.localizedStandard is used instead of ComparableComparator. All in all it is pretty much the similar to NSSortDescriptor which was used for sorting NSArray and NSMutableArray. New comparator types on the other hand can be used with many more types.

Using KeyPathComparator

As an example, let’s take a case of having an array of Player types where each player has played two rounds and therefore have two different scores. Additionally, each player type stores a competitor number as well.

struct Player {
let competitorNumber: Int
let round1: Int
let round2: Int
}
let players = [
Player(competitorNumber: 1, round1: 75, round2: 69),
Player(competitorNumber: 2, round1: 31, round2: 93),
Player(competitorNumber: 3, round1: 91, round2: 88),
Player(competitorNumber: 4, round1: 84, round2: 62),
Player(competitorNumber: 5, round1: 88, round2: 20),
]
view raw Player.swift hosted with ❤ by GitHub

If we want to sort the array of players by first and second round scores then it goes like this (note that order is set to reverse which gives us descending order):

let round1 = players.sorted(using: KeyPathComparator(\.round1, order: .reverse))
/*
player3 round1=91 round2=88
player5 round1=88 round2=20
player4 round1=84 round2=62
player1 round1=75 round2=69
player2 round1=31 round2=93
*/
let round2 = players.sorted(using: KeyPathComparator(\.round2, order: .reverse))
/*
player2 round1=31 round2=93
player3 round1=91 round2=88
player1 round1=75 round2=69
player4 round1=84 round2=62
player5 round1=88 round2=20
*/
view raw Sorted.swift hosted with ❤ by GitHub

Here we can see that sequences have sorted(using:) functions which take in either one comparator or several. An example of using several comparators is sorting the same players array by the highest score first and if two or more players hace the same highest score, then sorting by the worst score from these two rounds.

extension Player {
var bestRound: Int {
max(round1, round2)
}
var worstRound: Int {
min(round1, round2)
}
}
let maxScore = players.sorted(using: [
KeyPathComparator(\.bestRound, order: .reverse),
KeyPathComparator(\.worstRound, order: .reverse)
])
/*
Sorted by max score
player2 round1=31 round2=93
player3 round1=91 round2=88 <– equal best score
player5 round1=88 round2=20 <– equal best score
player4 round1=84 round2=62
player1 round1=75 round2=69
*/

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

Gradient text in SwiftUI

If we want to spice up the user interface, then we can make some titles in the app to use gradient colours. In WWDC’21 Apple introduced an API for making gradient text styles easy to create. The .foregroundStyle() view modifier takes in a type which conforms to ShapeStyle protocol. One of these types are gradient types in SwiftUI. Therefore, creating a fun gradient text is a matter of creating a text and applying a foregroundStyle view modifier with a gradient on it.

let gradientColors: [Color] = [.purple, .blue, .cyan, .green, .yellow, .orange, .red]
Text("Hello, world!")
.font(.system(size: 60))
.foregroundStyle(
.linearGradient(
colors: gradientColors,
startPoint: .leading,
endPoint: .trailing
)
)
Text("Hello, world!")
.font(.system(size: 60))
.foregroundStyle(
.ellipticalGradient(
colors: gradientColors
)
)
Text("Hello, world!")
.font(.system(size: 60))
.foregroundStyle(
.conicGradient(
colors: gradientColors,
center: .center
)
)
Text("Hello, world!")
.font(.system(size: 60))
.foregroundStyle(
.radialGradient(
colors: gradientColors,
center: .center,
startRadius: 50,
endRadius: 100
)
)
view raw Text.swift hosted with ❤ by GitHub

The new view modifier is great, but it is iOS 15+, macOS 12.0+. Another way for achieving this result is recreating it ourselves. This involves in overlaying the text with gradient and then masking it with the text.

Text("Hello, world!")
.font(.system(size: 60))
.myForegroundStyle(
LinearGradient(
colors: gradientColors,
startPoint: .leading,
endPoint: .trailing
)
)
.padding()
.background(.black)
extension View {
func myForegroundStyle<Content: View>(_ content: Content) -> some View {
self.overlay(content).mask(self)
}
}
view raw Mask.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 macOS Swift SwiftUI

Focusing views in SwiftUI

WWDC’21 introduced APIs for managing focus in SwiftUI. When dealing with multiple focusable views, we can create an enum for representing these and use the new @FocusState property wrapper along with focused() view modifier, where each of the focusable view binds its focus to an enum case. This can look like this:

struct ContentView: View {
@StateObject var viewModel = ViewModel()
enum FormEntry: Hashable {
case lastName, firstName, email
}
@FocusState var focused: FormEntry?
var body: some View {
VStack {
Text("Application")
TextField("Last Name", text: $viewModel.lastName, prompt: nil)
.focused($focused, equals: .lastName)
TextField("First Name", text: $viewModel.firstName, prompt: nil)
.focused($focused, equals: .firstName)
TextField("Email", text: $viewModel.email, prompt: nil)
.focused($focused, equals: .email)
Button("Reset") {
viewModel.reset()
focused = .lastName
}
}
.frame(idealWidth: 600)
.padding()
}
}

In this example we have FormEntry enum which represents focusable elements in the view. Each of the view has a focused() view modifier, which binds the current focus state to the text field if it matches with the enum case. Meaning, setting focused property to FormEntry.email will move the focus to the email text field. It works the other way around as well. If we tap on the email text field, SwiftUI will set the focused property value to FormEntry.email.

Sometimes we just want to control the focus only once. Let’s take an example use case where we enter an order id and after tapping a search button we would like to remove the focus from the order id text field. In this case, we can just use a boolean property and bind the focus state boolean property to the text field’s focused() view modifier. Setting the property value to false in the button action will tell SwiftUI to resign focus from the text field.

struct ContentView: View {
@StateObject var viewModel = ViewModel()
@FocusState var isOrderIDFocused: Bool
var body: some View {
VStack {
Text("Orders")
TextField("Order ID", text: $viewModel.orderId, prompt: nil)
.focused($isOrderIDFocused)
Button("Search") {
viewModel.search()
isOrderIDFocused = false
}
}
.padding()
}
}

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.