Xcode Cloud Logo

Some thoughts on Xcode Cloud

Anticipation

Ever since Apple acquired BuddyBuild back in 2018 there has been been speculation on what it’s plans are for providing automation infrastructure and tooling as part of its offering to developers. Indeed, when our team ran a sweepstakes for WWDC 2020, based on the format from the StackTrace podcast, it was the first item to be speculated on. It’s fair to say, that when Apple finally revealed the results of this at WWDC 2021, it was met with great excitement — at least from my team at work.

Xcode Cloud promises an easy to setup, secure, automated workflow for your apps, heavily integrated with Xcode and App Store Connect. But does it meet these promises? I attempted to build a pipeline similar to one I’ve previously written about to find out. You can find the demo pipeline in the repository for my open-source London Underground widgets.

Setup

There’s nothing much to say here, which in itself says a lot. You’ll need to enable Automatic Code Signing (if you haven’t already), but once you do, in true Apple-style, “it just works”. I think this is the first time I’ve used a CI/CD provider where the build passes on the first run with no code-signing issues and no missing dependencies.

Great work, Apple! ✅

Integration

Apple Tools

The biggest differentiator of Xcode Cloud from its competition is that it’s built right into Xcode and App Store Connect. You complete the initial setup within Xcode and you can view the results of your builds in both of these places. Combining this with the pull request support in Xcode 13, developers may have little reason to leave the IDE in a few years time. Should we expect an XcodeOS (à la Chrome OS) to be announced in 2025.

Other Tools

Version Control

Xcode Cloud works with Bitbucket, GitHub and GitLab version control providers. According to a highly unscientific Twitter poll, it looks like this should work for around 100% of teams. If you happen to use Azure DevOps (like me) – unlucky!

Build Scripts

Xcode Cloud allows you to implement your own build scripts at specific points in the workflow: after cloning the code, before running the build and then after running the build. Since one of the main selling points of Xcode Cloud is that it’s quick to setup and simple to use, the more custom scripts we implement, the less value we are getting from this, but it’s nice to know we have this power if we need it.

An illustration that shows the different steps Xcode Cloud performs when it performs an action, including the custom build scripts from left to right.
The Xcode Cloud Workflow Lifecycle, © Apple, 2021.

We just need to create a file called ci_post_clone.sh, ci_pre_xcodebuild.sh or ci_post_xcodebuild.sh in a folder called ci_scripts. Since Xcode Cloud agents have Homebrew pre-installed and configured, we’re able to quickly add most the tools we would use in iOS Development, including SwiftLint and Fastlane.

Don’t forget to make your scripts executable using chmod +x script_name!

If I’m honest, I’d love to see some additions to the pre-installed software here as build times are pretty lengthy when I have to install all my dependencies first. For comparison, I’ve made pretty good use of Azure DevOps in the past and their agents already come with both Fastlane and SwiftLint pre-installed.

SwiftLint

SwiftLint is a great way to ensure your whole codebase uses a consistent style. We can install it on Xcode Cloud by adding the following command in our post-clone (or other) script:

brew install swiftlint

It’s then trivial to run the tool, though we will need to pass in the path to our source code, which is helpfully provided by the $CI_WORKSPACE environment variable. There are a number of useful environment variables that are available during our builds, so it’s well worth reading through the documentation.

swiftlint --strict $CI_WORKSPACE

When running SwiftLint on the CI, I use the strict flag. This ensures that the whole team are following the code-style when merging into shared branches and prevents our project from building up with a large number of warnings, which may prevent more important issues from being surfaced.

A meme from the TV show, "The Office". A woman is shown two pieces of paper one saying "compiled successfully" and another saying "compiled with warnings". There is a subtitle saying "Corporate needs you to find the differences between this picture and this picture". The woman, who is labelled "developers" responds: "they're the same picture".
Fastlane

Fastlane can be installed in a very similar way to SwiftLint. Fastlane really supercharges our automation with a vast number of plugins that can help us integrate with a wide number of services. No doubt, Apple see Xcode Cloud as an alternative for some of Fastlane’s functionality, but with such a wide array of integration with third-party services (i.e. ability to upload symbol files to Firebase Crashlytics), it’s unlikely that it’s going to replace everything.

I’m not going to go into detail of what Fastlane plugins might be useful — there are other blog posts for that — but one example could be to add an “Alpha” or “Beta” badge to your app icon, so you can distinguish between them on the home-screen.

First we’d need to install the dependencies, once our clone is complete:

#!/bin/sh
set -e

brew install fastlane
fastlane add_plugin badge

Next, once we’re about to build, we should add the badge to our icons:

#!/bin/sh
set -e

if [ "$CI_WORKFLOW" = "Alpha Release" ];
then
    (cd $CI_WORKSPACE && fastlane badge_alpha)
elif [ "$CI_WORKFLOW" = "Beta Release" ];
then
    (cd $CI_WORKSPACE && fastlane badge_beta)
fi
SonarQube

I went down a bit of a rabbit-hole attempting to run SonarQube on Xcode Cloud. Suffice to say, it’s possible and should be easy enough for you to follow my lead.

I’ve spun off a separate article for this if you’re interested to find out how:

Communication

When builds complete, we often want to perform actions depending on whether they pass or fail. Xcode Cloud has a few options here, but it’s not as exhaustive as I’d like. The only options so far are to output to Slack or email. These are both great for letting our team know what’s happened, especially if something goes wrong or if there are new features to be tested. If you use another messaging platform, you’re out of luck for now, unless you implement a custom ci_post_xcodebuild.sh script yourself.

There’s also no support for any project management tools (i.e. Jira) yet, so I guess we will continue forgetting to move our tickets across the scrum board just like we always have!

Parallel Testing

One area where Xcode Cloud shines is allowing you to run your tests against multiple types of devices in parallel. I’ve attempted to do this locally, since parallel simulator testing was introduced in Xcode 9, but, at least for me, this has always ended up with the tests becoming flaky and randomly failing much more often: particularly with XCUITest cases. I’ve not written a massive number of tests yet, but I’ve not come across any non-deterministic behaviour so far.

What’s also worth noting here is that Xcode Cloud does run all these tests inside simulators of the device. If you want to test your apps on physical devices, you’ll still need your own device farm or to use something like BrowserStack’s App Live.

Vendor Tie-in

Perhaps this isn’t surprising from Apple, but the easy setup comes with the cost of heavy tie-in. There’s no way to import or export a pipeline from anywhere else. I have a feeling that it’s going to be a bit of an outlier in the future particularly as other providers move further towards pipelines, infrastructure and repository policies that are defined in code rather than using UI tools.

What about my server-side code?

Swift as a general purpose language is growing in popularity even outside of the Apple ecosystem. It’s being used to create server-side APIs in production by many companies around the world. Xcode Cloud is designed with Apple Platforms in mind but unfortunately, for the moment, this is limited to iOS, macOS, tvOS and watchOS apps. If you’re using something like Vapor to build your backend API, you’ll be greeted by this “No Apps or Frameworks” message. If you’re making use of (and paying for!) another CI/CD provider, then it’s probably harder to justify having two different solutions, but once Apple announce the pricing we’ll be able to know for sure.

Xcode window. The sidebar is set to show Cloud builds with a message reading "No Apps or Frameworks". The code editor shows a file called `route.swift` with a basic Hello World implementation.
A Hello World Vapor Swift App, Xcode Cloud builds are unavailable.

Summary

