How to Update a Swiftui View State from Outside (Uiviewcontroller for Example)

How to update a SwiftUI view state from outside (UIViewController for example)

@State is the wrong thing to use here. You'll need to use @ObservedObject.

@State: Used for when changes occur locally to your SwiftUI view - ie you change eyesOpened from a toggle or a button etc from within the SwiftUI view it self.

@ObservedObject: Binds your SwiftUI view to an external data source - ie an incoming notification or a change in your database, something external to your SwiftUI view.

I would highly recommend you watch the following WWDC video - Data Flow Through SwiftUI

UIHostingController embedded in UIViewController - how to update @State from outside?

var contentView: ContentView? /// keep reference to ContentView

This is wrong assumption, because ContentView is a struct, ie. value, so you keep a copy not a reference.

Instead we should use instance of view model class as reference to communicate from UIKit into SwiftUI. Find below a modified code with approach demo.

Tested with Xcode 12.4 / iOS 14.4

class ViewModel: ObservableObject {
@Published var name = "Name"
}

class ViewController: UIViewController {

private var vm: ViewModel!

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = UIColor.secondarySystemBackground

/// add the button
let button = UIButton()
button.frame = CGRect(x: 50, y: 50, width: 200, height: 100)
button.setTitle("Change name", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
view.addSubview(button)

/// add the SwiftUI ContentView
self.vm = ViewModel()
let contentView = ContentView(vm: self.vm)
let hostingController = UIHostingController(rootView: contentView)

addChild(hostingController)
view.insertSubview(hostingController.view, at: 0)
hostingController.view.frame = CGRect(x: 0, y: 400, width: view.bounds.width, height: view.bounds.height - 400)
hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostingController.didMove(toParent: self)
}

@objc func handleTap() {
vm.name = "Updated name" /// update the name
}
}

struct ContentView: View {
@ObservedObject var vm: ViewModel

var body: some View {
Text(vm.name)
}
}

How do I update a SwiftUI View in UIKit when value changes?

The accepted answer actually doesn't answer the original question "update a SwiftUI View in UIKit..."?

IMHO, when you want to interact with UIKit you can use a notification to update the progress view:

extension Notification.Name {
static var progress: Notification.Name { return .init("progress") }
}
class ViewController: UIViewController {
var progress: CGFloat = 0.5 {
didSet {
let userinfo: [String: CGFloat] = ["progress": self.progress]
NotificationCenter.default.post(Notification(name: .progress,
object: nil,
userInfo: userinfo))
}
}
var slider: UISlider = UISlider()
override func viewDidLoad() {
super.viewDidLoad()
slider.addTarget(self, action: #selector(sliderAction(_:)), for: .valueChanged)
slider.frame = CGRect(x: 0, y: 500, width: 200, height: 50)
// Do any additional setup after loading the view.

let frame = CGRect(x: 20, y: 200, width: 400, height: 400)

let childView = UIHostingController(rootView: Animate_Trim())
addChild(childView)
childView.view.frame = frame
view.addSubview(childView.view)
view.addSubview(slider)
childView.didMove(toParent: self)
}

@IBAction func sliderAction(_ sender: UISlider) {
progress = CGFloat(sender.value)
print("Progress: \(progress)")
}
}

struct Animate_Trim: View {
@State var progress: CGFloat = 0
var notificationChanged = NotificationCenter.default.publisher(for: .progress)
var body: some View {
VStack(spacing: 20) {
Circle()
.trim(from: 0, to: progress) // Animate trim
.stroke(Color.blue,
style: StrokeStyle(lineWidth: 40,
lineCap: CGLineCap.round))
.frame(height: 300)
.rotationEffect(.degrees(-90)) // Start from top
.padding(40)
.animation(.default)
.onReceive(notificationChanged) { note in
self.progress = note.userInfo!["progress"]! as! CGFloat
}
Spacer()
}.font(.title)
}
}

How do I update a SwiftUI View that was embedded into UIKit?

All you need is to throw away that custom subject, and use standard @Published, as below

class CircleModel: ObservableObject {

@Published var text: String

init(text: String) {
self.text = text
}
}

Tested on: Xcode 11.2 / iOS 13.2

SwiftUI: changing @State variable value does not update UI when called from outside

In SwiftUI, you can't reach into child views and modify their state reliably. Instead, shared state should be owned by the parent view.

Here's a modified version of your code that uses an ObservableObject with a reference held by the parent that gets passed down to the child:

class SharedState : ObservableObject {
@Published var isActionCalled = false
}

class ViewController: UIViewController {
var sharedState = SharedState()

override func viewDidLoad() {
super.viewDidLoad()

let swiftUIView = SwiftUIView(sharedState: self.sharedState)

let hostingController = UIHostingController(rootView: swiftUIView)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
hostingController.view.topAnchor.constraint(equalTo: self.view.topAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
hostingController.didMove(toParent: self)
}
}

struct SwiftUIView: View {
@StateObject var sharedState : SharedState

var body: some View {
VStack {
Form {
Section {
Button("Button 1") {
sharedState.isActionCalled = true
}
}.disabled(sharedState.isActionCalled)

Section {
Text("Click here to enabled the above button")
Button("Click") {
setIsActionCalled(value: false)
}
}
}

}
}

func setIsActionCalled(value: Bool) {
sharedState.isActionCalled = value
}
}

How do I trigger updateUIView of a UIViewRepresentable?

You need to create UIKit view inside makeUIView and via Binding pass only dependent data. That binding change, when related state - source of truth - changed, calls updateUIView, where you should update your UIKit view.

Here is simplified demo sketch only, to show concept (might have typos):

struct SpritzUIViewRepresentable : UIViewRepresentable{
@Binding var currentWord: SpritzSwiftWord
@Binding var backgroundColor: UIColor

func makeUIView(context: Context) -> SpritzSwiftView {
// create and configure view here
return SpritzSwiftView(frame: CGRect.zero) // frame does not matter here
}

func updateUIView(_ uiView: SpritzSwiftView, context: Context) {
// update view properties here from bound external data
uiView.backgroundColor = backgroundColor
uiView.updateWord(currentWord)
}
}

and button now should just change model data

    VStack {
Text("SpritzTest")
.padding()
SpritzUIViewRepresentable(backgroundColor: $backgroundColor, SpritzViewManager:$ssManager, currentWord: $currentWord)
.padding()
Button(action:
{
ssManager = SpritzSwiftManager(withText: "Text try one two three", andWordPerMinute: 200)

self.backgroundColor = .clear
ssManager.startReading { (word, finished) in
if !finished {
self.currentWord = word
}
}
})
{
Text("Start")
}

assuming updated properties

@State private var currentWord = SpritzSwiftWord(word: "")
@State private var backgroundColor = UIColor.white // whatever you want

SwiftUI: Forcing an Update

Setting currentPage, as it is a @State, will reload the whole body.



Related Topics



Leave a reply



Submit