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
iOS Uiimagepickercontroller: Any Way of Getting the Date of the Chosen Picture
Uitraitcollection Clarification
Rxswift - Generic Parameter 'Self' Could Not Be Inferred
Tableview with Searchcontroller - Deinit Not Called
Showing Hidden View Really Slow
Swift 3 - Loading Multiple Viewcontrollers at Launch
I Cannot Access My Array Objects Anywhere, How to Access Them in Swift
Using Simple Ping in Swift (Ios)
Extract 4 Bits of Bluetooth Hex Data
Records, Zone Doesn't Displayed in Dashboard and Delete Zone Issue Cloudkit
How to Create an Alert in a Swift File Model That Works for Various View Controller
Uibezierpath + Cashapelayer - Animate a Circle Filling Up
Error When Trying to Save a Captured Image in Swift
How to Catch Accessibility Focus Changed
The Array Value Should Be Sort Like (Alphabetic, Numbers and Special Characters)