But then, the devastating moment, destroying all happiness at once:
“Xcode.xip can’t be expanded because the current volume doesn’t have enough free space.”
Error prompt from the Xcodes App (github.com/RobotsAndPencils/XcodesApp)
The initial reaction: “Huh. Wait a minute? I do have enough space, don’t I?”. Then you check your disk usage statistics, and yes, looks like there should be enough free space.
So what is the problem and how can we solve it? Let’s narrow it down.
Here’s the solution to installing Xcode when you get this error:
Create a very large file (multiple GBs) using dd (or similar), wait a moment, then delete it (and clear your Trash). Now you have enough free space to install Xcode.
dd if=/dev/urandom of=temp_20GB_file bs=1024 count=$[1024*1024*20]
Still interested in the full reason why this is working? Keep reading.
We start wondering: “so how much space would I need now?”. If you checkout the Mac App Store entry for Xcode, it states 11.7 GB size. But, even if we have more available, it fails with the same error.
Unfortunately the download process of the App Store is quite obscure, and therefore not a great starting point to investigate the issue.
Instead we can directly download a compressed version of Xcode from the developer resources on developer.apple.com. When downloading the Xcode app, we are actually downloading an xip archive. To cite the man xip documentation: “The xip tool is used to create a digitally signed archive”. This is used in macOS to prove the authenticity of an archive.
Great, downloading worked out fine, I got the archive on my MacBook, so let’s look at it:
The archive has about 11 GB of file size. That is quite a lot of data, especially for an archive which is the compressed version of Xcode.app
.
So how much space do we really need for the full app, on our disk?
To be honest, I have no idea how 29.51 GB are 16.68 GB on disk
We need a whopping 30 GB of space on our disk. This is a challenge, especially for developers with small drives. Fortunately for me, when I got my MacBook Pro in 2017, I opted-in for a 512 GB version, so in this case there should be enough space left to fit a 30 GB app for sure.
Clicking on the Apple Symbol in the top-left corner and further clicking the “About This Mac” menu option, opens up the storage information.
Well, look at it: 39.54 GB of space is available.
Wait… so what is going on? Why is the install process dying with a “not enough space available”-warning even tough there should be at least 39.5 GB — 29,5 GB =
10 GB more available than necessary?
While inspecting this behavior, I found an interesting side-effect of the xip unarchiver: It checks for enough disk space before actually writing any data.
After tinkering with solutions and researching on the internet, I came up with a theory.
Disclaimer: This has not been verified by enough research (as my time is limited and solution-oriented) and I would love to hear your feedback either confirming or denying these assumptions, preferably per DM on Twitter.
In 2017 Apple introduced us the successor of the HFS+ file system, the “Apple File System” (APFS). A file system is the low-level technology which defines, how the data is stored on hardware, and how it can be read from it. It brings many great features with it, such as encryption, super-fast file duplication and increased data integrity.
Another great feature of APFS are Containers. To give you full context of what containers are, and why they are so awesome, I will give you a short summary on storage technologies (as far as I remember from my university lectures 😄).
A storage disk is split into blocks, each one consisting of multiple bytes of data. On Unix, each block has a size of 4KB. As an example: if we have a disk with 128 blocks with 4KB each, that means we have a total disk size of 512 KB.
As multiple file systems with different features (e.g. case-sensitive file names) exist, we eventually want to install multiple ones on the same disk. This requires us to split our disk space into multiple partitions.
A partition is a range of assigned blocks, e.g. partition A has block 0–63 and partition B is 64-127 assigned, and each partition is formatted with a specific file system (e.g. APFS, HFS+, NTFS, exFAT etc.).
The partition system is still widely used today, but it has a major drawback on usability: If we run out of space on partition A, the only way to use the free space of partition B is resizing (= reassigning blocks) of the latter one to the first one. Even worse, sometimes the blocks must be sequential, and so we can’t resize the partitions without shifting all data inside the blocks, by reading and writing them to a different position.
I had to do this once to fix a BootCamp installation and even tough I feel comfortable with low-level computing, it was a hurdle to deal with the partition table. Hopefully I’ll never have to do that again.
Luckily we got APFS containers now 🎉 These containers are built on top of partitions, with the great advantage of having a dynamic size.
Example: At first our 128 blocks are assigned to a single APFS partition. Then we create two containers A and B. Their current size is defined by the APFS controller software, and while writing large data, they grow as needed. When you delete files, it takes some time, but eventually the container shrinks down, so that the cleared space is available once again. Now the free space could also be used to grow the container B.
Sounds great, doesn’t it? Well yes, but on the other hand it is quite ironic that this dynamic “advantage” is actually the root of our problems, while installing Xcode.
As you can see in the screenshot earlier, about 40 GB of space is available, but if you open up the Disk Utility.app shipped with macOS, it states something different:
Disk Utility is a macOS application to inspect and format storage devices.
Even tough the storage information from “About This Mac” showed me the real 40 GB of available space, the container currently only has ~22GB of space assigned to it! This is not unexpected, because the container would grow while writing data and could therefore eventually use up the full 40 GB.
But it seems as the xip disk-space-check looks at the free space inside the container before writing, and not the fully available disk space, and therefore no writing is happening.
We found the contradiction causing our issue:
APFS containers grow while writing data, but the unarchiver won’t start writing data, because the container didn’t grow enough (yet).
To solve the contradiction, we have to force the container to grow. The only viable solution to do so, is writing a huge amount of data.
Unfortunately due to the nature of APFS we probably can’t simply duplicate large files on our computer (as this was one of the marketing features of the WWDC Keynote).
Now, we could download large test files, such as the ones from Hetzer.de. But it isn’t reasonable to create this large network traffic, if we simple need random, local bytes. Also it would take forever with low network bandwidth.
The easiest solution is using your favorite search engine to lookup “create large file macOS” and finding posts like this one or this one on StackOverflow.
The core of macOS is Unix, which offers a built-in random data stream file at /dev/urandom. To create a large file filled with random data, run the following command in your terminal of choice:
dd if=/dev/urandom of=temp_20GB_file bs=1024 count=$[1024*1024*20]
It will read the random data and write it to a file temp_10GB_file, which will indicate our APFS container to grow. After writing the data, we can delete the generated file and for a little while the container will be large enough for the xip-unarchiver-disk-space-check™ to pass.
Now you should have enough disk space to finish installing Xcode 🥳
This is an interesting behavior of macOS and the Apple File System, which might not be intended by their developers. I will go ahead and create a bug report to let them know about these findings. Maybe they can create a sustainable solution for us all.
I hope you enjoyed this story and hopefully it helped to fix your issue too. The idea for this article actually sparked by a post on Twitter, so if you want to read more content like this, make sure to follow me there 😁
If you have issues installing Xcode due to "not enough space" (8.22 GB) even tough you have enough (35.4 GB), check Disk Utility.
— Philip (Phil) Niedertscheider (@philprimes) July 19, 2021
You might have to reclaim the APFS container space by creating a 20GB file and delete it afterwards pic.twitter.com/AaegfiRrEE
You have a specific topic you want me to cover? Let me know! 😃
]]>…but with Postie you can elevate your capabilities even more!
The Next-Level Swift HTTP API Package
So what’s the problem with our current state-of-the-art?
The most popular Swift networking framework available, with its 36.000+ stars on GitHub, is Alamofire. It has a long history of improvements, refactorings and extensions since its initial release in 2014.
Unfortunately such a long history eventually leads to a bloated code, and you might not need all of the features included in a framework. A few years ago, I was happy to have a library which helps working with URLSession and took away the JSON parsing, all long before the release of JSONDecoder. Today we don’t need that anymore, as it became quite simple to work with responses using the built-in features.
To begin explaining the core concepts of Postie, let us refresh our knowledge about API definitions.
Originally called the Swagger API definition, the OpenAPI Specification is today’s common standard for API definitions. Just look at this snippet from the Petstore Example, including an endpoint to place an order:
swagger: "2.0"
host: "petstore.swagger.io"
basePath: "/v2"
paths:
/store/order:
post:
summary: "Place an order for a pet"
parameters:
- in: "body"
name: "body"
description: "order placed for purchasing the pet"
required: true
schema:
$ref: "#/definitions/Order"
responses:
"200":
description: "successful operation"
schema:
$ref: "#/definitions/Order"
"400":
description: "Invalid Order"
# this is added for the example
schema:
$ref: "#/definitions/Error"
/pet/{petId}: {} ...
definitions:
Order:
type: "object"
properties:
id:
type: "integer"
format: "int64"
petId:
type: "integer"
format: "int64"
quantity:
type: "integer"
format: "int32"
shipDate:
type: "string"
format: "date-time"
status:
type: "string"
description: "Order Status"
enum:
- "placed"
- "approved"
- "delivered"
complete:
type: "boolean"
default: false
Error:
type: "object"
properties:
message:
type: "string"
description: "Error message"
As you can see, the request and the response are very well defined. Unfortunately this endpoint brings a few caveats with it, as there are edge cases we need to cover during implementation:
Another topic, which I am not covering in this post, is authentication. Many different authentication mechanisms exists, including HTTP Basic (Username + Password), API Keys and OAuth tokens. All of these need to be handled differently and therefore it is too much for this introduction.
Now you know what challenges we are facing. So how can we leverage the power of Swift to help us define well-structured API code?
Postie is our new Swift package, which takes care of converting our API request types into URLRequest objects, sends them to the endpoint, receives the URLResponse and converts it back into our defined API response types.
The Swift compiler and its strong typing paradigm allows us to take care of all the data structure management. From a high-level perspective, the main concept uses the already built-in option of creating custom Encoder and Decoder, in combination with Swift 5.1’s property wrappers.
Sounds complicated, but fortunately for you, you don’t have to worry about how the magic of Postie works, instead you just have to define your API 🎉
As usual, an example is easier to understand, so let’s start off with a simple HTTP request for our /store/order endpoint:
POST /v2/store/order HTTP/2
Host: petstore.swagger.io
Accept: application/json
Content-Type: application/json
Content-Length: 129
{
"id": 1,
"petId": 2,
"quantity": 3,
"shipDate": "2021-07-04T08:21:56.169Z",
"status": "placed",
"complete": false
}
We can see that this request includes an HTTP Method, the URL path, a Host header with the remote domain, a Content-Type header declaring the type of data we are sending, and the actual JSON data in the body. Furthermore we also define an Accept header, which tells the remote endpoint what kind of data we would like to receive (also JSON).
So how can this request be declared using Postie?
We start off with the simplest approach and add more information further down the road.
Create the following request:
import Postie
struct CreateStoreOrder: Request {
// Ignores the response
typealias Response = EmptyResponse
}
Now we change the default HTTP method GET to the POST using the @RequestHTTPMethod property wrapper.
struct CreateStoreOrder: Request {
typealias Response = EmptyResponse
@RequestHTTPMethod var method = .post
}
Next we need to define the resource path using the @RequestPath property wrapper.
struct CreateStoreOrder: Request {
typealias Response = EmptyResponse
@RequestHTTPMethod var method = .post
@RequestPath var path = "/store/order"
}
**Note: **As explained earlier, we are *not *adding the prefix v2 to the request path, as the request type itself is not associated with the actual remote host. Instead we have to define the host URL and the prefix with our HTTP client:
import Foundation
import Postie
struct CreateStoreOrder: Request {
typealias Response = EmptyResponse
@RequestHTTPMethod var method = .post
@RequestPath var path = "/store/order"
}
let host = URL(string: "https://petstore.swagger.io")!
let basePath = "v2"
let client = HTTPAPIClient(url: host, pathPrefix: basePath)
Next, we need to add the request body. From the HTTP request we know that
To tackle 2nd requirement, change the type of CreateStoreOrder from Request to JSONRequest. This will indicate the encoding logic of Postie, that the request body should be converted to JSONdata, and the header Content-Type: application/json needs to be set.
This is also a great example of how the Swift compiler supports us. Immediately after changing the request type, it requires us to adapt the request to add a property body.
Declare a structure Body which must implement the Encodable pattern and you are all set.
struct CreateStoreOrder: JSONRequest {
typealias Response = EmptyResponse
@RequestHTTPMethod var method = .post
@RequestPath var path = "/store/order"
struct Body: Encodable {}
var body: Body
}
Now we could adapt the Body to have the same structure as our Order schema, but instead we define a Definitions structure so we can reuse it.
enum Definitions {
struct Order: Encodable {
enum Status: String, Encodable {
case placed
case approved
case delivered
}
let id: Int64
let petId: Int64
let quantity: Int32
let shipDate: String
let status: Status
var complete: Bool = false
}
}
struct CreateStoreOrder: JSONRequest {
typealias Response = EmptyResponse
@RequestHTTPMethod var method = .post
@RequestPath var path = "/store/order"
var body: Definitions.Order
}
Great! We are done with declaring our request type 🎉
It’s time to define our response type as well, so take a look at the expected HTTP response:
HTTP/2 200 OK
date: Sun, 04 Jul 2021 08:43:07 GMT
content-type: application/json
content-length: 212
{
"complete": false,
"id": 1,
"petId": 2,
"quantity": 3,
"shipDate": "2021-07-04T08:21:56.169Z",
"status": "placed"
}
Mainly it contains a response status code, response headers and the body data.
To access any information from the response, the associated type Response needs to become an actual struct. We used EmptyResponse earlier, which is a convenience type-alias for following:
struct CreateStoreOrder: JSONRequest {
struct Response: Decodable {
}
// ...request definition here...
}
As a first step, we want to read the response status code. Add a property using the wrapper @ResponseStatusCode.
Note: You can name the properties as you wish. If not required by the protocols (e.g. body) only the property wrapper is relevant.
struct CreateStoreOrder: JSONRequest {
struct Response: Decodable {
@ResponseStatusCode var statusCode
}
// ...request definition here...
}
When decoding the response, Postie will now find the statusCode property and see that it should be set with the actual HTTP response code.
Before defining the response body, let us quickly recap the OpenAPI definition:
responses:
"200":
description: "successful operation"
schema:
$ref: "#/definitions/Order"
"400":
description: "Invalid Order"
schema:
$ref: "#/definitions/Error"
Looks like we need to define two responses, which differ depending on the response code. This is also built-in in Postie, as you can not only define a @ResponseBody, but also a @ResponseErrorBody property, which only gets populated when the status code is between 400 and 499.
struct Response: Decodable {
@ResponseStatusCode var statusCode
@ResponseBody<Definitions.Order> var body
@ResponseErrorBody<Definitions.Error> var errorBody
}
To make this code snippet work, we need to change the Defintions.Order type to not only implement the Encodable protocol, but also the Decodable protocol. Furthermore we need to define the Definitions.Error which should be rather clear at this point.
enum Definitions {
struct Order: Encodable, Decodable {
enum Status: String, Encodable, Decodable {
case placed
case approved
case delivered
}
let id: Int64
let petId: Int64
let quantity: Int32
let shipDate: String
let status: Status
var complete: Bool = false
}
struct Error: Decodable {
let message: String
}
}
In the final step, we once again need to indicate the decoding logic of Postie, to expect a JSON request body, which is done by changing the Decodable protocol of the type Order to be a JSONDecodable instead (same for Error).
enum Definitions {
struct Order: Encodable, JSONDecodable {
enum Status: String, Encodable, Decodable {
case placed
case approved
case delivered
}
let id: Int64
let petId: Int64
let quantity: Int32
let shipDate: String
let status: Status
var complete: Bool = false
}
struct Error: JSONDecodable {
let message: String
}
}
Good job! Let’s give ourselves a pat on the back, our API definition is ready💪🏼
Using the request definition is easy. All you have to do is create an object CreateStoreOrder and send it using the HTTPAPIClient we declared earlier.
**Note: **Postie uses the asynchronous event framework Combine for it’s communication. As it uses the underlying URLSession other async patterns are (if requested) possible too.
// Create the request
let request = CreateStoreOrder(body: .init(
id: 1,
petId: 2,
quantity: 3,
shipDate: "2021-07-04T09:23:00Z",
status: .placed))
// Send the request
client.send(request)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Successfully sent request!")
case .failure(let error):
print("Something went wrong:", error)
}
}, receiveValue: { response in
print("Status Code:", response.statusCode)
if let body = response.body {
print("Successful response body:", body)
} else if let errorBody = response.body {
print("Error response body:", errorBody)
}
})
As our CreateStoreOrder has an associated Response type, we won’t have to define the expected response type again or worry about its parsing logic.
From now on we simply use our API.
There are many more features available, but I couldn’t cover them all in this story. It’s highly recommend that you checkout the vast README guide to see the full feature-set.
Just to give you a glance of what else is available:
@RequestHeader
defines request headers@ReponseHeader
to read a specific header from the response@QueryItem
to add typed fields to the request URL query@RequestPathParameter
to set typed parameters in the URL (such as the petId from our example)… and more!
You might be wondering why we consider Postie being different to other frameworks/packages.
I mentioned earlier that other frameworks are heavy-weight and include many features, which stay unused for most of the users. As Postie will eventually grow with its feature set too, our counter-measurements are keeping the core slim, probably implement a multi-package approach, and require as little information as possible, when defining the API.
Our approach using property wrappers enables just that. Other frameworks require to either pass the additional headers or values as function parameters when sending the request, but Postie stays true to an object-oriented approach:
A request is a single data object which contains all relevant information to receive the expected response.
Postie will eventually evolve into a fully-fledged HTTP framework, taking care of all the data conversion and requirements validation. The main goal is having an object-oriented Request-Response pattern, which allows a developer to worry less about how the API should be used, but instead what to do with it.
At the time of writing, Postie supports JSON and Form-URL-Encoded data, but we are also planning to support XML in the future.
With the rise of async-await in Swift 5.5 the current Combine-based sending logic will be extended. If requested, we will also include legacy-style callbacks.
Additional ideas include a Swiftgen template to automatically transform the OpenAPI specification directly into ready-to-use Postie request definitions.
Even tough the package is still under active development, we are going to use it for production apps at kulakula and techprimate to validate the usage, eventually bumping it to version 1.0.0
Follow the repository and submit your feature requests. It started as an Open Source project and should remain one, so we all can profit from each other. Found a bug? Let us know!
I also would love to hear what you think about this project. Follow me on Twitter and feel free to drop a DM with your thoughts. Also checkout my other articles. You have a specific topic you want me to cover? Let me know! 😃
]]>Even tough this story uses SwiftUI in this story, it is not the main scope, and only used for simpler code snippets. The concepts apply to any kind of Swift projects available, including UIKit/AppKit interfaces or even command line tools.
Take a look at the following example of a view showing a call-to-action message and the action button:
struct MyView: View {
var body: some View {
VStack {
Text("Tap on the button \"Tap me!\"")
Button("Tap me!", action: { /* do something */ })
}
}
}
If you use this code snippet in a SwiftUI app it will work fine and the call-to-action fulfills its purpose: it tells the user to tap on the button.
Some developers would stop thinking further about this code and keep going with the project, but you might have already noticed a potential issue: Changing the button text will lead to inconsistency!
struct MyView: View {
var body: some View {
VStack {
Text("Click on the button \"Tap me!\"")
Button("I'm a button", action: {})
}
}
}
The first main issue is code duplication, or specifically duplicated strings. When we change the label of the button, we also have to change the words in the message.
As an initial solution we decide to create a small static constant, which can be used in both cases.
struct MyView: View {
var body: some View {
VStack {
Text("Click on the button \"\(Strings.tapMe)\"")
Button(Strings.tapMe, action: {})
}
}
}
enum Strings {
static let tapMe = "Tap me!"
}
This easy change already improved our code on two ways:
Text
and the Button
are now guaranteed showing the same value.Your project grows and you keep adding more views, and eventually get to a finished version. The one you are proud to share with the world. Soon later you realize: “I have to translate the app, so more humans can use it” and you start looking into iOS/macOS localization techniques.
Fortunately this is quite easy to implement using the NSLocalizedString
macro/function, and so we can change our constant to apply localization.
enum Strings {
static let clickMe = NSLocalizedString(
"Tap me!", // <-- lookup key and default value
comment: "Label of button which calls for action")
}
What NSLocalizedString
does under the hood is straight forward: we pass it a string which is used a lookup key in the localized .strings
file. If a translation is found, it gets returned, otherwise the lookup key acts as a default value.
Additionally you create the relevant Localizable.strings file with the localized strings for the newly added language.
As I am from Austria, I’ll go with German as the second language for this story.
// Localizable.strings (German)
/* Label of button which calls for action */
"Tap me!" = "Tipp mich!"
Perfect. Once again you run your application with a different application language, and NSLocalizedString
uses the Tap me! as a key to lookup the translation Tipp mich!.
Quick Tip: You can change the current runtime language in the schema seetings
Unfortunately this introduced the same issue we defeated earlier: even tough the link between UI and the String constant is secured by compile-time safety, the link between our constant and the localization resource is not guaranteed!
This means, if we change the lookup key name in the NSLocalizedString
call (e.g. to Please tap me!), it won’t find the mapped translated string anymore. Even worse, we won’t notice it, as the build process does not fail (due to the default behavior of not translating, if not found).
The easiest solution is introducing static keys, but we do not want to show a static identifier to the user in our UI. Therefore we need to add a value parameter, which now provides the original string as the default value.
enum Strings {
static let clickMe = NSLocalizedString(
"call-to-action.button.text", // <-- only key
value: "Tap me!",
comment: "Label of button which calls for action")
}
To reflect our new changes to the localization file, you also change the localized .strings
file to match the key:
// Localizable.strings (German)
/* Label of button which calls for action */
"call-to-action.button.text" = "Tipp mich!"
These few changes already fixed the issue. But we are still not quite there yet. The linking between the constants and the translation files are still loose and far from being guaranteed.
Before going further down the improvement road, we need to add our message to the constants too. As it inserts the button text using String interpolation, but our translation files are only static strings, we need to adapt the code:
Text(String(format: "Click on the button \"%@\"", Strings.clickMe))
We use String(format:)
which takes a format/template string as the first parameter, and replaces all format specifiers (e.g. %@
) with the variadic parameters.
Quick Tip: Format specifiers are standardized for most programming languages. You can find a full list in the Apple Documentation.
Add another static key with the translated value to the Localizable.strings file and declare it as a constant in our enum:
// Localizable.strings (German)
/* Label of button which calls for action */
"call-to-action.button.text" = "Tipp mich!";
/* Format string for the call to action message */
"call-to-action.message.text" = "Tipp auf \"%@\"";
struct MyView: View {
var body: some View {
VStack {
Text(String(format: Strings.message, Strings.clickMe))
Button(Strings.clickMe, action: {})
}
}
}
enum Strings {
static let message = NSLocalizedString(
"call-to-action.message.text",
value: "Click on the button \"%@\"",
comment: "Format string for the call to action message")
static let clickMe = NSLocalizedString(
"call-to-action.button.text",
value: "Tap me!",
comment: "Label of button which calls for action")
}
Swift is a language with strong typing and the compiler does great work helping us finding common issues. It also helps us to think less about the preconditions of certain code, such as the required parameters for a function call.
As NSLocalizedString
and String(format:)
use string-based APIs, this type safety does not apply to them. Even worse it can lead to crashes when used incorrectly (by personal experience with os_log, which also uses format strings).
Luckily, we are skilled programmers, and can wrap the usage of String(format:)
in a function with a single parameter, to reduce the looseness of the link:
struct MyView: View {
var body: some View {
VStack {
Text(Strings.message(Strings.clickMe))
Button(Strings.clickMe, action: {})
}
}
}
enum Strings {
static let messageFormat = NSLocalizedString(
"call-to-action.message.text",
value: "Click on the button \"%@\"",
comment: "Format string for the call to action message")
static func message(_ p1: String) -> String {
String(format: messageFormat, p1)
}
static let clickMe = NSLocalizedString(
"call-to-action.button.text",
value: "Tap me!",
comment: "Label of button which calls for action")
}
What a clean solution 🤩 The constants include all necessary information, which most likely will not need to be edited soon, and the usage inside the view is quite elegant.
As Xcode still does not provide us with a validation tool between our custom constants and the localization files, these mappings need to be created by hand and checked by the developer manually.
So far we have always written our code first, then added the strings to our localization files. Even if we changed the code afterwards, you most likely will define a new constant first and later add the translation in the future too.
Doing it this way sounds like a logically coherent approach… but what if we switch it around? What if we do not need to create the enums, constants, helper functions, etc…. and instead just ask the Swift code completion for available resources? foreshadowing intensifies
Feels contradicting to our previous conclusions, but stick with me. You will love what’s coming next.
Swiftgen is a code generator for Swift code. It’s main purpose is reading existing data using different parsers (.strings, .xcassets, .json, etc.), combining it with versatile Stencil templates and writing it to compile-ready Swift code… automatically.
With over 7,100 ⭐️ on GitHub (at the time of writing this story) it is already a widely popular project, and with almost 6 years of active development a mature solution.
Their documentation is comprehensive and the getting started guides easy to understand, so here is only a rather quick summary to continue with our use case:
After installation we first need to create a configuration file swiftgen.yml with the following content:
strings:
inputs: en.lproj
outputs:
- templateName: structured-swift5
output: Generated/Strings.swift
As we do not want to define localization by hand in our code, create a Localizable.strings for the default language (in this case it is English), and write down the values previously defined in our constants:
// Localizable.strings (English)
/* Label of button which calls for action */
"call-to-action.button.text" = "Tap me!";
/* Format string for the call to action message */
"call-to-action.message.text" = "Click on the button \"%@\"";
Afterwards run the command swiftgen in the same folder as the configuration file (make sure your path to the localization folder is correct).
It will read our .strings
file, and create a strongly typed localization enum in the Generated/Strings.swift
file:
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
import Foundation
// swiftlint:disable superfluous_disable_command file_length implicit_return
// MARK: - Strings
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
internal enum L10n {
internal enum CallToAction {
internal enum Button {
/// Tap me!
internal static let text = L10n.tr("Localizable", "call-to-action.button.text")
}
internal enum Message {
/// Click on the button "%@"
internal static func text(_ p1: Any) -> String {
return L10n.tr("Localizable", "call-to-action.message.text", String(describing: p1))
}
}
}
}
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
// MARK: - Implementation Details
extension L10n {
private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
let format = BundleToken.bundle.localizedString(forKey: key, value: nil, table: table)
return String(format: format, locale: Locale.current, arguments: args)
}
}
// swiftlint:disable convenience_type
private final class BundleToken {
static let bundle: Bundle = {
#if SWIFT_PACKAGE
return Bundle.module
#else
return Bundle(for: BundleToken.self)
#endif
}()
}
// swiftlint:enable convenience_type
If you take a close look at the L10n enum, you might realize: “this looks similar to the constants enum we created earlier!” and you are correct.
After adding this file to our project, we can now delete the enum Strings {...}
introduced earlier, and use the generated L10n
instead:
struct MyView: View {
var body: some View {
VStack {
Text(L10n.CallToAction.Message.text(L10n.CallToAction.Button.text))
Button(L10n.CallToAction.Button.text, action: {})
}
}
}
Additionally we can add a build script phase which re-generates the Swift code during build-time, therefore making sure we only access actually given ones.
Quick Tip: the generation script must be run before the “Compile Sources” phase
Awesome! Without further manual work we are able to access our localizations without worrying about keys or parameters… especially when adding new ones 💪🏼
Swiftgen is an automation tool which takes care of generating code to safely access resources, which would otherwise only be available using a String-based API.
In this story we only explored a small subset of the capabilities of this code generator, which can be even more powerful when writing custom templates. To keep the scope of this story nice and tight, this will be explained in detail in another upcoming article, especially with a tutorial on code templates. Make sure to follow me on Twitter & Medium so you don’t miss it!
As mentioned before we would love to have a guarantee that a specific localization key is actually present in the default language localization file.
On the one hand, this is still not fulfilled, especially if the generated code is outdated and therefore defining different values than given in the .strings
files.
On the other hand, in combination with the build script, this is fairly close to how a built-in compiler/code-completion support would work, and therefore if we trust our automation tools… we can trust the mapping.
If you would like to know more, checkout my other articles, follow me on Twitter and feel free to drop me a DM. Tell me about other great build tools for Swift development! You have a specific topic you want me to cover? Let me know! 😃
]]>This article will show you these 3 ways of styling a SwiftUI.View
:
As a general rule of thumb, any approach is viable. In the end, it comes down to your general code-style guidelines and personal preferences.
The property wrapper you will find in chapter 3 “Styles in Environment”
This is one is straight forward and can be visualized with an example rather quickly:
struct InitializerBasedConfigurationView: View {
let backgroundColor: Color
let textColor: Color
var body: some View {
Text("Hello, world!")
.padding()
.background(backgroundColor)
.foregroundColor(textColor)
.cornerRadius(10)
}
}
InitializerBasedConfigurationView(backgroundColor: .green, textColor: .white)
This view takes two parameters backgroundColor and textColor, which are both required when instantiating the struct. They are also both constant let values, as the view most likely isn’t going to be mutated (at this point).
Conveniently Swift automatically synthesizes the (internal) required initializer, but they can also manually be defined be us:
struct InitializerBasedConfigurationView: View {
let backgroundColor: Color
let textColor: Color
init(backgroundColor: Color, textColor: Color) {
self.backgroundColor = backgroundColor
self.textColor = textColor
}
var body: some View {
Text("Hello, world!")
.padding()
.background(backgroundColor)
.foregroundColor(textColor)
.cornerRadius(10)
}
}
InitializerBasedConfigurationView(backgroundColor: .green, textColor: .white)
Quick Tip: Xcode also provides us with a built-in function to generate memberwise initializers. All you have to do is CMD(⌘) + left-click on the type name, and select the action.
Using a custom initializer allows us to add default values directly there without changing the let of the parameters to var ones:
struct InitializerBasedConfigurationView: View {
let backgroundColor: Color
let textColor: Color
init(backgroundColor: Color = .green, textColor: Color = .white) {
self.backgroundColor = backgroundColor
self.textColor = textColor
}
// ... rest of view
}
As mentioned before, Swift synthesizes only internal initializers, so in case your view is part of a package and needs to be public, you are required to use this approach. Otherwise the application using the package won’t be able to find or instantiate the view.
On the other hand, if this view is only used inside your app, you can also let the Swift compiler do the work for you 🚀 All that is needed is changing from let to var and directly set the default values on the instance properties:
struct InitializerBasedConfigurationView: View {
var backgroundColor: Color = .green
var textColor: Color = .white
// ... rest of view ...
}
// these are all valid now:
InitializerBasedConfigurationView()
InitializerBasedConfigurationView(backgroundColor: .blue)
InitializerBasedConfigurationView(backgroundColor: .black, textColor: .red)
Your views keep growing and requires more parameters to be set. As the initializer keeps growing too, it eventually becomes a large piece of code.
struct MethodChainingView: View {
var actionA: () -> Void = {}
var actionB: () -> Void = {}
var actionC: () -> Void = {}
var actionD: () -> Void = {}
var actionE: () -> Void = {}
var body: some View {
HStack {
Button(action: actionA) {
Text("Button A")
}
Button(action: actionB) {
Text("Button B")
}
Button(action: actionC) {
Text("Button C")
}
Button(action: actionD) {
Text("Button D")
}
Button(action: actionE) {
Text("Button E")
}
}
}
}
// Usage:
MethodChainingView(actionA: {
print("do something")
}, actionB: {
print("do something different")
}, actionC: {
print("do something very different")
}, actionD: {
print("do nothing")
}, actionE: {
print("what are you doing?")
})
However, from my personal experience at some point the Swift compiler has too much work to do at the same time and simply gives up (it crashes).
One approach of breaking down large initializers (with default values) is using a return-self-chaining pattern:
struct MethodChainingView: View {
private var actionA: () -> Void = {}
private var actionB: () -> Void = {}
// ... rest of viwe
func actionA(_ action: @escaping () -> Void) -> Self {
// You can't edit view directly, as it is immutable
var view = self
view.actionA = action
return view
}
func actionB(_ action: @escaping () -> Void) -> Self {
// You can't edit view directly, as it is immutable
var view = self
view.actionB = action
return view
}
}
// Usage:
MethodChainingView()
.actionA {
print("do something")
}
.actionB {
print("do something different")
}
As the view itself is immutable, but consists out of pure data (structs are not objects), we can create a local copy with var view = self
. As this is now a local variable, we can mutate it and set the action, before returning it.
Apart from manually configuring every single view we can define a global style guide. An example might look like the following:
enum Style {
enum Text {
static let headlineColor = Color.black
static let subheadlineColor = Color.gray
}
}
struct EnvironmentStylesheetsView: View {
var body: some View {
VStack {
Text("Headline")
.foregroundColor(Style.Text.headlineColor)
Text("Subheadline")
.foregroundColor(Style.Text.subheadlineColor)
}
}
}
Unfortunately, this solution has a big issue: Global static variables means, they are not customizable for different use cases (for example in an Xcode preview) 😕
Our solution is opting in for instance configuration once again:
struct Style {
struct Text {
var headlineColor = Color.black
var subheadlineColor = Color.gray
}
var text = Text()
}
struct EnvironmentStylesheetsView: View {
let style: Style
var body: some View {
VStack {
Text("Headline")
.foregroundColor(style.text.headlineColor)
Text("Subheadline")
.foregroundColor(style.text.subheadlineColor)
}
}
}
This looks promising, as we can now pass the style configuration into the view from where-ever we need it:
struct ContentView: View {
var body: some View {
// uses the default style
EnvironmentStylesheetsView(style: Style())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
// uses the customized style
EnvironmentStylesheetsView(style: customizedStyle)
}
static var customizedStyle: Style {
var style = Style()
style.text.headlineColor = .green
return style
}
}
Quite a clean solution. But you might already be wondering “But wait! How is this a global solution?” and your doubts are justified! This solution requires us to pass the style down to every single view, just as in the following code snippet:
struct ContentView: View {
var body: some View {
// can this Style instance truely be considered "global"??
Foo(style: Style())
}
}
struct Foo: View {
let style: Style
var body: some View {
Bar(style: style)
}
}
struct Bar: View {
let style: Style
var body: some View {
FooBar(style: style)
}
}
struct FooBar: View {
let style: Style
var body: some View {
Text("Content")
.foregroundColor(style.text.headlineColor)
}
}
It took three passes just to get the “global” style object into the nested FooBar view. This is unacceptable. We don’t want this much unnecessary code (especially because you also strive for clean code, don’t you?).
Okay so what else could we think off? Well, how about a mix between the static and the instance solution? All we need is a static object where we can set the style from Foo and read it from FooBar … sounds like some shared environment💡
SwiftUI introduced the property wrapper @Environment which allows us to read a value from the shared environment of our view🥳
As a first step, create a new EnvironmentKey
by creating a struct implementing the defaultValue:
struct StyleEnvironmentKey: EnvironmentKey {
static var defaultValue = Style()
}
Next you need to add the new environment key as an extension to the EnvironmentValues so it can be accessed from the property wrapper:
extension EnvironmentValues {
var style: Style {
get { self[StyleEnvironmentKey.self] }
set { self[StyleEnvironmentKey.self] = newValue }
}
}
Finally set the value using .environment(\.style, ...)
in the root view and read the value using the keypath of the style in @Environment(\.style)
in the child views:
struct ContentView: View {
var body: some View {
Foo()
.environment(\.style, customizedStyle)
}
var customizedStyle: Style {
var style = Style()
style.text.headlineColor = .green
return style
}
}
struct Foo: View {
var body: some View {
Bar()
}
}
struct Bar: View {
var body: some View {
FooBar()
}
}
struct FooBar: View {
@Environment(\.style) var style
var body: some View {
Text("Content")
.foregroundColor(style.text.headlineColor)
}
}
Awesome! No more unnecessary instance passing and still configurable from the root view 🚀
Our environment solution is already working pretty nice, but isn’t the following even cleaner?
struct FooBar: View {
@Theme(\.text.headlineColor) var headlineColor
var body: some View {
Text("Content")
.foregroundColor(headlineColor)
}
}
All you need for this beautiful syntax is creating a custom property wrapper @Theme
which wraps our environment configuration and accesses the style value by a keypath.
@propertyWrapper struct Theme<Value> {
@Environment(\.style) private var style
private let keyPath: KeyPath<Style, Value>
init(_ keyPath: KeyPath<Style, Value>) {
self.keyPath = keyPath
}
public var wrappedValue: Value {
style[keyPath: keyPath]
}
}
Even better, using a View
extension allows us to hide the usage of Environment entirely!
extension View {
func theme(_ theme: Style) -> some View {
self.environment(\.style, theme)
}
}
struct ContentView: View {
var body: some View {
Foo().theme(customizedStyle)
}
var customizedStyle: Style {
var style = Style()
style.text.headlineColor = .green
return style
}
}
Note: The reason the style is now called theme is quite honestly just a naming conflict of a property wrapper @Style with the struct Style. If you rename the style structure you can also use this name for the property wrapper.
SwiftUI offers multiple different ways of building our view hierarchy, and we explored just a few of them. Additional options such as e.g. ViewModifier already exist, and even more will surface in the future.
At the time of writing best practices don’t really exist yet, as the technology is still quite new. Instead we have different good practices to choose from and can focus on re-usability, customizability and cleanness of our code.
If you would like to know more, checkout my other articles, follow me on Twitter and feel free to drop me a DM. You have a specific topic you want me to cover? Let me know! 😃
]]>Just look at what you can do with it:
In this story we are going to cover the following topics:
In case you want to see the full library, checkout the GitHub repository CoolDown, our own Markdown parser at techprimate, which also includes a work-in-progress library CDSwiftUIMapper.
It’s highly recommended that you read my previous article “Creating your own Markdown Parser from Scratch in Swift” as we will reuse concepts from there.
Anyway here is a short recap of the explained concepts:
Usually an example is easier to understand, so please take a look at the following one:
The styling is done by GitHub Gists. The actual raw document looks like the following:
# My _awesome_ article
This is a simple markdown document with **bold** and _cursive_ text.
Also here is a simple bullet list:
- My first list item
- Another list item
Now when parsing the document using the markdown parser (in my case it’s CoolDown), the AST reprsentation looks like the following:
let nodes: [ASTNode] = [
.header(depth: 1, nodes: [
.text("My "),
.cursive("awesome"),
.text(" article")
]),
.paragraph(nodes: [
.text("This is a simple markdown document with "),
.bold("bold"),
.text(" and "),
.cursive("cursive"),
.text(" text.")
]),
.paragraph(nodes: [
.text("Also here is a simple bullet list:")
]),
.list(nodes: [
.bullet(nodes: [
.text("My first list item")
]),
.bullet(nodes: [
.text("Another list item")
])
])
]
Perfect! Three of our four steps are quite simple to understand, now lets get into the last step: converting the AST nodes into SwiftUI views.
When parsing our tree, we have to think of a mapping function:
every single kind of node will have its own view representation.
mapping: node → view
As an example, the list node from the previous code snippet, might be mapped to the following SwiftUI view code:
VStack {
HStack(alignment: .top) {
Text("-")
Text("My first list item")
}
HStack(alignment: .top) {
Text("-")
Text("Another list item")
}
}
As you can see, each node is mapped to a view structure:
.list
becomes a VStack
view.bullet
becomes a HStack
view, with a Text("-")
as the first element.text
becomes a Text
viewIt is necessary to add a mapping function for every single node type, and manage it in an efficient way. The easiest way to do so is creating a mapper class, which takes an array of nodes as the input, manages a set of mapping functions and *outputs a SwiftUI view *structure.
For all you (aspiring) computer scientists out there, the applied software pattern is also called the Strategy pattern, as the function always has the same signature, but differs in its implementation. [Strategy pattern - Wikipedia
In this article I will call them Resolver and they are defined like this:
public typealias Resolver<Node: ASTNode, Result> = (Node) -> Result
You might be wondering, what is going on, so here a quick overview:
ASTNode
that can be added as a generic constraint.Resolver
in our libraryThe different resolvers are managed in a mapper class:
public class CDSwiftUIMapper {
// MARK: - Properties
private let nodes: [ASTNode]
private var resolvers: [String: Resolver<ASTNode, AnyView>] = [:]
// MARK: - Initializer
public init(from nodes: [ASTNode]) {
self.nodes = nodes
}
// MARK: - Accessors
public func resolve() throws -> AnyView {
fatalError("not implemented")
}
public func resolve(node: ASTNode) -> AnyView {
fatalError("not implemented")
}
// MARK: - Modifiers
public func addResolver<Node: ASTNode, ElementView: View>(for nodeType: Node.Type, resolver: @escaping (CDSwiftUIMapper, Node) -> ElementView) {
resolvers[String(describing: nodeType)] = { node in
guard let node = node as? Node else {
preconditionFailure("Internal resolver mismatch, expected node type does not match modifier type. This should never be called.")
}
return AnyView(resolver(self, node))
}
}
}
The resolvers dictionary is a one-to-one map of different node type identifiers, to their corresponding mapping functions.
For this initial implementation, we decided to simply go with a String(describing: nodeType)
as the identifier, which converts the Swift type into a String
, e.g. String(describing: SwiftUI.Text.self)
becomes Text
.
A much cleaner approach would be adding a static identifier to ASTNode
which needs to be overwritten in every subclass. (“Hey Siri, remind me of static identifiers”).
During the implementation of this class we also hit the first limitation:
Which Result type should I use for the resolvers return value? One resolver might return SwiftUI.Text
while others might even return a custom view. It is also not possible to use the super type View as it is a protocol and the compiler will start to complain:
Unfortunately I couldn’t find a more elegant solution (yet), other than type erasing. Therefore it uses AnyView which wraps any SwiftUI view into an untyped view structure.
A great feature of the addResolver function, is strong generic typing outside the library, such as this example mapper:
let mapper = CDSwiftUIMapper(from: nodes)
mapper.addResolver(for: TextNode.self) { mapper, node in
Text(node.content) // node has type TextNode
.fixedSize(horizontal: false, vertical: true)
}
mapper.addResolver(for: BoldNode.self) { mapper, node in
Text(node.content) // node has type BoldNode
.bold()
.fixedSize(horizontal: false, vertical: true)
}
EDIT 18.09.2022: Using type-erasure was never the best implementation.Instead use @ViewBuilder
and switch
to resolve all mappings.
At this point we have successfully parsed our document into a node structure, with a mapping utility ready for being filled with resolvers.
Our first resolver is the one for list which contains a list of nodes. A simple resolver to get to the desired VStack structure would be the following:
mapper.addResolver(for: ListNode.self) { mapper, node in
VStack(alignment: .leading) {
ForEach(node.nodes, id: \.self) { node in
mapper.resolve(node: node)
}
}
}
This is a great example of the so called ContainerNode
, a node which contains more nested ones. We iterate each nested node mapper .resolve(node: node)
which takes care of looking up the necessary resolver. In the class CDSwiftUIMapper
mentioned above, you have probably noticed the fatalError("not implemented")
. This is a great time to implement them:
public func resolve() throws -> AnyView {
AnyView(
ForEach(nodes, id: \.self) { node in
self.resolve(node: node)
}
)
}
public func resolve(node: ASTNode) -> AnyView {
guard let resolver = resolvers[String(describing: type(of: node))] else {
return AnyView(Text("Missing resolver for node: " + node.description))
}
return resolver(node)
}
The function resolve takes the nodes set in the mapper during creation and resolves each one into an AnyView and combines them in an ForEach
.
If it misses a node resolver, it returns a warning text, as crashes should be avoided and are super hard to debug in Xcode Previews.
As a final step (to get to the original GIF at the beginning) add we add a new view MarkdownViewer which converts the input parameter text into nodes and after mapping wraps them in a ScrollView:
import SwiftUI
import CoolDownParser
import CoolDownSwiftUIMapper
struct MarkdownViewer: View {
let nodes: [ASTNode]
init(_ text: String) {
self.nodes = CDParser(text).nodes
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 10) {
content
}
.padding(.vertical, 20)
.padding(.horizontal, 25)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
var content: some View {
do {
let mapper = CDSwiftUIMapper(from: transform(nodes: nodes))
mapper.addResolver(for: TextNode.self) { mapper, node in
Text(node.content) // node has type TextNode
.fixedSize(horizontal: false, vertical: true)
}
// add more resolvers here
return try mapper.resolve()
} catch {
return AnyView(Text(error.localizedDescription))
}
}
}
Combine everything together and you have created a markdown viewer in SwiftUI! 🚀
Isn’t this cool? It is possible to build SwiftUI apps using Markdown 🤯 How practical this approach is, well, you can decide that yourself.
Here a few thoughts on what’s next:
If you would like to know more, checkout my other articles, follow me on Twitter and feel free to drop me a DM. You have a specific topic you want me to cover? Let me know! 😃
]]>**
or >
to create well formatted documents? Awesome! Many platforms use it on a daily basis, so you will eventually use it too.
Now, what if you need a markdown parser for your Swift application? Well, we could just use one of the well-tested ones (which can be found using your favorite search engine on GitHub), but instead… you can also create your own version.
All jokes aside: if possible, do not reinvent the wheel. If an existing framework with an active maintainer fits your needs, use that one. At techprimate we decided to create our own solution, CoolDown, because an upcoming app uses Markdown with many custom extensions, therefore it was more convenient to have full control.
This article will also give you a general idea on how to parse a structured plain text document. Here a quick outline what we will cover:
Markdown documents are written entirely in plain text, and any additional assets are only added as URL references.
Over the years multiple Markdown specs surfaced and many platforms (e.g. GitHub) adapted and extended it. Eventually it got standardized to remove ambiguity. For this tutorial, we will use the CommonMark 0.29 specification as a reference, as it is quite a common one (pun intended).
A major structural element of the document is the double-newline/empty line, as it structures our document into a sequence of blocks. Just look at the following example:
This is the first block of text.\n
\n
This is the second block of text.
It is quite obvious that these should be considered as two blocks, but the following is only a single block:
This is the first line of the block.\n
This is the second line of the block.
These blocks can be categorized even further into Leaf Blocks (e.g. headings), Container Blocks (e.g. lists) and Inlines (e.g. code spans). I won’t go further into detail for now, as you can just look at the detailed CommonMark documentation.
Now we need to take a closer look at a single block:
This is a full text *with some cursive* and some **bold text**.
This is still a single block, but it consists out of 5 inline blocks/elements:
This␣is␣a␣full␣text␣
(including the trailing whitespace)with␣some␣cursive
␣and␣some␣
bold␣text
.
Alright, at this point you have some basic understanding of what data we are dealing with. Now you need to know how to parse a plain text document in general. A rule of thumb:
Break the text it into the smallest possible chunks, before processing each chunk
As mentioned before, our document consists out of a sequence of blocks. This already makes our lives way easier, as we can now analyze the blocks individually.
Next we know that none of the Markdown elements span more than a single line. Even the following example, a multi-line code segment, can be seen as three “sub-blocks”. To simplify our naming, I will from now on refer to them as Fragments:
``` <-- opening fragment
print("Hello World!") <-- code fragment
``` <-- closing fragment
We already broke down a large document into blocks and afterwards into fragments. As the content of the individual fragments varies, we can not break it down any further.
By keeping this structure in mind we can create the following basic algorithm:
1. Split the document into blocks
2. Iterate all blocks
2.1 Split each block into fragments
2.2. Iterate all fragments
2.2.1 Parse the fragment them into nodes (e.g. bold text)
2.3 Merge related parsed nodes together (e.g. code block)
3. Merge the nodes even further into a document tree, representing the original document
Your time has come. It’s time to write some code 🔥
As our library is entirely logic based and works as a black box (text as input, parsed document as output) this is a great use-case for Test-Driven-Development (TDD). The main idea of this development strategy is first defining a test case, which will fail on purpose, and then writing the code to fix it.
As the first step create a new Swift package MarkdownParser using either your Terminal of choice and swift package init –type library or using Xcode:
Next, open up MarkdownParserTests.swift
and create your first test case:
final class MarkdownParserTests: XCTestCase {
func testParsing_emptyDocument_shouldReturnNone() {
let text = ""
let parser = MarkdownParser(text: text)
let nodes = parser.parse()
XCTAssertEqual(nodes, [])
}
}
This code is straight forward, but for the sake of the tutorial I will explain it: First define the input text, then create a parser using the input text and call parse() to convert it into a node tree. Finally we write a test assertion to check that it returns the expected result.
Xcode will usually tell you about the syntax issues rather quickly:
The only solution to fix this situation is implementing the class MarkdownParser
which fulfills the test expectations:
class MarkdownParser {
private let text: String
init(text: String) {
self.text = text
}
func parse() -> [MarkdownNode] {
return []
}
}
enum MarkdownNode: Equatable {}
Run the test again and it will not fail anymore (for now foreshadowing intensifies) 🎉
Before adding more functionality, add a new test case:
func testParsing_singleLineOfText_shouldReturnSingleTextNode() {
let text = "Hello World"
let parser = MarkdownParser(text: text)
let nodes = parser.parse()
XCTAssertEqual(nodes, [
.text(content: "Hello World")
])
}
Once again we need to fix our code to fulfill the expectations by adding a new content node type to our known node types
enum MarkdownNode: Equatable {
case text(content: String)
}
and changing our parser to satisfy both test cases:
class MarkdownParser {
private let text: String
init(text: String) {
self.text = text
}
func parse() -> [MarkdownNode] {
// In case the text is empty, return an empty sequence instead of any nodes
if text.isEmpty {
return []
}
return [
.text(content: text)
]
}
}
Note: In this tutorial I am using an enum to define the different nodes, because of it’s simplicity. You can also create struct’s or even classes to return the parsed nodes.
Alright, alright, alright… enough with the simple text parsing. By now you hopefully understand how TDD is working, so let’s jump a few steps forward and create a more advanced test case:
func testParsing_multipleTextBlocksWithNestedBold_shouldReturnMultipleParagraphs() {
let text = """
This is a text block **with some bold text**.
Another paragraph with more **BOLD** text.
"""
let parser = MarkdownParser(text: text)
let nodes = parser.parse()
XCTAssertEqual(nodes, [
.paragraph(nodes: [
.text("This is a text block "),
.bold("with some bold text"),
.text(".")
]),
.paragraph(nodes: [
.text("Another paragraph with more "),
.bold("BOLD"),
.text(" text."),
])
])
}
The first step to deal with this complex example is creating the necessary node types:
enum MarkdownNode: Equatable {
case paragraph(nodes: [MarkdownNode])
case text(String)
case bold(String)
}
Now remember the algorithm in the introduction: First we need to split the text into blocks to then iterate them. We do this in a testable way by creating a so called Lexer , a class to split our raw content into smaller chunks (the so called lexems). Additionally it implements the iterator protocol, to use a standardized looping mechanism:
import Foundation
/// A Lexer is used to iterate the so/called lexems, tokens in a string, basically an iterator.
class Lexer: IteratorProtocol {
/// Lexems to iterate
private let lexems: [String]
/// Current iterator position
private var index = 0
/// Creates a new Lexer by tokenizing the given expression string.
///
/// If the given expression is empty, or contains only empty characters (such as spaces),
/// `nil` is returned, indicating no further parsing is necessary
///
/// - Parameter expression: Expression used to tokenize and lex
convenience init?(raw expression: String, separator: String) {
let lexems = expression.components(separatedBy: separator)
guard !lexems.isEmpty else {
return nil
}
self.init(lexems: lexems)
}
/// Creates a new Lexer for iterating the given lexems.
/// - Parameter lexems: Tokens to iterate
init(lexems: [String]) {
assert(!lexems.isEmpty, "Lexer should have at least one value")
self.lexems = lexems
}
/// Returns the currently selected lexem, does not modify the cursor position.
var token: String {
lexems[index]
}
/// Returns the currently selected lexem and moves the cursor to the nex position.
func next() -> String? {
guard !isAtEnd else {
return nil
}
let token = lexems[index]
index += 1
return token
}
/// Returns truthy value if the end is reached, therefore all elements were iterated.
var isAtEnd: Bool {
index >= lexems.count
}
}
Our Markdown parser is growing and the first two steps of our algorithm are already implemented:
import Foundation
class MarkdownParser {
private let text: String
init(text: String) {
self.text = text
}
func parse() -> [MarkdownNode] {
guard !text.isEmpty, let lexer = Lexer(raw: text, separator: "\n\n") else {
return []
}
var result: [MarkdownNode] = [
// Leave this node here for now, so our original test cases are not failing
.text(text)
]
// Iterate the lexems/blocks until there are no more available
while let block = lexer.next() {
// TODO: parse the block
}
return result
}
}
Good job with the progress! Let’s take care of step 3 and 4 next:
3. Split each block into fragments
4. Iterate all fragments and parse them into nodes (e.g. bold text)
Create another class BlockParser
which will iterate every fragment in a block and parse them individually:
class BlockParser {
let text: String
init(text: String) {
self.text = text
}
func parse() -> [MarkdownNode] {
guard let lexer = Lexer(raw: text, separator: "\n") else {
return []
}
var result: [MarkdownNode] = []
while let fragment = lexer.next() {
// Leave this node here for now, so our original test cases are not failing
result += [
.text(text)
]
}
return result
}
}
Adapt the MarkdownParser.parse()
to use it for each block and finish step 3 of our algorithm:
...
var result: [MarkdownNode] = []
// Iterate the lexems/blocks until there are no more available
while let block = lexer.next() {
result += BlockParser(text: block).parse()
}
return result
...
Up until this point the structure of the document was well-known (blocks split by empty lines, fragments split by newline characters).
For the actual fragment parsing logic you can choose from multiple approaches (such as using Regex’s) but in this approach we are using a character-based lexer.
The fragment lexer differs from the previous ones, as it iterates the content by each character and also offers additional methods to peak at further characters (does not increase the iterator counter) and rewind to move the iterator backwards.
class FragmentLexer: IteratorProtocol {
let content: Substring
var offset: Int = 0
init(content: Substring) {
self.content = content
}
var currentCharacter: Character? {
guard offset >= 0 && offset < content.count else {
return nil
}
return content[content.index(content.startIndex, offsetBy: offset)]
}
func peakPrevious(count: Int = 1) -> Character? {
offset -= count
let character = currentCharacter
offset += count
return character
}
func next() -> Character? {
let character = self.currentCharacter
offset += 1
return character
}
func peakNext() -> Character? {
let character = next()
rewindCharacter()
return character
}
func rewindCharacter() {
assert(offset > 0, "Do not rewind below zero!")
offset -= 1
}
func rewindCharacters(count: Int) {
offset -= count
}
}
Using all the knowledge we gathered during this tutorial, let’s create the last missing parser, the FragmentParser
. This class is going to use our FragmentLexer and identify the different nodes by specific characters, as declared in the specification.
In the first version, we concatenate each character into a .text(...)
node to fulfill our second test case:
class FragmentParser {
let fragment: String
init(fragment: String) {
self.fragment = fragment
}
func parse() -> [MarkdownNode] {
var result: [MarkdownNode] = []
// Leave this node here for now, so our original test cases are not failing
let lexer = FragmentLexer(content: fragment)
while let character = lexer.next() {
if let lastNode = result.last, case MarkdownNode.text(let previousText) = lastNode {
result[result.count - 1] = .text(previousText + String(character))
} else {
result.append(.text(String(character)))
}
}
return result
}
}
This works fine for the simple use case, but for more complex use-cases we need an additional data structure to efficiently track the abstract nesting position inside the fragment.
To understand what is going on, we think through the parsing logic of a more complex block (even more complicated than the previous ones):
This is a text block **with bold _and cursive_** text.
Should map the following structure:
.paragraph(nodes: [
.text("This is a text block"),
.bold("with bold"),
.boldCursive("and cursive"),
.text(" text.")
]),
Our fragment parsing algorithm will therefore work something like this:
1. character = "T", result = []
--> add text node to result with content "T"
2. character = "h", result = [.text("T")]
--> append the character to the previous node
...
22. character = "*", result = [.text("This is a text block ")]
--> check if the next character is also a "*", if yes begin a bold text and skip the next one, otherwise begin a cursive text
23. character = "*" --> skipped
24. character = "w", result = [.text("This is a text block "), .bold("")]
25. character = "i", result = [.text("This is a text block "), .bold("w")]
...
34. character = "*", result = [.text("This is a text block "), .bold("with bold ")]
--> same check as above
35. character = "a", result = [.text("This is a text block "), .bold("with bold "), .boldCursive("")]
...
47. character = '*", result = [.text("This is a text block "), .bold("with bold "), .boldCursive("and cursive")]
--> exit boldCursive mode, and return to bold only mode
...
Please keep in mind, this is pseudo code and should only help to understand what is going on.
Now look at the following slightly different example:
This is a text block **with bold \*and none cursive** text.
After removing the last asterisk, the parsed structure will look a bit different:
.paragraph(nodes: [
.text("This is a text block"),
.bold("with bold *and none cursive"),
.text(" text.")
]),
Unfortunately this also depends on our parser, as the following output could be valid too:
.paragraph(nodes: [
.text("This is a text block *"),
.cursive("with bold "),
.text("and none cursive"),
.cursive(""),
.text(" text."),
]),
This is a software design decision you have to make. In case you are curious how I implemented it, checkout the BoldCursiveInlineSpec.swift of CoolDown on GitHub.
As an efficient way of tracking the nesting, I decided to use a stack, which adds an additional node on the beginning characters (such as **
) and removes it from the stack when the closing characters are found.
To not increase the complexity of this tutorial any further, I will not cover the exact implementation of the stack mechanism. If you want to know now more, CoolDown is very well commented.
Alright, this is the final FragmentParser
for the scope of this tutorial:
class FragmentParser {
let fragment: String
init(fragment: String) {
self.fragment = fragment
}
func parse() -> [MarkdownNode] {
var result: [MarkdownNode] = []
let lexer = FragmentLexer(content: fragment)
// Start iterating every character
while let character = lexer.next() {
// Check if the character is an asterisk
if character == "*" && lexer.peakNext() == "*" {
// Move the cursor once forward to skip the second asterisk
_ = lexer.next()
// Array to track the characters inside the bold inline segment
var characters = [Character]()
// Flag to check if we ran out of characters before closing the inline segment
var hasTerminated = false
// Character counter, in case case we need to rewind
var rewindCount = 2
// Iterate remaining characters until the bold segment finishes or the block runs out of charactesr
while let nestedChar = lexer.next() {
rewindCount += 1
if nestedChar == "*" && lexer.peakNext() == "*" {
// skip second asteriks
_ = lexer.next()
// exit the loop, as the bold segment is done
hasTerminated = true
break
} else {
characters.append(nestedChar)
}
}
// If the inline element didn't terminate correctly, it shall not be detected
if hasTerminated {
// we successfully parsed the bold block but only append it if it is not empty
if !characters.isEmpty {
result.append(.bold(String(characters)))
}
continue
} else {
// Rewind to beginning of fragment and parse it as non-bold
lexer.rewindCharacters(count: rewindCount)
}
}
// If there is an existing text node, append the character, otherwise create a new one
if let lastNode = result.last, case MarkdownNode.text(let previousText) = lastNode {
result[result.count - 1] = .text(previousText + String(character))
} else {
result.append(.text(String(character)))
}
}
return result
}
}
The code is commented so it should be self-explanatory. In this example you can also see why our FragmentLexer has additional peak and rewind methods.
When you run the test case once again, they still fails with the following result:
// we get this
[
.text("This is a text block "),
.bold("with some bold text"),
.text("."),
.text("Another paragraph with more "),
.bold("BOLD"),
.text(" text."),
]
// instead of this:
[
.paragraph(nodes: [
.text("This is a text block "),
.bold("with some bold text"),
.text(".")
]),
.paragraph(nodes: [
.text("Another paragraph with more "),
.bold("BOLD"),
.text(" text."),
])
]
If you are fine with the two blocks being merged into a single one, good job, change the tests and you are done 😄
If not, change the MarkdownParser.parse()
method to group the nodes per block and if more than one block was found, it shall wrap them in paragraph nodes:
func parse() -> [MarkdownNode] {
// Split text by empty lines
guard !text.isEmpty, let lexer = Lexer(raw: text, separator: "\n\n") else {
return []
}
var nodesPerBlock: [[MarkdownNode]] = []
// Iterate the lexems/blocks until there are no more available
while let block = lexer.next() {
nodesPerBlock.append(BlockParser(text: block).parse())
}
if nodesPerBlock.count == 1 {
return nodesPerBlock[0]
}
// We want to group nodes per block so we can identify them afterwards
return nodesPerBlock.map { .paragraph(nodes: $0) }
}
You made it! Congratulations 🥳
This tutorial only covered a small subset of the possibilities in using and parsing Markdown. Obviously the three tests are not enough to covert the implemented functionality, so make sure to write more tests!
I also referenced our custom parser @ techprimate called CoolDown multiple times, which is still a work-in-progress, but is eventually getting production ready. We decided to build it as an Open Source Swift package, so checkout the GitHub repository.
Next to actually writing a small working parser, you also got more insight into the document format itself. Now you should be able to pick it up from there and continue working on the parser.
If you would like to know more, checkout my other articles, follow me on Twitter and feel free to drop me a DM. You have a specific topic you want me to cover? Let me know! 😃
]]>Let me show you 5 code extensions for Swift, which I use on a daily basis. Every single one is explained in detail and recreated from its backstory/original intent.
In case you TL;DR and only want to see the code, scroll to each The Smart Solutions headline for the copy-paste ready code, or checkout the link in the conclusion.
Every developer has at least once experienced an “out-of-bounds” exception. These occur when you try to access an element at a position which is either negative or higher than the element count.
let values = ["A", "B", "C"]
values[0] // A
values[1] // B
values[2] // C
values[3] // Fatal error: Index out of range
We start to create bound checks before accessing the value. That leads to repetitive code even tough it always does the exact same thing: checking the index bounds.
if 2 < values.count {
values[2] // "C"
}
if 3 < values.count {
values[3] // won't be called
}
Let’s create a function to wrap the bounds check, taking the element index and the array of elements as parameters. To support any kind of element, we add a generic type T
. This function returns an Optional
value wrapping either the element or nil
if the index is out of bounds.
func getValue<T>(in elements: [T], at index: Int) -> T? {
guard index >= 0 && index < elements.count else {
return nil
}
return elements[index]
}
let values = ["A", "B", "C"]
getValue(in: values, at: 2) // "C"
getValue(in: values, at: 3) // nil
This works fine, but it is still quite verbose and (simply said) ugly compared to the original e.g. values[2]
. Especially because of the additional parameter values .
First off we want to get rid of values parameter and instead associate the function getValue with the array. As Swift supports extending classes and protocols, we can move our getValue into an extension of Array:
extension Array {
func getValue(at index: Int) -> Element? {
guard index >= 0 && index < self.count else {
return nil
}
return self[index]
}
}
let values = ["A", "B", "C"]
values.getValue(at: 2) // "C"
values.getValue(at: 3) // nil
To use even more of the sweet Swift syntax capabilities, change the function to be a subscript function.
extension Array {
subscript (safe index: Int) -> Element? {
guard index >= 0 && index < self.count else {
return nil
}
return self[index]
}
}
values[safe: 2] // "C"
values[safe: 3] // nil
Awesome! Our access call values[safe: 2]
looks almost identical to the original one values[2]
but provides us boundary safe access to the elements.
EDIT 04.05.2021:
Thanks to Daniil Vorobyev for his response on Medium! Here a even more generic example, which can be used for any class implementing the Collection protocol:
extension Collection {
public subscript(safe index: Self.Index) -> Iterator.Element? {
(startIndex ..< endIndex).contains(index) ? self[index] : nil
}
}
When working with optional values, we often need to compare them with nil for null-checking. Sometimes we use a default value, in case the value is in fact nil, to keep going.
Here an example method which returns a default value in case the parameter is nil
:
func unwrap(value: String?) -> String {
return value ?? "default value"
}
unwrap(value: "foo") // foo
unwrap(value: nil) // default value
But another edge exists too: empty Strings.
If we use this unwrap method with an empty string ""
it will return the same empty String. There are definitely use-cases where we don’t want this behavior, but instead treat the empty String the same way as nil
.
We have to extend our function with a length check:
func unwrap(value: String?) -> String {
let defaultValue = "default value"
guard let value = value else {
return defaultValue
}
if value.isEmpty {
return defaultValue
}
return value
}
unwrap(value: "foo") // foo
unwrap(value: "") // default value
unwrap(value: nil) // default value
Quite an ugly solution for such a simple fallback, right? So, how about compressing it into a single line of code?
func unwrapCompressed(value val: String?) -> String {
return val != nil && !val!.isEmpty ? val! : "default value"
}
unwrapCompressed(value: "foo") // foo
unwrapCompressed(value: "") // default value
unwrapCompressed(value: nil) // default value
It works, but neither is this solution readable nor “good” by any standards, especially when trying to avoid force-unwrapping !
(to reduce the potential of unhandled crashes).
Convert empty strings to nil and work with the built-in support of Optional
:
public extension String {
var nilIfEmpty: String? {
self.isEmpty ? nil : self
}
}
Using this smart extension, you can use e.g. if-let unwrapping for checking for nil and for empty strings at the same time:
var foo: String? = nil
if let value = foo?.nilIfEmpty {
bar(value) // not called
}
if let value = "".nilIfEmpty {
bar(value) // not called
}
if let value = "ABC".nilIfEmpty {
bar(value) // called with "ABC"
}
Additionally this extension allows you to use a default value using ??
when the string is empty:
let foo = "ABC" ?? "123" // ABC
let bar = "" ?? "456" // 456
On iOS, interfaces are built using UIKit
’s UIView
s, nested inside more UIViews, managed by a UIWindow
, eventually resulting in a view hierarchy (same for AppKit on macOS).
When developers interact with the UI, they most certainly need references to specific views, which are then stored inside instance variables.
Let’s take a look at an example view controller.
class ViewController: UIViewController {
private weak var someViewRef: UIView?
override func viewDidLoad() {
super.viewDidLoad()
let someView = UIView()
self.someViewRef = someView
self.view.addSubview(someView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Update the background on appear
someViewRef?.backgroundColor = .red
}
}
First we create someView and add it to the view hierarchy in viewDidLoad. Afterwards we set a weak reference to the someViewRef instance property so we can interact with the view in viewWillAppear :
Zooming in on the detail we want to improve:
(1) let someView = UIView()
(2) self.someViewRef = someView
(3) self.view.addSubview(someView)
We want to reduce these 3 lines of code, without compromising on readability. This might seem over-engineered for this small single use case, but think about a view controller where 20 or even 30 views are created –> 20–30 lines of code can be saved.
To really understand what is going on, you have to know about Automatic Reference Counting (ARC).
ARC was introduced in Objective-C as form of memory management back in iOS 5.
When creating the view in line (1), some memory (enough to hold an UIView) is allocated for the instance. In the same step, an internal counter is set to 1, as someView is a reference to this instance. When assigning the someViewRef
in lin (2) the counter increases by one. The final line (3) increases it once again (to a total count of 3), because the view hierarchy also holds a reference to the particular view.
At the end of viewDidLoad
method, all local variables and references are discarded, including someView. This decrements the counter, and it is left at 2 for someViewRef
and the view hierarchy (because of view.addSubview(…))
One of the core principles of UIKit/AppKit is letting the view hierarchy be the only one holding the strong references to the views.
So in case the view gets removed from the overall view hierarchy, the counter should be decremented to 0 and automatically be freed from the memory. This helps with reducing memory leaks when navigating through an app.
To comply with this principle, we always use weak
references, as they do not increment the ARC counter. As the view can be deallocated, and therefore the instance isn’t available anymore (it becomes nil
) it needs to be an Optional
type.
In the code example above someViewRef
is already declared as weak
, and so at the end of viewDidLoad
our counter value is 1 .
What happens if we combine the first two lines into a single one?
The compiler already tells us that this statement is going to be useless. We create a new instance, but do not increment the ARC due to weak
. Therefore the counter is still at 0 after executing the line of code and the instance is deallocated instantly.
Also the someViewRef
is now optional, and we would need to unwrap the UIView?
to add it to the parent view.
To summarize our requirements:
Seems like a tough problem, doesn’t it?
Luckily, Swift provides many syntax features, so we can build our custom solution.
In the first step, move the assignment of someViewRef into a global function (can be anywhere). One parameter is the weak inout reference, where we assign the instance to, and the the second one is the instance of the UIView .
func assign(someViewRef: inout Optional<UIView>,
someView: UIView) -> UIView {
someViewRef = someView
return someView
}
Our viewDidLoad can be transformed to the following:
override func viewDidLoad() {
super.viewDidLoad()
let someView = assign(someViewRef: &someViewRef,
someView: UIView())
self.view.addSubview(someView)
}
Great! A single line to both create the view and assign someView and someViewRef 🎉
It is still highly limited, as it only allows UIView
instances, but we can improve this by changing the parameter to be a generic type T
:
func assign<T>(target: inout Optional<T>, value: T) -> T {
target = value
return value
}
You can improve it even further, by changing the value parameter to be a closure returning the value (that might become interesting e.g. in case you want to use limited code scopes):
func assign<T>(target: inout Optional<T>, value: () -> T) -> T {
let instance = value()
target = instance
return instance
}
let someView = assign(target: &someViewRef, value: {
let view = UIView()
view.backgroundColor = .orange
return view
})
Unfortunately this breaks our previous usage:
We fix it by prepending the parameter type of value with @autoclosure and both are working once again 🔥
But how do you feel about these changes?
// we started with
let someView = UIView()
someViewRef = someView
// we are now at
let someView = assign(target: &someViewRef, value: UIView())
Using & for the reference, and parameter names and etc. lead to a quite verbose line of code… but at least it is one line of code, amirite?!😅
Was it worth it? Probably not… but luckily this isn’t even its final form 🤯
infix operator <--
public func <-- <T>(target: inout T?,
value: @autoclosure () -> T) -> T {
let val = value()
target = val
return val
}
Rename the function from assign to a sleek arrow <– and declare it as an infix operator (if you feel adventurous you can even use an emoji arrow ⬅️). Other examples of these infix operators are + and -. All of them take two parameters… one before, and one after the operator.
Our final solution fits all the constraints in a pretty, short syntax:
let someView = someViewRef <-- UIView()
How often do you count elements in an array? What is your approach? Is it one of the following ones?
let array = ["A", "A", "B", "A", "C"]
// 1.
var count = 0
for value in array {
if value == "A" {
count += 1
}
}
// 2.
count = 0
for value in array where value == "A" {
count += 1
}
// 3.
count = array.filter { $0 == "A" }.count
// 4.
// get creative, there are many more
Swift tries to be as human-friendly as possible, and our code should also try to reflect the human language.
So instead of filtering, counting, iterating etc. everywhere in your codebase, checkout this clean, small, universally applicable count(where:) extension (which honestly should exist in Swift standard library by default).
extension Sequence where Element: Equatable {
func count(where isIncluded: (Element) -> Bool) -> Int {
self.filter(isIncluded).count
}
}
By extending the Sequence protocol, other classes than Array are supported too, such as ArraySlice :
["A", "A", "B"]
.count(where: { $0 == "A" }) // 2
["B", "A", "B"]
.dropLast(1) // --> ArraySlice<String>
.count(where: { $0 == "B" }) // 1
This extension will most likely not be used as often as the others. Still it solves a struggle when working with Binding in SwiftUI.
Take the following example, showing two buttons where each one shows a different sheet:
struct ContentView: View {
@State var isPresentingSheet1 = false
@State var isPresentingSheet2 = false
var body: some View {
VStack {
Button("Show Sheet 1") {
isPresentingSheet1 = true
}
Button("Show Sheet 2") {
isPresentingSheet2 = true
}
}
.sheet(isPresented: $isPresentingSheet1) {
Text("Sheet 1")
}
.sheet(isPresented: $isPresentingSheet2) {
Text("Sheet 2")
}
}
}
Chaining the .sheet(isPresented:) {...}
feels quite naturally. Unfortunately this wasn’t actually working for a long time and got resolved only a few days ago with the release of iOS 14.5 (still broken in previous versions).
As I wanted to use the isPresented
version of .sheet()
instead of .sheet(item:)
(with some kind of enum declaring every single possible sheet) I tried to concatenate the two Binding<Bool>
instances:
Bummer. This was expected, but still I am not happy.
Fortunately we can overload the already existing infix operator && by simply creating a global function with the same name, but with two Binding<Bool>
parameters 🚀
public func && (lhs: Binding<Bool>,
rhs: Binding<Bool>) -> Binding<Bool> {
Binding<Bool>(get: { lhs.wrappedValue && rhs.wrappedValue },
set: { _ in fatalError("Not implemented") })
}
Binding<Bool>
is a property wrapper which holds a wrappedValue: Bool
. Every Binding has a getter and a setter closure, which returns a logical conjunction of the two parameters. As the setter method is undefined (which parameter should be changed?) we leave it with not implemented for now.
There are many more smart extension out their in the wildness of the internet. All the ones listed here, are also available, documented and tested in my toolbox Cabinet on GitHub.
If you would like to know more, checkout my other articles, follow me on Twitter and feel free to drop me a DM. You have a specific topic you want me to cover? Let me know! 😃
Edits:
04.05.2021 — Added response of Daniil Vorobyev
]]>This tutorial shows you, how to harness the impressive power of the Swift Package Manager (SPM) to create a clean, extensible and especially shared UI structure for your large-scale apps.
Note: This is a follow-up tutorial to Modularize Xcode Project using local Swift Packages and builds up on the topics mentioned there. In case you are already an advanced iOS/macOS/SPM user, go ahead, but if you are fairly new to the topic, I highly recommend you read my other article first.
At the time of writing, four main options of building user interfaces exist:
From my personal experience, storyboards are great for the initial development. Especially due to the simple view navigation using segues you can get a prototype running rather quickly. Over time more views get added and eventually Xcode starts struggling with rendering/processing/compiling the file. Slowly it starts to become more painful to work with (don’t get me started talking about dealing with merge-conflicts with those multi-thousands lines of XML configuration).
It gets even worse, when you just want to iterate a single view, but the large storyboard keeps changing multiple ones (e.g. due to layout updates). Or when it takes forever updating every single view again and again, when working with IBDesignables.
Oh, you don’t feel like navigating to a very nested sub view every. single. time? You want to use only that single view from the storyboard in a different app target? Of course you can always dequeue it, but make sure to include all necessary assets (even those you aren’t using right now), or otherwise it might fail to compile/run. 🙃
So… how can we improve our workflow? We go back to simpler UI building!
In this tutorial we are going to build a simple clock app with the following requirements:
As you can see this is a (stupidly) simple app, but the main focus is using the Swift Package Manager for building UI components, so it will suffice.
As the first step, create an empty Xcode project as our starting point and name it Clock:
Now you have a clean starting point and inside the project settings you need to add the iOS and the macOS app targets by clicking the little plus symbol in the bottom left corner:
For the iOS target, use the default App template with the UIKit App Delegate Lifecycle and name it Clock_iOS
.
For the bundle identifier, use your own reverse-domain, e.g. for me it’s com.techprimate because of my mobile app agency’s domain techprimate.com
Similarly for the macOS app, use the default App template with the AppKit App Delegate Lifecycle and name it Clock_macOS. As before, use your own organization identifier:
In the final project setup step, disable automatic code signing (we don’t need it right now, and usually it messes up your Apple Developer Account by creating unwanted provisioning profiles and app identifiers). You should still be able to run the the macOS application, with the setting Sign To Run Locally, and the iOS app in the simulator.
Congratulations on creating your cross-platform app Clock! Run both applications at least once to make sure they work fine. No worries, both screens will be a white void, as there is no UI to show yet, but we are going to fix that next.
As the clock logic must be shared by both applications, we create a local Swift package library ClockPackage and drag it into our Xcode project. Detailed step-by-step instructions can be found in my previous article.
$ cd root/of/my/project
$ mkdir ClockPackage
$ cd ClockPackage
$ swift package init --type library
Afterwards your project should look like this:
For simplicity, rename the ClockPackage
target to ClockUI
by changing the folder name and the declaration in the package manifest Package.swift. Also, we won’t use tests in this tutorial, so go ahead and delete the folder Tests and the test targets.
Now that our UI package is set up, you will create the XIB interface builder files. Both applications should offer the user a button to update the current time and display it in a label.
While using this SPM concept in one of the iOS/macOS projects at WolfVision, I realized that XIB interface files for macOS and iOS are not interchangeable. To visualize my experiences to you, we will now do it the wrong way and fix it afterwards.
Since Swift 5.3 it is possible to add resources to a Swift package. Apple created a pretty detailed documentation on how to handle them, but for the sake of the tutorial I will give you the quick summary:
Create a folder Resources inside Sources/ClockUI
Folder structure after adding the resources folder
Create an iOS View ClockViewController_iOS.xib
Create a macOS View ClockViewController_macOS.xib
Add both files as resources to the package manifest in the targets section:
...
targets: [
.target(name: "ClockUI", resources: [
.process("Resources/ClockViewController_iOS.xib"),
.process("Resources/ClockViewController_macOS.xib"),
])
]
...
As you can see here, we use process(path: String)
to compile the XIB files at build time into NIB files, which are then used at runtime for loading the UI.
To test the compilation of the package, select ClockUI as the run scheme in the top toolbar in Xcode. Try to build it once for Any Mac and once for Any iOS Device. It will fail both times.
Inside the *Report navigator *you can take a closer look at the two failed build logs. You will see that the macOS build failed due to the iOS XIB file, and vice versa.
As Swift packages do not support conditional resource compilation, we can’t use this exact project structure any further and have to change our libraries.
Create two additional libraries ClockUI_iOS
and ClockUI_macOS
with a folder Resource inside each one of them. Afterwards move the *.xib
files into their respective one and change the Package.swift manifest to reflect our new structure:
let package = Package(
name: "ClockPackage",
products: [
.library(name: "ClockUI", targets: ["ClockUI"]),
.library(name: "ClockUI_iOS", targets: ["ClockUI_iOS"]),
.library(name: "ClockUI_macOS", targets: ["ClockUI_macOS"]),
],
targets: [
.target(name: "ClockUI"),
.target(name: "ClockUI_iOS", resources: [
.process("Resources/ClockViewController_iOS.xib"),
]),
.target(name: "ClockUI_macOS", resources: [
.process("Resources/ClockViewController_macOS.xib"),
])
]
)
Now you can build the scheme ClockUI_iOS
for Any iOS Device, ClockUI_macOS
for Any Mac and ClockUI
for both of them successfully 🎉
Hope you don’t mind that I am skipping the detailed explanation of the creation of the UI interfaces inside our iOS/macOS XIB files (there so many UIKit/AppKit tutorials out there) and instead focus on the build architecture. Of course you can always checkout the full code in the GitHub repository.
First create a class ClockViewController
in a new file at ClockUI_iOS/ClockViewController.swift
with an @IBOutlet
for accessing the time label and an IBAction for as the target action for the button.
import UIKit
public class ClockViewController: UIViewController {
// MARK: - IB Outlets
@IBOutlet weak var timeLabel: UILabel!
// MARK: - IB Actions
@IBAction func didTapFetchButtonAction() {
print("did tap fetch")
}
}
Afterwards you need to connect the File’s Owner to the view controller. Make sure the module ClockUI_iOS is selected, or otherwise it won’t be able to resolve the class later. This also allows to connect the IBOutlet/IBAction to the code (don’t forget to set the view outlet!!)
Before continuing with the macOS equivalent, let’s present this interface in our iOS app. To do so, we first need to add the ClockUI_iOS
module as a framework to our iOS target:
If you followed along nicely, you can now import ClockUI_iOS
in the Clock_iOS/ViewController.swift
file and create an instance of the ClockViewController
:
import UIKit
import ClockUI_iOS
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let clockVC = ClockViewController()
self.addChild(clockVC)
// set a debug background color so we can see the view
clockVC.view.backgroundColor = .orange
clockVC.view.autoresizingMask = [
.flexibleWidth, .flexibleHeight
]
self.view.addSubview(clockVC.view)
}
}
As you can see, the whole view is now orange but still empty. Instead of creating an instance of ClockViewController
we need to load it from the XIB file by changing the following line
...
let clockVC = ClockViewController()
// becomes
let clockVC = ClockViewController.loadFromNib()
...
And add the loadFromNib()
method to the ClockViewController
:
// MARK: - Nib Loading
public static func loadFromNib() -> ClockViewController {
// Loads the compiled XIB = NIB file from the module
// resources bundle. Bundle.module includes all resources
// declared in the Package.swift manifest file
ClockViewController(nibName: "ClockViewController_iOS",
bundle: Bundle.module)
}
Run the app again and your button and label will show up 🎉
Lucky for us, the macOS implementation works exactly the same, but with the respective macOS equivalents of classes and modules:
Create the ClockPackage/ClockUI_macOS/ClockViewController.swift
public class ClockViewController: NSViewController {
// MARK: - IB Outlets
@IBOutlet weak var timeLabel: NSTextField!
// MARK: - IB Action
@IBAction func didClickFetchButtonAction(_ sender: Any) {
print("did click fetch")
}
// MARK: - Nib Loading
public static func loadFromNib() -> ClockViewController {
ClockViewController(nibName: "ClockViewController_macOS",
bundle: Bundle.module)
}
}
Once again, don’t forget to connect the view outlet, or it fails to instantiate the NIB.
If Xcode is not showing you the option to link view, add it as an
@IBOutlet weak var view: NSView!
in the view controller Swift class and then it should show up in the Xcode Interface Builder. After linking, you can simply delete line and it should still work fine, as NSViewController already owns a property with that name.
ClockUI_macOS
framework to the Clock_macOS
app targetClockUI_macOS
in Clock_macOS/ViewController.swift
loadFromNib
from the iOS package into the macOS packageAdd the view controller to the view hierarchy:
import Cocoa
import ClockUI_macOS
class ViewController: NSViewController {
override func viewDidAppear() {
super.viewDidAppear()
let clockVC = ClockViewController.loadFromNib()
self.addChild(clockVC)
clockVC.view.autoresizingMask = [.width, .height]
self.view.addSubview(clockVC.view)
}
}
Great job! You have now a running iOS and macOS application, using interface resources from Swift packages 🚀
As a final step, we want to create a service which is shared between the ClockUI_iOS and ClockUI_macOS packages. As we started off with a ClockUI package, it fits our needs perfectly, therefore rename the file ClockUI/UI.swift
to ClockUI/ClockService.swift
and create a class with the same name inside:
import Foundation
import Combine
public class ClockService {
// subject to subscribe for updates
public var currentTime = PassthroughSubject<String, Never>()
public init() {}
public func updateTime() {
let formatter = DateFormatter()
formatter.timeStyle = .full
currentTime.send(formatter.string(from: Date()))
}
}
As explained in the previous tutorial, add ClockUI as a dependency to the ClockUI_macOS
and ClockUI_iOS
libraries in the package manifest.
Quick summary on the implementation logic:
To use the service in our view controllers, create a local instance and subscribe to the currentTime publisher. As an example, here the final iOS view controller:
import UIKit
import ClockUI
import Combine
public class ClockViewController: UIViewController {
// MARK: - Services
private let service = ClockService()
private var timeCancellable: AnyCancellable!
// MARK: - IB Outlets
@IBOutlet weak var timeLabel: UILabel!
// MARK: - IB Actions
@IBAction func didTapFetchButtonAction() {
service.fetchTime()
}
// MARK: - View Life Cycle
public override func viewDidLoad() {
super.viewDidLoad()
timeCancellable = service.currentTime
.sink(receiveValue: { self.timeLabel.text = $0 })
}
// MARK: - Nib Loading
public static func loadFromNib() -> ClockViewController {
// Load the compiled XIB = NIB file from the
// module resources bundle
ClockViewController(nibName: "ClockViewController_iOS",
bundle: Bundle.module)
}
}
Run both applications, and voilà… you can update the time label when pressing the button in each one, with them behaving in the same way!
You can find the full code in the GitHub repository.
We did it. We created two applications using non-compatible interfaces builders with the same UI logic.
In case you are still wondering, why one should go through the hassle of splitting the project into such fragmented packages, let me explain further:
If you would like to know more, checkout my other articles, follow me on Twitter and feel free to drop me a DM. You have a specific topic you want me to cover? Let me know! 😃
]]>But how can we leverage this dependency structure even further? Is external code the only reason for using a package manager?
Our codebases are growing with every single new file. First, we create a folder structure to organize our .swift files, but then even the slightest code requires Xcode to recompile everything. Our build process becomes slower… and slower… and ….… goes away to grab a coffee while waiting for Xcode to finish compilation ….… slower.
Even worse when working with feature-rich, large-scale apps. They become clunky and you spend a lot of time waiting for rebuilding unchanged parts when you just want to iterate your own new, fresh feature.
Example:
A feature-rich receipt tracking app, which connects to your bank account for matching transactions, uses a cloud for live synchronization, sharing accounts with friends, etc. You want to add a scanning feature, which takes a photo and converts it into the receipt data your app uses.
Swift Package Manager allows us to create small, reusable code packages. On the one hand, this allows us to isolate unchanged code during the build process, and on the other hand, it allows us to simply create a spin-off demo version of the app, with only the necessary parts to improve a single feature.
Continuing the example above: Using local SPM packages, you can create a small prototyping app that only shows the scan feature. When the feature is done, it can be used in the main app.
Let me give you a quick overview of how we are going to build our own multi-platform *Calculator *as an iOS app and a command-line tool (the guide for creating the iOS app can be applied for macOS too):
If you are more interested in the final solution, check out this GitHub repository for the final code.
iOS app and command-line executable offering the same functionality
Start off launching your Terminal of choice (for me it’s iTerm2). Then go ahead and create a new folder called Calculator and afterward change the working directory into that folder:
$ mkdir Calculator
$ cd Calculator
The next step is initializing our Swift package. The Swift Command Line Interface (CLI) allows us to create multiple types of packages. To figure which ones, run swift package init –help for a list:
$ swift package init --help
OVERVIEW: Initialize a new package
OPTIONS:
--name Provide custom package name
--type empty|library|executable|system-module|manifest
Our main focus is on the library and executable. If you are just creating a library package, run swift package init --type library
. But in our case, we want to start with an executable (leading $ means it is a command):
$ swift package init --type executable
Creating executable package: Calculator
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/Calculator/main.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/CalculatorTests/
Creating Tests/CalculatorTests/CalculatorTests.swift
Creating Tests/CalculatorTests/XCTestManifests.swift
Awesome! You created your first Swift package 🔥
Our folder structure now looks like the following:
Calculator
├── Package.swift
├── README.md
├── Sources
│ └── Calculator
│ └── main.swift
└── Tests
├── CalculatorTests
│ ├── CalculatorTests.swift
│ └── XCTestManifests.swift
└── LinuxMain.swift
To start working, simply open/double-click the Package.swift
file and Xcode will recognize it as a package(-project).
As this blog post is not so much of a tutorial about building a calculator in Swift, I am providing you only simple implementation steps in the comments (let me know on Twitter if you want a more detailed tutorial).
Place the following code in your main.swift
file:
import Foundation
// CommandLine gives us access to the given CLI arguments
let arguments = CommandLine.arguments
// We expect three parameters: first number, operator, second number
func printUsage(message: String) {
let name = URL(string: CommandLine.arguments[0])!.lastPathComponent
print("usage: " + name + " number1 [+ | - | / | *] number2")
print(" " + message)
}
// The first one is the binary name, so in total 4 arguments
guard arguments.count == 4 else {
printUsage(message: "You need to provide two numbers and an operator")
exit(1);
}
// We expect the first parameter to be a number
guard let number1 = Double(arguments[1]) else {
printUsage(message: arguments[1] + " is not a valid number")
exit(1);
}
// We expect the second parameter, to be one of our operators
enum Operator: String {
case plus = "+"
case minus = "-"
case divide = "/"
case multiply = "*"
}
guard let op = Operator(rawValue: arguments[2]) else {
printUsage(message: arguments[2] + " is not a known operator")
exit(1);
}
// We expect the third parameter to also be a number
guard let number2 = Double(arguments[3]) else {
printUsage(message: arguments[3] + " is not a valid number")
exit(1);
}
// Calculation function using our two numbers and the operator
func calculate(number1: Double, op: Operator, number2: Double) -> Double {
switch op {
case .plus:
return number1 + number2
case .minus:
return number1 - number2
case .divide:
return number1 / number2
case .multiply:
return number1 * number2
}
}
// Calculate the result
let result = calculate(number1: number1, op: op, number2: number2)
// Print result to output
print("Result: \(result)")
To use your new calculator, go back to the terminal and inside the package folder, use the swift run command to test the implementation:
$ swift run Calculator 13 + 14
Result: 27.0
We got the first of our two applications up and running. Before we continue to create the iOS app, let’s review the code and figure out, which parts should be shared by all the applications.
Two parts of the code are relevant:
So let’s start by creating a new library. First off we clean up the default Package.swift
manifest file by removing all the comments and unused arguments:
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "Calculator",
targets: [
.target(name: "Calculator"),
.testTarget(name: "CalculatorTests",
dependencies: ["Calculator"]),
]
)
Now create a new folder inside the Sources folder called CalculatorCore which is our shared application core logic.
Inside this newly created folder we are creating two new Swift files:
First, create the file Operator.swift
and move the enum Operator {...}
declaration in there:
enum Operator: String {
case plus = "+"
case minus = "-"
case divide = "/"
case multiply = "*"
}
Second, create another file calculate.swift
and move the calculate(…) function into there:
// Calculation function using our two numbers and the operator
func calculate(number1: Double, op: Operator, number2: Double) -> Double {
switch op {
case .plus:
return number1 + number2
case .minus:
return number1 - number2
case .divide:
return number1 / number2
case .multiply:
return number1 * number2
}
}
After moving out the code, your main.swift
file should now look like this:
import Darwin
import Foundation
// CommandLine gives us access to the given arguments
let arguments = CommandLine.arguments
// We expect three parameters: first number, operator, second number
func printUsage(message: String) {
let name = URL(string: CommandLine.arguments[0])!.lastPathComponent
print("usage: " + name + " number1 [+ | - | / | *] number2")
print(" " + message)
}
// The first one is the binary name, so in total 4 arguments
guard arguments.count == 4 else {
printUsage(message: "You need to provide two numbers and an operator")
exit(1);
}
// We expect the first parameter to be a number
guard let number1 = Double(arguments[1]) else {
printUsage(message: arguments[1] + " is not a valid number")
exit(1);
}
// We expect the second parameter, to be one of our operators
guard let op = Operator(rawValue: arguments[2]) else {
printUsage(message: arguments[2] + " is not a known operator")
exit(1);
}
// We expect the third parameter to also be a number
guard let number2 = Double(arguments[3]) else {
printUsage(message: arguments[3] + " is not a valid number")
exit(1);
}
// Calculate the result
let result = calculate(number1: number1, op: op, number2: number2)
// Print result to output
print("Result: \(result)")
If you try to run your application once again, it will greet you with an error saying it can’t find type Operator
nor the function calculate
anymore.
This is expected, so now we have to finish creating the library CalculatorCore
and add it as a dependency to our app target Calculator
. To do so, all we need is to declare the library in our Package.swift
:
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "Calculator",
targets: [
.target(name: "CalculatorCore"),
.target(name: "Calculator",
dependencies: ["CalculatorCore"]),
.testTarget(name: "CalculatorTests",
dependencies: ["Calculator"]),
]
)
If you try to run the application once more, you will still see the same errors. The reason behind this behavior is the missing import CalculatorCore
in the main.swift
:
import Foundation
import CalculatorCore
// CommandLine gives us access to the given arguments
...
Additionally, the (in my opinion great) isolation capabilities of Swift packages require us to declare both Operator
and calculate
as public
or otherwise, they won’t be available outside the package:
// in Operator.swift:
public enum Operator { ... }
// in calculate.swift:
public func calculate(...) -> Double { ... }
Run your application using swift run and it should be working once again.
Good job so far! You already created a command-line executable and an SPM library. Now we expand it even further and create an iOS app using our SPM library calculation logic.
For this tutorial, we will be using a SwiftUI app, as it is the future of iOS/macOS app development, and allows us to create a simple calculator way faster than using traditional UIKit.
Open up Xcode and click on File/New/Project
Now select App in the iOS tab, name it *Calculator_iOS *and select SwiftUI for all the settings.
Make sure to place the project in your main Calculator folder, and you should end up with the following file structure:
As we don’t need the nested iOS folder, close the Xcode project and move the content up one level. Additionally, we move the package into its own subfolder Calculator
, so afterwards your folder structure should look like this:
Now open up the Calculator_iOS.xcodeproj
, select your simulator of choice and run the initial application to make sure everything is fine and working.
As the next step, we go ahead and create our calculator UI using two text fields and an operator selection. Replace your struct ContentView {…} inside the ContentView.swift with the following code and run the application once again:
struct ContentView: View {
@State var number1 = ""
@State var op = "+"
@State var number2 = ""
var body: some View {
VStack {
TextField("Number 1", text: $number1)
.keyboardType(.numberPad)
.padding(10)
.cornerRadius(5)
Picker("Operator", selection: $op) {
ForEach(["+", "-", "*", "/"], id: \.self) { op in
Text(op)
}
}
.pickerStyle(SegmentedPickerStyle())
TextField("Number 2", text: $number2)
.keyboardType(.numberPad)
.padding(10)
.cornerRadius(5)
Divider()
Text("Result: " + result)
.padding(10)
}
.padding(20)
}
var result: String {
return "?"
}
}
Your basic calculator is done as you can enter numbers and select an operator:
Next is adding our local Swift package as an iOS application dependency. This step is not documented or known that well, but very easy. All you have to do is drag the folder CalculatorPackage into the Calculator_iOS file browser at the very top:
And afterwards, Xcode will detect the folder as a local package:
Before we can actually add our library to the iOS project, we need to declare it as a product inside the Package.swift
. As a library product can bundle multiple targets together, we need to add the CalculatorCore as the targets parameter.
let package = Package(
name: "Calculator",
products: [
.library(name: "CalculatorCore",
targets: ["CalculatorCore"])
],
targets: [
.target(name: "CalculatorCore"),
.target(name: "Calculator",
dependencies: ["CalculatorCore"]),
.testTarget(name: "CalculatorTests",
dependencies: ["Calculator"]),
]
)
As a final step you have to add the CalculatorCore library as a dependency to the iOS app target, by clicking on the plus + in the target settings in the Frameworks, Libraries, and Embedded Content section and selecting it in the list:
This is it. Your local Swift Package is now available inside your iOS app 🎉
Inside the ContentView.swift we can add the import CalculatorCore at the top of the file and once again we can use the Operator
type and calculate
function inside the computed property result :
var result: String {
guard let num1 = Double(number1) else {
return number1 + " is not a valid number"
}
guard let num2 = Double(number2) else {
return number2 + " is not a valid number"
}
// Force unwrap the operator for now,
// as we can be sure that we only added known ones
let op = Operator(rawValue: self.op)!
let result = calculate(number1: num1, op: op, number2: num2)
return result.description
}
Run the app once again and you can use the calculator inside iOS:
Our shared codebase is now ready to grow, but we want to keep the maintenance of our individual apps under control.
At this point you have two quite ugly lines of code in your programs:
// main.swift, Line 10:
print("usage: " + name + " number1 [+ | - | / | *] number2")
// ContentView.swift, Line 24:
ForEach(["+", "-", "*", "/"], id: \.self) { op in ...
Both of these lines manually list the operators we have implemented, and if we add another one to the enum Operator, they won’t be updated. Even worse we might forget to add it to one of our apps.
Let’s fix this, by adding the CaseIterable protocol to the Operator enum, which gives us Operator.allCases
, a synthesized Array with all available operators.
public enum Operator: String, CaseIterable {
case plus = "+"
case minus = "-"
case divide = "/"
case multiply = "*"
}
Inside the ContentView.swift change the ForEach to use the .allCases instead:
ForEach(Operator.allCases, id: \.self) { op in
Text(op.rawValue)
}
As ForEach inside the Picker adds the operator as a tag to the Text object, we now have to change the selection property too:
...
@State var op: Operator = .plus
...
This way can also get rid of the force-unwrap inside var result: String {..}
Inside the main.swift
of our CLI application, you can now dynamically create the operator list in the printUsage
function:
func printUsage(message: String) {
let name = URL(string: CommandLine.arguments[0])!
.lastPathComponent
let operators = Operator.allCases
.map(\.rawValue)
.joined(separator: " | ")
print("usage: \(name) number1 [\(operators)] number2")
print(" " + message)
}
Great, do you want to add another operator? No worries, just extend the enum Operator by another case and implement it in the calculate() function 🎉
In this step, we want to add a debug logger to our CalculatorCore. We could just use the print() method, but that wouldn’t be as much fun, right? 😄
Creating more local packages is straightforward. As before, create a folder inside Sources with the package name. In this case, it is going to be CalculatorLogger
and it contains a single Logger.swift
file:
public class Logger {
public static func warn(_ message: String) {
print("⚠️ " + message)
}
public static func debug(_ message: String) {
print("🔍 " + message)
}
}
Afterwards, create a new target in the package manifest and add it as a dependency to the CalculatorCore package
targets: [
.target(name: "CalculatorCore", dependencies: [
"CalculatorLogger"
]),
.target(name: "CalculatorLogger"),
...
]
and import it in our calculate.swift
file:
import CalculatorLogger
// Calculation function using our two numbers and the operator
public func calculate(number1: Double, op: Operator, number2: Double) -> Double {
Logger.debug("Now calculating \(number1) with \(number2) using \(op)")
...
}
This gives the following output when running swift run inside the CalculatorPackage directory:
$ swift run Calculator 13 + 14
[3/3] Linking Calculator
🔍 Now calculating 13.0 with 14.0 using plus
Result: 27.0
This is it. If you followed along you have now created a multi-platform app using the same core logic 🚀 Some closing remarks on why this is useful:
While writing this article I realized it is not possible to cover the more advanced capabilities, such as per-platform UI modules using interfaces to communicate in a VIPER pattern (which is something I am currently using in a large-scale iOS/macOS cross-platform app). Therefore I will cover advanced topics, such as how SPM can help you transitioning from UIKit/AppKit to SwiftUI using XIB files into their own packages, in a future article (make sure to follow me to get notified!).
If you would like to know more, check out my other articles, follow me on Twitter and feel free to drop me a DM. Do you have a specific topic you want me to cover? Let me know! 😃
EDIT 13.04.2021: Added an example for spin-off app for large-scale projects
UPDATE 19.04.2021: I just published the follow-up article!
]]>The following story will start off less technical, and instead more historical/personal. Skip to “My first time with GatsbyJS” if you only want to hear about the implementation 😊
Combining GatsbyJS & Airtable makes it easy to build a web app
Almost 4 years ago, in 2017, I moved from a small city to Austria’s Capitol, Vienna, to study Software Engineering at Vienna University of Technology (TU Wien). As I have mostly been self-taught in the 4 years before, I finally wanted to learn it by the book. Next to studying I started to manifest my know-how on iOS mobile app development, eventually founding my own mobile app development agency techprimate in 2018.
Another three years passed, and I am still living in Vienna, still studying, still building software as my passion. After some failed projects in 2019 and struggling with university at that time, pivoting my goals became necessary. In 2020 I decided to invest more time into becoming known in the developer community, to have a larger network where I can share my knowledge and learn from others. The first step was doing a summer internship as a back-end developer at fusonic, and afterward starting as a part-time iOS/macOS developer at WolfVision. During that time I was able to meet great new developers, learn how others work, and apply my capabilities.
One day, after listening to a great talk of Max Stoiber “How Open Source changed (his) life”, one major thought kept stuck in my head:
Share with others, what you are currently working on. There is no value in keeping everything to yourself, especially if you never get to release it to the world… build it in public!
This isn’t quite a quote of Max (as I just made these words up in my head while writing this article 😄), but basically what I got from his talk is, that all the side-projects on my computer or in my private git repositories are just decaying, as everything around keeps evolving.
Well… I have already been a part of the Open Source community, when I started TPPF, a Swift PDF Generator framework, in 2016, and even though I am personally not using it anymore, the community using it keeps growing every day. This is enough reason for me to keep maintaining it (as much as possible). Because of its success and me actively relying on Open Source too in many aspects, I became a strong Open Source advocate!
As an (unfortunate) example, I also experienced where being too secretive can lead to: In the past, we built a school app at techprimate. As we feared competition we kept it as secret as possible, which lead to the project failing… because we didn’t have enough feedback, when it became inevitably necessary.
All this led to a new strategy for 2021:
Sharing my content with others & building projects in the public, will help others and lead to feedback eventually!
This is my motivation to write blog posts, like the one you are reading right now! 😄
An awesome monthly online event called “iOS Dev Happy Hour” was launched about 7 months ago on Twitter (by Allen W) and last month I finally got the chance to participate. It showed me once again how diverse and fascinating the international iOS Developer Community is, how important a great network can be, and how interesting the projects of other developers are.
During some research on local events in Vienna afterward, a new idea hit me: A website, where mobile app developers (such as myself) can find other apps, and furthermore their developers, in the same area. This has a lot of potential, as eventually we can use the platform to gather information about other events/projects/etc. which are going on in Vienna, and eventually become an important resource in the Viennese mobile developer & startup community.
The concept is quite simple:
We want to build a community! And luckily we had the perfect domain available: vienna-app.dev
The idea was born. And as my co-founder Jules liked the idea too, we did some brainstorming on how to implement it on a UX and technical level.
In the previous years, next to building apps, I also got into back-end development — starting with my very first little Node.js express server hosted on Heroku (I had no idea how else), to learning how to use Docker to isolate projects into containers, to becoming an (early) adopter of the serverless Framework, and eventually up to using AWS services to built highly distributed services.
In the end, the power of understanding many technologies leads to a software developer’s downfall… we over-engineer simple solutions.
As another (personal) example, our domain techprimate.com went through different stages of complexity too:
The story doesn’t end here, as now we have so much going on at techprimate, that manually editing every single HTML website becomes repetitive. We will look into code generators and maybe even a back-end with admin area further down the road — but this time our main focus will stay on the content of the website, rather than the artistic aspect!
As I am a still huge fan of automation (one of the reasons why I am building kiwi) repeating stupid tasks over and over should not be the everyday reality, so I had to find a balance between the static & the dynamic aspect for building the vienna-app.dev community.
While looking into static page generators for techprimate.com a few candidates popped up, such as Jekyll, Hugo, Publish, and more… but in the end, I decided to give GatsbyJS a shot for vienna-app.dev.
The hardest parts of the web, made simple. — gatsbyjs.com
Gatsby builds a React website and uses server-side rendering to generate a website based on static source you (might) have only available at the build time. Sounds good so far.
As mentioned before, the community website should allow developers to submit their apps, therefore some kind of backend service is needed. Luckily Jules is currently researching all kinds of nocode/lowcode solutions for another project and introduced me to Airtable, a service I would describe as an “easy-to-use-database with automation”.
With Airtable it was quite simple to create a table with the necessary fields:
Now, this isn’t such a big requirement, I could have easily built this using a Postgres database too, but for me, the important key feature is the simple form creation. Airtable allows me to offer interested developers to directly submit their apps into my database. Then I utilize automation scripts to create the dataset necessary for generating the static HTML website with GatsbyJS. Additionally due to its simple interface I don’t have to do any coding to get it all working 🎉
… well, to be honest, in the end, it wasn’t an entirely no-code-database, as I had to write a small Automation in JavaScript to merge the original submission with further update requests, but that’s a completely different story to building a fully-blown API server.
Now I have my site generator and my database and it’s time for some developer magic ✨
Gatsby is based on a plugin structure, where source plugins provide data to the application from different providers (e.g. from a file or an API) and other plugins to add more features or transform data into our HTML/JS/CSS website.
Luckily the gatsby-source-airtable plugin exists 🔥
The Airtable source plugin fetches the data at the start of the development server and saves it into a temporary file which can then be queried using the default Gatsby GraphQL data interface.
We purchased a Gatsby template that perfectly fitted our needs: sleek design and perfect for showcasing many apps. Now, whenever the Airtable data changes, I can re-run the build process locally and deploy the updated website 🚀
Now when it came to deploying the static website I had a few options:
I decided to go with the latter one, as I have recently created learned how to use Docker Swarm for automatic deployment updates.
Creating your own docker image is fairly simple, especially when using multi-stage builds (which are also quite new). First, you create your Dockerfile with one stage building the website, and another stage copying the build products into a static server (e.g. NGINX).
I won’t go into detail about multi-stage builds (if you want a full tutorial, let me know on Twitter). The final Dockerfile looks like this one:
Dockerfile for building multi-stage GatsbyJS website using NGINX
Now I can build, tag and push the application and update my servers using versioning in my Docker Swarm 🥳
If the project gains more traction this process will get more automated or even be extended using a live API at some point, but as mentioned before: Start small!
This is version v1.0 of vienna-app.dev, therefore it is quite bare and it will take some time to improve and more developers to submit their apps. Anyways I am very happy with the very quick progress after a single week of development and excited to see where it goes!
If you would like to know more, check out my other posts, follow me on Twitter, and feel free to drop me a DM.
Do you have more ideas to improve this project? Let me know!
You are/know someone living or working in Vienna and building mobile apps? Submit your apps! 😃
]]>