UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

What's new in Swift 5.8

Back-deployable APIs, more implicit self upgrades, improved result builders, and more!

Paul Hudson       @twostraws

Although many major Swift changes are currently percolating through Swift Evolution, Swift 5.8 itself is more of a clean up release: there are additions, yes, but there are more improvements that refine functionality that was already in widespread use. Hopefully this should make adoption much easier, particularly after the mammoth set of changes that were squeezed into Swift 5.7!

In this article I’m going to walk you through the most important changes this time around, providing code examples and explanations so you can try it all yourself. You’ll need Xcode 14.3 or later to use this, although some changes require a specific compiler flag before Swift 6 finally happens.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Lift all limitations on variables in result builders

SE-0373 relaxes some of the restrictions on variables when used inside result builders, allowing us to write code that would previously have been disallowed by the compiler.

For example, in Swift 5.8 we can use lazy variables directly inside result builders, like so:

struct ContentView: View {
    var body: some View {
        VStack {
            lazy var user = fetchUsername()
            Text("Hello, \(user).")
        }
        .padding()
    }

    func fetchUsername() -> String {
        "@twostraws"
    }
}

That shows the concept, but doesn’t provide any benefit because the lazy variable is always used – there’s no difference between using lazy var and let in that code. To see where it’s actually useful takes a longer code example, like this one:

// The user is an active subscriber, not an active subscriber, or we don't know their status yet.
enum UserState {
    case subscriber, nonsubscriber, unknown
}

// Two small pieces of information about the user
struct User {
    var id: UUID
    var username: String
}

struct ContentView: View {
    @State private var state = UserState.unknown

    var body: some View {
        VStack {
            lazy var user = fetchUsername()

            switch state {
            case .subscriber:
                Text("Hello, \(user.username). Here's what's new for subscribers…")
            case .nonsubscriber:
                Text("Hello, \(user.username). Here's why you should subscribe…")
                Button("Subscribe now") {
                    startSubscription(for: user)
                }
            case .unknown:
                Text("Sign up today!")
            }
        }
        .padding()
    }

    // Example function that would do complex work
    func fetchUsername() -> User {
        User(id: UUID(), username: "Anonymous")
    }

    func startSubscription(for user: User) {
        print("Starting subscription…")
    }
}

This approach solves problems that would appear in the alternatives:

  • If we didn’t use lazy, then fetchUsername() would be called in all three cases of state, even when it isn’t used in one.
  • If we removed lazy and placed the call to fetchUsername() inside the two cases then we would be duplicating code – not a massive problem with a simple one liner, but you can imagine how this would cause problems in more complex code.
  • If we moved user out to a computed property, it would be called a second time when the user clicked the "Subscribe now" button.

This change also allows us to use property wrappers and local computed properties inside result builders, although I suspect they will be less useful. For example, this kind of code is now allowed:

struct ContentView: View {
    var body: some View {
        @AppStorage("counter") var tapCount = 0

        Button("Count: \(tapCount)") {
            tapCount += 1
        }
    }
}

However, although that will cause the underlying UserDefaults value to change with each tap, using @AppStorage in this way won’t cause the body property to be reinvoked every time tapCount changes – our UI won’t automatically be updated to reflect the change.

Function back deployment

SE-0376 adds a new @backDeployed attribute that makes it possible to allow new APIs to be used on older versions of frameworks. It works by writing the code for a function into your app’s binary then performing a runtime check: if your user is on a suitably new version of the operating system then the system’s own version of the function will be used, otherwise the version copied into your app’s binary will be used instead.

On the surface this sounds like a fantastic way for Apple to make some new features retroactively available in earlier operating systems, but I don’t think it’s some kind of silver bullet – @backDeployed applies only to functions, methods, subscripts, and computed properties, so while it might work great for smaller API changes such as the fontDesign() modifier introduced in iOS 16.1, it wouldn’t work for any code that requires new types to be used, such as the new scrollBounceBehavior() modifier that relies on a new ScrollBounceBehavior struct.

As an example, iOS 16.4 introduced a monospaced(_ isActive:) variant for Text. If this were using @backDeployed, the SwiftUI team might ensure the modifier is available to whatever earliest version of SwiftUI supports the implementation code they actually need, like this:

extension Text {
    @backDeployed(before: iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4)
    @available(iOS 14.0, macOS 11, tvOS 14.0, watchOS 7.0, *)
    public func monospaced(_ isActive: Bool) -> Text {
        fatalError("Implementation here")
    }
}

If the modifier were implemented like that, at runtime Swift would use the system’s copy of SwiftUI if it has that modifier already, otherwise using the back-deployed version back to iOS 14.0 and similar. In practice, although that exposes no new types publicly and so seems like an easy choice for back deployment, we don't know what types SwiftUI uses internally, so it's not easy to predict what can and can't be back deployed.

Allow implicit self for weak self captures, after self is unwrapped

