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
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.

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.

Categories
macOS Swift SwiftUI

Sidebar layout on macOS in SwiftUI

A common UI layout on macOS has a sidebar and detail view side by side. The sidebar contains a list of items, where the selected item is displayed on the right and displays details of it. One would expect that creating such a view hierarchy in SwiftUI should be pretty easy to set up. In this post, we’ll take a look at how to create a basic view with sidebar which supports selection.

Building the layout

We’ll build a simple sample app which shows a list of fruits in the sidebar and when clicking on any of the fruits, the right pane displays the name of the fruit. Therefore, we’ll need a struct representing a fruit, a view model for storing the list of fruits, and a property for storing the selected fruit in the sidebar.

struct Fruit: Identifiable {
let id = UUID().uuidString
let name: String
}
final class ViewModel: ObservableObject {
init(fruits: [Fruit] = ViewModel.defaultFruits) {
self.fruits = fruits
self.selectedId = fruits[1].id
}
@Published var fruits: [Fruit]
@Published var selectedId: String?
static let defaultFruits: [Fruit] = ["Apple", "Orange", "Pear"].map({ Fruit(name: $0) })
}

We can create the layout with NavigationView and NavigationLink. Inside the NavigationView we’ll first add a List where each of the item in the list is represented by a NavigationLink. One of the NavigationLink initializers takes a title, tag, and selection binding. Tag is used for identifying items in the list and setting one of the tag values to the selection binding will make the sidebar to select the item programmatically. Also, we’ll need to set the list style to “sidebar” which adds the appropriate styling to it. Finally, we’ll add a Text element, which acts as a placeholder view when there is no selection. And that is all what we need to do to get going with a view with sidebar and detail pane.

struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.fruits) { item in
NavigationLink(item.name, tag: item.id, selection: $viewModel.selectedId) {
Text(item.name)
.navigationTitle(item.name)
}
}
}
.listStyle(.sidebar)
Text("No selection")
}
}
}
A sample app with sidebar on the left showing 3 items and detail view on the right displaying the name of the selected item.
The final sample app with a sidebar.

Summary

We used NavigationView and NavigationLink to create a common layout for macOS apps, which features a sidebar with list of items and detailed view on the right. With only a bit of code, we were able to set it up.

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

Image converter with AsyncSequence

WWDC’21 brought us a new protocol named AsyncSequence. As the name stands, it represents a sequence of asynchronous elements. For trying out the new API we’ll build a tiny ThumbnailSequence which takes in a list of image names and by iterating the sequence, we’ll get back scaled thumbnails for those image names one by one. The image scaling runs on a background thread.

The AsyncSequence protocol comes with two associated types: Element and AsyncIterator. Element represents the type which is produced by the sequence, and AsyncIterator is the type responsible for reproducing elements. Same as with sequences, but the main difference is that accessing each of the element is asynchronous. Therefore, for creating a custom type ThumbnailSequence which conforms to AsyncSequence we’ll, set the associated type Element to be equal to UIImage, and implement a custom iterator. ThumbnailSequence initializers takes a list of image names and also defines a max scaled image size. Additionally, we’ll take advantage of the new byPreparingThumbnail(ofSize) async method for scaling the image. Implementation of the async sequence is shown below:

struct ThumbnailSequence: AsyncSequence {
typealias AsyncIterator = Iterator
typealias Element = UIImage
var imageNames: [String]
let maxSize: CGSize
func makeAsyncIterator() -> Iterator {
return Iterator(imageNames: imageNames, maxSize: maxSize)
}
struct Iterator: AsyncIteratorProtocol {
typealias Element = UIImage
var imageNames: [String]
let maxSize: CGSize
mutating func next() async -> UIImage? {
guard !imageNames.isEmpty else { return nil }
guard let image = UIImage(named: imageNames.removeFirst()) else { return nil }
let ratio = image.size.height / maxSize.height // simplified scaling
return await image.byPreparingThumbnail(ofSize: CGSize(width: image.size.width / ratio, height: maxSize.height))
}
}
}

With a simple async sequence created, we can hook it up to a SwiftUI view. WWDC’21 also brought a new task view modifier, which is invoked when the view appears and cancelled when the view is removed. In the task view modifier we’ll loop over the sequence and one by one load UIImages which then are set to a local images array which in turn is connected to a LazyVStack. The flow we’ll get is that we are loading images one by one, and after every image load we’ll add a new item to the stack.

