I have a hypothesis why it's so. If my understanding is correct, the behavior is a natural result of how @Binding works.
The official document doesn't describe how @Binding works. So I used to think it contains a reference pointing to a remote data backend. I didn't expect the binding wrapper variable itself changed. But when I used Self._printChanges() to debug view invalidation, I found the binding wrapper variable often changed. That puzzled me a lot in the past.
Now I have a completely different explanation on how it works. Let's put side how @Binding updates the remote data backend, we'll just focus on how it invalidates view. If my (new) understanding is correct, there is really no magic here, it invalidates view just because the caller of the view creates a new binding value when recreating the view. The explanation fits well with my observation that binding variable often changes.
Honestly speaking, I can hardly imagine it works like this. I don't understand why Apple doesn't add this to the doc. In my opinion, this is a very important piece of information that influences how people design SwiftUI apps. For one thing, this explains why @EnvironmentObject and @Binding have very different behavior in view invalidation and why they don't work well together.
Let's go back to my original question. In my original explanation, I thought binding wrapper variable contained a reference pointing to a data backend (BTW, I still think this part is true), so I expected it should return new value when data model changes. In my new explanation, what happens seems more complex. I can't really give a description of the details because I don't know. But the result is, when the view is invalidated by the change in @EnvironmentObject, due to the way how @Binding works, the mechanism to update binding wrapper variable, as well as its wrapped variable, isn't started yet. That's the reason we still read an old value in binding.
Does it have to be so? Well, I doubt it. In my opinion, view validation and view redraw (I mean, calling view's body) should be different phases. For example, it could be possible to just set a dirty flag to invalidate view and only recall view body after all data are synced.
The takeaway (note they are just my understanding):
@EnvironmentObject and @ObservableObject invalidate views through objectWillChange publisher. Invalidating view is implemented by recalling view's body. Since it's impossible to control the order of which receiver receives the data from a publisher, the order of which view's body getting called is arbitrary.
On the other hand, @Binding doesn't initiate view invalidation directly. It's a result of its caller's body call, plus the fact that binding wrapper variable changes (perhaps as a indicator of the backend data change). So it has ordering - it can only happens when its parent view (or ancestor view) is invalidated due to @EnvironmentObject, @ObservableObject, or @State change.
Anyway, with this plausible explanation I can continue to write my SwiftUI app (otherwise it would be like move in the dark).
Post
Replies
Boosts
Views
Activity
Never mind. See here.
While my above hypothesis might be right, there is another more important issue in my original question and code. The issue is I should avoid accessing data model in view rendering code. To put it in another way, pass value, instead of id. I knew this is a common practice in SwiftUI, but I had some difficulty in understanding it. It finally clicked when I read Paulw11's answer on SO.
I said it should avoid accessing data model in view rendering code in above answer. It's not correct. See my newest answer here.
The key point: SwiftUI is complex. It's unreliable to write code by assuming when/how view body gets recalled. As a result the data model API should be lenient. My original assumption that it's OK to use force unwrapping doesn't work. That's the reason why I observed various crashes but others don't.
I ran into the issue today and spent all night searching for workarounds but none worked. Then I find iOS 15.4 beta 3 and Xcode 13.3 beta 2 was released earlier this month. I'm going to upgrade my system tomorrow (it's late night here). Does anyone happen to know if the issue has been resolved on the latest release of iOS? Thanks.
I ran into the same issue after upgrading to Xcode 14.0 beta 5. After spending one day in clueless experiments, I finally figured out what the root cause is and how to fix it. I think the error message isn't clear. At first sight, it seems to suggest that it's not allowed to change another published property (for example, in sink()) when one published property changes. That's not true (otherwise it would be a big limitation and defeat the purpose of the data model). From my experiments what it means is a scenario like the following:
Views like Sheet(), Picker(), etc. take a binding and change the binding implicitly.
You pass a view model's published property to those views as the binding parameter.
In your view model's code, when that published property changes, other published properties are changed accordingly.
This is the scenario not allowed in beta 5.
So, how to solve the issue? My solution is to introduce a local state to avoid simultaneous changes and use onChange() to sync the local state change with view model. This requires a little bit more code but the error message is gone.
Below is a simple example to demonstrate the issue:
import SwiftUI
final class ViewModel: ObservableObject {
static let shared: ViewModel = .init()
@Published var isPresented: Bool = false
}
struct ContentView: View {
@StateObject var vm: ViewModel = .shared
var body: some View {
VStack(spacing: 8) {
Button("Show Sheet") {
vm.isPresented.toggle()
}
}
.sheet(isPresented: $vm.isPresented) {
SheetView(vm: .shared)
}
}
}
struct SheetView: View {
@ObservedObject var vm: ViewModel
var body: some View {
NavigationStack {
Button("Change Values in VM") {
// Calling dismiss() have the same issue
vm.isPresented = false
}
.navigationTitle("Sheet View")
}
}
}
Below is an example demonstrating how to fix the above issue:
import SwiftUI
final class ViewModel: ObservableObject {
static let shared: ViewModel = .init()
@Published var isPresented: Bool = false
}
struct ContentView: View {
@StateObject var vm: ViewModel = .shared
@State var isPresented: Bool = false
var body: some View {
VStack(spacing: 8) {
Button("Show Sheet") {
isPresented.toggle()
}
}
.sheet(isPresented: $isPresented) {
SheetView(vm: .shared)
}
.onChange(of: isPresented) { isPresented in
vm.isPresented = isPresented
}
}
}
struct SheetView: View {
@Environment(\.dismiss) var dismiss
@ObservedObject var vm: ViewModel
var body: some View {
NavigationStack {
Button("Change Values in VM") {
dismiss()
}
.navigationTitle("Sheet View")
}
}
}
I think this is quite a big change in SwiftUI and I can't believe it shows up as late as in beta 5. My guess is that it has been in SwiftUI 4 since the first day but the error message was added in beta 5. Again I really don't understand why SwiftUI team can't publish an architecture paper for each release to cover important designs like this but just keep the developers outside Apple trying and guessing.
For those who are still struggling with the issue, this is probably the simplest workaround (in both concept and implementation).
func delayedWrite<Value>(_ value: Binding<Value?>) -> Binding<Value?> {
return Binding() {
value.wrappedValue
} set: { newValue in
DispatchQueue.main.async {
value.wrappedValue = newValue
}
}
}
Then just wrap the binding which causes the issue with the above function. That's it.
It's so simple that I don't mind any more if Apple would fix the issue or not. Happy hacking :)
You are right to worry about this. It is not difficult for users to access the app's data files.
Thanks for the confirmation. With access to app data files, network sniffing, and process tracing (I'm not familiar with macOS, but I suppose there must be tools of this kind), it seems running an iOS app on Apple Silicon makes it way more easier for people to hack the app. I don't understand why Apple supports it.
They can access them on iOS too, though it takes a bit more effort.
Could you elaborate it a bit? I think that's only possible on a jailbreaked phone, isn't it? But my impression is that it becomes very hard to jailbreak the recent IOS releases, so I take it for granted that app data files can't be accessed by others in iOS.
Another question. If what you said is true, is it a common practice to encrypt app data files? (the encryption key can be hardcoded in the app's code and that should thwart most attempts).
Same issue here. It's a EXC_BAD_ACCESS error which doesn't make sense (I'm very sure because the same code worked fine in Xcode 14.0 in the past several months and I never observed the crash). BTW, the crash occurred only in simulators (it's not random and can be reproduced consistently) but not on my phone. It seems to be an issue with the iOS image in simulators, not the compiler.
I developed my app in Xcode 14.0 and installed Xcode 14.2 just to prepare for submitting the app. I'm downloading Xcode 14.1 and hope it will work fine.
(deleted)
More information. I find the following on Xcode 14.2 download page (emphasis mine):
It includes SDKs for iOS 16.2, iPadOS 16.2, tvOS 16.1, watchOS 9.1, and macOS Ventura 13.1.
(see: https://developer.apple.com/download/applications/. The same information can be found in its release notes.)
So Xcode 14.2 doesn't support macOS 13.2?! I don't understand it. How come Apple releases a new OS version without a new Xcode version working on it?
Now that I have upgraded my OS to 13.2, what options do I have? Thanks for any help.
Hi, Claude31, thanks for the help.
This is the error message (or do you mean a complete crash log as described in this page?)
Thread 7: EXC_BAD_ACCESS (code=2, address=0x70000be7cfb0)
And this is the stack trace of a test I written to reproduce the crash. The test contains no UI code (I ran into the issue in my app at first). The test calls API of a library in my app, so the stack trace is perhaps meanlingless to you. More on this below.
I have been thinking about the issue today and now I suspect it might be a stack overflow issue, mainly because I can't think of any other reason why it crashed (more on this below). It may also explains why the issue doesn't occur on my phone (for example, could it be that the cooperative thread on phone has larger stack size than that on macOS? BTW, my laptop has Intel CPU, not sure it makes a difference)
Below are some details which may help to understand the background of the issue:
The code that crashed are quite simple. My app has a demo mode. When user activates the demo mode, the app runs an async func to generate demo data, and pass the generated data back to the main thread. The crash occurred in the async func to generate the demo data.
While the internal of the code to generate the data are complex, it's quite simple from architecture perspective because a) it doesn't interact with any other part of the code in the app, and b) it uses only structs internally and return value of a struct. So there isn't a obvious reason why it could crash due to memeory error.
I was able to reproduce the issue in a non-ui test (see stack trace above). Unfortunately I wasn't able to reproduce the issue without using my library. For example, creating a large (e.g, size of 8M) array or dictionary in an async func doens't crash. I think it's because array and dictionary have very small footprint in stack (they are mainly in heap to support COW behavior). That said, I use only value types (structs, array, dictonary, enum) in my app, so I have no idea why I could have stack overflow issue.
If I change the async func to a plain old sync func and call it in main thread, the code worked fine. That's why I think it's might be a stack size issue, because main thread has larger size than worker thread. That said, I have developed the app for more than one year and did a lot of testing, including provisioning a large set of data in main thread. I never saw the crash issue in main thread (as I explained above, I suppose it's because arrays and dictonary has small footprint in stacks). So, while worker thread has smaller stack size, I didn't expect this issue.
Also, as I decribed in previous posts, I run into the issue after I upgraded my OS and Xcode. It worked fine on macOS 13.0 + Xcode 14.0. Could it be some thing changed on macOS that caused the crash (I don't mean it's a bug)?
Now I'm trying to figure out how to verify this is really a stack overflow issue and identify which function the overflow occurs. The internals of the demo data generation file is complex: it generates raw data, then parse the data to generate derived data and repeat (there are recursion invoved, thought I don't think recusion is the reason caused the stack overflow, I suspect it's the size of the data instead). The crash log seems helpless. The size(ofValue:) doesn't help either, because as I said above most of the storage in array and dictionary are in heap. Do you have any suggestions? I'm also thinking to ask in Swift forum what's the general approach to identify stackoverflow issue. Thanks.
UPDATE: while I'm not sure, I'm investigating if it's a stack overflow issue now (I didn't realize it at first because it used to work fine). See my reply to Claude31 below. I apology for the misleading title (unfortunately I can't change it).
It's a stack overflow caused by a known issue with enum with large payload size. See discussion here.
The upgrade was a red herring (sorry for the false alarm).
when you go into settings on an iPhone (is that one screen?) then when you click (for example) notifications (is that another screen) then say you go deeper in notifications to another sub heading “scheduled summary” (is that another screen)
I don't know if "screen" is a formal or popular terminology, but I think your intuition is correct. An analogy: a screen is to an app as a web page is to a web site.
this may be a stupid question but i found it hard online to find the meaning of screens in apps and also think surely this can’t be included as screens because there is so many??
Whether it's fair to pay a developer based on the screens in the app depends on the details of the app. It can also be based on features or the development time needed (I'm not in this business so I don't know which is more popular). Either way you'll need some technical background to evaluate it. I'd suggest you ask people around you if you're not sure.
once the app is up and running, is it easy to add more information regularly myself or would I need someone with coding experience? (this would just be some words, a photo and maybe a table)
You need to add this as a key requirement to your app, because it affects how the app is designed (it sounds like you'll need a server also).
BTW, software development isn't one-off thing. It needs maintenance and future enhancement. You need to take it into account when making decisions.