Even in SwiftUI dinosaur from the past can help
Leon Dobnik
Mobile software architect (iOS) | Corporate mobility | Corporate iOS secure apps | Banking | Team leadership
Since the release of iOS17 I see more and more companies migrate to the SwiftUI or at least starting to learn it. In short the SwiftUI is the new way of building UI, but behind the scenes UIKit is still with us (not just in our hearts). For example the .sheet modifier in SwiftUI behind the scenes opens a new UIViewController and displays the sheet.
If you ever had a requirement in UIKIt to dismiss one UIViewController and present another one you saw the following message Warning: Attempt to present < finishViewController: 0x1e56e0a0 > on < ViewController: 0x1ec3e000> whose view is not in the window hierarchy!. The are many reasons for this issue, but often the first action (animation) of dismiss wasn't completed before another one was started. Now let's go to the SwiftUI...
Imagine that you UI can present two bottom sheets and they're defined the following way:
@State private var showSheet1 = true
@State private var showSheet2 = false
...
.sheet(isPresented: $showSheet1) {
Sheet1View(viewModel: viewModel)
.dynamicPresentationDetent()
.interactiveDismissDisabled()
}
.sheet(isPresented: $showSheet2) {
Sheet2View()
.dynamicPresentationDetent()
.interactiveDismissDisabled()
}
Now imagine the following state: a view is presenting sheet1 and sheet2 must be presented. So the following code must be executed:
func switchToSheet2() {
showSheet1 = false
showSheet2 = true
}
It some cases it may happen that sheet2 won't be displayed or UI will stuck at sheet1. If I am not mistaken one of possible reasons is explained above. Now there are really multiple ways how you can address this issue - use onDismiss closure of the .sheet modifier, hack with DispatchQueue.main.async, modify the way UI works, use timer, ... whatever. But let's say that onDismiss is not an option, doesn't help (for example handling sheets from VM, complicates logic, etc...). What can help to solve you the problem? The answer is CATransaction. This dinosaur is with us since iOS 2.0
领英推荐
The CATransaction class has helped me numerous times in UIKit when some animation, transition was executing and something else in UI was triggered too fast. So what we can do? First let's write an extension to CATransaction and bring its interface to modern world:
extension CATransaction {
static func execute(transactionBlock: () -> Void, completionBlock: (() -> Void)?) {
CATransaction.begin()
CATransaction.setCompletionBlock(completionBlock)
transactionBlock()
CATransaction.commit()
}
}
Now by having this in place we can write the following in the SwiftUI view:
func switchToSheet2() {
CATransaction.execute(
transactionBlock: { showSheet1 = false },
completionBlock: { showSheet2 = true })
}
Now the sheet1 should dismiss and sheet2 appear without a problem. Remember once again: if possible try to use SwiftUI constructs to handle things (Apple can change the underlying mechanisms how things work - but if nothing else works, you can give it a try.
There is a very good blog that can explain a lot what is happening behind the scenes with SwiftUI and CATransaction: https://rensbr.eu/blog/swiftui-render-loop/ .