struct ContentView: View {
let imageNamesToLoad = ["Screenshot1", "Screenshot2", "Screenshot3", "Screenshot4", "Screenshot5", "Screenshot6", "Screenshot7", "Screenshot8", "Screenshot9", "Screenshot10"]
@State private var images = [UIImage]()
var body: some View {
ScrollView {
LazyVStack {
ForEach(images, id: \.self) { image in
Image(uiImage: image)
}
}
}
.task {
for await image in ThumbnailSequence(imageNames: imageNamesToLoad, maxSize: CGSize(width: 256, height: 256)) {
print("Loaded \(image)")
self.images.append(image)
}
}
}
}
Screenshot of a iPhone simulator showing a view with vertical list of images.
The final sample app displaying images in a vertical stack.

Summary

In this post, we took a quick look at the AsyncSequence protocol and created a pipeline which converts image names to scaled image instances one by one. After that, we connected the pipeline to a SwiftUI view.

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 SwiftUI

Exploring AttributedString and custom attributes

WWDC’21 brought new APIs for creating attributed strings. AttributedString is a new value type which provides a type-safe API for dealing with attributes, its also localizable, supports limited amount of Markdown syntax, and can be even archived and unarchived thanks to the Codable support. In this blog post, we’ll take a look at the new API and will compose some attributed strings.

In addition to AttributedString type, there are many supporting types. For example, AttributeContainer is a collection of attributes which can be applied to the attributed string with one go. AttributeScope protocol groups attributes and AttributeScopes type contains several groups of attributes like AttributeScopes.swiftUI for attributed strings rendered in SwiftUI views. There is also AttributeScopes.uiKit, AttributeScopes.appKit, and AttributeScopes.foundation. Let’s now see how to create attributed strings and render them in a SwiftUI view.

An AttributedString with attributes applied to multiple ranges.

The attributed string visible above contains multiple attributes starting with background color attribute and finishing with a link attribute. In the snippet below we can find different ways how to set attributes: searching for a range, manually creating a range, using AttributedContainer for setting multiple attributes at once, and also setting attributes to the whole string. As this string is displayed in a SwiftUI view then all the used attributes are part of the SwiftUI attribute scope.

var string = AttributedString(localized: "The quick brown fox jumps over the lazy dog")
// Add attribute to the whole string
string.font = Font.system(size: 14, weight: .heavy, design: .rounded)
// Add a single attribute to a range
if let range = string.range(of: "The") {
string[range].backgroundColor = Color.indigo
}
if let range = string.range(of: "lazy") {
string[range].strikethroughColor = .yellow
string[range].strikethroughStyle = .single
}
// Add a link
if let range = string.range(of: "dog") {
string[range].link = URL(string: "https://www.apple.com")!
}
// Create a range manually with indices
let start = string.characters.index(string.startIndex, offsetBy: 10)
let end = string.characters.index(start, offsetBy: 5)
string[start..<end].foregroundColor = .brown
// Add multiple attributes at once to a range
if let range = string.range(of: "jumps") {
var attributeContainer = AttributeContainer()
attributeContainer.baselineOffset = 4
attributeContainer.kern = 5
attributeContainer.underlineColor = .cyan
attributeContainer.underlineStyle = .patternDot
string[range].mergeAttributes(attributeContainer)
}

If we would like to see the ranges attributes were set to, we can use the runs API. A single run is a set of attributes shared by a single range. If we print all the runs, in this case 9, then it would look like this:

The {
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	SwiftUI.BackgroundColor = indigo
}
 quick  {
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
}
brown {
	NSPresentationIntent = [paragraph (id 1)]
	NSLanguage = en
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	SwiftUI.ForegroundColor = brown
}
 fox  {
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
}
jumps {
	SwiftUI.UnderlineColor = cyan
	NSLanguage = en
	SwiftUI.Kern = 5.0
	SwiftUI.BaselineOffset = 4.0
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSPresentationIntent = [paragraph (id 1)]
	NSUnderline = NSUnderlineStyle(rawValue: 256)
}
 over the  {
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
}
lazy {
	SwiftUI.StrikethroughColor = yellow
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
	NSStrikethrough = NSUnderlineStyle(rawValue: 1)
}
  {
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
}
dog {
	NSLink = https://www.apple.com
	SwiftUI.Font = Font(provider: SwiftUI.(unknown context at $7fff5bb7d400).FontBox<SwiftUI.Font.(unknown context at $7fff5bbfdb68).SystemProvider>)
	NSLanguage = en
	NSPresentationIntent = [paragraph (id 1)]
}