Xcode Cloud is probably going to work great for you if you’re new to automation or don’t have any CI/CD workflow setup for your project yet. Depending on the pricing, I’ll be happy to continue using it for my standalone side-projects. However, for me, it’s not quite compelling enough to invest time in switching over an existing pipeline from another provider.

In the future, I’d love to see some additions to the pre-installed software (SwiftLint and Fastlane in particular), access to the code in the post-testing step (see my SonarQube post) and support for server-side Swift projects.

How have you found Xcode Cloud? Are you going to adopt it? Let me know on Twitter:

What I’ve learnt from Advent of Code 21

As I only made it to day 8 last year, I was even more determined to finish this year’s Advent of Code. I have to admit it’s been hard to find the time on occasion, but it’s been one of my top priorities for the month of December and I’ve managed to complete each task with the day, every day. If you follow me on Twitter, I apologise for the sheer amount I posted about this over the course of the last month 🤣

Of course, I’ll talk about my solutions, but I’ve also included some other great solutions, written in Swift, that I’ve learnt from, and I’ll reference as I go through:

Readability

When working under time pressure, aiming for readable code is often one of things we stop doing. In general, Swift can be quite a readable language; I’ve seen a lot of illegible code (usually in Python) flying around this year. Usually this is a result of variables being given generic, rather than meaningful names, like x, y, perms or index.

Daniel Tull‘s solutions are a great example at prioritising readability even when under pressure, he’s done a fantastic job at abstracting code out of each day’s solution into helper files, and then separating the files themselves using extensions. All the variables are well named and it’s always easy to tell what each line of code is doing.

Consider his Day 7 solution. On Day 7, we needed to find the cheapest solution for moving a set of crabs into a horizontal alignment from their given staring positions. Both part 1 and part 2 of Daniel’s solution reuse the same general solution and the rest reads almost like spoken English. We receive an input, get the first line and separate it at each comma and transform each value to integer.

let positions = try input.lines
    .first.unwrapped
    .split(separator: ",")
    .map(Int.init)

Then we find the minimum and maximum values. For each value between the minimum and maximum, we test the total costs of moving the crabs and return the minimum value.

let min = try positions.min().unwrapped
let max = try positions.max().unwrapped
let amounts = positions.countByElement

return try (min...max).map { proposed -> Int in
    amounts.map { position, amount in
        cost(proposed, position) * amount
    }
    .sum
}
.min()
.unwrapped

When writing readable code, another important factor is trying to reduce distractions and clutter. As well as brevity, this can include not reinventing the wheel. Regarding this, I was very impressed by Sima Nerush‘s Day 1 implementation. She’s used Swift’s new Algorithm’s package to produce one of the most minimal implementations I’ve seen. Completing both challenges in just 23 lines is incredible, in contrast my implementation was more than double this!

import Algorithms
import Foundation
final class Day1: Day {
    func part1(_ input: String) -> CustomStringConvertible {
        return input
            .split(separator: "\n")
            .compactMap { Int($0) }
            .adjacentPairs()
            .count { $0 < $1 }
    }
    
    func part2(_ input: String) -> CustomStringConvertible {
        return input
            .split(separator: "\n")
            .compactMap { Int($0) }
            .windows(ofCount: 3)
            .adjacentPairs()
            .count {
                $0.sum < $1.sum
            }
    }
}

Complexity

Since there are two (related) challenges to complete each day, often the first is easier than the second. On several occasions the difference is simply down to a requirement for additional computation. A solution that may quickly return an answer for the first task, may take hours or days to run for the extension task.

For me, this has been one of the hardest parts of Advent of Code. It’s been a few years since I was at university studying the theory of computational complexity and I’ve definitely gotten a little rusty. As most mobile devices now have an unbelievable amount of computing power (see the graph below!), I don’t often need to produce code that is highly efficient in my day to day work. However, by knowing these techniques, we can write better code and reduce the impact of our inefficient code on the device’s battery life.

iPhone 13's A15 Bionic GPU gains the most impressive in 5 years
Apple Silicon Single Core CPU Performance from iPhone 5s to iPhone 13 (c) Creative Strategies

By reading other’s solutions, I’ve found some handy tricks that I’ve managed to incorporate into some of my own code. In particular, I struggled with the extension puzzle of Day 6 and had to take some inspiration. For the first half, I implemented the solution modelling every single fish:

func solve(filename: String) throws {
    let fish = try openFile(filename: filename)
        .filter { !$0.isWhitespace }
        .components(separatedBy: ",")
        .compactMap(Int.init)
    return (0..<80).reduce(fish) { fish, _ in
        fish.flatMap(nextDay)
    }.count
}

func nextDay(fish: Int) -> [Int] {
    guard fish > 0 else {
        return [6, 8]
    }
    return [fish - 1]
}

Unfortunately, as the fish population grows exponentially, this method becomes untenable once we run for longer than about 100 days. The code takes a very long time to run and will likely even cause the computer to run out of memory! I needed to do better.

I loved this implementation from Abizer, where instead of tracking each fish, he tracks the number of fish that are on each of the 8 days of the reproductive cycle. I especially liked the use of Dictionary(_ keysAndValues: S, uniquingKeysWith combine: (Value, Value) throws -> Value) for summing up the fish for each day. So, for example, this initialiser, combined with the map turns [0, 1, 2, 2, 3, 3, 4, 5] into [0: 1, 1: 1, 2: 2, 3: 2, 4: 1, 5: 1].

public static func part1(_ input: String) -> String {
    let inputDictionary = generateDictionary(input)
    return "\(countPopulation(of: inputDictionary, over: 80))"
}
static func generateDictionary(_ input: String) -> [Int: Int] {
    Dictionary(input.intsFromLine.map { ($0, 1) },
               uniquingKeysWith: +)
}
static func countPopulation(of input: [Int: Int], over: Int) -> Int {
    (0 ..< over).reduce(into: input) { start, _ in
        var end = [Int: Int]()
        start.forEach { key, value in
            if key == 0 {
                end[6, default: 0] += value
                end[8] = value
            } else {
                end[key - 1, default: 0] += value
            }
        }
        start = end
    }
    .values
    .reduce(0, +)
}

I hadn’t come across this dictionary initialiser before, but it really came in handy and I used it on days 6, 7, 8 and 14.

Immutability

At university one of the most difficult modules I took was on Functional Programming in Haskell. At the time I couldn’t quite get my head around the concept of immutability in programming. I struggled to grasp how to handle changes in state when using value types. It’s definitely true that immutability becomes harder to use when state is involved; by definition state can change. Generally the solution is to change the whole state at the same time.

For most of these challenges, since the state can be fairly lightweight while the computation is heavy, immutability can be a great choice. In general, immutable objects provide a number of benefits. In our world of increasing parallelism (iPhone 13 has 6 CPU cores, 16 NE cores and 4 GPU cores), immutable objects can safely be passed between threads without risking synchronisation issues. We can also implement better encapsulation, passing our objects into other functions secure in the knowledge that they won’t be modified. This has the additional benefit of making our code easier to test as we don’t have any side-effects to analyse.

Consider my solution to Day 6, above. Each day we return a completely new array representing the new state of the fish population. When state is simply represented by an array of integers like this, it’s simple enough. For more complex types of state, such as Day 4’s sets of bingo boards, I regressed to my old habits of mutability, with the boards being edited rather than a new state being created. I’ve seen a number of solutions similar to mine, which avoid mutability for everything part from marking the number of each board after it’s called. However if you’ve managed an implementation with complete immutability, I’d be interested to see it.

Testing

