Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I am trying to integrate a SwiftUI view that animates on changes to a @State variable (originally progress was @State private progress: CGFloat = 0.5 in the SwiftUI View), into an existing UIKit application. I have read through lots of search results on integrating SwiftUI into UIKit (@State, @Binding, @Environment, etc.), but can't seem to figure this out.

I create a very simple example of what I am attempting to do, as I believe once I see this answer I can adopt it to my existing application.

The Storyboard is simply View controller with a UISlider. The code below displays the SwiftUI view, but I want it to update as I move the UISlider.

import SwiftUI

class ViewController: UIViewController {

    var progress: CGFloat = 0.5

    override func viewDidLoad() {
        super.viewDidLoad()
        // 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(progress: progress))
        addChild(childView)
        childView.view.frame = frame
        view.addSubview(childView.view)
        childView.didMove(toParent: self)
    }

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

}

struct Animate_Trim: View {
    var progress: CGFloat

    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)

            Spacer()

        }.font(.title)
    }
}```
like image 479
Dan Schümacher Avatar asked Feb 06 '20 00:02

Dan Schümacher


People also ask

How do I refresh a view in SwiftUI?

To add the pull to refresh functionality to our SwiftUI List, simply use the . refreshable modifier. List(emojiSet, id: \. self) { emoji in Text(emoji) } .

Can you mix SwiftUI and UIKit?

The hosting controller is compatible with UIKit since it is a subclass of UIViewController . The purpose of the UIHostingController is to enclose a SwiftUI view so that it can be integrated into an existing UIKit based project.

Can you use SwiftUI and UIKit in the same project?

This SwiftUI UIKit integration solution can provide the experience of using SwiftUI without creating a new app from scratch. Adding SwiftUI to an existing UIKit project will also make it easier for us to migrate the app to SwiftUI in the future.

How do I add a SwiftUI view to UIKit?

Adding a SwiftUI view to a UIKit view First, we add the hosting controller as a child to the current view controller. The view is added to the view hierarchy of the current view controller. Constraints are set up in code to update the boundaries of our SwiftUI view.


2 Answers

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)
  }
}
like image 186
nine stones Avatar answered Oct 22 '22 19:10

nine stones


If you do not want to use NotificationCenter. You could use just @Published and assign or sink.

I wrote a working example in a Playground to show the concept:

//This code runs on Xcode playground
import Combine
import SwiftUI

class ObservableSlider: ObservableObject {
    @Published public var value: Double = 0.0
}

class YourViewController {
    var observableSlider:ObservableSlider = ObservableSlider()
    private var cancellables: Set<AnyCancellable> = []
    let hostController = YourHostingController() // I put it here for the sake of the example, but you do need a reference to the Hosting Controller.

    init(){ // In a real VC this code would probably be on viewDidLoad

        let swiftUIView = hostController.rootView

        //This is where values of SwiftUI view and UIKit get glued together
        self.observableSlider.$value.assign(to: \.observableSlider.value, on: swiftUIView).store(in:&self.cancellables)
    }
    func updateSlider() {
        observableSlider.value = 8.5
    }
}

// In real app it would be something like:
//class YourHostingController<YourSwiftUIView> UIHostingController
class YourHostingController {
    var rootView = YourSwiftUIView()

//In a real Hosting controller you would do something like:
//    required init?(coder aDecoder: NSCoder){
//         super.init(coder: aDecoder, rootView: YourSwiftUIView())
//     }
}

struct YourSwiftUIView: View{
    var body: some View {
        EmptyView() // Your real SwiftUI body...
    }
    @ObservedObject var observableSlider: ObservableSlider = ObservableSlider()
    func showValue(){
        print(observableSlider.value)
    }
    init(){
        print(observableSlider.value)
    }
}

let yourVC = YourViewController() // Inits view and prints 0.0
yourVC.updateSlider() // Updates from UIKit to 8.5
yourVC.hostController.rootView.showValue() // Value in SwiftUI View is updated (prints 8.5)
like image 37
MrAn3 Avatar answered Oct 22 '22 21:10

MrAn3