Highlight
Swift 5.9 introduces features such as if/switch expressions, parameter packs (Parameter Packs), Swift Macros, @Observable macros, ~Copyable ownership control, C++ interop, and custom Actor Executor, making code expression more concise, generic APIs more flexible, and code generated at compile time safer, while extending Swift’s performance and security advantages to mixed code bases with C++ and server-side distributed programs.
Core Content
Simplified assignment of if/switch expressions
In the past, you had to give based on conditionsletVariable assignment can only be done using nested ternary operators or immediate closure hacks. The code is ugly and error-prone.
(03:06)
Swift 5.9 allows if/else and switch to be used directly as expressions. Initialize variables, properties, and even return statements using familiar conditional statements.
(03:46)
Parameter package final reload explosion
Have you ever written code where a function has 6 overloaded versions to support different numbers of parameters? Each version has exactly the same logic except for the number of parameters. What’s worse is that the 7th parameter will cause a compilation error.
(06:03)
Parameter bags allow you to write a generic function that can handle any number of type parameters while retaining static type information for each parameter. The calling method is exactly the same as when using overloading.
(07:36)
Swift Macros generate code at compile time
Macros are the most important new feature in Swift 5.9. It allows new code to be generated based on your code at compile time, reducing boilerplate code while maintaining type safety.
(10:01)
for exampleassert(a == b)When it fails, a normal assertion just tells you “Assertion failed”, you don’t know the specific values of a and b. Assertions implemented as macros can be expanded into code that captures the value of each subexpression and tells you “a is 10 and b is 17” on failure.
(11:02)
@ObservableMacros are examples of what Apple itself uses in frameworks. It takes what originally needed to be written manuallyObservableObject + @Published + objectWillChange.send()All automated while enabling more fine-grained dependency tracking.
(16:57)
~Copyable controls value type copying
Swift value types can be copied arbitrarily by default. This is a good thing in most cases, but when dealing with resources such as file descriptors, network connections, etc., copying can lead to duplicate frees or resource leaks.
(23:59)
~CopyableLets you declare that a value type is not copyable. CooperateconsumingMethod, the compiler can prevent “use of released resources” errors at compile time instead of waiting for a runtime crash.
(26:06)
C++ interop
Swift 5.9 implements bidirectional interop with C++. You can directly call methods of C++ classes, access properties, and use STL containers in Swift; you can also call Swift functions and use Swift structures in C++.
(28:52)
This means that large C++ code bases can be gradually introduced to Swift without requiring a one-time rewrite.
Custom Actor Executor
The default executor for an Actor is a system-managed serial queue. But when you need to bind an Actor to a specific dispatch queue (such as SQLite’s serial queue), there was no way before.
(35:30)
Swift 5.9 allows you to customize Actor executors.DispatchSerialQueueAlready built inSerialExecutorFor protocol support, you only need to declare it in the ActorunownedExecutorThat’s it.
(35:58)
Detailed Content
if expression replaces the ternary operator
// Before: nested ternary operators are hard to read
let bullet =
isRoot && (count == 0 || !willExpand) ? ""
: count == 0 ? "- "
: maxDepth <= 0 ? "▹ " : "▿ "
(03:06)
// Swift 5.9: use an if expression
let bullet =
if isRoot && (count == 0 || !willExpand) { "" }
else if count == 0 { "- " }
else if maxDepth <= 0 { "▹ " }
else { "▿ " }
(03:19)
// Before: property initialization required a closure hack
let attributedName = AttributedString(markdown: displayName)
(03:30)
// Swift 5.9: initialize directly with an if expression
let attributedName =
if let displayName, !displayName.isEmpty {
AttributedString(markdown: displayName)
} else {
"Untitled"
}
(03:46)
Key points:
- if/else and switch can now be used anywhere an expression is expected
- Each branch must return the same type of value
- Particularly suitable for scenarios such as variable initialization, attribute default values, function return, etc.
- Much more readable than ternary operator nesting
Parameter pack alternative overloading
// Before: write overloads for each argument count
func evaluate<Result>(_ request: Request<Result>) -> Result
func evaluate<R1, R2>(_ r1: Request<R1>, _ r2: Request<R2>) -> (R1, R2)
func evaluate<R1, R2, R3>(_ r1: Request<R1>, _ r2: Request<R2>, _ r3: Request<R3>) -> (R1, R2, R3)
// ... stopped after 6 arguments
// The 7th argument causes a compile error: Extra argument in call
let results = evaluator.evaluate(r1, r2, r3, r4, r5, r6, r7)
(06:35)
// Swift 5.9: one function handles any count
func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)
(07:36)
// Call syntax stays the same as before
let value = RequestEvaluator().evaluate(request)
let (x, y) = RequestEvaluator().evaluate(r1, r2)
let (x, y, z) = RequestEvaluator().evaluate(r1, r2, r3)
(08:21)
Key points:
each ResultDeclare a type parameter pack -repeat Request<each Result>expands to any number of parameters -repeat each ResultExpand into the corresponding number of tuple elements in the return type- The calling syntax is exactly the same as overloading, and users are unaware of it
- No more artificial limit of “max N parameters”
Basic usage of Swift Macros
// Import the macro package
import PowerAssert
// Use the macro: show each subexpression's value on failure
#assert(max(a, b) == c)
// Output: Assertion failed: max(a, b) == c
// | | | | |
// | 10 17 10 17
// false
(11:02)
// Macro declaration
@freestanding(expression)
public macro assert(_ condition: Bool)
// Macro definition: points to the compiler plugin
public macro assert(_ condition: Bool) = #externalMacro(
module: "PowerAssertPlugin",
type: "PowerAssertMacro"
)
(12:07)
// Code after macro expansion
PowerAssert.Assertion("#assert(a == b)") {
$0.capture(a, column: 8) == $0.capture(b, column: 13)
}
(13:14)
Key points:
- Hong Yi
#at the beginning, such as#assert、#Predicate @freestanding(expression)Represents a stand-alone expression macro- Macro parameters will be type-checked, just like ordinary functions
- Macros are expanded at compile time to generate normal Swift code
- Xcode supports expanding macros to view the generated code, and also supports setting breakpoints in the code generated by the macro.
@Observable macro simplifies data observation
// Before: ObservableObject + @Published
final class Person: ObservableObject {
@Published var name: String
@Published var age: Int
@Published var isFavorite: Bool
}
struct ContentView: View {
@ObservedObject var person: Person
var body: some View {
Text("Hello, \(person.name)")
}
}
(16:57)
// Swift 5.9: @Observable macro
@Observable final class Person {
var name: String
var age: Int
var isFavorite: Bool
}
struct ContentView: View {
var person: Person
var body: some View {
Text("Hello, \(person.name)")
}
}
(17:25)
Key points:
@Observableyes@attachedMacro, including three roles: member, memberAttribute and conformance- Automatically add observation tracking for each attribute, no need to write it manually
@Published- no longer needed in view@ObservedObject, just quote the attribute directly - Only update the attributes actually read, granularity ratio
objectWillChangefiner
~Copyable controls resource life cycle
// Problem: structs can be copied freely, causing file descriptors to be closed repeatedly
struct FileDescriptor {
private var fd: CInt
init(descriptor: CInt) { self.fd = descriptor }
func close() {
Darwin.close(fd)
}
}
// Fix: mark it as noncopyable
struct FileDescriptor: ~Copyable {
private var fd: CInt
init(descriptor: CInt) { self.fd = descriptor }
func write(buffer: [UInt8]) throws { /* ... */ }
consuming func close() {
Darwin.close(fd)
}
deinit {
Darwin.close(fd)
}
}
(26:06)
// The compiler prevents incorrect usage at compile time
let file = FileDescriptor(fd: descriptor)
file.close() // consuming call, file's lifetime ends
file.write(buffer: data) // Compile error: 'file' used after consuming
(27:20)
Key points:
~CopyableIndicates that the value type is not implicitly copyable -consuming funcIndicates that calling this method will consume the ownership of the value- The compiler tracks the life cycle of values and prevents use-after-consume at compile time
- Suitable for managing non-copyable resources such as file descriptors, network connections, locks, etc.
C++ interop
// C++ header file Person.h
// struct Person {
// std::string name;
// unsigned getAge() const;
// };
// std::vector<Person> everyone();
// Call C++ APIs directly from Swift
func greetAdults() {
for person in everyone().filter { $0.getAge() >= 18 } {
print("Hello, \(person.name)!")
}
}
(28:52)
// Swift code
struct LabeledPoint {
var x = 0.0, y = 0.0
var label: String = "origin"
mutating func moveBy(x deltaX: Double, y deltaY: Double) { /* ... */ }
var magnitude: Double { /* ... */ }
}
// Call Swift from C++
#include <Geometry-Swift.h>
void test() {
Point origin = Point()
Point unit = Point::init(1.0, 1.0, "unit")
unit.moveBy(2, -2)
std::cout << unit.label << " moved to " << unit.magnitude() << std::endl
}
(29:51)
Key points:
- Swift can directly use C++ classes, methods, properties and STL containers
- C++ can call Swift structures and methods through the generated header file
- Supports mapping of value semantics and reference semantics
- Ideal for gradually migrating a C++ code base to Swift
Custom Actor Executor
// Default Actor: uses a system-managed serial queue
actor MyConnection {
private var database: UnsafeMutablePointer<sqlite3>
init(filename: String) throws { /* ... */ }
func pruneOldEntries() { /* ... */ }
}
// Custom Actor: bound to a specified DispatchQueue
actor MyConnection {
private var database: UnsafeMutablePointer<sqlite3>
private let queue: DispatchSerialQueue
nonisolated var unownedExecutor: UnownedSerialExecutor {
queue.asUnownedSerialExecutor()
}
init(filename: String, queue: DispatchSerialQueue) throws { /* ... */ }
func pruneOldEntries() { /* ... */ }
}
(35:30)
Key points:
nonisolated var unownedExecutorSpecify the executor used by the Actor -DispatchSerialQueueBuilt-in supportSerialExecutorAgreement- After customizing the executor, all methods of the Actor are executed on the specified queue
- Suitable for scenarios that need to be bound to specific threads/queues, such as SQLite, Core Data
Core Takeaways
1. Replace nested ternary operators in projects with if/switch expressions
Search multiple levels of nesting in the code base?:operator, especially for variable initialization scenarios. Swift 5.9’s if expressions make code intent clearer. The entrance is Xcode’s search function, search?and:nested mode.
2. Directly use @Observable instead of ObservableObject in new projects
The data model of the new project can be used directly@Observable class Model, no longer needed in the view@ObservedObject. Dependency tracking is more accurate and code is simpler. The entrance isimport Observation,delete@PublishedandObservableObjectprotocol.
3. Explore parameter packages to simplify your generic API
If you have any “overloaded for different number of arguments” API, refactor it into a function using parameter packs. User calling methods remain unchanged, and maintenance costs are significantly reduced. The entrance is learningeachandrepeatKeyword syntax.
4. Use macros to eliminate boilerplate code in projects
Examine the project for recurring code patterns, such as Equatable implementations, custom key mappings for Codable, and case detection properties for enumerations. These can all be automated using macros. The entry point is to create a Swift Package type macro project, refer to@CaseDetectionrealization.
5. Add ~Copyable to the underlying resource management type
If your project has structures that manage file descriptors, network handles, and GPU resources, consider marking them as~Copyableand addconsumingmethod. The compiler will catch resource usage errors at compile time. The entrance is added on the resource packaging type: ~Copyable。
Related Sessions
- Write Swift macros — A complete tutorial on writing Swift macros from scratch
- Expand on Swift macros — Advanced usage and debugging techniques of macros
- Beyond the basics of structured concurrency — Actors and concurrency advanced
- Use Swift with C++ — A detailed guide to Swift and C++ interoperability
Comments
GitHub Issues · utterances