At the start of the month I set up my project as a bunch of standalone script files. One of the things I’ve longed for, particularly with the later challenges is the ability to write some unit tests for some of my code. This would really help me to ensure that my code is outputting the correct results against the examples that are given.

On Day 18, in particular, I went down a rabbit hole from having not read the question properly. Here, where we had to split a single regular number that is 10 or greater, I was splitting every regular number that was 10 or greater. By writing a unit test of input vs expected output, I would have saved myself a lot of time!

If any regular number is 10 or greater, the leftmost such regular number splits.

Day 18, Advent of Code, 2021

This is exactly what Dave DeLong has done, using a Swift Package to allow testing. This seems like a great way to set up the project and I’ll definitely be following his lead next year. A Swift Package, much like the Swift Playgrounds that others have used, also makes it far easier to share code between challenges, in stark contrast to my scripting where I’ve needed to copy and paste small snippets into each individual solution.

Remote Builds

A random one, but I thought Leif Gehrmann‘s use of GitHub actions to run his code was great. It takes your current hardware out of the picture and lets you independently prove how quickly your solutions run for comparison against others.

Thanks for Reading

Thanks to Eric Wastl and all the sponsors for making Advent of Code 21 happen. The puzzles have been super interesting; I’ve definitely felt challenged, but I’ve relished it and seeing the innovative ideas and solutions that others have had has made it even more compelling to be a part of. I’ll be seeing how I can start to use the things I’ve learnt in day to day work, and I’m already looking forward to next year’s challenge!

Attempting SonarQube Analysis on Xcode Cloud

SonarQube can be a great tool for finding smells, bugs and duplications in your code. I like to use a combination of SonarQube and SwiftLint to enforce quality standards on the codebases I work on. These tools can help to ensure developers always meet the required standards in their code, and can reduce (or even prevent) the amount of bike-shedding on our merge requests.

Bike-shedding is where we spend a disproportionate amount of time discussing trivial things and leave important matters undiscussed. This is usually because the important items are more complex and we don’t spend the time to fully understand them, so we focus on the those that we can understand quickly. From a development perspective, I’ve seen intense debates on what the format for a pull request title should be, while the code itself is violating multiple SOLID principles.

Here I’ll be implementing SonarQube for a project I’ve written about before: my London Underground Status app. I’ll be using the Fastlane plugin for SonarQube and attempting to output a report on code quality and test coverage from an Xcode Cloud pull request validation build. I’m using SonarCloud for this, as it’s free for open source projects, but you can also use this for your own privately hosted SonarQube instances.

I won’t cover how to create an Xcode Cloud build, as others have covered that already and the Apple Documentation is fairly well written.

Prerequisites

Xcode Cloud agents are currently quite light on pre-installed software. They have Xcode, Homebrew, anything that comes pre-installed on macOS and that’s about it. To run Sonar Analysis, we’ll need to install three additional things on the build agent: fastlane, sonar-scanner and the fastlane plugin for converting Xcode’s test coverage output into JUnit format.

We can do this in our ci_pre_xcodebuild.sh script. As a single agent performs the build before handing off to multiple agents to run the tests in parallel, placing this in the ci_post_clone.sh won’t work as this is only run on the initial build agent.

#!/bin/sh
set -e

brew install fastlane
brew install sonar-scanner

fastlane add_plugin xcresult_to_junit

Main Branch Analysis

We can start by performing an analysis on a shared branch, i.e. develop or main. This will help us to understand the overall health of our code-base. I’ve used the Fastlane plugin for SonarQube as it was the easiest way to install and run SonarQube on the build agent.

We can call this directly, or create a Fastfile:

fastlane run sonar \
   project_key:"tube-status-ios" \
   project_name:"tube-status-ios" \
   project_version:"1.0" \
   project_language:"swift" \
   sonar_runner_args:"-Dsonar.projectBaseDir=$CI_WORKSPACE -Dsonar.c.file.suffixes=- -Dsonar.cpp.file.suffixes=- -Dsonar.objc.file.suffixes=- -Dsonar.pullrequest.provider=github" \
   sources_path:$CI_WORKSPACE \
   sonar_organization:"oliver-binns" \
   sonar_login:$SONAR_TOKEN \
   sonar_url:"https://sonarcloud.io" \
Coverage: 0.0% on 8.3k new lines. Greater than or equal to 80% coverage is required. Red status.
0.0% code covered by the unit test suite

As you can see, this analysis has failed, in part because we haven’t met the code-coverage requirements. When running a test action, Xcode Cloud will provide us with a $CI_RESULT_BUNDLE_PATH variable which we can use to provide coverage results to SonarQube. Fastlane has a plugin which lets us convert from the xcresult file that Xcode outputs into the JUnit format that SonarQube requires:

default_platform(:ios)

platform :ios do
  desc "Exports Test Coverage and Code Quality Analysis to SonarCloud"
  lane :sonar_analysis do |options|
    xcresult_to_junit(
      xcresult_path: options[:result_path],
      output_path: "#{options[:workspace]}/test_output"
    )
    sonar(
      project_key: "tube-status-ios",
      project_name: "tube-status-ios",
      project_version: "1.0",
      project_language: "swift",
      exclusions: "vendor",
      sonar_runner_args: "-Dsonar.projectBaseDir=#{options[:workspace]} -Dsonar.c.file.suffixes=- -Dsonar.cpp.file.suffixes=- -Dsonar.objc.file.suffixes=- -Dsonar.pullrequest.provider=github -Dsonar.junit.report_paths=#{options[:workspace]}/test_output",
      sources_path: options[:workspace],
      sonar_organization: "oliver-binns",
      sonar_login: options[:sonar_token],
      sonar_url: "https://sonarcloud.io",
    )
  end
end

Great, we can now see how much of our code is covered and where we can improve. For me and my Tube Status demo project, it seems there’s a long way to go!

SonarQube report. 2.1k lines of code. Version 1, last analysis 14 days ago. Commit ID 2d3252a4. Quality Gate: Failed. 1 Failed Condition. New Code, since about 1 month ago. Reliability: 0 bugs. Maintainability: 1 code smell. Security: 0 vulnerabilities. Security Review: 0 security hotspots. Coverage: 8.6% coverage on 499 new lines to cover. Duplications 0.0% duplications on 2.1k new lines.
SonarQube Report complete with test coverage metrics.

Pull Request Analysis

When performing a pull request analysis, we need to pass some additional parameters to SonarQube so that it can determine the differences between the two branches. Luckily, these are all parameters that Xcode Cloud provides us as Environment Variables: $CI_PULL_REQUEST_TARGET_BRANCH, $CI_PULL_REQUEST_SOURCE_BRANCH and $CI_PULL_REQUEST_NUMBER. We can pass these in, as above, and then consume them as additional parameters in our Fastlane file.

default_platform(:ios)

platform :ios do
  desc "Exports Test Coverage and Code Quality Analysis to SonarCloud"
  lane :sonar_analysis do |options|
    xcresult_to_junit(
      ...
    )
    sonar(
      project_key: "tube-status-ios",
      ...,
      pull_request_branch: options[:source_branch],
      pull_request_base: options[:target_branch],
      pull_request_key: options[:pr_number] 
    )
  end
end

When Xcode retrieves our code for a pull request, it creates a new repository and checks out the target branch. After this it merges in the source branch. The outcome of this is that the agent only has one branch: which has the same name as our target branch (likely develop or main). This is a problem as when we compare the difference against the target branch, we’ll find no changes.

If we just run the same script that we used for our main branch, with the additional parameters, we’ll get a very boring report:

