SwiftUI/Combine notification overwrites my struct with incorrect values
Look in the documentation for the Published
property wrapper
There you will find the sentence:
When the property changes, publishing occurs in the property’s willSet
block, meaning subscribers receive the new value before it’s actually
set on the property
With the unspoken caveat that the value of the property will be set after the subscribers receive the new value.
So your $frame
publisher fires in the willSet
of the frame
property and it is passed the value that frame will be set to after publishing happens. Your subscriber changes the frame of the object, and prints that new value in the "Writing" string.
But once the publishing is done the system does the set
part of the property change and carefully overwrites the changes you made in your subscriber with the original value.
When you delay when your subscriber, then instead of having the subscription make the change immediately, it publishes a block on the main queue to be run later so you don't see the value change during the "current" event handling cycle that's changing the slider.
Found a strange behaviour of @State when combined to the new Navigation Stack - Is it a bug or am I doing it wrong?
Edited to add: smallest amount of code that will force the navigation stack to see views of the same type as brand new instead of caching them is to add .id(some Hashable)
to the view declaration.
In this case Fruit is hashable so we can just add .id(fruit)
NavigationStack(path: $fruitViewController.visibilityStack) {
...
.navigationDestination(for: Fruit.self) { fruit in FruitView(fruitViewController: fruitViewController, fruit: fruit).id(fruit)
}
}
I would say that Adkarma has a point. The view is not "deiniting" as one might expect because they've haven't simply loaded a new fruit. They've edited the actual navigation path.
The thing is the view's identity is being preserved because (simplification/educated guess warning) it is NavigationStack[0] as? FruitView
and NavigationStack[0] as FruitView
is still there. The NavigationStack
will not fully clean it up until the RootView has reappeared. NavigationStack
doesn't clean up any views until the new one has appeared.
So, if you change FruitViewController
to:
func openView(fruit: Fruit) {
visibilityStack.append(fruit)
}
The NavigationStack will work as expected and you will continue to add views to the stack - I've added a counter
and the visibilityStack
to the FruitView to make what's happening clearer as well as some messages to make it clearer when things get made and cleaned up.
struct ContentView: View {
@StateObject private var fruitController = FruitViewController()
var body: some View {
NavigationStack(path: $fruitController.visibilityStack) {
VStack {
...
}
.onAppear() {
print("RootView Appeared")
}
.navigationDestination(for: Fruit.self) {
fruit in FruitView(fruit: fruit).environmentObject(fruitController)
}
}
}
struct FruitView: View {
// the state that will change and not be initialised
@State private var showMoreText = false
@State private var counter = 0
@EnvironmentObject var fruitViewController: FruitViewController
var fruit: Fruit
var body: some View {
VStack {
Text("Selected fruit: " + fruit.id)
Text("How many updates: \(counter)")
if (showMoreText) {
Text("The text should disappear when moving to another fruit")
HStack(spacing: 10) {
Text("Who is in the fruitList")
ForEach(fruitViewController.fruitList) {
aFruit in Button(aFruit.id) {
counter += 1
fruitViewController.openView(fruit: aFruit)
}
}
}
HStack(spacing:10) {
Text("Who is in the visibility stack")
ForEach(fruitViewController.visibilityStack) {
aFruit in Button(aFruit.id) {
counter += 1
fruitViewController.openView(fruit: aFruit)
}
}
}
} else {
Button("Show other fruits", action: showButtons)
}
}.onDisappear() {
print("Fruit view \(fruit.id) is gone")
}.onAppear() {
print("Hi I'm \(fruit.id), I'm new here.")
//dump(env)
}
}
// let's change the state
func showButtons() {
showMoreText = true
}
}
But this doesn't seem to be the behavior Adkarma wants. They don't want to go deeper and deeper. They want a hard swap at the identical position? Correct? The NavigationStack
seems to try to increase efficiency by not destroying an existing view of the same type, which of course leaves the @State objects intact.
The navigation path is being driven by a binding to an Array
inside an ObservableObject
. The NavigationStack
continues to believe that it is at fruitViewController.visibilityStack[0]
... because it is. It doesn't seem to care about the content inside the wrapper beyond its Type.
The preserved FruitView will re-run the body code, but since it isn't a "new view" (It's still good old NavigationStack[0] as FruitView)it will not hard refresh the @State vars.
But "I zero'd that out!" you say, but NavigationStack
seems to have copy of it that DIDN'T, and it won't until it can make a new view appear, which it doesn't need to because there is in fact still something at NavigationStack[0] right away again.
I think Adkarma is spot on that this is related to deep linking and here are some articles I quite liked about that:
- https://www.pointfree.co/blog/posts/78-reverse-engineering-swiftui-s-navigationpath-codability
- https://swiftwithmajid.com/2022/06/21/mastering-navigationstack-in-swiftui-deep-linking/
Also interesting:
- https://swiftui-lab.com/swiftui-id/
For what it's worth, to "get it done" I would just reset the variables in View when there is a content change in the Nav array explicitly checking with an onReceive
, but I agree that seems a bit messy. I also would be interested if anyone has a more elegant solution to a hard swap of a navigation path that ends up at the same type of view at the same "distance" from the root like Adkarma's example.
struct FruitView: View {
// the state that will change and not be initialised
@State private var showMoreText = false
@State private var counter = 0
@EnvironmentObject var fruitViewController: FruitViewController
var fruit: Fruit
var body: some View {
VStack {
Text("Selected fruit: " + fruit.id)
Text("How many updates: \(counter)")
if (showMoreText) {
Text("The text should disappear when moving to another fruit")
HStack(spacing: 10) {
Text("Who is in the fruitList")
ForEach(fruitViewController.fruitList) {
aFruit in Button(aFruit.id) {
counter += 1
fruitViewController.openView(fruit: aFruit)
}
}
}
} else {
Button("Show other fruits", action: showButtons)
}
}.onReceive(fruitViewController.$visibilityStack) { value in
counter = 0
showMoreText = false
}
}
// let's change the state
func showButtons() {
showMoreText = true
}
}
FWIW a NavigationPath
will also be looking at position/type pair rather than complete content so you will still need to look at it with .onRecieve
or use an .id(some Hashable)
on the view calls.
Edited to add:
A strange thing the following function "works" (it send the user to the root view for a split second and then navigates back out), up until the time is decreased under the amount of time it takes the rootView to load.
func jumpView(fruit: Fruit) {
visibilityStack.removeAll()
Task { @MainActor in
//The time delay may be device/app specific.
try? await Task.sleep(nanoseconds: 500_000_000)
visibilityStack.append(fruit)
}
}
SwiftUI withAnimation causes unexpected behaviour
The origin of the problem is the fact that self.blocks.remove(at:)
returns a value that you are not handling. If you explicitly ignore that value, then your code works as expected whether or not the print
statement is there.
withAnimation {
_ = self.blocks.remove(at: self.blocks.firstIndex(of: b)!)
}
Swift has a feature that a closure with a single line of code returns the value of that statement as the return of the closure. So in this case, if you don't ignore the return value of remove(at:)
it returns a Block
and Block
then becomes the returned type of the withAnimation
closure.
withAnimation
is defined like this:
func withAnimation<Result>(_ animation: Animation? = .default, _ body: () throws -> Result) rethrows -> Result
It is a generic function that takes a closure. The return type of that closure determines the type of the generic placeholder which in turn determines the return type of withAnimation
itself.
So, if you don't ignore the return type of remove(at:)
, the withAnimation<Block>
function will return a Block
.
If you ignore the return value of remove(at:)
, the statement becomes one that has no return, (that is, it returns ()
or Void
). Thus, the withAnimation
function become withAnimation<Void>
and it returns Void
.
Now, because the closure to BlockRow
has only a single line of code when you delete the print statement, its return value is the return value of the single statement, which is now Void
:
BlockRow(block: block) { b in
withAnimation {
_ = self.blocks.remove(at: self.blocks.firstIndex(of: b)!)
}
}
and this matches the type that the closure to BlockRow
is expecting for its onDelete
closure.
In your original code, the print
statement caused the closure to BlockRow
to have 2 lines of code, thus avoiding Swift's feature of using the single line of code to determine the return type of the closure. You did get a warning that you weren't using the Block
that was being returned from the withAnimation<Block>
function. @Asperi's answer fixed that warning by assigning the Block
returned by withAnimation<Block>
to _
. This has the same effect as my suggested solution, but it is handling the problem one level higher instead of at the source of the problem.
Why doesn't the compiler complain that you are ignoring the return value of remove(at:)
?
remove(at:)
is explicitly designed to allow you to discard the returned result which is why you don't get a warning that it returns a value that you aren't handling.
@discardableResult mutating func remove(at i: Self.Index) -> Self.Element
But as you see, this lead to the confusing result you encountered. You were using remove(at:)
as if it returned Void
, but it in fact was returning the Block
that was removed from your array. This then lead to the whole chain of events that lead to your issue.
How do I execute a function on ClassB when something changes in ClassA?
You need a PassThroughSubject
to allow your views to be triggered to do something when the @Published
values change. Then, on your view, you use an .onReceive
to trigger your functions.
import SwiftUI
import Combine
struct ContentView: View {
@EnvironmentObject var game: Game
@EnvironmentObject var scores: ScoresStore
func saveScore() {
scores.addScore(newScore: game.score)
}
var body: some View {
Button(action: { saveScore }) {
Text("New Game?")
}
.onReceive(game.objectDidChange, perform: { _ in
//Do something here
})
.onReceive(scores.objectDidChange, perform: { _ in
//Do something here
})
}
}
final class Game: NSObject, ObservableObject, Codable {
let objectDidChange = PassthroughSubject<Void, Never>()
var deck: [Card] = []
@Published var piles: [[Card]] = [[],[],[],[]] {
didSet {
self.objectDidChange.send()
}
}
var score: Int {...}
}
final class ScoresStore: NSObject, Codable, ObservableObject, Identifiable {
let objectDidChange = PassthroughSubject<Void, Never>()
@Published var highScores: [Score] = [] {
didSet {
self.objectDidChange.send()
}
}
func addScore(newScore: Int, date: Date = Date()) {
// Do things to add the score to the array
}
}
Every time Game.piles
or ScoreStore.highScores
updates, you will get a notice to the .onReceive
in your view, so you can deal with it. I suspect you will want to keep track of the game score this way as well, and trigger on that change. You can then test for whatever you want and implement whatever action you desire. You will also note that I did this on a didSet
. You will also see this as an objectWillSet
placed into a willSet
on the variable. Both work, but the willSet
sends a trigger before the actual update, whereas didSet
sends it after. It may not make a difference, but I have had this as an issue in the past. Lastly, I imported Combine
at the top, but I did these in one file. Wherever you define the passThroughSubject
you will need to import Combine
. The view does not actually need to import it. Good luck.
Communication between ViewModels with SwiftUI and Combine (ObservableObject vs Binding)
I would like to suggest a few improvements to your architecture.
DISCLAIMER:
Note that the following implementation is a suggestion how to approach the Master-Detail problem. There are countless more approaches and this one is just one of severals which I would suggest.
When things become more complex, you probably would prefer a unidirectional data flow approach between your view model and view. This basically means, no two way bindings to the view state.
Unidirectional means, that your SwiftUI views deal basically with constant external state which they render without asking. Instead of mutating the backing variable from a two way Binding directly, views send actions (aka events) to the view model. The view model processes theses events and sends out a new view state taking the whole logic into account.
By the way, this unidirectional data flow is inherent to the MVVM pattern. So, when you use a View Model, you should not use two way bindings that mutate the "View State". Otherwise this would not be MVVM and using the term View Model would be incorrect or at least confusing.
The consequence is, that your views will not perform any logic, all logic is delegated to the view model.
In your Master - Detail problem, this also means, that NavigationLinks will not be directly performed by the Master View. Instead, the fact that a user has tapped the NavigationLink will be send to the view model as an action. The view model then decides, whether to show a detail view, or not, or demands to show an alert, a modal sheet, or what ever it deems necessary what the view has to render.
Likewise, if the user taps the "back" button, the view will not be immediately popped from the Navigation stack. Instead, the view model receives an action. Again, it decides what to do.
This approach lets you intercept the data flow at strategically important "locations" and let you handle the situation more easily and in a correct way.
In a Master-Detail problem, especially in your example where architectural decisions are yet to be made, there is always the question who (which component) is responsible to create a Detail View Model (if required) and which part composes the Detail View and the Detail View Model and dynamically puts this into the view system (somehow) and removes it again when done (if required).
If we make the proposition, that a View Model should create a Detail View Model, which IMHO is reasonable, and if we further assume, that a user can issue an action that eventually ends up showing a detail view and with the suggestions made before, a possible solution in SwiftUI may look as follows:
(Note, I will not use your example, but create a new one with more generic names. So, hopefully, you can see where your example maps into my example)
So, we need these parts
- a master view
- a master view model,
- a detail view
- a detail view model
- possibly additional views for decomposing several aspects and separation of concerns
The master view:
struct MasterView: View {
let items: [MasterViewModel.Item]
let selection: MasterViewModel.Selection?
let selectDetail: (_ id: MasterViewModel.Item.ID) -> Void
let unselectDetail: () -> Void
...
The master view uses a "state" which is comprised of the items it should draw in a list view. Additionally, it has two action functions selectDetail
and unselectDetail
. I am pretty sure it is clear, what these mean, but we will see later how they get used by the master view.
Additionally, we have a Selection
property, which is an optional, and you might guess what it means: when it is not nil, it will render the detail view. If it is nil, it does not render a detail view. Pretty easy. Again, hold on where we see how it is used and what it is precisely.
When we look at the body of the master view, we implement the NavigationLink in a special form, so that we fulfil our unidirectional data flow requirement:
var body: some View {
List {
ForEach(items, id: \.id) { element in
NavigationLink(
tag: element.id,
selection: link()) {
if let selection = self.selection {
DetailContainerView(
viewModel: selection.viewModel)
}
} label: {
Text("\(element.name)")
}
}
}
}
The NavigationLink uses the "selectable destination" form, whose signature is
init<V>(tag: V, selection: Binding<V?>, destination: () -> Destination, label: () -> Label)
This creates a navigation link that presents the destination view when a bound selection variable equals a given tag value.
See docs here.
The tag
is the unique id of the item (here element.id
).
The selection
parameter, which is a Binding<Item.ID?>
is the result of the function link()
which will be shown below:
func link() -> Binding<MasterViewModel.Item.ID?> {
Binding {
self.selection?.id
} set: { id in
print("link: \(String(describing: id))")
if let id = id {
selectDetail(id)
} else {
unselectDetail()
}
}
}
As you can see, link
returns the proper binding. However, one crucial fact you can see here is, that we do not use "two way bindings". Instead, we route the actions that would mutate the backing variable of the binding to action functions. These actions will eventually be performed by the view model, which we will see later.
Please note the two action functions:
selectDetail(:)
and
unselectDetail()
The getter of the binding works as usual: it just returns the id
of the item.
This above, and the implementation of the two actions are enough to make push and pop from the Navigation stack work.
Need to edit the items, or pass some data from the Detail View to the Master View?
Just use this:
unselectDetail(mutatedItem: Item)
and an internal @Sate var item: Items
in the Detail View plus logic in the Detail View Controller, or let the master and detail view model communicate to each other (see below).
With these parts, the Master View is complete.
But what is this Selection
thingy?
This value will be created by the Master View Model. It is defined as follows:
struct Selection: Identifiable {
var id: Item.ID
var viewModel: DetailViewModel
}
So, pretty easy. What's important to note is, that there is a Detail View Model. And since the Master View Model creates this "selection", it also has to create the detail view model - as our proposition has stated above.
Here, we make the assumption, that a view model has enough information at hand at the right time to create a fully configured detail (or child) view model.
The Master View Model
This view model has a few responsibilities. I will show the code, which should be pretty self-explanatory:
final class MasterViewModel: ObservableObject {
struct ViewState {
var items: [Item] = []
var selection: Selection? = nil
}
struct Item: Identifiable {
var id: Int
var name: String
}
struct Selection: Identifiable {
var id: Item.ID
var viewModel: DetailViewModel
}
@Published private(set) var viewState: ViewState
init(items: [Item]) {
self.viewState = .init(items: items, selection: nil)
}
func selectDetail(id: Item.ID) {
guard let item = viewState.items.first(where: { id == $0.id } ) else {
return
}
let detailViewModel = DetailViewModel(
item: .init(id: item.id,
name: item.name,
description: "description of \(item.name)",
image: URL(string: "a")!)
)
self.viewState.selection = Selection(
id: item.id,
viewModel: detailViewModel)
}
func unselectDetail() {
self.viewState.selection = nil
}
}
So, basically, it has a ViewState
, which is precisely the "single source of truth" from the perspective of the view which has to just render this thing, without asking any questions.
This view state also contains the "Selection" value. Honestly, we may debate whether this is part of the view state or not, but I made it short, and put it into the view state, and thus, the view model only publishes one value, the View State. This makes this implementation more suitable to be refactored into a generic ..., but I don't want to distress.
Of course, the view model implements the effects of the action functions
selectDetail(:)
and unselect()
.
It also has to create the Detail View Model. In this example, it just fakes it.
There's not much else to do for a Master View Model.
Detail View
The detail view is just for demonstration and as short as possible:
struct DetailView: View {
let item: DetailViewModel.Item
var body: some View {
HStack {
Text("\(item.id)")
Text("\(item.name)")
Text("\(item.description)")
}
}
}
You may notice, that it uses a constant view state (let item
).
In your example, you may want to have actions, like "save" or something that is performed by the user.
Detail View Model
Also, pretty simple. Here, in your problem, you may want to put more logic in which handles the user's actions.
final class DetailViewModel: ObservableObject {
struct Item: Identifiable {
var id: Int
var name: String
var description: String
var image: URL
}
struct ViewState {
var item: Item
}
@Published private(set) var viewState: ViewState
init(item: Item) {
self.viewState = .init(item: item)
}
}
Caution: over simplification!
Here, in this example, the two view models don't communicate with each other. In a more practical solution, you may have more complicated things to solve, which involve communication between these view models. You would likely not implement this directly in the View Models, rather implement "Stores" which have Inputs, State, and possibly Outputs, perform their logic using finite state machines, and which can be interconnected so you have a system of "States" which eventually comprise your "AppState" which publishes its state to the view models, which in turn transform this to the view state for their views.
Wiring up
Here, some helper views come into play. They just help to wire up the view models with the views:
struct DetailContainerView: View {
@ObservedObject private(set) var viewModel: DetailViewModel
var body: some View {
DetailView(item: viewModel.viewState.item)
}
}
This sets up the view state, BUT ALSO separates the Detail View from the Detail View Model, since the view does not need to know anything about a view model. This makes it more easy to reuse the DetailView as a component.
struct MasterContainerView: View {
@ObservedObject private(set) var viewModel: MasterViewModel
var body: some View {
MasterView(
items: viewModel.viewState.items,
selection: viewModel.viewState.selection,
selectDetail: viewModel.selectDetail(id:),
unselectDetail: viewModel.unselectDetail)
}
}
Same here, decouple the MasterView from the MasterViewModel and setup actions and view state.
For your playgrounds:
struct ContentView: View {
@StateObject var viewModel = MasterViewModel(items: [
.init(id: 1, name: "John"),
.init(id: 2, name: "Bob"),
.init(id: 3, name: "Mary"),
])
var body: some View {
NavigationView {
MasterContainerView(viewModel: viewModel)
}
.navigationViewStyle(.stack)
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(ContentView())
Have fun! ;)
SwiftUI: ViewModifier doesn't listen to onReceive events
ok, this works for me:
func body(content: Content) -> some View {
content
.onAppear() // <--- this makes it work
.onReceive(viewModel.$myProperty) { theValue in
print("-----> The Value is \(theValue)") // <--- this will be executed
}
}
Pass @Published where @Binding is required (SwiftUI, Combine)
If I understand your question correctly, you are probably looking for something like that:
final class ViewModel: ObservableObject {
@Published var sourceProperty: String = ""
private lazy var logic = Logic(data: $sourceProperty)
private var cancellable: AnyCancellable?
init() {
cancellable = logic.$output
.sink { result in
print("got result: \(result)")
}
}
}
final class Logic: ObservableObject {
@Published private(set) var output: String = ""
init(data: Published<String>.Publisher) {
data
.map { $0 + "ABCDE" }
.assign(to: &$output)
}
}
SwiftUI ChildView Not recalculated on state change
This was a silly mistake on my part. The problem was actually immature implementation of Equatable which is actually no longer needed as of Swift 4.1+ (same for Hashable). I originally added it to ensure I could compare Cards (needed for index(of:) method), however, since I was only checking comparing id's, it was messing with SwiftUI's internal comparison algorithm for redrawing. SwiftUI too was using my Equatable implementation and thinking oh the Card actually has not changed!
Related Topics
Swiftui List .Ondelete: Index Out of Range
How to Reinterpret_Cast in Swift
<= Is Not a Prefix Unary Operator
Realitykit Entity Synchronization Is Always Nil
How to Use Mtlblitcommandencoder for Copying Interlaced Video Fields into a Mtlbuffer
Programmatically Setting Texture in Scene Generated by Reality Composer
Skipnext Skipprevious Google Cast Greyed Out
Generating a Simple Algebraic Expression in Swift
Didbegincontact Not Being Called Swift
Must Call a Designated Initializer of The Superclass 'Day' Error
How to Collect The Return Value of a Function (Swift 3)
Allow Siri Remote Menu Button When Play/Pause Button Is Overridden
Create Objects/Instances in Variables Swift
Playing Multiple Wav Out Multiple Channels Avaudioengine