I'm experiencing a crash on an iOS app that I can't find a reason to exist at all.
I'm using SwiftUI and have an @Observable object, inside this object I have a property with a UIScrollView, this scroll view I show in the app using UIViewRepresentable.
My use case requires me to use a UIKit UIScrollView, and not a SwiftUI ScrollView.
I understand some people may have arguments against these approaches that I'm taking, and I appreciate them, but they are important for my use case.
I don't understand why crashing on exactly the first tap, doesn't matter where I touch, as long as I touch on the UIScrollView.
I don't have any Gesture.
Adding @ObservationIgnored to my property containing the UIScrollView doesn't fix the issue.
UIView using UIViewRepresentable16.3@ObservableSwift 6.1This is the stack trace
*** Assertion failure in -[UIGestureGraphEdge initWithLabel:sourceNode:targetNode:directed:], UIGestureGraphEdge.m:28
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: targetNode'
*** First throw call stack:
(
0 CoreFoundation 0x00000001804b910c __exceptionPreprocess + 172
1 libobjc.A.dylib 0x0000000180092da8 objc_exception_throw + 72
2 Foundation 0x0000000180e67c70 _userInfoForFileAndLine + 0
3 UIKitCore 0x0000000185646e98 -[UIGestureGraphEdge initWithLabel:sourceNode:targetNode:directed:] + 316
4 UIKitCore 0x0000000185643fb4 -[UIGestureGraph addUniqueEdgeWithLabel:sourceNode:targetNode:directed:properties:] + 328
5 UIKitCore 0x0000000185655a48 _UIGestureEnvironmentUpdate + 952
6 UIKitCore 0x00000001856553ac -[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 324
7 UIKitCore 0x00000001856550f8 -[UIGestureEnvironment _updateForEvent:window:] + 156
8 UIKitCore 0x0000000185b6be10 -[UIWindow sendEvent:] + 2824
9 UIKitCore 0x0000000185b4b80c -[UIApplication sendEvent:] + 376
10 UIKitCore 0x0000000185bd5c70 __dispatchPreprocessedEventFromEventQueue + 1156
11 UIKitCore 0x0000000185bd8c00 __processEventQueue + 5592
12 UIKitCore 0x0000000185bd0f10 updateCycleEntry + 156
13 UIKitCore 0x00000001850a5cec _UIUpdateSequenceRun + 76
14 UIKitCore 0x0000000185a60858 schedulerStepScheduledMainSection + 168
15 UIKitCore 0x0000000185a5fc90 runloopSourceCallback + 80
16 CoreFoundation 0x000000018041d294 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
17 CoreFoundation 0x000000018041d1dc __CFRunLoopDoSource0 + 172
18 CoreFoundation 0x000000018041c940 __CFRunLoopDoSources0 + 232
19 CoreFoundation 0x0000000180416e84 __CFRunLoopRun + 788
20 CoreFoundation 0x00000001804166f4 CFRunLoopRunSpecific + 552
21 GraphicsServices 0x00000001905e5b10 GSEventRunModal + 160
22 UIKitCore 0x0000000185b319dc -[UIApplication _run] + 796
23 UIKitCore 0x0000000185b35bd4 UIApplicationMain + 124
24 SwiftUI 0x00000001d34b9040 $s7SwiftUI17KitRendererCommon33_ACC2C5639A7D76F611E170E831FCA491LLys5NeverOyXlXpFAESpySpys4Int8VGSgGXEfU_ + 164
25 SwiftUI 0x00000001d34b8d68 $s7SwiftUI6runAppys5NeverOxAA0D0RzlF + 84
26 SwiftUI 0x00000001d323d01c $s7SwiftUI3AppPAAE4mainyyFZ + 148
27 Web.debug.dylib 0x0000000101f04830 $s3Web3iOSV5$mainyyFZ + 40
28 Web.debug.dylib 0x0000000101f058e0 __debug_main_executable_dylib_entry_point + 12
29 dyld 0x0000000100f69410 start_sim + 20
30 ??? 0x00000001010aa274 0x0 + 4312441460
)
This crash log is completely useless and I can't find any other detail of why the crash is happening and how to solve it.
Took me 3 days full time to pinpoint out the root cause to the combination of UIScrollView with Observable and SwiftUI (there is no issue with plain UIViews).
I wanted to share it in here so that next time it happens to me or to anyone else is less painful to fix.
My @Observable object is instantiated on my @main App struct , and the UIScrollView is instantiated in the init of the @Observable. Meaning the UIScrollView is created exactly at app launch.
The UIScrollView is created before the first Window is ready (or at the same time, but here is a race condition).
The scroll view itself must have some intrinsic gestures, and the low level underlying workings of gestures require the Window to detect touch events, since the Window is not ready while creating the UIScrollView, those gestures behave unexpectedly, hence the targetNode and the UIGestureGraphEdge in the stack trace.
Move initialisation of the UIScrollView to a later time.
This is a tiny simplified example of how I was creating my @Observable
Note: As mentioned before, I have a property in my
@Observablethat contains aUIScrollView, this is displayed usingUIViewRepresentable, and while I understand some people may have strong feelings against this approach I would like to focus the discussion on trying to solve the crash, instead of the architecture of my app.
@Observable
class ViewModel {
var myScroll = UIScrollView()
}
@main
struct MyApp: App {
@State private var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
VStack {
/**
My view hierarchy displaying
`viewModel.myScroll`
using a `UIViewRepresentable`
*/
}
}
}
}
Initialise the UIScrollView at a later stage
@Observable
class ViewModel {
var myScroll: UIScrollView?
}
@main
struct MyApp: App {
@State private var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
VStack {
/**
My view hierarchy displaying
`viewModel.myScroll`
using a `UIViewRepresentable`
*/
}
.onAppear {
/**
View hierarchy is ready, including `Window`.
This is just for illustration purposes,
you may find a better place and time to `init`
your `UIScrollView`.
*/
guard viewModel.myScroll == nil else { return }
viewModel.myScroll = UIScrollView()
}
}
}
}
Combining SwiftUI and UIKit is not perfect, so there are lots of caveats along the way, and of course debugging becomes a nightmare.
I understand the architecture of my app is not conventional, and I don't expect a lot of folks experiencing this issue, but in case you do I hope this helps you find a solution.
My example uses a UIScrollView, but the same issue applies to any subclass of it, including UITableView, UICollectionView and WKWebView, etc.