I'm so confused: why @State var inside a View do not change when you call a method on this view from some outer View?

I hope my sample code is self explanatory:

import SwiftUI

struct ConfuseMe: View {
    
    struct Foo: View {
        // how can I change flag from outside of this view?
        @State private var flag = false
        
        // calling this doesn't change flag, why?
        func doFoo() {
            self.flag = true
            print("flag = \(self.flag) <==== why not true now?!...")
        }
        
        var body: some View {
            VStack {
                Text("😕")
                    .font(.system(size: 100))
                Text(verbatim: "self.flag = \(self.flag)")
                Button("Only here work?") {
                    self.flag.toggle()      // can only change flag from here...
                }
            }
        }
    }
    
    let foo = Foo()
    
    var body: some View {
        VStack {
            foo
            Spacer()
            Button("Hmmmm...") {
                self.foo.doFoo()    // What's going on? @State flag inside foo never change!
            }
        }
    }
}

struct ConfuseMe_Previews: PreviewProvider {
    static var previews: some View {
        ConfuseMe()
    }
}

So how to accomplish what I want? Seems there is something confusing me going on with how @State var behave...

1 Like

That‘s not how State is meant to be used. State is meant to be a private storage to a view and you should only give some subview control of it through a Binding. You should not try to expose State to any superview.

One more hint, all views are fully immutable. We, the framework users, have no ability to mutate a view. If a superview happen to call its body, the chain will recreate your view, and if it‘s different from before then this is the only mutation you can have at max. Any other mutation is handled through State, Binding, ObservedObject and EnvironmentObject.

That said, your foo value is not really a reference to the subview. body only provides a description to the framework on how you would like to build your UI. The framework will interpret the given description and try its best to do the right thing.

Also the name View is a little misleading, as any conforming type is not automatically a view that gets rendered. Such conforming type doesn‘t even have to render at all. A View is more like a Protocol which describes some behavior or a recipe for a potential view.

2 Likes

As @DevAndArtist said, ConfuseMe uses Foo, so any state of Foo should be invisible to ConfuseMe. It's better to have flag be variable of ConfuseMe, and provide it to Foo via binding.

Further, I wouldn't recommend you to save View into variable and reuse it multiple times. SwiftUI seems to rely on the creation/access order to figure out the hierarchy/dependency of Views. Saving View and reusing it can mess with that. Usually you can refactor the structure to avoid that (In this case move flag to the superview).

Safest to create the necessary View inside body call.

2 Likes

To add on that, it would be fine if instead of a foo constant you had a foo computed property which is get-only. This also highlights that it‘s not really a reference to a subview like in UIKit.

These may sound contradictory at first glance, but they aren't.

When you use foo inside body call, it'll compute value on the fly, essentially creating and returning a new view while you're still in body.

I just need to trigger some animation inside Foo() when the "Hmmm..." button is pressed in ConfuseMe(), and I need Foo() to animate back to its original state...:

struct Foo: View {
...
    func doFoo() {
        // start animation somehow...
        // then...
        // some later, animate back to the original state (I was using asynAfter(), but whatever)...
        // giving it's not possible to change @State here
        // what can I do to achieve my animation objective?
    }

....

    var body: some View {
     ....
    }
}

I can have @State in ConfuseMe, pass the binding into Foo, but how can I make Foo observe this @State change? Or I should use something else?

I'm not sure I understand it correctly. In your example you're using doFoo in Button's action, so you're safely outside of read-state and can change @State. You can use withAnimation while you're at it.

Also, you can use @Binding:

struct Foo: View {
  @Binding var flag: Bool

  func doFoo() {
    withAnimation { // You want animation?
      flag = true
    }
  }

  ...
}

struct ConfuseMe: View {
  @State var flag = false

  var body: some View {
    ...
    Foo(flag: $flag)
    ...
  }
}