SonarQube analysis results. 0 new lines. Quality Gate: Passed. Last analysis 7 minutes ago. Commit hash. Reliability: A rating, 0 bugs. Maintainability: A rating, 0 code smells. Security: A rating, 0 vulnerabilities. Security Review: A rating, 0 security hotspots.
A SonarQube report from Xcode Cloud showing no changes against the develop branch.

We have to do some Git magic to add the true target branch back into the refspec.

  1. Create a new branch, we can just call it temp.
  2. Delete the target branch so that we have no reference to it.
  3. Reset the refspec to be able to retrieve the true target branch again.
  4. Perform a git fetch to retrieve a clean reference to the remote target branch.
#!/bin/sh
set -e

git -C $CI_WORKSPACE checkout -b temp
git -C $CI_WORKSPACE branch -d $CI_PULL_REQUEST_TARGET_BRANCH

# fetch a reference to the develop branch on GitHub
# this will allow SonarQube analysis to work
git -C $CI_WORKSPACE config remote.origin.fetch \
"+refs/heads/$CI_PULL_REQUEST_SOURCE_BRANCH:refs/remotes/origin/$CI_PULL_REQUEST_SOURCE_BRANCH"
git -C $CI_WORKSPACE config remote.origin.fetch \
"+refs/heads/$CI_PULL_REQUEST_TARGET_BRANCH:refs/remotes/origin/$CI_PULL_REQUEST_TARGET_BRANCH"
git -C $CI_WORKSPACE fetch

That’s better, we can now run an analysis and changes will get detected: 47 new lines.

SonarQube report. 47 new lines. From branch feature/sonar-qube to develop. Last analysis 2 days ago, commit ID fbb4d929. 1 warning. Quality gate failed. 1 failed condition. Reliability: 0 bugs. Maintainability: 0 code smells. Security: 0 vulnerabilities. Security Review: 0 security hotspots. Coverage: 0.0% coverage on 26 new lines to cover. 0.0% estimated after merge. Duplications 0.0% duplications on 47 new lines. 1.3% estimated after merge.
A SonarQube report from Xcode Cloud showing 47 lines changed against the develop branch.

Summary

As you can see, it’s possible to get some of the features of SonarQube working on Xcode Cloud. Unfortunately, a lack of pre-installed software makes it slow to run, and a number of the implementation details make it tricky to implement. There no guarantee that this will be improved in the future, or that future changes won’t break the workarounds that we’ve implemented to get this to work. All in all, if you require SonarQube, I’d probably suggest steering clear of Xcode Cloud for the timebeing.

Checkout the final implementation on GitHub:

Find me on Twitter:

Creating Great Enterprise Apps for iOS

On Enterprise Apps

What’s wrong?

“There’s an App for that” is perhaps one of the most well-known advertising slogans of the last ten years. Indeed, since the birth of the iPhone, owners of mobile devices have come to enjoy high-quality, often freely available, applications for achieving virtually any task they can imagine.

For publicly available apps, user experience is often make or break; the App Store makes it relatively easy to find an alternative for any common app. In contrast however, Enterprise apps are generally mandated by a company’s IT department or some other internal function. Since these enterprise apps aren’t therefore subject to normal market-forces, I’ve found that they can often be of significantly lower quality, both in terms of user experience and feature sets.

Here’s a great clip from Steve Jobs talking about exactly this issue:

Steve Jobs at D8 Conference, 2010

There is seemingly little incentive for companies to improve apps which are compulsory for their employees. Or is there? More companies are starting recognise the link between internal employee user experience and employee retention. Retaining the best employees can be key to a company’s success, particularly within the technology industry.

This is likely to become an even more important battleground in the post-COVID world, as companies from around the world compete to hire the very best people. When people are working remotely- the majority of their interaction with the company they work for and its processes are via the various apps and other software it provides.

How can we fix it?

Consider the alternatives

There are a huge number of off-the-shelf enterprise apps available. Don’t try and build your own internal app for emailing or messaging: just provide your employees with a great existing app such as Outlook or Slack. You’ll find this far cheaper (and much less stressful!) than embarking on a whole development journey.

For existing internal content, such as news items or other text-based information, which are delivered through the web when using desktop: consider optimising these sites for mobile rather than developing a mobile specific app. Let the users view this in Safari, so they have the ability to enter Reader Mode, and don’t force them to access the content through a “fake”, web-view based mobile app. There’s no shame in delivering great content through the web, there is in providing users with a poor experience through a badly made web-based app.

Don’t overcomplicate things

Still here? You must be sure a mobile app is the right thing for your use-case.

Disclaimer: because of where my knowledge and experience lies, I’m going to assume we’re focussing on a native iOS app from this point on. Some, but not all, of the point below will apply to native Android apps, cross-platform apps and hybrid apps. There are also plenty of other articles that will discuss the pros and cons of such things.

Ok: let’s start with a simple app then. This one is potentially more for app designers than developers but, as with most iOS development, I’d suggest using native components and standard UI flows wherever possible. The majority of apps in this environment can be created using tab-bars, navigation controllers, table views and the odd collection view: think Apple Music, Notes or even third-party apps like the new GitHub app.

This will allow you to build your app much quicker without worrying how to make your custom components. If you try and build the whole app with custom components, you can quite quickly end up with a problem of quantity over quality and end up with a poorer overall experience.

Enterprise Apps on iOS

Benefits

When creating a bespoke enterprise apps, that is to say one which is made for a specific organisation, you are often able to gather much better information about your users and how they will interact with your app.

Firstly, when companies provide devices to their users, developers can often make assumptions and tailor apps to these specific devices. For example, in some projects I’ve worked on we’ve only had to consider one specific iPhone or iPad screen-size, which can massively simplify both development and testing.*

* Note I’d still recommend following best practice and using autolayout and size classes to support other devices. This way you’ll find it easier to add new devices in the future as well as potentially making your app more accessible by supporting dynamic type.

Secondly, managing devices means we can add constraints, such as ensuring all our users are running the latest version of iOS. This is very exciting for developers as it means we can always use the latest APIs and features in iOS, that usually we have to wait for! As a reminder: in iOS 14 this means App Clips, Widgets and more!

Deployment

In late 2019, Apple added support for Custom Apps deployment to iOS. This Custom Apps approach is effectively an overhaul of the not-quite-yet deprecated Developer Enterprise Program (DEP). Since then Apple has dramatically restricted entry to the DEP, likely as a response to one particular, high-profile, breach of contract.

For those still using the DEP, Custom Apps offers a few key advantages, over the former approach:

Apps are uploaded to App Store Connect, as for public App Store apps
Downloads can therefore benefit from all the great App Store features: both internal and external users can access pre-release versions of the app using TestFlight, and downloads can be much faster thanks to Apple provided worldwide CDN hosting as well as app thinning.

On the flip-side, since they’re now distributed through Apple, they must go through App Review in the same way as publicly App Store apps. If you’re used to developing for the App Store anyway, you’ll be used to this. Those moving from DEP to Custom Apps may find App Review adds an extra few days to their deployment process.

Apps are re-signed by Apple
As apps are deployed to devices, they are re-signed with Apple’s master certificate. Apps signed with this certificate will not expire every year as they do with Enterprise-signed installations. Furthermore, users no longer have to install additional certificates or provisioning profiles on their device, a key gain for device security as we’re not teaching users bad habits!

Apps can be distributed to clients
Whereas the Enterprise Program allows businesses to distribute apps to their own employees only, Custom Apps allows apps to be distributed to users at other organisations. This is great news for third-party Enterprise app developers, as apps no longer need to be re-signed by their clients before distribution.

