What is new in Swift 5.9?

In this article, We will discuss the most significant changes in Swift 5.9 release, along with code snippets and explanations. Here we will talk about three main feature in swift 5.9.

Below are the key point of ‘What’s new in Swift 5.9

    1.   if and switch expressions (Expressive code): 

This is way to express yourself in your code. It allowing if/else and switch statements to be used  as expression, providing a nice way to clean up your code.

We take an example of initialising a let variable based on some complex condition. In below code snippet. there are  multiple ternary operators to fulfil the all cases.

    let shape = isNew && (count == 0 || !willExpand) ? ""
        : count == 0 ? "triangle"
        : maxDepth <= 0 ? "square" : "rectangle"

Now in Swift 5.9, it does help reduce a little extra syntax in the language. We can write above code with much more familiar way and readable chain with if else condition. You can directly assign the if else result to any variable.

   let shape = if isNew && (count == 0 || !willExpand)  { ""}
        else if count == 0 { "triangle" }
        else if maxDepth <= 0 {"square"}
        else {"rectangle"}

You can assign result of optional binding as well:

       let resultValue = if let model = modelName, !model.isEmpty {
            "Success"
        } else {
            "Fail"
        }

We can use Switch  for multiple condition:

  let amount = switch x {
            case 0: "none"
            case 1 ... 5: "a few"
            case 6 ... 10: "several"
            default: "lots"
        }

In Short, This meant that it is possible now  to directly assign the result of an if or a switch to a variable or to pass it as an argument.

You can find more detail in SE-0380 adds the ability for us to use if and switch as expressions in several situations.

    2.  Value and Type Parameter Packs

There is currently a fixed number of type parameters required by generic functions. Writing a generic function that takes an arbitrary number of arguments of different types is not feasible; instead, one of the following workarounds is needed:

     a. By using Any…

     b. Using a single tuple type argument instead of separate type arguments

     c. Overloading for each argument length with an artificial limit

We can take an example that execute above workaround to take an arbitrary number of arguments of different types in Generic function.

Here we have Car, Bike and Truck class. Car and Truck are four Wheeler and Bike is 2 Wheeler.

class Car {
    var name: String?
    var price: String?
} 

class Bike {
    var name: String?
    var price: String?
}

class Truck {
    var name: String?
    var price: String?
}

This is function with variadic parameter for grouping four wheeler and two wheeler.

    func pairVehicle<T, U>(fourWheel: T..., twoWheel: U...) -> ([(T, U)]) {
        assert(fourWheel.count == twoWheel.count, "You must provide equal numbers of both fourWheel and twoWheel.")
        var result = [(T, U)]()
        for i in 0..<fourWheel.count {
            result.append((fourWheel[i], twoWheel[i]))
        }
        return result
    }

In this example, We are using tuples to return group of the vehicles. Calling function:

 let ducati = Bike()
        let harley = Bike()
        let ninja = Bike()
        
        let bmw = Car()
        let audi = Car()
        let mercedes = Car()

       print(pairVehicle(fourWheel: bmw, audi, mercedes, twoWheel: ducati, harley, ninja))

////Output:[(Example.Car, Example.Bike), (Example.Car, Example.Bike), (Example.Car, Example.Bike)]

In fourWheel and twoWheel, parameter, we can send only single type object. As in above example we are sending fourWheel as car only, and in twoWheel, we are sending bikes

if we tried to use bus or truck  model in the ‘fourWheel’ parameter then Swift would refuse to build our code – it needs the types of all the fourWheel and twoWheel to be the same. 

       let truck = Truck()
       print(pairVehicle(fourWheel: bmw, audi, mercedes, truck, twoWheel: ducati, harley, ninja))
// It throws error.

To address this problem, we could use ‘Any’ to send multiple type of four wheel in parameter, but parameter packs provide a much more elegant solution.

func pairVehicle<each T, each U>(fourWheel: repeat each T, twoWheel: repeat each U) -> (repeat (each T, each U)) {
          return (repeat (each fourWheel, each twoWheel))
  }
 print(pairVehicle(fourWheel: bmw, audi, mercedes, twoWheel: ducati, harley, ninja))

