SwiftUI bugs and defects
February 01, 2022When working on a SwiftUI app you probably experienced double-edge sword effect: while tremendously pleasant to use SwiftUI especially with teammates you also have to face unexpected/uncomprehensible/weird bugs or behaviours.
That's exactly what happened to me. Here is a list of some of the most unexpected bugs I faced while developing in SwiftUI.
This article report bugs present in iOS 14+.
StateObject not deallocated
When supporting iOS 14+ you start using a lot of @StateObject
here and there. It's a very useful property wrapper because the object lifecycle is linked to the view.
Nonetheless you start seeing strange behaviours (including crashes) in your app. After investigating you discover that your @StateObject
is actually... not deallocated when the view is destroyed 😱
Maybe you did something wrong? Well maybe not! If you use a NavigationView
you actually need to add an extra line in order for you @StateObject
to be released upon view destruction: .navigationViewStyle(StackNavigationViewStyle())
.
Your StateObject will now get deallocated at the same time than your views. Does it make sense not having this behaviour by default? No. Welcome in SwiftUI (weirdnesses).
StateObject reallocated when going background/foreground
This is probably one of the strangest bug I've ever seen in SwiftUI: everytime you go background/foreground your @StateObject
is recreated making you lose data.
If you still use .navigationBarItems
(deprecated in iOS 14) then look no further: replacing it with .toolbar
fix the issue!
/// before: StateObject reallocated every time the scenePhase become active
.navigationBarItems(trailing: saveButton)
/// after: StateObject is allocated once
.toolbar { saveButton }
View not dismissed
You just fixed previous bug just to find out you get another one... When trying to dismiss a view it does not work anymore! 😤
/// the view is presented but does not dismiss using `dismissButton`
.toolbar {
createUserButton
.sheet($createUser) {
CreateUserScreen()
.toolbar { dismissButton }
}
}
Well seem sheet
is not playing well inside toolbar
. You need to put it outside in order for it to work again:
/// the view is presented but does not dismiss using `dismissButton`
.toolbar {
createUserButton
}
.sheet($createUser) {
CreateUserScreen()
.toolbar { dismissButton }
}
Property wrapper didSet called multiple times
If you use didSet
on property wrappers beware that on iOS 15 didSet
is called twice with a TextField
binding. You'll have to add extra check to avoid it.
As a general note be careful when using didSet on a property wrapper: on regular attributes didSet
is not reentrant but on property wrapper it is!
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// this won't cause currentLevel.didSet to be called again
currentLevel = AudioChannel.thresholdLevel
}
}
}
@Published var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// currentLevel is a PropertyWrapper: currentLevel.didSet will be called again
currentLevel = AudioChannel.thresholdLevel
}
}
}
I consider it as a bug and hope seeing it fixed in a upper release. If you think the same then please open a bug report to apple.