The new AttributedString API also supports custom attributes. Custom attributes need to conform to a AttributeStringKey protocol in bare minimum. But when we would like to benefit from using the custom attribute in Markdown and also allowing to decode and encode it to data with Codable then we would need to conform to MarkdownDecodableAttributedStringKey and CodableAttributedStringKey respectively. In a simple example, let’s create a new attribute named MessageAttribute which can store a value of Message struct with id and value fields. The MessageAttribute needs to define the type it stores and a name used when encoding and decoding. In addition, we’ll need to add a new attribute scope which contains the new attribute. As we intend to use the new attribute in a SwiftUI app then we’ll add swiftUI attributes to the scope as well.

struct Message: Hashable, Codable {
let id: String
let value: String
}
struct MessageAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
typealias Value = Message
static var name: String = "secretMessage"
}
extension AttributeScopes {
struct MyAppAttributes: AttributeScope {
let message: MessageAttribute
let swiftUI: SwiftUIAttributes
}
var myApp: MyAppAttributes.Type { MyAppAttributes.self }
}

With this set we can create attributed strings with this attribute either using markdown syntax or adding the attribute manually. Markdown syntax for custom attributes uses caret followed with square brackets and with the content in brackets after that. We also need to make sure to pass custom attribute scope into the AttributedString initializer as well. One thing what I have not figured out is how to create a completely custom appearance for custom attributes in SwiftUI views, like we can do in UIKit views.

var string = AttributedString(localized: "This contains my message attribute", including: \.myApp)
let start = string.startIndex
let end = string.characters.index(start, offsetBy: 4)
string[start..<end].myApp.message = Message(id: "message_id", value: "secret")
// print(string.runs):
// This {
// NSPresentationIntent = [paragraph (id 1)]
// NSLanguage = en
// secretMessage = Message(id: "message_id", value: "secret")
// }
// contains my message attribute {
// NSPresentationIntent = [paragraph (id 1)]
// NSLanguage = en
// }
let string = AttributedString(localized: "This contains my ^[message](secretMessage: {id: 'message_id', value: 'Hello!'}) attribute in markdown", including: \.myApp)
// print(string.runs)
// This contains my {
// NSPresentationIntent = [paragraph (id 1)]
// NSLanguage = en
// }
// message {
// NSLanguage = en
// NSPresentationIntent = [paragraph (id 1)]
// secretMessage = Message(id: "message_id", value: "Hello!")
// }
// attribute in markdown {
// NSLanguage = en
// NSPresentationIntent = [paragraph (id 1)]
// }

Summary

We took a look on AttributedString, AttributeContainer, AttributeScope, and created attributed strings with the new API. With this knowledge, we got going with the new API and can continue exploring it further. The last thing to mention is that AttributedString can be converted to and from NSAttributedString with breeze.

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

Custom string interpolation in Swift

ExpressibleByStringInterpolation is a protocol which makes it possible to compose strings with expressions evaluated at runtime. Interpolated strings are created with adding a \(some code) to a string. Those expressions are evaluated and a final string is created. This protocol, among other things, enables customizing strings what are created by those expressions. At the end of the post we have created a custom interpolation type which handles encodable and string representable types.

ExpressibleByStringInterpolation is a protocol which enables a type to be initialized with string interpolation. The protocol inherits from multiple other protocols, when going from top to down then it looks like this: ExpressibleByStringLiteral, ExpressibleByExtendedGraphemeClusterLiteral, and ExpressibleByUnicodeScalarLiteral. So it is a total of 4 levels of inheritance. That is important to know because if we add the protocol to a custom type then Xcode tells us about many functions the custom type needs to implement. Many of these functions already provide default implementation.

Let’s start with adding custom types Entry add EntryStorage. The storage type just keeps a collection of entries. The entry, for now, contains a string value, but we will expand the type in a way that the storage’s add function can be called with a string interpolation: storage.add("Entry (index)"). It will be very similar to OSLogMessage in Apple’s os framework.

