Swift/UnwrapDiscover about Swift, iOS and architecture patterns

SwiftUI bugs and defects

February 01, 2022

When 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.