Reputation: 265
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)
}
}```
Upvotes: 10
Views: 10099
Reputation: 1395
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)
Upvotes: 11
Reputation: 3438
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)
}
}
Upvotes: 9
Reputation: 17534
Combine is your friend ...
I did simple example, solely with SwiftUI, but using the same scenario
import SwiftUI
class Model: ObservableObject {
@Published var progress: Double = 0.2
}
struct SliderView: View {
@EnvironmentObject var slidermodel: Model
var body: some View {
// this is not part of state of the View !!!
// the bindig is created directly to your global EnnvironmentObject
// be sure that it is available by
// creating the SwiftUI view that provides the window contents
// in your SceneDelegate.scene(....)
// let model = Model()
// let contentView = ContentView().environmentObject(model)
let binding = Binding<Double>(get: { () -> Double in
self.slidermodel.progress
}) { (value) in
self.slidermodel.progress = value
}
return Slider(value: binding, in: 0.0 ... 1.0)
}
}
struct ContentView: View {
@EnvironmentObject var model: Model
var body: some View {
VStack {
Circle()
.trim(from: 0, to: CGFloat(model.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)
SliderView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Model())
}
}
and all is nicely working together
Upvotes: -2