Source editing just keeps getting better and better
While Xcode 13 added some major features such as Vim mode, version control, DocC, and Xcode Cloud, Xcode 14 opts to return to the basics and polish some core features that make the whole experience faster and smarter to use.
In this article I want to walk through some of the many improvements introduced in Xcode 14, many of which weren’t even mentioned by Apple, as well as a few things that I’m less convinced about. Let’s get straight into it…
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
This year is another bumper year for Xcode’s source editor – it just keeps getting smarter and smarter in ways that I hadn’t even imagined were possible.
As an example, let’s say we had a Player
struct such as this one:
struct Player: Identifiable {
var id = UUID()
var name: String
var score = 0
}
Swift will automatically generate a memberwise initializer for that because it’s a struct, but it’s common to want to customize that initializer. So, Xcode 14 will now autocomplete a memberwise initializer for us – just start typing init
inside the struct and it will complete to this:
init(id: UUID = UUID(), name: String, score: Int = 0) {
self.id = id
self.name = name
self.score = score
}
So, now we might say that the score must always be at least 0:
self.score = max(0, score)
This even works with Codable
– if you add a Codable
conformance to the struct then start typing encode
inside the struct, it will autocomplete this:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.name, forKey: .name)
try container.encode(self.score, forKey: .score)
}
This relieves a lot of boilerplate for those times when you want to make one small change to your Codable
conformance.
Moving on, we might create an array of users inside a SwiftUI view, but here Xcode does something very interesting: as it shows autocomplete suggestions for Player
it will show parameters with default values in italics, and pressing return to accept the completion will skip out those values.
So, we might end up with an array like this one:
let players = [
Player(name: "Dani"), Player(name: "Jamie"), Player(name: "Roy"), Player(name: "Sam")
]
This works really well in some situations, but I find it a little annoying in many others. For example, when working with the new Swift Charts framework you might write code like this:
Chart {
BarMark(
}
When opening the parenthesis for BarMark
you’ll see a bunch of autocompletion suggestions such as BarMark(x:yStart:yEnd:width:stacking:)
, but if you hit return to accept that completion Xcode will fill in only BarMark(x:)
. This feels particularly strange when you’ve used the arrow keys to choose a particular variation, only to have Xcode effectively ignore you.
If this annoys you too, here are three tips to make life easier:
.width
after the chart to get .frame(width:)
.Another change to code completion is its ability to collapse overloads, which is particularly useful in SwiftUI code where there are often overloads for Text
, StringProtocol
, and LocalizedStringKey
. Any function call that has variations based only on the type of its parameters will now get collapsed down to a single listing in Xcode 14, and you’ll see a number to the right such as “+3 more”.
For example, if you try adding the badge()
modifier to our chart, you’ll see Xcode offers badge(_ count: Int)
as the default, with “+3 more” to the right. These are all overloads with the same parameters and return types, and you can reveal them all by pressing the right cursor on your keyboard.
There are some code completions that really took me by surprise at first, because they were just so smart. For example, typing lplay
in a SwiftUI view offered to autocomplete this:
List(players) { player in
Text(player.name)
}
In one completion, Xcode detected the array, converted it to a List
, singularized player
, and picked out a string property from there for a Text
view.
In this case we only want the player names, so we could use map()
to extract just that property from each Player
instance. Here Xcode uses the name of the property we’re creating to automatically complete a map()
call that extracts the correct property – try these two, for example:
let names = pl
let scores = pl
They will complete like so:
let names = players.map { $0.name }
let scores = players.map { $0.score }
Again, Xcode is singularizing the constant names and using the result to find a matching property name in the Player
struct – just brilliant.
And there are stacks of smaller changes too. One of my favorites is when you wrap one view in a container in SwiftUI, because Xcode will now automatically indent the contents. To try this in our List
example, try adding a VStack
around it, like this:
VStack {
List(players) { player in
Text(player.name)
}
As soon as you add an extra closing brace to the end, the List
contents will move in one level. When I first saw this happen I had to undo the change and do it again, because it fixed such a common issue I had.
Another small tweak is the automatic import of modules, which has been replaced with an Xcode Fix It prompt. This is definitely going to make some folks happy, because occasionally the auto import would get things wrong and cause problems. To try it out yourself, try create a new Swift file called Settings.swift, and give it this code:
struct Settings {
var username: String
var avatarColor: Color
}
That won’t compile because it uses Color
from SwiftUI, but if you select Xcode’s error message you’ll see a button to import SwiftUI and resolve the issue entirely.
Last but not least, you might have noticed little sticky headers while you’re scrolling around in Xcode, and they are a small but marvelous addition to the source editor. Xcode 14 will now automatically keep pinned at the top whatever struct or method you’re in, giving just that extra little bit of context when working with more complex files.
I know, I know: I could talk about the source editor improvements all day, but obviously Xcode 14 comes with a raft of improvements beyond just the way we enter code.
First, Xcode’s library now includes SF Symbols support, just like Swift Playgrounds 4. To activate it, press Shift+Cmd+L, then select the new Symbols Library option. This is a small but useful addition, but annoyingly it seems to use your Mac’s built-in selection of symbols rather than whatever device you’re currently targeting. So, for me that means I currently see all the symbols supported by macOS 12.4 even when in an app target that uses iOS 16 – I don’t get to see all the new SF Symbols 4 icons, for example.
Second, Xcode is now able to automatically create all app icon size variations from a single image. To activate this option, go to your asset catalog and select AppIcon, then use the attributes inspector to change All Sizes to Single Size.
Now, before you rush off and do that for all your projects, I have an important warning: many, many icons just won’t look good when Xcode scales them down for you. If your icons has no small details and uses chunkier lines or shapes there’s a good chance you’ll be fine, but for more complex icons you will probably find that scaling them down by hand and adding extra hinting will yield a much better result.
Third, DocC received a huge upgrade so that it supports app projects, which was something that was sorely missed back when DocC was introduced. When it works well I think this will be a really fantastic way to introduce newcomers to your project codebase, but right now I found it a bit crashy – it couldn’t generate the documentation for Unwrap, for example. When it does work, you’ll be pleased to know that exporting to hosted sites such as GitHub is easier now.
Fourth, the chooser that handles your run destination now remembers your recent choices and places them at the top of the list. This is particularly useful for times where you regularly flip between two or three devices, such as a small iPhone, a large iPhone, and an iPad.
And saving the best until last, SwiftUI previews are now interactive by default, which means you get much faster iteration between code and previews. On Apple Silicon Macs the speed here is quite remarkable: there’s maybe a quarter-second delay between making changes and getting to interact with them live on the canvas.
That alone is neat, but I think the feature everyone is going to love most is the new Variants button directly below your current simulator, which is able to show multiple previews at the same time for color scheme variations, orientation variations, and Dynamic Type variations – there is simply no faster way to see exactly how one view works in a variety of options.
One slight downside to this new live-everywhere functionality is that a commonly used approach to previewing is now no longer possible: you can’t place multiple views into a single preview and see them side by side any more. Many of us were using this to create a storyboard-style layout for many views so we could see how different views looked side by side, or how one view looked when placed inside other containers. With this new live change, this kind of code renders each view inside a separate tab:
struct Storyboard_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
}
ReadingView(article: .example)
ArticleRow(article: .example)
}
}
It’s a bit of a pain to lose the original functionality, but I think it’s made up for by the built-in variants.
Apple made a big splash about the build performance improvements in Xcode 14, including up to 2x faster linking, 25% faster building, and 30% faster testing, but the one thing that’s most likely to impact big projects is the new Build Timeline feature that shows exactly how Xcode spent its time building your projects. Try it yourself by going to the Product menu and choosing Perform Action > Build with Timing Summary.
Once your build completes, you should see Build Timing Summary in your log along with all Xcode’s other messages – right-click that and choose Show in Timeline to see exactly what work Xcode was doing. Chances are you’ll notice two things:
For a first iteration this is a really great feature, and I look forward to seeing how Apple expands it in the future. In particular, Swift is able to produce function-by-function compile times, so it would be great if we could drill down into one file that was compiling particularly slowly to see exactly what the problem was.
Before I’m done, there’s one last performance improvement I need to mention because it’s something you’ll notice before you even launch Xcode 14 for the first time: Apple put Xcode back on a diet, and the download now includes SDKs for macOS and iOS but not tvOS and watchOS. This means the download is faster and the unxip process is also faster, but also that when you first run Xcode 14 you’ll have the option to add download the extras if you need them.
Xcode’s source editor gets smarter and smarter, common developer annoyances like app icons, DocC for apps, and SF Symbols integrations are solved, SwiftUI previews got a major power up, and we have a renewed focus on performance – this is such a great upgrade that I already don’t want to go back to Xcode 13.
Once any initial bugs are fixed (looking at you, DocC!), the focus will inevitably turn to next year and Xcode 15. I keep hoping to see codegen for assets so we can write Image(.logo)
rather than Image("logo")
, or extensions so that other developers can add extra functionality to the main IDE, but what features do you most want to see?
Tweet me @twostraws and let me know!
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.