Confusion around @MainActor isolation vs. ie. nonisolated View.scrollPosition()

Hi guys,

(first post here, also being far away from fully understanding Swift 6 strict concurrency, so forgive me if the answer might be too obvious.)

I have fatal bugs in Swift 6, somehow related to Bindings and – in my case – the scrollPosition modifier in SwiftUI. The problem appears only in conjunction with compiler optimization levels other than [-Onone].

For the sake of isolating the bugs further, I'd like to understand, why .scrollPosition is nonisolated while Views are isolated to the @MainActor. I suspect that something is going on in my code with incorrect context switching?

@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
nonisolated public func scrollPosition(id: Binding<(some Hashable)?>, anchor: UnitPoint? = nil) -> some View

Thanks a lot!

1 Like

This may be something we're investigating actually... I think there's some situations where the isolation checker's newly injected checks may trigger crashes which are technically correct, but may cause problems like that.

Would you mind sharing the exact crash backtrace, as well as code snippet at which use the issue appears? Thanks in advance,

I assume it only happens in Swift 6 language mode, right?

As a workaround you may need to refrain from using the Swift 6 language mode in this specific module if this is the isolation checker triggering, until the behavior is adjusted a bit.

1 Like

Wow, that was fast – thank you.

It does happen too, when I switch back to Swift 5, still compiling though in Xcode 16 beta, first build. I cannot compile my code in Xcode 15 anymore.

What I can say, is, it does happen with targeting both iOS 17.x or 18.0, and with all future Swift 6 features compiler flags on OR off.

I wouldn't mind to share a backtrace, but honestly, I´ll have to find out, what exactly that would be. Being a newbie when it comes to things that deep. I´ll look into it and try to isolate further, what´s going on.

By backtrace I mean when the app crashes (I'll assume you're using Xcode), on the right hand side there will be the Thread 1 for example, and the trace of the crash. You can select all those lines and "Copy" and paste them like this:

Thread 1#0	0x00000001a367a640 in _swift_runtime_on_report ()
#1	0x00000001a3756214 in _swift_stdlib_reportFatalErrorInFile ()
#2	0x00000001a32de0e8 in closure #1 in closure #1 in _assertionFailure(_:_:file:line:flags:) ()
#3	0x00000001a32dd400 in _assertionFailure(_:_:file:line:flags:) ()
#4	0x0000000100690c04 in closure #1 in ContentView.body.getter at /tmp/App/ContentView.swift:19
#5	0x000000022d104df0 in closure #1 in 
... < snip /> please paste the whole output though
#84	0x00000001006922fc in static App.$main() ()
#85	0x00000001006923cc in main at /tmp/App/App.swift:11
#86	0x000000019162f274 in start ()

So a trace like this would help us confirm what kind of crash you're seeing.

OK, I see, do you need to see the backtraces for all threads in parallel, or just the one (Main) which pops open, when the crash happens? Xcode, yes.

There will be one which is "crashed", so that one.

Ok, first attempt. However, all the annotations you have in your example are missing. Is that useful? I could drop one or two other situations, too.

