Introduction: Why Performance Matters from Day One
When you start writing in Swift, it's easy to focus only on functionality: "as long as it works." However, experienced developers know: performance optimization is not a final touch, but a foundation that is laid from the first lines of code. A slow application loses users, drains the battery, and gets bad reviews.
In this article, we will break down the key aspects of optimizing Swift applications for iOS and macOS. You will learn how to avoid typical beginner mistakes, learn to work with memory, threads, and profiling. Each section contains practical code examples that you can immediately apply in your projects.
1. Memory Management: ARC and Avoiding Retain Cycles
Swift uses Automatic Reference Counting (ARC) for memory management. This means objects are released when there are no more strong references to them. But if you create a cyclic reference (retain cycle), memory will never be freed — a leak will occur.
Weak and Unowned References
Use weak and unowned to break cycles. This is most often needed in closures and delegates.
class ViewController: UIViewController { var networkManager: NetworkManager? func fetchData() { networkManager?.fetch { [weak self] result in guard let self = self else { return } // Handle the result self.updateUI(with: result) } }}When to use:
weak— if the reference can becomenil(e.g., delegate).unowned— if you are sure the object will outlive the closure (e.g., parent controller).
Avoid Unnecessary Copies
Value types (structs, enums) are copied upon assignment. For large data, use class or pass by reference using inout.
// Bad: copying a large arrayfunc processLargeArray(_ array: [Int]) { // array — a copy}
// Good: passing by referencefunc processLargeArray(_ array: inout [Int]) { // array — a reference to the original}2. Working with Multithreading: GCD and async/await
iOS and macOS are multithreaded systems. If you perform heavy operations (data loading, image processing) on the main thread, the interface "freezes." Use Grand Central Dispatch (GCD) or the modern async/await.
GCD: Basic Patterns
DispatchQueue.global(qos: .userInitiated).async { // Heavy work in the background let result = performHeavyCalculation() DispatchQueue.main.async { // Update UI on the main thread self.label.text = "\\(result)" }}QoS (Quality of Service):
.userInteractive— for UI (almost instant)..userInitiated— for user actions (seconds)..utility— for long-running tasks (progress bars)..background— for background synchronization (not urgent).
Async/await (Swift 5.5+)
A more readable way to work with asynchrony:
func fetchImage(from url: URL) async throws -> UIImage { let (data, _) = try await URLSession.shared.data(from: url) guard let image = UIImage(data: data) else { throw URLError(.badServerResponse) } return image}
// Call in TaskTask { do { let image = try await fetchImage(from: url) self.imageView.image = image } catch { print("Download error: \\(er