siteline / swiftui-introspect

Introspect underlying UIKit/AppKit components from SwiftUI
MIT License
5.66k stars 350 forks source link

Customisation neglected after state updates #116

Closed 4ndrewHarri5 closed 1 year ago

4ndrewHarri5 commented 3 years ago

This behaviour has been noticed on TextField. I have noticed that when a state variable updates, the styles set in introspectTextField are discarded.

For example, I have a textfield which is styled to different colours when the text is empty. I use the introspect modifier to set the placeholder text colour.

@State private var text: String = ""
...
TextField("title", text: $text)
  .foregroundColor(text.isEmpty ? .red : .blue)
  .accentColor(text.isEmpty ? .red : .blue)
  .padding(.all, 5)
  .background(text.isEmpty ? Color.red.opacity(0.1) : Color.blue.opacity(0.1))
  .cornerRadius(5)
  .fixedSize(horizontal: true, vertical: false)
  .introspectTextField { textField in
      textField.attributedPlaceholder = NSAttributedString(string: "placeholder",
      attributes: [NSAttributedString.Key.foregroundColor: UIColor(Color.red)])
}

On the first render, the state is set to an empty string, so the placeholder should be displayed. The placeholder is shown as expected with the colour as red and text as "placeholder". Once I start to type, then delete the text to get the placeholder back, the placeholder changes to "title" and the style has been set to the default.

Before typing:

Screenshot 2021-05-08 at 18 48 31

After typing and removing the text:

Screenshot 2021-05-08 at 18 45 22

This behaviour also occurs when there is no other TextField style modifiers other than introspectTextField

Thanks!

SplittyDev commented 3 years ago

Hi, I really wish we could do something about this since it has been a recurring theme in our issue section.

SwiftUI sometimes seems to recreate the underlying UIKit controls behind the scenes, which causes them to lose the custom configuration. The proprietary scene graph of SwiftUI makes it hard to find out how and when specific things are re-rendered.

One way you could possibly work around this is to keep a weak reference to the UITextField/NSTextField, so you can keep a reference to it even when the view is re-rendered and then apply your customization again whenever the text changes.

That workaround could look somewhat like this (untested pseudo-code):

weak var textField: UITextField

@State private var text: String = "" {
  didSet {
    textField?.attributedPlaceholder = NSAttributedString(
        string: "placeholder",
        attributes: [NSAttributedString.Key.foregroundColor: UIColor(Color.red)]
    )
  }
}

// ...

TextField("title", text: $text)
  // ...
  .introspectTextField { textField in
    this.textField = textField;
  }
jjuric commented 2 years ago

Yeah sorry for commenting on an old issue, but the way @SplittyDev suggested cannot work. The reason is because self is immutable inside the introspect closure.

Honestly, seems like this library fails whenever there are state updates triggered, SwiftUI just squashes whatever changes introspect made at the beginning. Seems like the only way to handle this (for below iOS 15) is by using a custom ViewRepresentable for the textfield, but that just brings other problems to the mix. (e.g. tap gestures get overridden by parent views, problems with switching between first responders during state changes...)

SplittyDev commented 2 years ago

Honestly, seems like this library fails whenever there are state updates triggered, SwiftUI just squashes whatever changes introspect made at the beginning.

Yeah, this is definitely a big issue. Introspect is pretty much a hack, and it will never really be stable because SwiftUI doesn't make any guarantees about the underlying system components.

SwiftUI components seem to be recreating their underlying system components when specific conditions change and we have no way of detecting this. SwiftUI also doesn't seem to be triggering all the view modifiers again whenever this happens, and it doesn't seem like we can actually hook deeper into the system to properly react to that.

The fact that SwiftUI isn't open source only makes this harder, since it's not easily (or even at all) possible to understand the exact mechanisms behind the SwiftUI state update/rendering pipeline.

In a perfect world, there wouldn't be a need for a library like Introspect and I can only hope that Apple decides to expose a lot more functionality that's already achievable in UIKit/AppKit through modifiers.

We've seen Apple introduce more modifiers in the past, hopefully this trend continues until one day there isn't a need for this library anymore.

Until then, Introspect remains pretty much the only way to work with SwiftUI while having another degree of control over the underlying components, and many developers rely on it. We're trying our best to make this process as stable as possible, but some issues just aren't fixable.