Ah....I see now, I can use the @Binding var in doFoo()...I understand. I think it'll work for my need. Thanks!

You can also mutate @State in doFoo (since, as I said, it's outside of read-only state), if you end up using @State for something else.

I don't like having the parent view passing in a binding to a @State var it owns, just so to kick start something to happen inside the child view. It seems wrong to me. So here is what I end up:

import SwiftUI

struct BarView: View {
    
    struct Foo: View {
        class Notifier: ObservableObject {
            @Published var counter = 0
        }
        @ObservedObject private var notifier = Notifier()
        @State private var animationFlag = false
        static private let animationDuration = 0.5
        
        
        // trigger some action when this is called
        func doFoo() {
            notifier.counter = 1
        }
        
        var body: some View {
            VStack {
                ZStack {
                   Text(verbatim: "🤔").font(.system(size: 150))
                        .scaleEffect(animationFlag ? 1.2 : 1)
                        .animation(Animation.linear(duration: Self.animationDuration))
                }
                .frame(width: 200, height: 200)
                Text(verbatim: "self.flag = \(self.animationFlag)")
            }
            .onReceive(notifier.objectWillChange) { _ in
                self.animationFlag = true
                DispatchQueue.main.asyncAfter(deadline: .now() + Self.animationDuration) {
                    self.animationFlag = false
                }
            }
        }
    }
    
    let foo = Foo()    // seems okay to do this way?
    
    var body: some View {
        VStack {
            foo
            Spacer()
            Button("Action!...") {
                self.foo.doFoo()
            }
        }
    }
}

struct ConfuseMe_Previews: PreviewProvider {
    static var previews: some View {
        BarView()
    }
}

So it's back to how I wanted originally. But instead use an @ObservedObject to indirectly cause the internal @State var change.

I wonder, the way I originally try to change the @State var, which is wrong, why SwiftUI does not catch it and print some error message...

Except @Binding is exactly how you're supposed to communicate state changes between views that care about the same value. All you're doing here is manually recreating the same sort of process, but worse.

1 Like

The parent view can tell Foo view to animate, that's all. The parent should not maintain any state for this. Whatever the state is internal to Foo and should not be exposed. Also, I can have any number of these child Foo views, I don't want the parent to have to add a @State and pass in a @Binding with each Foo child view.

My implementation may not be the best and I like to know if there is better way. But its api is what I think is right. For my need here, I don't think passing in a binding is correct. No?

I thought I can avoid having to use the @ObservedObject by doing this:

        // trigger some action when this is called
        func doFoo() {
            DispatchQueue.main.async {
                self.animationFlag = true
                DispatchQueue.main.asyncAfter(deadline: .now() + Self.animationDuration) {
                    self.animationFlag = false
                }
            }
        }

but it doesn't work. I don't understand why not!?

You shouldn’t manually setup animation endpoints like that. Usually withAnimation will suffice.

withAnimation

May I ask how apply withAnimation to my sample code? It's doing exactly what I want, but not in the right way?

I looked through the code. There are a few things to note, so I'm gonna go through each of them here.


self.animationFlag = true
DispatchQueue.main.asyncAfter(deadline: .now() + Self.animationDuration) {
  self.animationFlag = false
}

Avoid doing this at all cost. It may look like it's working, but there are multiple problems that come with it.

  • If the animation is cancelled, the latter part is not cancelled with it. Each part of the animation needs to be managed separately, preventing smooth redirection between different animations.
  • If the user repeatedly trigger the animation, it'd easily lead to scenario where you need to be aware of the asynchrony. You need to handle the call properly whether the sequence is TFTFTF, TTTFFF or some other sequence.

Just the latter reason is already enough to avoid doing it in SwiftUI that boast the elimination of such synchronisation problem.

Instead, combine all the animation fragments into one big animation. In this case, you can do:

struct FooView: View {
  static var animationDuration = 1.0
  @State var animationFlag = false

  func doFoo() {
    animationFlag.toggle()
  }

  var body: some View {
    ...
    Text(verbatim: "🤔").font(.system(size: 150))
      .modifier(Effect(animationFlag))
      .animation(Animation.linear(duration: Self.animationDuration))
    ...
  }
}

private struct Effect: GeometryEffect {
  init(_ value: Bool) {
    animatableData = value ? 0 : 1
  }

  var animatableData: CGFloat

  func effectValue(size: CGSize) -> ProjectionTransform {
    let maxScale: CGFloat = 4, minScale: CGFloat = 1
    let scale = maxScale - (maxScale - minScale) * 2 * abs(0.5 - animatableData)
    let transform = CGAffineTransform.identity
      .translatedBy(x: -size.width * (scale - 1) / 2, y: -size.height * (scale - 1) / 2)
      .scaledBy(x: scale, y: scale)
    return ProjectionTransform(transform)
  }
}

When you doFoo, you ends up creating new Effect with different animatableData value in the body call. SwiftUI will

  • Detect that the Effect.animatableData has changed
  • Interpolate the value of animatableData, and
  • Repeatedly call effectValue and apply the result to the View (in this case, Text).

doFoo toggles animatableData between 0 and 1 depending on the animationFlag and so we can make custom transformation that scales the view at 1.0 when animatableData is 0 or 1, and at 1.2 when animatableData is 0.5.

Now you can work with the animation as one cohesive group, and if the animation is interrupted, SwiftUI can simply interpolate the current location to the new destination.

Note that GeometryEffect conforms to Animatable which allows for the interpolation mentioned above. Animatable requires animatableData to be VectorArithmatic. Things like Float and Double already conform to that out of the box, and AnimatablePair helps you quickly compose more complex data from simpler ones.

Should you need more complex animation, you can draw-it-yourself using Shape, or use AnimatableModifier. I find this SwiftUI Lab series on Advance Animation to be useful.

Also, we don't need withAnimation because Text already has .animation modifier.

In the end, when you design the animation, try to think what is the destination you want to reach from the event, and create single animation that reaches (from the current state) all the way to animation. Splitting the event transition into multiple chunks should be done with great care.


struct Foo: View {
  @ObservedObject private var notifier = Notifier()
}

Nothing wrong with this, but since notifier will never mutate, I find Environment wrapper to be more suitable.

struct BarView: View {
  let fooTrigger = ObservableObjectPublisher()

  var body: some View {
    VStack {
      FooView()
        .environment(\.fooTrigger, fooTrigger)

      Spacer()
      Button("Action!...") {
        self.fooTrigger.send()
      }
    }
  }
}

struct FooView: some View {
  ...
  // Type inferred from the type of `EnvironmentValues.fooTrigger`
  @Environment(\.fooTrigger) private var trigger

  var body: some View {
    VStack {
      ...
    }
    .onReceive(trigger, perform: self.doFoo)
  }
}

extension EnvironmentValues {
  private struct FooTrigger: EnvironmentKey {
      static var defaultValue: ObservableObjectPublisher { .init() }
  }

  var fooTrigger: ObservableObjectPublisher {
      get { self[FooTrigger.self] }
      set { self[FooTrigger.self] = newValue }
  }
}

This way, even if the superview don’t specify the fooTrigger it’ll still fetch a default value, which seems to be a reasonable behavior.

You’ll need to specify fooTrigger (via .environment) if you want to make sure they have the same object. SwiftUI seems to call defaultValue separately on each view, resulting in a different Publisher.


struct BarView: View {
  let foo = Foo()    // seems okay to do this way?
}

You're holding it the wrong way :stuck_out_tongue:.

You should NOT treat View as having identity of any kind. The only identity each view has is its location in the view hierarchy (BooView is the parent of Foo because it's accessed inside body). In fact, this

var body: some View {
  let x = ...
  return VStack {
    x
    x
  }
}

already won't do what you think.

It is not that foo hold any significance more that where it appears in the body method. So any logic relying on the identity of view is incorrect almost immediately.

You'll see that I avoid referencing Foo in multiple places and instead pass in fooTrigger via Environment. This way, even if new FooView is created, and replace the old one (this should be a good mental model when using SwiftUIView), fooTrigger will know where to send message to.


I see that you nest Foo inside BarView. I find it easier to maintain if I do Foo as a private struct in the same file as BarView. It'd achieve the similar level of access control strictness, but much easier to refactor and move things around.


The code

struct BarView: View {
    let fooTrigger = ObservableObjectPublisher()

    var body: some View {
        VStack {
            FooView()
                .environment(\.fooTrigger, fooTrigger)

            Spacer()
            Button("Action!...") {
                self.fooTrigger.send()
            }
        }
    }
}

struct FooView: View {
    static var animationDuration = 1.0

    @Environment(\.fooTrigger) private var trigger
    @State var animationFlag = false

    func doFoo() {
        animationFlag.toggle()
    }

    var body: some View {
        VStack {
            ZStack {
                Text(verbatim: "🤔").font(.system(size: 150))
                    .modifier(Effect(animationFlag))
                    .animation(Animation.linear(duration: Self.animationDuration))
            }
            .frame(width: 200, height: 200)
            Text(verbatim: "self.flag = \(self.animationFlag)")
        }
        .onReceive(trigger, perform: self.doFoo)
    }
}
private struct Effect: GeometryEffect {
    init(_ value: Bool) {
        animatableData = value ? 0 : 1
    }

    var animatableData: CGFloat

    func effectValue(size: CGSize) -> ProjectionTransform {
        let maxScale: CGFloat = 1.2, minScale: CGFloat = 1
        let scale = maxScale - (maxScale - minScale) * 2 * abs(0.5 - animatableData)
        let transform = CGAffineTransform.identity
            .translatedBy(x: -size.width * (scale - 1) / 2, y: -size.height * (scale - 1) / 2)
            .scaledBy(x: scale, y: scale)
        return ProjectionTransform(transform)
    }
}

extension EnvironmentValues {
    private struct FooTrigger: EnvironmentKey {
        static var defaultValue: ObservableObjectPublisher { .init() }
    }

    var fooTrigger: ObservableObjectPublisher {
        get { self[FooTrigger.self] }
        set { self[FooTrigger.self] = newValue }
    }
}
3 Likes

:smiling_face_with_three_hearts: Thank you so much. Once again, you give me the answer I so needed! Can't thank you enough!

I got the idea from Paul Hudson: https://www.youtube.com/watch?v=zERJoLafnGY. So it's not a good practice to hold on to view this way, it can easily lead to wrong usage...

The only thing I don't like is using @Environment/.environment() to pass in the publisher. I have multiple FooView's. So i just pass the trigger as parameter to FooView.init(), this way, BarView can create trigger/FooView as needed. Anything wrong with this approach?

import SwiftUI
import Combine

struct BarView: View {
    let fooTrigger = ObservableObjectPublisher()

    var body: some View {
        VStack {
            FooView(trigger: fooTrigger)
//                .environment(\.fooTrigger, fooTrigger)

            Spacer()
            Button("Action!...") {
                self.fooTrigger.send()
            }
        }
    }
}

struct FooView: View {
    static var animationDuration = 1.0

//    @Environment(\.fooTrigger) private var trigger
    let trigger: ObservableObjectPublisher
    @State var animationFlag = false

    func doFoo() {
        animationFlag.toggle()
    }

    var body: some View {
        VStack {
            ZStack {
                Text(verbatim: "🤔").font(.system(size: 150))
                    .modifier(Effect(animationFlag))
                    .animation(Animation.linear(duration: Self.animationDuration))
            }
            .frame(width: 200, height: 200)
            Text(verbatim: "self.flag = \(self.animationFlag)")
        }
        .onReceive(trigger, perform: self.doFoo)
    }
}

private struct Effect: GeometryEffect {
    init(_ value: Bool) {
        animatableData = value ? 0 : 1
    }

    var animatableData: CGFloat

    func effectValue(size: CGSize) -> ProjectionTransform {
        let maxScale: CGFloat = 1.2, minScale: CGFloat = 1
        let scale = maxScale - (maxScale - minScale) * 2 * abs(0.5 - animatableData)
        let transform = CGAffineTransform.identity
            .translatedBy(x: -size.width * (scale - 1) / 2, y: -size.height * (scale - 1) / 2)
            .scaledBy(x: scale, y: scale)
        return ProjectionTransform(transform)
    }
}

//extension EnvironmentValues {
//    private struct FooTrigger: EnvironmentKey {
//        static var defaultValue: ObservableObjectPublisher { .init() }
//    }
//
//    var fooTrigger: ObservableObjectPublisher {
//        get { self[FooTrigger.self] }
//        set { self[FooTrigger.self] = newValue }
//    }
//}

struct BarView_Previews: PreviewProvider {
    static var previews: some View {
        BarView()
    }
}

I thought of directly passing in fooTrigger too. The only downsides are about ergonomics.

  • You need to supply fooTrigger to every instance of FooView. Some superview may not want to do that if it doesn't need to trigger the event.
  • You may need to pass them in multiple-layers deep (like ObservedObject vs EnvironmentObject).

Otherwise, to the best of my knowledge, this should work identically to my Environment case, and to be fair, your ObservedObject case too. So if the downsides don't apply to your use case, passing them through init would probably be simpler.

Hmm, interesting. The video is not wrong per se, but the usage shown in there is rather primitive like reducing clutter. It'd be like storing fixed value, eg. greetingMessage or animationDuration. You wouldn't expect animationDuration to have the same identity when you use it twice, whatever that may mean. Using it as an identity anchor is outside of such use case (and it looks easy to misstep there).

1 Like

As a note, I find it easier to use AnimatableModifier than GeometryEffect:

struct FooView: View {
  var body: some View {
    ...
    Text(...)
      .modifier(Modifier(animationFlag))
    ...
  }
}

struct Modifier: AnimatableModifier {
  var animatableData: CGFloat

  init(_ value: Bool) {
    animatableData = value ? 0 : 1
  }

  func body(content: Content) -> some View {
    content
        .scaleEffect(currentScale)
  }

  private var currentScale: CGFloat {
    let maxScale: CGFloat = 1.2, minScale: CGFloat = 1

    let currentValue = abs(0.5 - animatableData)
    return maxScale - (maxScale - minScale) * 2 * currentValue
  }
}

Same difference. AnimatableModifier conforms to Animatable and so SwiftUI can interpolate the content. The difference being

  • GeometryEffect returns a transformation matrix given a view size, while
  • AnimatableModifier returns a new view given an old view.
1 Like

Argh...I think there is a problem with how the animation behave in each direction: the animation curve is cut in half, the first half for animating forward, second half reverse. Ideally the entire curve is used to animate each direction. As is, it only looks right for linear, but not for any other like easeInOut/spring/etc. The whole forward/reverse animation should look exactly like with Animation.repeatCount(2, autoreverses: true). But unfortunately, with Animation.repeatCount(2, autoreverses: true), the view jumps/ends at its new "state", not just ends at its origin. I don't know how to solve this. Unless somehow specify "compound" animation curve..

This page shows graph of each animation curve: Getting Started with SwiftUI Animations | raywenderlich.com

Regarding GeometryEffect vs. AnimatableModifier: I am just so happy you show both ways. I was unable to figure out how to use GeometryEffect for my animation. Now I know how :smile: . I think AnimatableModifier is more flexible as you can apply any kind of modifiers to content, not just a transform.