Let’s address each of the four separate events that are occurring there:

  1. <each T, each U> creates two type parameter packs, T and U.
  2. repeat each T and repeat each U are a pack expansion, which is what expands the parameter pack into actual values – it’s the equivalent of T….
  3. The return type means we’re sending back tuples of paired Two wheeler and four wheeler, one each from T and U.
  4. Our return keyword is what does the real work: it uses a pack expansion expression to take one value from T and one from U, putting them together into the returned value.

We can send truck as four wheel in it. That will work:  
print(pairVehicle(fourWheel: bmw, audi, truck, twoWheel: ducati, harley, ninja))

   3.  Swift Macros

When you compile your source code, macros change it, saving you from having to write repetitive code by hand. Swift expands any macros in your code during compilation and then builds your code normally. And It reduce your number of line of the code. 

Note: We won’t discuss the benefits or drawbacks of the macro here.

At a very high level, a macro takes part of the program’s source code at compile time and translates it into other source code that is then compiled into the program. There are three fundamental questions about the use of macros:

  • What kind of translation can the macro perform?
  • When is a macro expanded?
  • How does the compiler expand the macro?

Swift has two kinds of macros:

1. Freestanding macros appear on their own, without being attached to a declaration.

2. Attached macros modify the declaration that they’re attached to.

Freestanding Macros –

A number sign (#) is written before the name of a freestanding macro, and any arguments are written to the macro in parenthesis after the name. For Example- 

We take an example of #URL macro, It is not swift in-built macro. It is created by third party. You can access it by integrate SPM by below GitHub link –

https://github.com/davidsteppenbeck/URL.git

In below code snippet, you can check the url validation on compile time instead of runtime. returning an unwrapped value when the URL is valid:

What’s new in Swift 5.9, url macro

If the URL is not valid, our macro verifies it and generates a compile-time error. We won’t experience runtime crashes when trying to unwrap an invalid URL, Below is snippet for compile time error:

What’s new in Swift 5.9,  url macro

We take another example of Freestanding Macro from the Swift standard library.

 func countNumbers() {
        print("Currently running \(#function)")
        #warning("Something's wrong")
 }

When you compile this code, Swift calls that macro’s implementation, which replaces #function with the name of the current function. When you run this code and call countNumbers(), it prints “Currently running countNumbers()”. In the second line, #warning calls the warning(_:) macro from the Swift standard library to produce a custom compile-time warning.

Attached Macros –

To call an attached macro, you write an at sign (@) before its name, and you write any arguments to the macro in parentheses after its name.

Attached macros modify the declaration of the function that they’re attached to. They add extra code to that declaration, like defining a new method or adding conformance to a protocol.

For example, consider the following code that doesn’t use macros:

struct CarModel: OptionSet {
    let rawValue: Int
    static let nuts = CarModel(rawValue: 1 << 0)
    static let cherry = CarModel(rawValue: 1 << 1)
    static let fudge = CarModel(rawValue: 1 << 2)
}

In this code, each of the options in the CarModel option set includes a call to the initializer, which is repetitive and manual. It would be easy to make a mistake when adding a new option, like typing the wrong number at the end of the line.

Here’s a version of this code that uses a macro instead:

@OptionSet<Int>
struct CarModel {
    private enum Options: Int {
        case audi
        case mercedes
        case bmw
    }
}

This version of CarModel calls an @OptionSet macro. The macro reads the list of cases in the private enumeration, generates the list of constants for each option, and adds a conformance to the OptionSet protocol.

Lets take another example, the aforemented macro to add a completion handler would be declared as follows:

@attached(peer, names: overloaded)
macro AddCompletionHandler(parameterName: String = "completionHandler")

The macro can be used as follows:

@AddCompletionHandler(parameterName: "onCompletion")
func fetchAvatar(_ username: String) async -> Image? { ... }

The use of the macro is attached to fetchAvatar, and generates a peer declaration alongside fetchAvatar whose name is “overloaded” with fetchAvatar. The generated declaration is:

// Expansion of the macro produces the following.
func fetchAvatar(_ username: String, onCompletion: @escaping (Image?) -> Void) {
  Task.detached {
    onCompletion(await fetchAvatar(username))
  }
}

References –

https://github.com/apple/swift-evolution/blob/main/proposals/0393-parameter-packs.md

Conclusion:

Conclusion of this article is Swift provided us powerful features to make our coding faster and reduce the lines of code.
Swift brought significant improvements, better performance and module stability for simplified development, enhancing Swift’s reliability.

Leave a Reply

Your email address will not be published. Required fields are marked *