struct EntryStorage {
private(set) var entries: [Entry] = []
mutating func add(_ entry: Entry) {
entries.append(entry)
}
}
struct Entry {
private(set) var value: String
}

With this set, let’s add ExpressibleByStringInterpolation conformance to the Entry type along with a custom interpolation type: EntryInterpolation. ExpressibleByStringInterpolation protocol comes with an associatedtype StringInterpolation which is by default set to DefaultStringInterpolation. If we want to use custom interpolation type then we can implement the init(stringInterpolation: EntryInterpolation) with the custom type and Swift will understand that we’ll be using our own type here. No need to add typealias StringInterpolation = EntryInterpolation (although we could for clarity). The custom EntryInterpolation type needs to conform to protocol StringInterpolationProtocol. The protocol requires us to implement an init method and a appendLiteral function. The custom type will have a property for storing multiple interpolated values because it needs to represents all the expressions in a single string. For example: "Text (expression1) more text (expression2)".

struct Entry: ExpressibleByStringInterpolation {
// typealias StringInterpolation = EntryInterpolation
private(set) var value: String
init(stringLiteral value: String) {
self.value = value
}
init(stringInterpolation: EntryInterpolation) {
self.value = stringInterpolation.values.joined()
}
}
struct EntryInterpolation: StringInterpolationProtocol {
private(set) var values: [String]
init(literalCapacity: Int, interpolationCount: Int) {
self.values = []
}
mutating func appendLiteral(_ literal: StringLiteralType) {
values.append(literal)
}
}

With this implementation we can write code which looks like this:

var entryStorage = EntryStorage()
entryStorage.add("Entry 1")

Note that the add method takes an argument of the type Entry but here we are passing a string to the function. This works because the Entry type conforms to the ExpressibleByStringLiteral protocol which the ExpressibleByStringInterpolation includes.

Now we have basics set up and we can go and add additional functions to the EntryInterpolation type. At first, we’ll add a generic function enabling us to create interpolated strings with expressions which return a type conforming to the CustomStringConvertible protocol. There are numerous types which implement this protocol and therefore we get a support of interpolating each of those. For example, Int and Array types conform to it.

extension EntryInterpolation {
mutating func appendInterpolation<T: CustomStringConvertible>(_ value: T) {
values.append(value.description)
}
}
let index = 2
let items = ["Item 1", "Item 2"]
entryStorage.add("Entry \(index): items=\(items)")
// Entry 2: items=["Item 1", "Item 2"]

Sometimes we might want to pass in encodable types directly with customizable formats. Note how the interpolated expression gets a support to the custom format argument. That is because Swift converts each of the expressions to calls to appendInterpolation which can have additional arguments.

extension EntryInterpolation {
mutating func appendInterpolation<T: Encodable>(_ value: T, jsonFormat: JSONEncoder.OutputFormatting = [.prettyPrinted, .sortedKeys]) {
let encoder = JSONEncoder()
encoder.outputFormatting = jsonFormat
let data = try? encoder.encode(value)
values.append(String(data: data ?? Data(), encoding: .utf8) ?? "invalid")
}
}
struct User: Encodable {
let name: String
let age: Int
}
let user = User(name: "Appleseed", age: 20)
entryStorage.add("Entry 3: \(user, jsonFormat: .prettyPrinted)")
entryStorage.add("Entry 3: \(user, jsonFormat: .sortedKeys)")
// Entry 3: {
// "name" : "Appleseed",
// "age" : 20
//}
//Entry 3: {"age":20,"name":"Appleseed"}

It is worth taking a look on the interface of the OSLogInterpolation type and all the appendInterpolation functions it implements. As seen so far, it is pretty easy to extend a custom interpolation type with functions like these.

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

Project

SwiftStringInterpolationPlayground (Xcode 12.4)

Categories
macOS Swift SwiftUI

Reading Fastfiles with document based SwiftUI app on macOS

WWDC’20 brought an addition to SwiftUI apps which enables to create document based applications in SwiftUI. Along with DocumentGroup API two new protocols were added: FileDocument and ReferenceFileDocument. The first one is meant for value types and the latter one for class type document types. For seeing how much effort it takes to create a document based SwiftUI app, let’s create a small macOS app which reads Fastlane’s Fastfiles.

Mac app window showing a list of lanes from a Fastfile
The final applications which displays a list of lanes.

Xcode comes with a pretty nice template for document based application. For this specific case we will go with macOS’ document app template.