N.B. Custom Apps requires Apple Business Manager, or its education sibling: Apple School Manager. As of Dec 2020, these are still only available in 69 countries, so if you are deploying outside of these regions you will need to apply to be part of the DEP.

Mobile Device Management

Most large organisations want to have some level of control over the data that their employees store on their computers, and mobile is no exception to this. Mobile Device Management solutions are the solution for this.

There are many off-the-shelf products for Mobile Device Management, but from a developer’s point of view, they all perform the same main functions:

  • automatically configuring the device for access to employer provided tools, such as email or VPN access
  • restricting certain features of the device depending on purpose:
    • i.e. iOS devices can be used in Kiosk mode for a specific application
    • full web-access can be blocked, or only certain WiFi networks may be allowed

Managed App Configuration

All of the main Mobile Device Management solutions support Apple’s Managed App Configuration.

The online documentation for Managed App Configuration is pretty poor, but the concept is simple. The organisation can provide an XML-based .plist file containing various values that can be read by the app.

Managed App Configuration is not exclusive to Enterprise-only apps. You can add support for configuration to your App Store apps too, which will allow organisations to customise / pre-configure the app for their employees! For example: Microsoft Outlook for iOS can be pre-populated with a user’s email credentials so employees don’t need to set this up manually.

Conclusion

As remote working becomes more the norm than the exception, the amount we interact with our enterprise apps is ever increasing. The steps I’ve laid out should be a great starting point on your journey to provide your employees or users the best possible user experience when interacting with your enterprise apps.


Get in touch?

Going iOS native with WordPress

WordPress is one of the most popular website platforms available, in fact it powers just under a third of the entire web, including this very website. WordPress is a great tool for all kinds of websites, and the web in general is great for distributing content to a massive number of users, but it cannot match the user experience that can be provided through native apps.

As most of my work focusses on iOS, I decided to use the WordPress API to see if I could make my blog (this website) available as a native app. I released this recently – you may even be using it to read this post – though I’ll let you be the judge of how well it’s been executed.

Why?

Web

Supporting Web allows the site to be accessed by the widest possible number of users. The website is available to users on Desktop PCs, smartphones from all manufacturers*, tablets and even TVs.

*Disclaimer: I haven’t checked them all.

Native

Native apps feel more intuitive to users as they tend to follow the UI standards of the individual operating system more closely. While this app is relatively simple, making it native would allow it to make the most of the vast array of APIs and hardware available on iOS devices.

For starters, this app makes the most of the accessibility features in iOS by providing support for Dynamic Type and VoiceOver, as well as giving users the ability to read posts while they are offline. You can see from these images that the iOS app obeys the user’s dynamic type choice while the web app displays text at the default size.

Blog post viewed on the web in Safari
Blog post viewed in the native iOS app

While it is possible to support dynamic type on the web, we don’t get as much control over it, nor as much given for free as we do using the iOS SDK.

Going native also would allow us to make the most of additional iOS features such as widgets, augmented reality, Pencil, iMessage apps, and so much more.

How?

Backend

Since I use WordPress for writing my blog, there is no need for any extra work to create a backend API for the app. WordPress provides a REST API which I can use to easily query the site to retrieve the list and content of my posts to display to the user. Easy, right?

[
    {
        "id": 132,
        "date": "2020-09-05T16:00:41",
        "modified": "2020-10-15T07:50:39",
        "slug": "going-for-gold-taking-full-advantage-of-apple-platforms",
        "link": "https://www.oliverbinns.co.uk/2020/09/05/going-for-gold-taking-full-advantage-of-apple-platforms/",
        "title": {
            "rendered": "Going for Gold- Taking full advantage of Apple Platforms"
        },
        "content": {
            "rendered": "
                <p class=\"has-drop-cap\">
                    In a recently published <a href=\"https://www.oliverbinns.co.uk/2020/06/27/create-a-tube-status-home-screen-widget-for-ios-14/\">
                    blog-post on building widgets for iOS 14
                </a>
                ..."
        }
    }
]

We can represent this as a UML diagram, to give visual representation of the different objects we can create in Swift and how they are related.

UML Class Diagram for WordPress post API

If you’ve read some of my previous posts, you’ll be familiar with Swift’s Decodable protocol for decoding values from JSON representation.

Although we can convert this directly into a Swift struct representation

For Swift beginners: a struct is similar to a class, but more lightweight and recommended as the default building block for Swift objects. For a full comparison of the two, I’d recommend reading this chapter from the Swift Language Guide.

struct Post: Decodable {
    let id: String
    let slug: String
    let title: String
    let link: URL
    let jetpack_featured_media_url: URL?
    let date_gmt: Date
}

It would be much better to use CodingKeys to transform these variables to more closely follow the Swift Naming Guidelines, including the use of camel-case rather than snake-case:

struct Post: Decodable {
    let id: String
    let slug: String
    let title: String
    let link: URL
    let imageURL: URL?
    let publishedDate: Date

    enum CodingKeys: String, CodingKeys {
        case id, slug, title, link,
        case publishedDate = "date_gmt"
        case imageURL = "jetpack_featured_media_url"
    }
}

Not so fast…

While this means we can easily get the lists of posts using a simple API query in our iOS app, we still need to convert that rendered content into native iOS components. I defined a set of components that I use to write the blog and created an enum in Swift:

enum PostContent {
    case heading1(String)
    case heading2(String)
    
    case body(NSAttributedString)
    case image(URL)

    case horizontalRule
    ...
}

I used SwiftSoup to parse the HTML string provided by WordPress:

let xml = try? SwiftSoup.parse(contentHTML)

We can create an initialiser for converting SwiftSoup‘s Element type into our enum:

extension PostContent {
    init(element: Element) throws {
        switch element.tagName() {
        case "h1":
            self = try .heading1(element.text())
        case "hr":
            self = .horizontalRule
        // etc.
        // ...
        }
    }
}

then map our rendered content into the PostContent type we just created:

let content = xml?.body()?.children().compactMap {
    try? PostContent(element: $0)
}

ViewBuilder

Now that we’ve converted the raw HTML string into a data model, we can think about transforming this into views that we can display on the screen. I’ve used SwiftUI for this, but we could add specific implementations for UIKit or AppKit, or even something a bit more off-piste such as exporting to PDF or another type of document.

SwiftUI’s ViewBuilder allows us to easily map our internal PostContent type into its SwiftUI representation, using a set of very readable code.

@ViewBuilder
func viewForContent(_ content: PostContent) -> some View {
    switch content {
    case .heading1(let string):
        Text(string).font(.title)
    case .heading2(let string):
        Text(string).font(.title2)
    case .horizontalRule:
        Divider()
    // etc.
    // ...
    }
}

Using SwiftUI, we get support for many of the great native features I described above, including dynamic type, dark mode and more, out-of-the-box. This works great for the majority of the components. As you can see from the code-snippet above, some elements, such as the horizontal rule, can be mapped directly. However some may require a little bit more work.

Paragraphs

Overview

Supporting rich-text with bold text, links and more isn’t easy in SwiftUI as it has no out-of-the-box support for rendering HTML or NSAttributedString. I found the easiest way to implement is to bridge into UIKit with a UIViewRepresentable class to display a UILabel which will display the HTML content provided by the API using NSAttributedString. If all this sounds like Greek to you, don’t worry: I will explain it step-by-step!

Decoding

Our response from the server will return content which should be displayed like this:

“This is a paragraph containing some bold, underlined text.”