0x19809a088 <+24>:  b.ne   0x19809a09c               ; <+44>
    0x19809a08c <+28>:  sub    x0, x19, #0x8
    0x19809a090 <+32>:  ldp    x29, x30, [sp, #0x10]
    0x19809a094 <+36>:  ldp    x20, x19, [sp], #0x20
    0x19809a098 <+40>:  retab  
    0x19809a09c <+44>:  tbz    x1, #0x3f, 0x19809a14c    ; <+220>
    0x19809a0a0 <+48>:  mov    x3, x2
    0x19809a0a4 <+52>:  mov    x4, #0x0                  ; =0 
    0x19809a0a8 <+56>:  lsl    x8, x1, #3
    0x19809a0ac <+60>:  add    x0, x8, #0x10
    0x19809a0b0 <+64>:  mov    x5, #0x0                  ; =0 
->  0x19809a0b4 <+68>:  casp   x4, x5, x4, x5, [x0]
    0x19809a0b8 <+72>:  cmp    w2, #0x1
    0x19809a0bc <+76>:  b.ne   0x19809a134               ; <+196>
    0x19809a0c0 <+80>:  mov    x7, x5
    0x19809a0c4 <+84>:  lsr    x9, x5, #32
    0x19809a0c8 <+88>:  lsl    x8, x3, #33
    0x19809a0cc <+92>:  adds   x10, x8, x4
    0x19809a0d0 <+96>:  b.mi   0x19809a118               ; <+168>
    0x19809a0d4 <+100>: mov    w11, w7
    0x19809a0d8 <+104>: mov    x5, x11
    0x19809a0dc <+108>: bfi    x5, x9, #32, #32
    0x19809a0e0 <+112>: mov    x6, x4
    0x19809a0e4 <+116>: mov    x7, x5
    0x19809a0e8 <+120>: casp   x6, x7, x10, x11, [x0]
    0x19809a0ec <+124>: eor    x9, x7, x5
    0x19809a0f0 <+128>: eor    x10, x6, x4
    0x19809a0f4 <+132>: orr    x9, x10, x9
    0x19809a0f8 <+136>: cbz    x9, 0x19809a08c           ; <+28>
    0x19809a0fc <+140>: lsr    x12, x7, #32
    0x19809a100 <+144>: mov    x4, x6
    0x19809a104 <+148>: mov    x9, x12
    0x19809a108 <+152>: adds   x10, x8, x6
    0x19809a10c <+156>: b.pl   0x19809a0d4               ; <+100>
    0x19809a110 <+160>: mov    x9, x12
    0x19809a114 <+164>: mov    x4, x6
    0x19809a118 <+168>: cmn    w4, #0x1
    0x19809a11c <+172>: b.eq   0x19809a08c               ; <+28>
    0x19809a120 <+176>: mov    w2, w7
    0x19809a124 <+180>: bfi    x2, x9, #32, #32
    0x19809a128 <+184>: mov    x1, x4
    0x19809a12c <+188>: bl     0x19809a150               ; swift::RefCounts<swift::SideTableRefCountBits>::incrementSlow(swift::SideTableRefCountBits, unsigned int)
    0x19809a130 <+192>: b      0x19809a08c               ; <+28>
    0x19809a134 <+196>: tbz    x4, #0x3f, 0x19809a0c0    ; <+80>
    0x19809a138 <+200>: and    x8, x4, #0xffffffff
    0x19809a13c <+204>: mov    w9, #-0x1                 ; =-1 
    0x19809a140 <+208>: cmp    x8, x9
    0x19809a144 <+212>: b.eq   0x19809a08c               ; <+28>
    0x19809a148 <+216>: b      0x19809a0c0               ; <+80>
    0x19809a14c <+220>: bl     0x19804680c               ; swift::swift_abortRetainOverflow()

There are these annotations(?), too, which I keep seeing in crashes somehow related to my problem(s).

#0	0x0000000197ea1da0 in _stringCompareFastUTF8Abnormal(_:_:expecting:) ()

and

...
    0x197ea1e04 <+176>: bl     0x197e31b9c               ; Swift._decodeScalar(_: Swift.UnsafeBufferPointer<Swift.UInt8>, startingAt: Swift.Int) -> (Swift.Unicode.Scalar, scalarLength: Swift.Int)
    0x197ea1e08 <+180>: mov    x25, x0
    0x197ea1e0c <+184>: mov    x28, x1
    0x197ea1e10 <+188>: mov    x0, x21
    0x197ea1e14 <+192>: mov    x2, x26
    0x197ea1e18 <+196>: bl     0x197e31b9c               ; Swift._decodeScalar(_: Swift.UnsafeBufferPointer<Swift.UInt8>, startingAt: Swift.Int) -> (Swift.Unicode.Scalar, scalarLength: Swift.Int)
    0x197ea1e1c <+200>: mov    x27, x0
    0x197ea1e20 <+204>: str    x1, [sp, #0x8]
    0x197ea1e24 <+208>: cmp    w25, #0x300
    0x197ea1e28 <+212>: b.lo   0x197ea1e3c               ; <+232>
    0x197ea1e2c <+216>: mov    x0, x25
    0x197ea1e30 <+220>: bl     0x1980fc058               ; _swift_stdlib_getNormData
...

That's "inside" of a frame, you need to look to the left. Like in this image:

But that already doesn't look like the issue I was thinking about actually, so even moreso I'd be interested in a backtrace for it.

Uh, got it. Shift-selecting the lines in the console gives me this:

Thread 1 Queue : com.apple.main-thread (serial)
#0	0x000000019809a0b4 in swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1>>::incrementSlow(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) ()
#1	0x000000019804ddd0 in swift::metadataimpl::ValueWitnesses<swift::metadataimpl::SwiftRetainableBox>::initializeWithCopy(swift::OpaqueValue*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*) ()
#2	0x000000019801f12c in initializeWithCopy for ClosedRange<>.Index ()
#3	0x00000001c201e814 in AG::LayoutDescriptor::Compare::Enum::Enum(AG::swift::metadata const*, AG::LayoutDescriptor::Compare::Enum::Mode, unsigned int, unsigned long, unsigned char const*, unsigned char const*, unsigned char*, unsigned char*, bool) ()
#4	0x00000001c201e0f4 in AG::LayoutDescriptor::Compare::operator()(unsigned char const*, unsigned char const*, unsigned char const*, unsigned long, unsigned int) ()
#5	0x00000001c201db20 in AG::LayoutDescriptor::compare(unsigned char const*, unsigned char const*, unsigned char const*, unsigned long, unsigned int) ()
#6	0x00000001c201dcb4 in AG::LayoutDescriptor::Compare::operator()(unsigned char const*, unsigned char const*, unsigned char const*, unsigned long, unsigned int) ()
#7	0x00000001c201db20 in AG::LayoutDescriptor::compare(unsigned char const*, unsigned char const*, unsigned char const*, unsigned long, unsigned int) ()
#8	0x000000019d1cbf68 in ___lldb_unnamed_symbol54378 ()
#9	0x0000000197e10450 in withUnsafePointer<τ_0_0, τ_0_1>(to:_:) ()
#10	0x000000019d1cbc60 in ___lldb_unnamed_symbol54360 ()
#11	0x000000019d1cbad8 in ___lldb_unnamed_symbol54349 ()
#12	0x0000000197e10450 in withUnsafePointer<τ_0_0, τ_0_1>(to:_:) ()
#13	0x000000019d51fee0 in ___lldb_unnamed_symbol68163 ()
#14	0x000000019d51fe10 in ___lldb_unnamed_symbol68162 ()
#15	0x000000019d51fb04 in ___lldb_unnamed_symbol68152 ()
#16	0x000000019d51f95c in ___lldb_unnamed_symbol68151 ()
#17	0x000000019d51f91c in ___lldb_unnamed_symbol68150 ()
#18	0x00000001c201e670 in AGDispatchEquatable ()
#19	0x00000001c201e21c in AG::LayoutDescriptor::Compare::operator()(unsigned char const*, unsigned char const*, unsigned char const*, unsigned long, unsigned int) ()
#20	0x00000001c201de64 in AG::LayoutDescriptor::Compare::operator()(unsigned char const*, unsigned char const*, unsigned char const*, unsigned long, unsigned int) ()
#21	0x00000001c201db20 in AG::LayoutDescriptor::compare(unsigned char const*, unsigned char const*, unsigned char const*, unsigned long, unsigned int) ()
#22	0x00000001c201d824 in AGGraphSetOutputValue ()
#23	0x000000019d322e90 in ___lldb_unnamed_symbol58617 ()
#24	0x000000019d322dd4 in ___lldb_unnamed_symbol58614 ()
#25	0x0000000197e10450 in withUnsafePointer<τ_0_0, τ_0_1>(to:_:) ()
#26	0x000000019d5ef1b8 in ___lldb_unnamed_symbol75966 ()
#27	0x000000019d3c5b30 in ___lldb_unnamed_symbol61998 ()
#28	0x00000001c201d010 in AG::Graph::UpdateStack::update() ()
#29	0x00000001c201cbfc in AG::Graph::update_attribute(AG::data::ptr<AG::Node>, unsigned int) ()
#30	0x00000001c201c7d8 in AG::Subgraph::update(unsigned int) ()
#31	0x000000019d42f068 in ___lldb_unnamed_symbol64017 ()
#32	0x000000019d43678c in ___lldb_unnamed_symbol64203 ()
#33	0x000000019d430430 in ___lldb_unnamed_symbol64126 ()
#34	0x000000019d42de60 in ___lldb_unnamed_symbol64003 ()
#35	0x000000019d42d830 in ___lldb_unnamed_symbol64001 ()
#36	0x000000019d42b9a4 in ___lldb_unnamed_symbol63978 ()
#37	0x000000019d42b6a0 in ___lldb_unnamed_symbol63970 ()
#38	0x000000019d431c40 in ___lldb_unnamed_symbol64177 ()
#39	0x000000019d42f4e8 in ___lldb_unnamed_symbol64114 ()
#40	0x000000019d32d13c in ___lldb_unnamed_symbol58723 ()
#41	0x000000019d32d048 in ___lldb_unnamed_symbol58721 ()
#42	0x000000019d32cffc in ___lldb_unnamed_symbol58720 ()
#43	0x000000019939d658 in __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ ()
#44	0x000000019939d414 in __CFRunLoopDoObservers ()
#45	0x00000001993cc54c in __CFRunLoopRun ()
#46	0x00000001993cbcd8 in CFRunLoopRunSpecific ()
#47	0x00000001de27c1a8 in GSEventRunModal ()
#48	0x000000019ba0490c in -[UIApplication _run] ()
#49	0x000000019bab89d0 in UIApplicationMain ()
#50	0x000000019d5bc148 in ___lldb_unnamed_symbol74307 ()
#51	0x000000019d568714 in ___lldb_unnamed_symbol71237 ()
#52	0x000000019d5744d0 in ___lldb_unnamed_symbol71675 ()
#53	0x0000000105118e84 in static TimerioApp.$main() [inlined] ()
#54	0x0000000105118df4 in main ()
#55	0x00000001bca7de4c in start ()

Is this more useful?

I want to add these infos:

  • I have multiple user actions in my current situation, which lead to the / a problem. Most of them vanish, when commenting out .scrollPosition (with a full Binding constructor get {} set {} inside) at a certain point in the view hierarchy.

  • However, I have removed others before by getting rid of one or the other Binding. Not sure if there is a real correlation, since I have worked against many, many hundreds of warnings and errors in the same work session.

  • It does not happen with a simulator run destination on iOS18. I don't have iOS18 on any of my hardware devices.

Hi,

thanks for your support yesterday.

Is my backtrace somehow useful and you intent to look into it at some point? Or does it rather seem like I am doing something wrong?

Does it look more like a Swift 6 (with iOS 17.x?) bug or my very own self-made bug?

Thanks again,
Timo

It does not seem like a Swift 6 crash in the sense I was looking for: I was looking for an isolation violation crash, which would not be a "bug" of Swift but a properly detected threading issue.

This looks like some potential threading issue or trying to refer to an already destroyed object -- which makes me think of threading issues.

It will be either a problem in your code, or in a framework, but from this it's hard to tell to be honest. You may want to file a feedback with a reproducer application and steps to reproduce. This way we'd have a better chance diagnosing in case it's a framework issue (the forums don't really deal with Apple SDK questions, which is why I'm suggesting to take the feedback route).

OK, makes perfect sense. Thanks a lot for your help. I´ll see what I can do.

Hi! If you end up filing the feedback, could you please share its number here? Thank you!

Hi Sima,

thanks for taking care. I wasn’t able to isolate the problem from my codebase yet. I could easily be the case that I do something wrong on my end.

Will try to dig deeper - and reply asap.

Thanks
Timo

I made a little progress at least.

Never filed a bug before – do you think this suitable for a report, even without a reproducible program?

What I can say is this summary:

  • Debugger symbol: stringCompareFastUTF8Abnormal(::expecting:)
  • Fatal bug on main thread when accessing values through a Binding, see below
  • appears only with compiler optimization levels other than [-Onone]
  • happens on device and simulator, tested with iOS17.0 - 17.5.1
  • does not happen under iOS18.0, tested only in Simulator
  • both Xcode 16 beta1 and beta2
  • Only since Swift6

I have identified a TextField accessing a simple property in a store object.
Also, I am accessing it within a ForEach loop, which does not have access to the Bindable since going through a hierarchy of views.

This does not work since Swift6:

TextField(text: Binding(get: { itemInLoop.title }, set: { itemInLoop.title = $0 }), axis: .horizontal)

While this does not crash:

TextField(text: .constant("Constant makes no sense"), axis: .horizontal)

My model - nonisolated is the only way I found to preserve @MainActor conformance:

@MainActor
@Observable final class CalendarSet: Identifiable, Hashable, Equatable, Sendable {
  ...
  nonisolated static func == (lhs: Root.T.Model.CalendarSet, rhs: Root.T.Model.CalendarSet) -> Bool {
              lhs.id == rhs.id
  }
          
  nonisolated func hash(into hasher: inout Hasher) {
              hasher.combine(id)
  }
  ...
  var title: String
}

This might be somehow related, although leading to another problem: Bad Access in this case. Still, its the same Binding constructor under Swift6:

ScrollView()
...
.scrollPosition(id: Binding(
  get: { storeNav.modelX },
  set: { newValue, transaction in
      guard let newValue else { return }
      guard newValue != storeNav.modelX else { return } ...
  })
)

...
enum ModelX: String, Hashable, Sendable {
    case one
    case two
}

Hi Sima, I have now submitted a bug: FB14092736, which is a reflection of post no. 16 in this thread. Hope it helps.

2 Likes

I'm experiencing the same crash with the same settings (TextField with Binding and crash in Swift._stringCompareFastUTF8Abnormal only in release mode). If I remove the TextField, the crash goes away. This is code that worked before and has not been touched in a while. I'm also using Xcode 16 Beta 2 and targeting iOS 17+

1 Like