SE-0365 takes another step towards letting us remove self from closures by allowing an implicit self in places where a weak self capture has been unwrapped.

For example, in the code below we have a closure that captures self weakly, but then unwraps self immediately:

class TimerController {
    var timer: Timer?
    var fireCount = 0

    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
            guard let self else { return }
            print("Timer has fired \(fireCount) times")
            fireCount += 1
        }
    }
}

That code would not have compiled before Swift 5.8, because both instances of fireCount in the closure would need to be written self.fireCount.

Concise magic file names

SE-0274 adjusts the #file magic identifier to use the format Module/Filename, e.g. MyApp/ContentView.swift. Previously, #file contained the whole path to the Swift file, e.g. /Users/twostraws/Desktop/WhatsNewInSwift/WhatsNewInSwift/ContentView.swift, which is unnecessarily long and also likely to contain things you’d rather not reveal.

Important: Right now this behavior is not enabled by default. SE-0362 adds a new -enable-upcoming-feature compiler flag designed to let developers opt into new features before they are fully enabled in the language, so to enable the new #file behavior you should add -enable-upcoming-feature ConciseMagicFile to Other Swift Flags in Xcode.

If you’d like to have the old behavior after this flag is enabled, you should use #filePath instead:

// New behavior, when enabled
print(#file)

// Old behavior, when needed
print(#filePath)

The Swift Evolution proposal for this change is worth reading because it mentions surprisingly large improvements in binary size and execution performance, and also for this quite brilliant paragraph explaining why having the full path is often a bad idea:

“The full path to a source file may contain a developer's username, hints about the configuration of a build farm, proprietary versions or identifiers, or the Sailor Scout you named an external disk after.”

Opening existential arguments to optional parameters

SE-0375 extends a Swift 5.7 feature that allowed us to call generic functions using a protocol, fixing a small but annoying inconsistency: Swift 5.7 would not allow this behavior with optionals, whereas Swift 5.8 does.

For example, this code worked great in Swift 5.7, because it uses a non-optional T parameter:

func double<T: Numeric>(_ number: T) -> T {
    number * 2
}

let first = 1
let second = 2.0
let third: Float = 3

let numbers: [any Numeric] = [first, second, third]

for number in numbers {
    print(double(number))
}

In Swift 5.8, that same parameter can now be optional, like this:

func optionalDouble<T: Numeric>(_ number: T?) -> T {
    let numberToDouble = number ?? 0
    return  numberToDouble * 2
}

let first = 1
let second = 2.0
let third: Float = 3

let numbers: [any Numeric] = [first, second, third]

for number in numbers {
    print(optionalDouble(number))
}

In Swift 5.7 that would have issued the rather baffling error message “Type 'any Numeric' cannot conform to 'Numeric’”, so it’s good to see this inconsistency resolved.

Collection downcasts in cast patterns are now supported

This resolves another small but potentially annoying inconsistency in Swift where downcasting a collection – e.g. casting an array of ClassA to an array of another type that inherits from ClassA – would not be allowed in some circumstances.

For example, this code is now valid in Swift 5.8, whereas it would not have worked previously:

class Pet { }
class Dog: Pet {
    func bark() { print("Woof!") }
}

func bark(using pets: [Pet]) {
    switch pets {
    case let pets as [Dog]:
        for pet in pets {
            pet.bark()
        }
    default:
        print("No barking today.")
    }
}

Before Swift 5.8 that would have led to the error message, “Collection downcast in cast pattern is not implemented; use an explicit downcast to '[Dog]' instead.” In practice, syntax such as if let dogs = pets as? [Dog] { worked just fine, so I would imagine that error was rarely seen. However, this change does mean another language inconsistency is resolved, which is always welcome.

And there’s more!

There are two extra changes that are worth mentioning briefly.

First, SE-0368 introduces a new StaticBigInt type that ought to make it easier to add new, larger integer types in the future.

And second, SE-0372 adjusts the documentation of Swift’s sorting functions to mark them as stable, which means if two elements in an array are considered to be equal they will stay in the same relative order in the sorted array – they will stay together in the sorted array, and also in the original order they had. Swift’s sorting had been stable for some time but now it’s official.

Plus there are two extra features slated to ship in Swift 5.8 that aren’t currently available in the current Xcode betas – it’s likely they’ll be enabled with the final release of Xcode 14.3, but we’ll need to wait a while longer to find out for sure.

They are:

Where next?

With WWDC on the horizon it’s no surprise there are some huge Swift changes brewing, but fortunately for all of us they are almost entirely coming in a later release – probably Swift 5.9 at WWDC, although Swift 6 isn’t impossible.

However, it’s been interesting to note a growing rumble of unhappiness amongst Swift developers who have watched countless waves of Swift Evolution go by while we still suffer from one apparently unavoidable problem: debugging in Swift just doesn’t work well.

Let’s hope that improves sometimes soon…

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.