A template picker in Xcode showing macOS document app template being selected.
Xcode template picker.

The default template is set up in a way that it can be used for reading and writing text documents. Let’s go and modify the created app. Fastfiles in Fastlane do not have any file extensions and therefore we’ll need to use public.data Uniform Type Identifiers (UTI) type which enables the app to open it. This has a side-effect as well, now the app can open lots of other files as well, so we should probably add a validation step which makes sure we are trying to read a Fastfile. As the app is going to deal with public.data UTI types and the app itself does not define any custom UTI types then Document Types and Imported Type Identifiers in the Info.plist can be removed.

Like mentioned before, SwiftUI brought a new way of creating document based applications. Document types are presented either with value or reference type. FileDocument is a protocol which adds an init method with read support, a write method and supported UTI type declarations for reading and writing. It is a pretty compact protocol when thinking about the interface UIDcoument has. Something to keep in mind is that every implemented method in the document must be thread-safe because reading and writing always happens on background threads. But let’s take a look at the implementation of a document which represents a Fastfile document:

struct FastfileDocument: FileDocument {
let fastfileContents: String
init(contents: String) {
self.fastfileContents = contents
}
init(configuration: ReadConfiguration) throws {
// TODO: validate the file name
guard let data = configuration.file.regularFileContents else { throw CocoaError(.fileReadCorruptFile) }
guard let string = String(data: data, encoding: .utf8) else { throw CocoaError(.fileReadCorruptFile) }
fastfileContents = string
}
static var readableContentTypes: [UTType] {
return [UTType("public.data")!]
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = fastfileContents.data(using: .utf8)!
return FileWrapper(regularFileWithContents: data)
}
// MARK: Accessing Lanes
func lanes() -> [Lane] {
return FastfileParser.lanes(in: fastfileContents)
}
}
struct Lane: Equatable, Identifiable {
let name: String
let documentation: String
var id: String {
return name
}
}

The FastfileParser was covered by the previous blog post if you would like to take a look: Adding prefixMap for expensive operations in Swift. In summary, the document just reads the whole Fastfile into the memory and provides a method for parsing lanes. Note that the protocol requires to have a write method defined as well although, at least for now, we are not going to use it. Having the document created, the next step is building a small UI which shows a list of lanes.

DocumentGroup is a new scene type which manages everything around creating, viewing, and saving documents. Therefore, for viewing a document we’ll need to create a DocumentGroup for viewing and provide a SwiftUI view which can display the document. DocumentGroup takes care of showing the open panel and coordinating the view creation. The example SwiftUI app looks like this:

@main
struct LaneControlApp: App {
var body: some Scene {
DocumentGroup(viewing: FastfileDocument.self) { file in
LaneListView(viewModel: LaneListView.ViewModel(document: file.document))
}
}
}
view raw App.swift hosted with ❤ by GitHub

A list view for showing the list of lanes is pretty straight-forward: LaneListView uses the List view component and displays individual rows with the LaneRowView. The row view shows the name of the lane and the description found in the Fastfile document.

struct LaneListView: View {
@StateObject var viewModel: ViewModel
var body: some View {
List {
ForEach(viewModel.lanes) { lane in
LaneRowView(lane: lane)
}
}
.frame(minWidth: 200, minHeight: 200)
}
final class ViewModel: ObservableObject {
let lanes: [Lane]
init(document: FastfileDocument) {
lanes = document.lanes()
}
}
}
struct LaneRowView: View {
let lane: Lane
var body: some View {
VStack(spacing: 8) {
Text(lane.name)
.font(.headline)
if !lane.documentation.isEmpty {
Text(lane.documentation)
.font(.subheadline)
.multilineTextAlignment(.center)
}
}
.frame(maxWidth: .greatestFiniteMagnitude)
.foregroundColor(.white)
.padding(12)
.background(Color.accentColor)
.cornerRadius(12)
}
}

Summary

DocumentGroup, FileDocument, and ReferenceFileDocument APIs are the building blocks for building document based apps with SwiftUI. Getting a simple document based app up and running does not require much code at all.

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

Signal Path 2.4 for macOS, iOS and iPadOS

Signal Path version 2.4 is out. This time it is a smaller release concentrating on optimizing file accesses when playing audio or just scrolling through the file.

Download on the Mac App Store Download on the App Store