How to Update a Swiftui View That Was Embedded into Uikit

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

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)
}
}

Include SwiftUI views in existing UIKit application

edit 05/06/19: Added information about UIHostingController as suggested by @Departamento B in his answer. Credits go to him!



Using SwiftUI within UIKit

One can use SwiftUI components in existing UIKit environments by wrapping a SwiftUI View into a UIHostingController like this:

let swiftUIView = SomeSwiftUIView() // swiftUIView is View
let viewCtrl = UIHostingController(rootView: swiftUIView)

It's also possible to override UIHostingController and customize it to one's needs, e. g. by setting the preferredStatusBarStyle manually if it doesn't work via SwiftUI as expected.

UIHostingController is documented here.



Using UIKit within SwiftUI

If an existing UIKit view should be used in a SwiftUI environment, the UIViewRepresentable protocol is there to help! It is documented here and can be seen in action in this official Apple tutorial.


Compatibility

Please note that you cannot use SwiftUI on iOS versions < iOS 13, as SwiftUI is only available on iOS 13 and above. See this post for more information.

If you want to use SwiftUI in a project with a target below iOS 13, you need to tag your SwiftUI structs with @available(iOS 13.0.0, *) attribute.

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)
}
}

Inserting UIKit content into a SwiftUI view hierarchy

The basic strategy is to use a View that represents the content you want to bring in from a UIViewController. Your View is going to conform to UIViewCotrollerRepresentable and use the functions of that protocol to create and manage the UIKit content.

The UIViewControllerRepresentable documentation is here

And, as was commented on your original post by vadian, there is A tutorial with sample code

With the sample code above, I would rename "ViewController" to be something like PassBaseViewController or PBViewController, then you would create a View that derives from UIViewControllerRepresentable

You end up with a file called PBViewController.swift that has your code from above:

import Passbase
import UIKit

class PBViewController: UIViewController, PassbaseDelegate {
override func viewDidLoad() {
super.viewDidLoad()
PassbaseSDK.delegate = self
// Optional - You can prefill the email to skip that step.
Passbase.prefillUserEmail = "testuser@yourproject.com"
let button = PassbaseButton(frame: CGRect(x: 40, y: 90, width: 300, height: 60))
self.view.addSubview(button)
}

... and the rest of the code from your question here ...

Then (probably in another file, but not necessarily) you could create the SwiftUIView that uses that view controller:

struct PassBaseView : UIViewControllerRepresentable {
typealias UIViewControllerType = PBViewController

func makeUIViewController(context: Context) -> PBViewController {
return PBViewController()
}

func updateUIViewController(_ uiViewController: PBViewController, context: Context) {
/* code here to make changes to the view controller if necessary when this view is updated*/
}
}


Related Topics



Leave a reply



Submit