However, since the server returns raw HTML text, if we decode this directly into a Swift string, we will display:

“This is a paragraph containing some <b>bold, <u>underlined</u></b> text.”

Luckily the iOS SDK provides an easy way to turn this into an NSAttributedString which can be displayed on the screen with the bold and underlined characters we would expect.

let excerpt = try? NSMutableAttributedString(
    data: Data(excerpt.utf8),
    options: [.documentType: NSAttributedString.DocumentType.html],
    documentAttributes: nil)

You can find more details in this article from Paul Hudson.

Rendering

Unfortunately SwiftUI doesn’t support rendering of NSAttributedString out-of-the-box, but with a bit of work, we can create a new AttributedText struct which will!

To do this, I’ve chosen to use UITextView, rather than UILabel, as it has inbuilt support for HTML links- but don’t forget to disable editing!

struct SwiftUILabel: UIViewRepresentable {
    // We can pass in the text we initialised above!
    @State var attributedString: NSAttributedString?

    func makeUIView(context: UIViewRepresentableContext<Self> -> UITextView {
        UITextView()
    }

    func updateUIView(_ uiView: UITextView, 
                      context: UIViewRepresentableContext<Self>) {
        uiView.isEditable = false
        guard let attributedString = attributedString else {
            return
        }
        uiView.attributedText = attributedString
    }
}

Great, now let’s run it.

HTML mark-up rendered as a UILabel within SwiftUI.

It works! Our text appears bold, italic, underlined and even in a monospaced font as we would expect. We even get support for numbered and un-numbered bulletpoints.

There’s just a small problem, and it’s hard to spot, but SwiftUI doesn’t seem to obey the intrinsic content size of the UILabel, so we get some large gaps in between our content. Here’s the comparison:

SwiftUI adds too much padding above and below the text.
This causes some layout issues in the scroll view.

I’ve found that the best way to do this is to place the SwiftUILabel inside a wrapper which can manually set the frame to the height. We can use a binding to pass the expected height through to the inner type: this allows both to stay up-to-date with the latest value, with any changes being rendered on the screen.

import SwiftUI
import UIKit

struct AttributedText: View {
    // We can pass in the text we initialised above!
    @State var attributedText: NSAttributedString?
    @State private var desiredHeight: CGFloat = 0
    
    var body: some View {
        HTMLText(attributedString: $attributedText,
                 desiredHeight: $desiredHeight)
        .frame(height: desiredHeight)
    }
}

struct SwiftUILabel: UIViewRepresentable {
    // We can pass in the text we initialised above!
    // N.B. This has been changed to a binding now!
    @Binding var attributedString: NSAttributedString?

    // A binding references the state of the parent class
    // Any updates we make here, trigger an update on the screen
    @Binding var desiredHeight: CGFloat

    func makeUIView(context: UIViewRepresentableContext<Self> -> UITextView {
        UITextView()
    }

    func updateUIView(_ uiView: UITextView, 
                      context: UIViewRepresentableContext<Self>) {
        guard let attributedString = attributedString else {
            return
        }
        uiView.attributedText = attributedString

        DispatchQueue.main.async {
            let size = uiView.intrinsicContentSize
            guard size.height != self.desiredHeight else { 
                return
            }
            self.desiredHeight = size.height
        }
    }
}

If you know a better way of doing this in SwiftUI, please do let me know!

In the final code, I’ve also added support for links within text which is fairly easy to do thanks to UITextView.

It works!

We’ve used the WordPress API to natively render a website on iOS with support for Dynamic Type and Dark Mode.

The entire source code for this app is available on GitHub:

Look out for a future article where I’ll describe how I made this app available without users needing to download it using App Clips for iOS 14.

Going for Gold- Taking full advantage of Apple Platforms

In a recently published blog-post on building widgets for iOS 14, I showed how to implement a home-screen widget to display the latest status of the London Underground network. In the sample project, I showed how a shared module could be used to share everything from networking code to SwiftUI views between both the actual app and the widget.

Since SwiftUI is supported on all of Apple’s platforms, and we can now create apps entirely with SwiftUI, I thought it would be interesting to see how easily we can adapt a similar app to support all Apple platforms, be accessible for all users, and implement as many flagship features as possible. SwiftUI makes it super easy to do this, but as developers we still need have each of these features at the back of our minds when we are working.

Sample code for this post is available on GitHub.
Make sure to checkout the going-for-gold branch of the repository!

iOS

WidgetKit / iOS

Let’s start assuming that we have implemented this, if you want more details on this, check out my previous blog post on the topic.

Dark Mode

In SwiftUI, we can implement dark mode very easily as part of our existing work. In-fact, when using default Views and Labels in SwiftUI these will work out of the box. If you are manually specifying colours, just be careful to use Color.label instead of Color.black and Color.systemBackground instead of Color.white for anything that should adapt when run with dark mode enabled.

There may be occasions where specific As you may have noticed, the existing sample app I created for Widgets didn’t specify many colours, apart from those of the Tube Lines. When overlaying text on these backgrounds, we always want to use white even in dark mode, so we can specify this manually.

Text(update.line.displayName)
    .foregroundColor(.white)
    .padding()
    .background(update.line.color)
Line name colour must remain the same, status text must adapt

TL;DR: Only use white and black when the colours should remain the same between dark and light modes.

Dynamic Type

As with dark mode, if we are using the default Labels in SwiftUI, we get dynamic type for free though there are a number of things that we should do to improve how it behaves when we start to implement more complex interfaces. Let’s check out how our Tube Status example works at the largest accessibility dynamic type size (AX5).

AX5 Font Size
AX5 Font Size
(Optimised for Dynamic Type)

Not too bad: since we’ve used the inbuilt SwiftUI fonts the size increases our body font from 17 to 53 points automatically. The containers get adjusted accordingly and we can read all the detailed information and see the status icon and the colour that represents the line.

However, two key bits of information have been truncated. We can no longer see the full name of the line or the status type. An experienced Londoner might be able to infer the information but this isn’t very accessible for tourists.

It would be much better to split these pieces of information to be split across multiple lines at this font-size. We can adapt this snippet from Hacking with Swift to automatically switch between a horizontal and vertical view when the user switches accessibility size. Instead of monitoring the size class, we need to check the sizeCategory.

@Environment(\.sizeCategory) private var sizeCategory

var body: some View {
    Group {
        if sizeCategory >= .accessibilityMedium {
            VStack(spacing: 0, content: content)
        } else {
            HStack(spacing: 0, content: content)
        }
    }
}

In general, horizontal grids, such as carousels are more difficult to provide dynamic type support as there is often nowhere for the text to expand into as the default is for the screen to scroll vertically.

Always test your app with different dynamic type sizes to ensure that your views don’t break if users enable this feature.

Voiceover

SwiftUI also provides excellent support for Voiceover straight out of the box. If you run the app with VoiceOver enabled you’ll notice that all of the elements will be read out as you navigate down the screen. This “free” implementation here is a great starting point but there are definitely things we can do to improve it.

Here, each of our text labels is classed as an individual element, which means we need to navigate through the three different elements to get to the next line status. We should combine these into a single accessibility element so that the user can navigate to the specific tube line they need much more quickly. Once they find the correct line, they can still hear each of the labels, since they are grouped.

VStack {
    LineStatusView(update: update)
    // ... etc.
}
.accessibilityElement(children: .combine)

However, upon combining the elements, you’ll notice that Voiceover says that the combined elements is an “image”. We can fix this by adding the following two lines.

.accessibility(addTraits: [.isStaticText])
.accessibility(removeTraits: [.isImage])

This is obviously just a quick overview of what’s possible with some of the accessibility features in SwiftUI and iOS. For further reading I’d recommend checking out Rob Whitaker‘s blog.


iPadOS

For iPadOS we can use exactly the same codebase as we used for iOS. There’s not a huge amount more we would want to do specifically for iPad for such a simple app. One main distinction is that we can display our app in multiple windows. Since we’re already using WindowGroup as our SwiftUI scene, we get this ability for free.

Tube Status app running in multiple windows on iPad.

