Top Mistakes of Beginner Swift Developers: Advanced Level
When you transition from writing simple SwiftUI screens to creating complex multithreaded applications with custom architecture, the number of pitfalls increases dramatically. Many developers, having mastered the basic Swift syntax, consider themselves ready for production but make critical mistakes that lead to memory leaks, unstable performance, and hard-to-find bugs. In this article, we will analyze four categories of mistakes most often encountered by intermediate-level developers.
1. Memory Leaks in Closures: Not Just [weak self]
The most common mistake is using [weak self] without understanding when it is actually needed. Many developers put weak self everywhere there is a closure, which leads to premature deallocation of objects. Or, conversely, they forget about strong reference cycles in non-obvious places.
Typical mistake: creating a strong reference to an object inside a closure that the object itself holds.
class NetworkManager { var onCompletion: (() -> Void)? func fetchData() { // ERROR: closure captures self strongly onCompletion = { self.updateUI() } } func updateUI() { print("UI updated") }}Correct solution: use [weak self] or [unowned self] depending on the lifetime of the objects.
class NetworkManager { var onCompletion: (() -> Void)? func fetchData() { onCompletion = { [weak self] in guard let self = self else { return } self.updateUI() } } func updateUI() { print("UI updated") }}Advanced nuance: be careful with [unowned self]. If the object is deallocated and the closure is called, the app will crash. Use unowned only when you are 100% sure that the object will outlive the closure.
2. Incorrect Work with async/await and Actors
With the introduction of Structured Concurrency in Swift 5.5, many developers started actively using async/await but forget about MainActor and Sendable. This leads to race conditions and updating the UI from a background thread.
Typical mistake: calling an asynchronous function that should run on the main thread without explicitly specifying @MainActor.
class ViewModel: ObservableObject { @Published var data: [String] = [] // ERROR: function is not marked @MainActor func loadData() async { let result = await fetchFromNetwork() // This could execute on a background thread! self.data = result } func fetchFromNetwork() async -> [String] { try? await Task.sleep(nanoseconds: 1_000_000_000) return ["Item1", "Item2"] }}Correct solution: use @MainActor for functions that update the UI.
class ViewModel: ObservableObject { @Published var data: [String] = [] @MainActor // Now guaranteed to run on the main thread func loadData() async { let result = await fetchFromNetwork() self.data = result } func fetchFromNetwork() async -> [String] { try? await Task.sleep(nanoseconds: 1_000_000_000) return ["Item1", "Item2"] }}Advanced nuance: don't forget about nonisolated and actor. If you use actors, all public methods are asynchronous by default. This can lead to unexpected delays if you don't plan the isolation carefully.