Skip to content

StateObject anti-pattern info#21

Merged
EngOmarElsayed merged 5 commits intoAvdLee:mainfrom
lanserxt:main
Feb 18, 2026
Merged

StateObject anti-pattern info#21
EngOmarElsayed merged 5 commits intoAvdLee:mainfrom
lanserxt:main

Conversation

@lanserxt
Copy link
Contributor

@lanserxt lanserxt commented Feb 8, 2026

From discussion in previous PR: #7

Need to highlight in Skill that using such init, in general, is not very good pattern and breaks the logical scope.

CC @malhal

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d2e2975eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@EngOmarElsayed
Copy link
Collaborator

It's better to say use such a thing with Parent View (Main View screen) because in such case it is valid useCase and will not break ownership lifeCycle. But don't say it is anti pattern because one of the core Philosophy of this skill is that we don't to force any specific pattern or way of writing code.

@malhal
Copy link

malhal commented Feb 9, 2026

It’s not valid, see the discussion.

@EngOmarElsayed
Copy link
Collaborator

It’s not valid, see the discussion.

I saw the discussion and the thing is: when we init the @StateObject in the init of the View using StateObject(initial: _), all what we are doing is the following setting the default value of the State doing so doesn't break lifecycle of the State at all.

What makes it a problem is when you expect the State to update when you change the value given to the view, but in case of you know that this is the parent view (Main View of the screen) and it takes a parameter as an input (not a state) and you use it in the init it's safe and valid approach, putting a side any architecture approach.

See reference (StateObject is the same as State different is StateObject is for objects):
image
image
image

@malhal
Copy link

malhal commented Feb 9, 2026

I'm not sure where those pages are from but those pages and your explanation are the exact anti-pattern we need the skill to avoid. SwiftUI is the architecture and the architecture requires lifting the state into parent View and pass Binding down for write access (or let if its read only).

Explained more in the discussion here:
#7 (comment)

e.g.

struct CounterView: View {
    @Binding private var value: Int
  
    var body: some View {
         Button(" Increment: \(value)") {
            value += 1
        }
    }
}

struct SomeParent: View {
    @State var counter = Counter()

    var body: some View {
        CounterView(value: $counter.value)
    }
}

struct Counter {
    var value = 0
    // other vars or would be no point in having a struct to relate them

    struct mutating func reset() { // the people that use the anti-pattern are usually not aware of mutating func
        value = 0
    }
}

@EngOmarElsayed
Copy link
Collaborator

When you add a parameter (initial value ) for a view this doesn't mean we are removing the state from the parent view, all what it does is giving initial value of the state the same as the initial vale we put to the state after =.

So Actually doing so has zero effect and it's a valid SwiftUI code that doesn't break the framework, maybe it's not the best architecture discussion and we don't include or force any architecture preferences in this skill.

And by the way this book is created by objc.io specifically by chris eidhof. He is one of the top people in SwiftUI check him out.

@malhal
Copy link

malhal commented Feb 10, 2026

Passing an initial value to a child's @State creates a 'Split Source of Truth.' Once the child View is initialized, the parent can no longer update that value. The child 'owns' its own copy now, and the two will drift apart, creating a real mess to get back in sync.

Could you show the specific use case you're working on? It's better to update the parent’s state before the child view appears, rather than trying to sync two separate states. And that can actually be tricky for someone new to SwiftUI, it might need the use of a struct or a computed Binding.

@lanserxt
Copy link
Contributor Author

I've made a snippet to play with based on ObjC SwiftUI example:

import SwiftUI

struct Counter: View {
    
    init(value: Int = 0) {
        _value = State(wrappedValue: value)
    }
    
    @State private var value: Int
    
    var body: some View {
        Button("Increment: \(value)") {
            value += 1
        }
    }
    
}

struct StateBindView: View {
    
    @State private var value: Int = 0
    
    var body: some View {
        Divider()
        Counter(value: value)
        Divider()
        Counter(value: value)
            .id(value)
        Divider()
        Counter(value: 5)
        Divider()
        Button("Increment: \(value)") {
            value += 1
        }
    }
}

#Preview {
    StateBindView()
}

As we can see, it decouples from rendering tree if we will use such init. Last paragraph says: it's not very useful.

My suggestion: Let's remove "anti-pattern sentence" suggested by Codex.

@malhal
Copy link

malhal commented Feb 11, 2026

edited

Here it is without anti-patterns:

struct Counter: View {

    @Binding var value: Int
    
    var body: some View {
        Button("Increment: \(value)") {
            value += 1
        }
    }
}

struct StateBindView: View {
    
    @State private var value1: Int = 0
    @State private var value2: Int = 0
    @State private var value3: Int = 5
    
    var body: some View {
        Divider()
        Counter(value: $value1)
        Divider()
        Counter(value: $value2)
            //.id(value) another anti-pattern
        Divider()
        Counter(value: $value3)
        Divider()
        Button("Increment: \(value1)") {
            value1 += 1
            value2 = value1
        }
    }
}

@EngOmarElsayed
Copy link
Collaborator

Passing an initial value to a child's @State creates a 'Split Source of Truth.' Once the child View is initialized, the parent can no longer update that value. The child 'owns' its own copy now, and the two will drift apart, creating a real mess to get back in sync.

Could you show the specific use case you're working on? It's better to update the parent’s state before the child view appears, rather than trying to sync two separate states. And that can actually be tricky for someone new to SwiftUI, it might need the use of a struct or a computed Binding.

I am not talking about passing initial value to a child's @State in this case it's not good but I am talking about parent view in case of parent view this is a valid case and doesn't break anything and doesn't 'Split Source of Truth' because you are just giving initial value for the parent view.

This is why it's valid to add it to this skill with that context in the skill.

@EngOmarElsayed
Copy link
Collaborator

EngOmarElsayed commented Feb 11, 2026

The goal of the example of the book is to show what the effect of doing such a thing to a child view but if the view is parent view it's valid to use it. I truly agree with all what you say, it's all right but for parent views it's a valid thing to do again butting aside any preferences.

@AvdLee
Copy link
Owner

AvdLee commented Feb 16, 2026

@EngOmarElsayed reading your comments it sounds like you generally agree that it's better to avoid using StateObject(initial: _), but it's not necessary an anti-pattern as you see valid usecases for it.

If so, can we change the contribution to cover the exact use cases where it's allowed, and make the Skill understand that it's better in general to not use it?

Co-authored-by: Antoine van der Lee <4329185+AvdLee@users.noreply.github.com>
@EngOmarElsayed
Copy link
Collaborator

Thanks @lanserxt ♥️

@EngOmarElsayed EngOmarElsayed merged commit ade2813 into AvdLee:main Feb 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants