Crash when using @Shared appStorage during certain conditions
Description
I'm seeing a crash with a similar backtrace to https://github.com/pointfreeco/swift-composable-architecture/issues/3247, but appears not to be caused by a thread issue.
I'm using a @Shared appStorage key roughly like:
@Shared(.appStorage("key")) var isEnabled: Bool = true
When launching the app it crashes. I'm still working on a simplified repro but having a hard time isolating the exact code causing the problem, but thought I'd post the issue in case anyone else has an idea.
If you look at the backtrace, it appears an NSAttributedString is performing layout (which isn't in my code) which triggers an NSUserDefault update which in turn causes the @Shared storage to update immediately during the same render cycle?The only relevant logs related to the crash are:
precondition failure: setting value during update: 219864
Checklist
- [ ] I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
- [ ] If possible, I've reproduced the issue using the
mainbranch of this package. - [X] This issue hasn't been addressed in an existing GitHub issue or discussion.
Expected behavior
No response
Actual behavior
No response
Steps to reproduce
No response
The Composable Architecture version information
1.13.1
Destination operating system
iOS 17.5
Xcode version information
Xcode 15.4
Swift Compiler version information
No response
Hi @zachwaugh, thanks for the report, but unfortunately without a repro it's hard to diagnose. The precondition failure message ("precondition failure: setting value during update") seems to clearly describe the problem: in the act of executing a view's body a value is being mutated. If you do this with @State, SwiftUI will even produce a purple runtime warning letting you know this isn't a good thing to do.
Only one idea comes to mind. In the stack trace we have mainActorNow to execute some work on the main thread as soon as possible, i.e. synchronously if already on the main thread, otherwise DispatchQueue.main.async. If instead we always incurred a thread hop, then that would guarantee the state does not change while in a view body.
Are you in a position to reproduce this precondition easily in your local environment? Would you be willing to try replacing mainActorNow with DispatchQueue.main.async and see if that fixes things?
Getting a similarily suspicious crash on writing to FileStorageKey backed Shared.
The line open #1 <A><A1, B1>(_:) in Shared.currentValue.getter seems to be same as in the crash above.
We started getting this crash after updating to 1.14.0 from 1.12.1.
#0 (null) in std::__1::__function::__func<_gatherGenericParameters(swift::TargetContextDescriptor<swift::InProcess> const*, __swift::__runtime::llvm::ArrayRef<swift::MetadataOrPack>, swift::TargetMetadata<swift... ()
#1 (null) in swift_getTypeByMangledNameImpl(swift::MetadataRequest, __swift::__runtime::llvm::StringRef, void const* const*, std::__1::function<void const* (unsigned int, unsigned int)>, std::__1::function<swif... ()
#2 (null) in swift_getTypeByMangledName ()
#3 (null) in swift::_checkGenericRequirements(__swift::__runtime::llvm::ArrayRef<swift::TargetGenericRequirementDescriptor<swift::InProcess>>, __swift::__runtime::llvm::SmallVectorImpl<void const*>&, std::__1::... ()
#4 (null) in _gatherGenericParameters(swift::TargetContextDescriptor<swift::InProcess> const*, __swift::__runtime::llvm::ArrayRef<swift::MetadataOrPack>, swift::TargetMetadata<swift::InProcess> const*, __swift:... ()
#5 (null) in (anonymous namespace)::DecodedMetadataBuilder::createBoundGenericType(swift::TargetContextDescriptor<swift::InProcess> const*, __swift::__runtime::llvm::ArrayRef<swift::MetadataOrPack>, swift::Meta... ()
#6 (null) in swift::Demangle::__runtime::TypeDecoder<(anonymous namespace)::DecodedMetadataBuilder>::decodeMangledType(swift::Demangle::__runtime::Node*, unsigned int, bool) ()
#7 (null) in swift_getTypeByMangledNodeImpl(swift::MetadataRequest, swift::Demangle::__runtime::Demangler&, swift::Demangle::__runtime::Node*, void const* const*, std::__1::function<void const* (unsigned int, u... ()
#8 (null) in swift_getTypeByMangledNode ()
#9 (null) in swift_getTypeByMangledNameImpl(swift::MetadataRequest, __swift::__runtime::llvm::StringRef, void const* const*, std::__1::function<void const* (unsigned int, unsigned int)>, std::__1::function<swif... ()
#10 (null) in swift_getTypeByMangledName ()
#11 (null) in swift_getTypeByMangledNameInEnvironment ()
#12 (null) in _resolveKeyPathGenericArgReference(_:genericEnvironment:arguments:) ()
#13 (null) in specialized _walkKeyPathPattern<A>(_:walker:) ()
#14 (null) in _getKeyPathClassAndInstanceSizeFromPattern(_:_:) ()
#15 (null) in _swift_getKeyPath(pattern:arguments:) ()
#16 0x0000000104a61e34 in ValueReference.value.getter at ComposableArchitecture/SharedState/References/ValueReference.swift:355
#17 0x0000000104a66da0 in open #1 <A><A1, B1>(_:) in Shared.currentValue.getter at ComposableArchitecture/SharedState/Shared.swift:260
#18 (null) in Shared._wrappedValue.getter ()
Hi @andreyz, your crash does not seem to be related to the one discussed here. The one in this issue has stack frames with AG::Graph, which is from AttributeGraph that powers SwiftUI. Your crash is just pure Swift code, and has something to do with opening existentials and/or mangling type names.
And as always, a reproduction is the best way to get help on this.
@zachwaugh Can you give #3437 a try and see if it addresses things for you?
Same issue when using @Shared(.appStorage) in multiple features, which causes a crash at startup.
I tried the shared-async-subscription branch, and the problem no longer appears on my end.
@stephencelis I've removed use of @Shared from my app after hitting a few crashes, so I don't have it in there at the moment to test. But I'll go back to an earlier commit when I have some time and see if it's resolved. Thanks for looking into it!
@zachwaugh Curious if you ever found the repro? We'd love to address this bug but want to test the currently open PR as well as an alternative solution.
@stephencelis sorry for the delay, I finally had a chance to test this. I just added back in the @Share(.appStorage) usage and confirmed it still crashes in Xcode 16.1/iOS 18.1 for TCA as of 1.15.2. Updated to the shared-async-subscription branch and it does not crash, at least on launch. I haven't had a chance to do in-depth testing, but I think that branch does fix the crash I reported.
@zachwaugh Thanks! I think #3487 has a better fix if you have a chance to test it. We've had @tgrapperon confirm it fixes a crash in his application, and I believe it was the same root cause. We'll be merging and releasing it soon, but if for whatever reason it doesn't fix it as well for you as the other PR we'll use that solution instead.
Updated to 1.16.0 and confirmed the other method did fix the crash as well once I discovered the note about not using . in keys, which I was doing. Once I removed that, it's all good. That's a subtle thing that would be good to call out in the docs somewhere.