Introduction
The Swift 5.8 release includes features like implicit self for weak self captures, conditional attribute compilation, new type StaticBigInt in the standard library, and more.
In this article, I will walk you through the essential features with Examples and explanations so you can try them yourself; you will need XCode 14.3 or later to use this.
This release includes some major features are,
- Allow implicit self for weak self captures after self is unwrapped
- Lift all limitations on variables in result builders
- Function Back Deployment
- StaticBigInt
- Concise magic file names
- Opening existential arguments to optional parameters
Allow implicit self for weak self captures after self is unwrapped
Implicit self is permitted in closures when self is written explicitly in the capture list. We should extend this support to weak selfcaptures and permits implicit self as long as the self has been unwrapped.
class ViewController {
let button: Button
func setup() {
button.tapHandler = { [weak self] in
guard let self else { return }
dismiss()
}
}
func dismiss() { ... }
}
Enabling implicit self
All of the following forms support optional unwrapping and enable implicit self for the following scope where self is non-optional.
button.tapHandler = { [weak self] in
guard let self = self else { return }
dismiss()
}
button.tapHandler = { [weak self] in
if let self {
dismiss()
}
}
button.tapHandler = { [weak self] in
if let self = self {
dismiss()
}
}
button.tapHandler = { [weak self] in
while let self {
dismiss()
}
}
button.tapHandler = { [weak self] in
while let self = self {
dismiss()
}
}
Like with implicit self for strong and unowned captures, the compiler will synthesize an implicit self. For calls to properties/methods on self inside a closure that uses weak self.
If the self has not been unwrapped, you will get a compiler error.
button.tapHandler = { [weak self] in
// error: explicit use of 'self' is required when 'self' is optional,
// to make control flow explicit
// fix-it: reference 'self?.' explicitly
dismiss()
}
Lift all limitations on variables in result builders
In Swift 5.8, there are some limitations on variables that can be used within result builders. Notably, the variables used within a result builder must be explicitly declared @resultBuilder and conform to the ResultBuilder protocol. Also, the variables must be annotated with @discardableResult to indicate that they are not being used in the final result.
Result builder allows variables of all of these kinds to be declared in a function that a result builder will transform,
- uninitialized variables (only if supported by the builder)
- default-initialized variables (e.g. variables with optional type)
- computed variables
- observed variables
- variables with property wrappers
- lazy variables
These variables will be treated just like they are treated in regular functions. All ordinary semantic checks to verify their validity will still be performed, and the compiler will reject invalid declarations (based on the standard rules).
Function Back Deployment
Function back deployment in Swift refers to the ability to write code compatible with older versions of the Swift language runtime. This is important because newer versions of Swift may introduce changes or deprecate features that could cause code written for older versions of Swift to break or produce unexpected behaviour.
The @backDeployed attribute may apply to functions, methods, and subscripts. Properties may also have the attribute if they do not have storage. The attribute takes a comma-separated list of one or more platform versions, so declarations that are available on more than one platform can be back-deployed on multiple platforms with a single attribute. The following are examples of legal uses of the attribute:
Applying the @backDeployed attribute to a function
Suppose you have a function calculateSum that was introduced in iOS 14.0, and you want to make it available on older versions of iOS. You can add the @backDeployed attribute to the function with the argument "iOS 13.0":
@backDeployed(iOS 13.0) func calculateSum(_ a: Int, _ b: Int) -> Int { return a + b }
This function can now be called on iOS 14.0 and iOS 13.0, even though it was only introduced in iOS 14.0.
Applying the @backDeployed attribute to a property
Suppose you have a property myProperty that was introduced in macOS 11.0, and you want to make it available on older macOS versions. You can add the @backDeployed attribute to the property with the argument "macOS 10.15":
@backDeployed(macOS 10.15) var myProperty: String { return "Hello, world!" }
Now, this property can be accessed on both macOS 11.0 and macOS 10.15, even though it was only introduced in macOS 11.0. Note that this only works because the property doesn't have storage - if it did, it wouldn't be possible to back-deploy it.
Applying the @backDeployed attribute to a subscript
Suppose you have a subscript myArray that was introduced in tvOS 14.0, and you want to make it available on older versions of tvOS. You can add the @backDeployed attribute to the subscript with the arguments "tvOS 13.0" and "tvOS 12.0":
@backDeployed(tvOS 13.0, tvOS 12.0) subscript(index: Int) -> String { return "Element \(index)" }
This subscript can now be accessed on tvOS 14.0, tvOS 13.0, and tvOS 12.0, even though it was only introduced in tvOS 14.0. Note that you can specify multiple arguments to the @backDeployed attribute, separated by commas, to make the declaration available on multiple platforms.
StaticBigInt
StaticBigInt models a mathematical integer, where distinctions visible in source code (such as the base/radix and leading zeros) are erased. It doesn't conform to any numeric protocols because new values of the type can't be built at runtime. Instead, it provides a limited API that can be used to extract the integer value it represents.
struct StaticBigInt {
private let magnitude: [UInt]
private let radix: UInt
init(_ magnitude: [UInt], radix: UInt = 10) {
self.magnitude = magnitude
self.radix = radix
}
func toInt() -> Int {
// Convert the magnitude to a single integer value
var result = 0
for digit in magnitude.reversed() {
result = result * Int(radix) + Int(digit)
}
return result
}
}
In this implementation, a StaticBigInt is represented by an array of UInt values that make up its magnitude. The radix parameter specifies the base the number represents (defaulting to base 10).
Note. The StaticBigInt type does not conform to any numeric protocols (such as Numeric or Comparable) because it is impossible to build new values of the type at runtime. Instead, it provides a limited API consisting of only one method, toInt(), which can extract the integer value that the StaticBigInt represents.
Here's an example of how you might use this StaticBigInt type.
let magnitude: [UInt] = [1, 2, 3, 4, 5]
let number = StaticBigInt(magnitude)
print(number.toInt()) // Output: 12345
In this example, we create a StaticBigInt with magnitude [1, 2, 3, 4, 5], the integer value 12345 in base 10. We then use the toInt() method to extract the integer value and print it to the console.
Concise magic file names
The concise magic file will change the string that has #file evaluates to—instead of evaluating to the full path, it will now have the format <module-name>/<file-name>. For those applications which still need a full path, we will provide a new magic identifier, #filePath. These features will otherwise behave the same as the old #file, including capturing the call site location when used in default arguments. The standard library's assertion and error functions will continue to use #file.
// MyModule.swift
func log(message: String, file: String = #file) {
let components = file.split(separator: "/")
let moduleName = components.first ?? ""
let fileName = components.last ?? ""
print("[\(moduleName)/\(fileName)] \(message)")
}
// main.swift
import MyModule
log("Hello, world!")
In this example, we define a log function in a file called MyModule.swift. This function takes a message parameter and an optional file parameter that defaults to the #file magic identifier.
Under the proposed changes, the #file identifier will evaluate to a string in the format <module-name>/<file-name> rather than the full path. To extract the module name and file name from this string, we split it into components using the separator.
In the log function, we then use these components to construct a log message in the format [<module-name>/<file-name>] <message>, which we print to the console.
In main.swift, we import MyModule and call the log function with a simple message. The output of this program will be something like this,
[MyModule/MyModule.swift] Hello, world!
Note. The #filePath magic identifier under the proposed changes will provide access to the full path if needed.
Opening existential arguments to optional parameters
Opening existential arguments to optional parameters would allow developers to use optional parameters in function signatures that have existential types.
An existential type is a type that hides the concrete implementation of a value behind a protocol or interface. For example, the type Array in Swift is an existential type that hides the underlying implementation of an array behind the Collection and Sequence protocols.
In Swift, functions that take existential arguments cannot have optional parameters. This is because optional parameters are represented as a combination of a parameter and a default value, and it's impossible to provide a default value for an existential type.
However, with the proposed feature, developers could use optional parameters in function signatures with existential types by providing a default value of nil. For example:
protocol MyProtocol {
func myFunction()
}
func myFunctionWrapper(arg: MyProtocol?, optionalArg: Int?) {
// ...
}
In this example, the myFunctionWrapper function takes an existential argument of type MyProtocol? and an optional parameter of type Int. The MyProtocol? argument is an existential type that can be any type that conforms to the MyProtocol protocol; during the Int?, the argument is an optional parameter with a default value of nil.