 Watch

The App

Until now, all our work has been focussed on iOS and iPadOS. We will now add the additional requirement of supporting other platforms. Since all the platforms support Swift and SwiftUI, we just need to make our shared framework compile to code for each of the additional platforms so that we can reuse our views across each.

Tube Status app running on watchOS

I used this tutorial to ensure that the same Shared framework we created in the previous blog post will support all four platforms. It will mean that we can, as we did with the widget project, share all of our code relating to networking, domain models and even SwiftUI views between our iOS app and the other Apple platforms.

Once we’ve enhanced the shared framework, it’s time to create a new target for our watch app. This should be familiar from the previous tutorial: in Xcode, go to File → New → Target → watchOS → Watch App and click Next. This will create a standalone app for Apple Watch that doesn’t require our existing iOS app to be installed. As we have previously, we want to use SwiftUI and the SwiftUI lifecycle for the app: this allows us to reuse the most amount of code between platforms. Select to include Complication which we will cover below, but not Notification scene which is not relevant for our simple tube app (since it does not implement push notifications).

Setup your watchOS Target in Xcode: SwiftUI Interface, SwiftUI Lifecycle, Swift Language, Include Complication.
Setup your watchOS Target in Xcode

Next, we just need to ensure we include the Shared framework into our watch app:

Be careful to include the Shared framework in the Watch app

The watch app template will include an App declaration and a template ContentView. Since we already have a content view in our shared framework, we can delete the provided one, and simply import the shared one into the app.

import Shared
import SwiftUI

@main
struct TubeStatusApp: App {
    @ObservedObject private var viewModel = StatusViewModel(client: .init())

    var body: some Scene {
        WindowGroup {
            ScrollableContentView(updates: viewModel.status)
        }
    }
}

One minor change, will fix the truncation showed in the screenshot above. We’ve already implemented logic for our AdaptiveStack to display vertically at higher dynamic type-sizes, but we can use this for our watch app too:

private var shouldDisplayVertically: Bool {
     #if os(iOS)
     return sizeCategory >= .accessibilityMedium
     #elseif os(watchOS)
     return true
     #else
     return false
     #endif
}

That’s it: the app should run seamlessly on watchOS (and it looks great too!)

Tube Status app optimised for watchOS
Tube Status app optimised for watchOS
Much better!

Complication

Adding a watchOS complication isn’t too difficult. It uses a lot of similar concepts to the iOS Widget we implemented previously. However, on the Apple Watch space is much more limited so we will need to massively reduce the amount of information we can provide.

There is no support for using Intents to supply user-configurable parameters for complications. This means we will have to provide separate complication descriptor for each of the tube lines we want to support. We do this using the getComplicationDescriptors method.

func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
    let descriptors = Line.allCases
        .map { $0.rawValue }
        .map {
            CLKComplicationDescriptor(identifier: $0,
                                      displayName: "\($0) Status",
                                      supportedFamilies: CLKComplicationFamily.allCases)
        }
    // Call the handler with the currently supported complication descriptors
    handler(descriptors)
}

As with Widgets, we provide data to our complication using a TimelineEntry type. As before, for apps like public transit status which can’t provide future forecasts, we can only ever provide a single reliable timeline entry using the getCurrentTimelineEntry method.

func getCurrentTimelineEntry(for complication: CLKComplication,
                             withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
    // Use the identifier to retrieve the Line from the selected complication
    guard let line = Line(rawValue: complication.identifier) else { handler(nil); return }
    // Reuse our StatusService from the Shared framework to retrieve status data
    StatusService.getStatus(client: .init(), for: line) { [weak self] statuses in
        guard let status = statuses.first,
              let template = self?.getTemplateForLineStatusUpdate(status, matching: complication)
        else { handler(nil); return }
        // Call the async handler with the current timeline entry
        handler(CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template))
    }
}

For iOS Widgets our view implemented in a separate configuration type, decoupled from our WidgetKit.TimelineEntry model. In contrast the CLKComplicationTimelineEntry type for complications contains all the information needed to display our complication.

All we need to do now is to map our status information (LineStatusUpdate) into the CLKComplicationTemplate type by implementing the getTemplateForLineStatusUpdate(status: LineStatusUpdate, matching: CLKComplication) method we have used in the snippet above:

func getTemplateForLineStatusUpdate(_ update: LineStatusUpdate,
                                    matching complication: CLKComplication) -> CLKComplicationTemplate? {
let template: CLKComplicationTemplate?
    switch complication.family {
    case .modularSmall:
        template = CLKComplicationTemplateModularSmallStackImage(line1ImageProvider: imageProvider, 
                                                                 line2TextProvider: header)
    // Implement the CLKComplicationTemplate for each other 
    // supported complication format
    ...
    @unknown default:
        template = nil
    }
    template?.tintColor = UIColor(Line.bakerloo.color)
    return template
}

Three methods implemented; another great feature available for our users.

watchOS Complications give our users the tube status for various lines with a quick glance.

 TV

When I was writing this section of the article my partner asked me: why would on earth would we want to display the tube status on our TV?

How about something like a large real-time display that we can check at work just before we leave the office to go home? Here’s a photo of the one we have at my day-job- not (YET) using this implementation! Or we could even use it to power the display boards at the station.

Since we’ve already done the cross-platform work to make our shared framework to run across both iOS and watchOS, adding support for the TV should be trivial:

  • Add a new target for supporting tvOS in the same way as before:
    File → New → Target → tvOS → App.
  • Implement the app declaration, we can actually use exactly the same declaration (watchOS: TubeStatusApp.swift) that we used for the watch above.
  • Build and run!
Tube Status app running on tvOS
Tube Status app running on tvOS

As an alternative, we could use the StaticContentView we implemented for our iOS Widget to display all the statuses and hide the detailed descriptions. This would be more practical if we are indeed running in a Kiosk mode.

Tube Status app running on tvOS without scroll

Clearly this is a design question, but it illustrates how the code reuse across the different platforms can give us greater flexibility in our design choices.

macOS

Finally, we come to the longest standing platform, macOS. By now, our app is pretty good at adapting to various screen-sizes and contexts.

We can add a new target for supporting macOS in the same way as before:
File → New → Target → macOS → App.

If we run the app now, we will see a blank screen and output to the console complaining of issues connecting to the Internet. This is because by default macOS apps run inside an App Sandbox, preventing access to files, networking and other system functionality such as hardware. Not to fear- enabling network connectivity for our app is as simple as ensuring the checkbox is set correctly in Signing & Capabilities for the target.

We need to ensure that Outgoings Connections are allowed for our macOS app

Run again and we should see the familiar Tube Status view:

Tube Status app running on macOS

As with all the other platforms, this should fully support Dark Mode, Voiceover, etc. when set within macOS System Preferences.

There are plenty of additional features we could implement for macOS.

In Total

There are so many further tweaks and improvements we can make, but using SwiftUI we’ve managed to very quickly add support

