After further investigation, I have confirmed the following (submitted as FB12348064):
The AppStorage property wrapper uses KVO to observe UserDefaults. This makes writing to UserDefaults unsafe on non-main threads, even if the AppStorage is not using the same key as is being written. Any write to UserDefaults causes a lookup of all observers. If the AppStorage is deallocated during that lookup, there will be a crash.
The following reliably crashes for me within a few hundred iterations, tested with both Xcode 14.3.1 and 15b1:
import SwiftUI
struct ContentView: View {
@State var n: Int = 0
// This song-and-dance is to make sure that AppStorageView is destroyed and recreated.
// The two views are visually identical to make the output easier to read. One has @AppStorage,
// the other does not. This causes very fast registering/deregistering from UserDefaults KVO.
func bodyView() -> AnyView {
if n % 2 == 0 {
return AnyView(
ForEach(0..<10) { _ in
AppStorageView(n: n)
})
} else {
return AnyView(
ForEach(0..<10) { _ in
NoAppStorageView(n: n)
})
}
}
var body: some View {
bodyView()
.task {
// Churn UserDefaults on a background thread.
Task.detached {
while true {
UserDefaults.standard.set(Date(), forKey: "randomOtherUserDefaultsKey")
await Task.yield()
}
}
// At the same time, churn the Views to create and destroy AppStorage observations.
while true {
n += 1
await Task.yield()
}
}
}
}
// View with @AppStorage observation
struct AppStorageView: View {
var n: Int
@AppStorage("appStorageValue") var appStorageValue = false
var body: some View { LogView(n: n) }
}
// View without @AppStorage observation
struct NoAppStorageView: View {
var n: Int
var body: some View { LogView(n: n) }
}
struct LogView: View {
var n: Int
var body: some View {
HStack {
Text("App Storage Test: \(n)")
Spacer()
}
}
}
Post
Replies
Boosts
Views
Activity
I've noticed that AppStorage uses KVO to observe NSUserDefaults. Is it possible that using AppStorage causes writing to NSUserDefaults to no longer be thread-safe, since SwiftUI will get notified on the wrong thread?
To automate @stragerneds's workaround, you can add the following script to the Pre-action for Profiling:
# Make sure to set the shell to zsh, not bash
#
# For Instruments, re-sign binary with get-task-allow entitlement
codesign -s - -v -f --entitlements =(echo -n '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"\>
<plist version="1.0">
<dict>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>') ${TARGET_BUILD_DIR}/${PRODUCT_NAME}
I've written up a more complete discussion at https://cocoaphony.micro.blog/2022/10/29/solving-required-kernel.html.
(Moving to a reply rather than a comment to be more easily found.)
I believe that interleaving is the key. I see the same symptoms on planar formats, but interleaved formats work, even for 2 channel audio. I expect this is an undocumented requirement (and an Apple bug). Tested on iOS 16.1.
I think I've finally figured out what's going on, but I'm at a loss for how I would be able to guess this subtlety from the code or the documentation.
I believe what is happening is that, as was noted in the session, quakes has a mutable getter ("changes to the request are committed whenever the results getter is called"). So if you wrote the obvious code:
// This is fine because quakes hasn't changed yet
quakes.sectionIdentifier = sortBy.section
// This would apply the `sectionIdentifier` change and trigger an unnecessary fetch (?)
quakes.sortDescriptors = sortBy.descriptors
So the recommended code avoids this by only touching the property wrapper once. Despite quakes and config both being structs, they have reference semantics. But despite both referencing the same "object," they have different access semantics due to the property wrapper.
This seems incredibly subtle, contrary to standard Swift value/reference semantics, and unmentioned in the documentation. Am I understanding it correctly? Is there any way I should be able guess this behavior?
xcodebuild determines this based on the -destination option. How do we apply this when a static library is not built with Xcode? I have a static library (in C) built for several architectures, but I haven't found any way to build an xcframework that associates the correct architecture with the simulator. When I try to construct an xcframework just by appending them all:
for lib in *.a; do xcodebuild -create-xcframework -library $lib -headers include -output PEQ.xcframework; done
the linking product that consumes them throws a warning:
ld: warning: ignoring file .../Release-iphonesimulator/PEQ-x8664.a, missing required architecture arm64 ...
And cannot find the symbols. I've also tried linking just the x8664 and arm64 versions in a single call:
xcodebuild -create-xcframework -library PEQ-arm64.a -headers include -library PEQ-x86_64.a -headers include -output PEQ.xcframework
(If I try to include the i386 or armv7 versions, it complains that they "represent two equivalent library definitions")
There is no xcodeproj for these static libraries. They are proprietary and do not even have source code for them. How do I associate the correct static library with each platform and simulator?
@dhoerl, this seems to be very similar to your issue (in that the libraries are built outside of Xcode), so did this solution work?
Note that I don't actually care about creating an xcframework. I don't need to distribute this framework. I'm just trying to link existing static libraries into our application, and have it work on device and simulator.
Remove AVAudioSessionCategoryOptionAllowBluetooth This option forces you into HFP mode, which has inferior audio. The …A2DP option is the only one you want for this.