Overall, we’ve implemented apps that work across:

  • All four distinct Apple platforms with their individual nuances
  • VoiceOver and dynamic type
  • Quick access to information through widgets on iOS, macOS and watchOS complications
  • Both dark and light mode

Sample code for this post is available on GitHub.
Make sure to checkout the going-for-gold branch of the repository!

Enjoyed this article?
Share it to your network, and let me know what you thought!

Constructing Data with Swift Result Builders

When Apple introduced SwiftUI in 2019, they showed how Swift 5.1’s result builders could be used to quickly, and readably, build user interfaces containing a wide range of elements. In Swift 5.4 (bundled with Xcode 12.5 or later), these have been renamed “result builders” and are now a public language feature. Therefore, as developers we can readily use them ourselves when building arrays of objects in our code.

In iOS apps we often use UIAlertController to display error messages. I often see the following extension on UIViewController, allowing alerts to be easily presented throughout the app.

extension UIViewController {
    func presentAlert(title: String, message: String,
                      actions: [UIAlertAction]) {
        let alert = UIAlertController(title: title, message: message,
                                      preferredStyle: .alert)
        actions.forEach { alert.addAction($0) }
        present(alert, animated: true)
    }
}

Often this results in some logic at the call site when there are alerts that change depending on the message being displayed. For example, we may want to handle an error that comes back from one of our API calls. In this case, the error may either be resolvable or not depending on its HTTP status code. If it is a resolvable error we want to give the user the option to retry.

func handleAPIError(_ error: NetworkError) {
    var actions = [UIAlertAction(title: "Cancel", style: .cancel)]
    let errorIsResolvable = (500...599).contains(error.code)
    if errorIsResolvable {
        actions.append(
            UIAlertAction(title: "Retry", style: .default) { _ in
                retryRequest()
            }
        )
    }
    
    navigationController?.presentAlert(title: "An Error Occurred",
                                       message: error.localizedDescription,
                                       actions: actions)
}

This logic isn’t too complex, but if we were to have a number of conditions or actions to append it can become messy very quickly. To prevent this, we could create a result builder for in-place of the array of actions that we were originally passing in.

This will allow us to add actions, or not, in the same way that we do when building our SwiftUI views.

@resultBuilder
public struct UIAlertActionBuilder {
    public static func buildBlock() -> [UIAlertAction] {
        []
    }

    public static func buildBlock(_ elements: UIAlertAction...) -> [UIAlertAction] {
        elements.compactMap { $0 }
    }

    public static func buildBlock(_ elements: [UIAlertAction]...) -> [UIAlertAction] {
        elements.flatMap { $0 }
    }

    public static func buildIf(_ elements: [UIAlertAction]?) -> [UIAlertAction] {
        elements ?? []
    }
}
extension UIViewController {
    func presentAlert(title: String?, message: String?,
                      @UIAlertActionBuilder actions: () -> [UIAlertAction]) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        actions().forEach { alert.addAction($0) }
        present(alert, animated: true)
    }
}

Our original logic can now become much more readable (and declarative).

func handleAPIError(message: String, code: Int) {
    let errorIsResolvable = (500...599).contains(code)
    navigationController?.presentAlert(title: "An Error Occurred",
                                       message: message) {
        UIAlertAction(title: "Cancel", style: .cancel)
        if errorIsResolvable {
            UIAlertAction(title: "Retry", style: .default) { _ in
                retryRequest()
            }
        }
    }
}

Without much change, we can even take this one step further by creating a generic ArrayBuilder. This will allow us to build arrays of any type that can be useful in all sorts of places in our codebase.

@resultBuilder
public struct ArrayBuilder<T> {
    public static func buildBlock() -> [T] {
        []
    }
    
    // ... etc.
}

We can also implement the methods for buildEither so that we can support else branches when building arrays.

public static func buildEither(first: [T]) -> [T] {
    first
}

public static func buildEither(second: [T]) -> [T] {
    second
}

In order to use this new ArrayBuilder in place of our UIAlertActionBuilder.
We would simply use the following snippet.

func presentAlert(title: String?, message: String?,
                  @ArrayBuilder<UIAlertAction> actions: () -> [UIAlertAction])

As you can see, result builders can be an invaluable tool to help create lists of data cleanly in Swift. There are often places in our apps where elements may or may not appear.

It’s helped me to massively reduce the amount of code needed, for example in Settings menus, for some of my apps: where certain settings are shown or hidden depending on others and the profile of the user who views the page.


Get in touch?

Building (almost) anything on Bitrise using Docker

Bitrise is gaining a lot of users in the mobile development community- but did you know you can also use it as a CI tool for non-mobile projects too?

For Little Journey we use Bitrise to build our full stack of applications including front-end (Angular) and back-end (Vapor) web. The web applications are both deployed to a Linode server.

Docker 18.09 came with a new tool called BuildKit which allows you to export executables built inside a container so that they can be deployed directly on the target platform.

DOCKER_BUILDKIT=1 docker build --output type=tar,dest=release.tar .

By setting the DOCKER_BUILDKIT variable, we can output a tarfile containing the build artifacts we need, such as executables or compiled code.

In order to do this, we need to create a Dockerfile with a multi-stage (two stages) build. The first stage will install any dependencies and run the build. The second stage copies our build files into a clean container so that we can export just the build files that we need, rather than the whole codebase.

FROM swift:xenial AS build-stage
WORKDIR /root
COPY . .
RUN apt-get -qq update && apt-get install -yq libssl-dev libicu-dev
RUN swift build -c release

FROM scratch AS export-stage
# Vapor Swift stores build files in .build/release
COPY --from=build-stage /root/.build/release /

For our front-end Angular app, the deployment is now as simple as transferring this tar file to the relevant folder on our Linode server and decompressing it by running tar -xvf release.tar from the command line.

For our Vapor API, we can ensure that the executable is built in a container that matches our server (Ubuntu 16.04 LTS) so that it will run correctly when deployed to the server, even though we are building it using a macOS agent on Bitrise.

Docker is also great for running our tests so that we can be sure they all pass before each pull request gets merged into our main codebase.

Multi-stage Docker builds are also great for doing this. We can add a test-stage between our initial setup / build and export stages.

If required, it’s also possible to add additional phases for different release environments (such as pre-production) environments if different build configurations are required.

# Create a Lightweight Node environment with our 
# dependencies to use as a base container
FROM timbru31/node-alpine-firefox AS base
WORKDIR /root
COPY . .
# Skip Chromium download as we use Firefox for testing
RUN npm config set puppeteer_skip_chromium_download true -g
# Install our dependencies
RUN npm install

# Create a test container (on top of base), set path to Firefox
# Run lint to ensure code-style and run the unit tests
FROM base AS test
ENV FIREFOX_BIN=/usr/bin/firefox
RUN npm run-script lint && npm test

# Create a build container (on top of base) and run the build
FROM base AS prod
RUN npm run-script build-prod

# Create an empty container copying the output
FROM scratch AS build-prod
COPY --from=prod /dist/little-journey /

We can now pass a stage target parameter into Docker to either

  • run the tests for pull requests:
    docker build --target test .
  • build an executable on code-merge:
    DOCKER_BUILDKIT=1 docker build --output type=tar,dest=release.tar --target build-beta .

Want to know more?

I’d recommend this article from @ZachSimone on how to deploy a Vapor app to a Linode Ubuntu server.

Check out the full documentation on Docker BuildKit and Bitrise.

We use these techniques for building our front-end (Admin Panel) and back-end web applications at Little Journey. Little Journey is an interactive, virtual reality (VR) mobile app designed to prepare children aged 3 to 12 years for day-case surgery.