Passing Value from SwiftUI to UIViewController Using UIHostingController - ios

I have a SwiftUI view, which consists of a TextField. I want that whenever I type in the TextField it should send the value to the control on the UIKit UIViewController.
// Here is the ContentView
class ContentViewDelegate: ObservableObject {
var didChange = PassthroughSubject<ContentViewDelegate, Never>()
var name: String = "" {
didSet {
self.didChange.send(self)
}
}
}
struct ContentView: View {
#ObservedObject var delegate: ContentViewDelegate
init(delegate: ContentViewDelegate) {
self.delegate = delegate
}
var body: some View {
VStack {
TextField("Enter name", text: self.$delegate.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
}.padding()
.background(Color.green)
}
}
I checked didChange does get fired in the above code. But in the code below, the sink is never fired.
class ViewController: UIViewController {
private var delegate = ContentViewDelegate()
private var contentView: ContentView!
override func viewDidLoad() {
super.viewDidLoad()
self.contentView = ContentView(delegate: self.delegate)
let controller = UIHostingController(rootView: self.contentView)
controller.view.translatesAutoresizingMaskIntoConstraints = false
self.addChild(controller)
self.view.addSubview(controller.view)
controller.didMove(toParent: self)
NSLayoutConstraint.activate([
controller.view.widthAnchor.constraint(equalToConstant: 200),
controller.view.heightAnchor.constraint(equalToConstant: 44),
controller.view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
controller.view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
])
_ = self.delegate.didChange.sink { delegate in
print(delegate.name)
}
}
Any ideas why didChange.sink is not getting fired?

If you assign the publisher to _ then it is deallocated when viewDidLoad returns. Early examples from Apple show the assignment to _ and it used to work.
You need to ensure you keep a strong reference to your publisher using a property:
class ViewController: UIViewController {
private var delegate = ContentViewDelegate()
private var contentView: ContentView!
private var textChangePublisher: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
self.contentView = ContentView(delegate: self.delegate)
let controller = UIHostingController(rootView: self.contentView)
controller.view.translatesAutoresizingMaskIntoConstraints = false
self.addChild(controller)
self.view.addSubview(controller.view)
controller.didMove(toParent: self)
NSLayoutConstraint.activate([
controller.view.widthAnchor.constraint(equalToConstant: 200),
controller.view.heightAnchor.constraint(equalToConstant: 44),
controller.view.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
controller.view.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
])
self.textChangePublisher = self.delegate.didChange.sink { delegate in
print(delegate.name)
}
}

Related

SwiftUI View is not updating when #Published changed in ObservableObject

I know there are a lot of questions out there with similar issues but almost all of them are reference/value issues. My case is a little different
I have a Viewcontroller that leverages UIHostingController and add's a SwiftUI view.
I have a viewmodel that's an ObservableObject and referenced in ViewController and then passed to SwiftUI View.
However if I update some values in ObservableObject it is not reflected to SwiftUI
What am I doing wrong here?
class DeveloperScreenViewController1: UIViewController {
let developerScreenViewModel: DeveloperScreenViewModel
private lazy var contentView: UIHostingController = {
UIHostingController(rootView: DeveloperScreenView(developerScreenViewModel: developerScreenViewModel))
}()
init(viewModel: DeveloperScreenViewModel) {
self.developerScreenViewModel = viewModel
super.init(nibName: nil, bundle: Bundle.main)
}
override func viewDidLoad() {
super.viewDidLoad()
setupConstraints()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.developerScreenViewModel.setup()
}
private func setupConstraints() {
// add swiftUI view as a subview
addChild(self.contentView)
self.contentView.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(self.contentView.view)
// set constraints to match the parent viewcontroller's view
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: contentView.view.leadingAnchor),
view.trailingAnchor.constraint(equalTo: contentView.view.trailingAnchor),
view.topAnchor.constraint(equalTo: contentView.view.topAnchor),
view.bottomAnchor.constraint(equalTo: contentView.view.bottomAnchor)
])
}
And then in ViewModel class
class DeveloperScreenViewModel: ObservableObject {
#Published var environments: [Endpoint] = []
#Published var realTimeUpdateToggle:Bool = false
public init() {
// self.setup()
}
public func setup() {
self.initializeListItems()
self.initializeRealTimeToggle()
}
private func initializeListItems() {
self.environments = EnvironmentStore.shared.changeableEndpoints
}
func initializeRealTimeToggle() {
realTimeUpdateToggle = RealTimeUpdatesDeveloperUtils.isEnabled()
}
and SwiftUI is something like
struct DeveloperScreenView: View {
#ObservedObject var developerScreenViewModel: DeveloperScreenViewModel
var body: some View {
VStack {
DeveloperScreenEnvironmentView(endPoints: developerScreenViewModel.environments)
DeveloperScreenRealTimeUpdateToggleView(realTimeToggleItem: developerScreenViewModel.realTimeUpdateToggle)
}
}
When changes are made to this vars, lets say add a new element to array or set realTimeUpdateToggle to true, view in SwiftUI just dont get updated
#Published var environments: [Endpoint] = []
#Published var realTimeUpdateToggle:Bool = false

How do I access a #StateObject in a SwiftUI view and update it from a view controller?

I have the following StateObject in my SwiftUI view:
#StateObject var state = AnnotateImageToolbarState()
Where AnnotateImageToolbarState:
final class AnnotateImageToolbarState: ObservableObject {
enum Constants {
static let defaultColor: Color = .black
}
#Published var selectedColor: Color? = Constants.defaultColor
#Published var isEraserSelected = false
#Published var isAnnotateText = false
func resetDefaults() {
selectedColor = Constants.defaultColor
isEraserSelected = false
}
}
I init my SwiftUI View in a view controller:
// MARK: - Views
private let toolbar = AnnotateImageToolbar()
private lazy var hostingController = UIHostingController(rootView: toolbar)
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
// MARK: - Setup
private func setupViews() {
view.backgroundColor = Constants.backgroundColor
addChild(hostingController)
view.addSubview(hostingController.view)
updateToolbarConstraints(isHidden: false)
}
I want to be able to update values in the state from the view controller, eg in my view controller:
private func updateToolbarType() {
toolbar.state.isAnnotateText = true
}
However I get the error Accessing StateObject's object without being installed on a View. This will create a new instance each time.
How would I go about changing the state values from the view controller?

Need Help Maintaining Swift Environment Object

Trying to place a SwiftUI View into a ViewController using UIHostingController, but receiving the following error:
No ObservableObject of type UIStateModel found. A View.environmentObject(_:) for UIStateModel may be missing as an ancestor of this view.
All help appreciated, I've been quite stuck :(.
ViewController.swift
class ViewController: UIViewController {
#EnvironmentObject var UIModel: UIStateModel
var uiState: UIStateModel = UIStateModel()
override func viewDidLoad() {
super.viewDidLoad()
addCarousel()
}
func addCarousel(){
let snapC : SnapCarousel
let contentView : UIHostingController<SnapCarousel>
uiState = UIStateModel()
snapC = SnapCarousel( UIState: uiState )
contentView = UIHostingController(rootView: snapC)
addChild(contentView)
view.addSubview(contentView.view)
contentView.didMove(toParent: self)
}
}
Carousel.swift
struct SnapCarousel: View {
var UIState: UIStateModel
//Carousel instantiation
}
struct Card: Decodable, Hashable, Identifiable {
//code
}
public class UIStateModel: ObservableObject {
#Published var activeCard: Int = 0
#Published var screenDrag: Float = 0.0
}
struct Carousel<Items : View> : View {
#EnvironmentObject var UIState: UIStateModel
//card management
}
struct Canvas<Content : View> : View {
// view manager
}
struct Item<Content: View>: View {
#EnvironmentObject var UIState: UIStateModel
//code for card view
}
You need to register UIStateModel as an #EnvironmentObject so that it can be injected automatically in the subviews.
First, update SnapCarousel to hold the environmentObject:
struct SnapCarousel: View {
#EnvironmentObject var UIState: UIStateModel
//Carousel instantiation
}
To register model as environment object, when register with SnapCarousel as:
let snapC = SnapCarousel()
snapC.environmentObject(uiState)
If you don't need a reference to uiState, you can simplify the whole thing to:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
addCarousel()
}
func addCarousel() {
let controller = UIHostingController(rootView: SnapCarousel().environmentObject(UIStateModel()))
addChild(controller)
view.addSubview(controller.view)
controller.didMove(toParent: self)
// add some constraints to fill the parent view
NSLayoutConstraint.activate([
controller.view.widthAnchor.constraint(equalTo: view.widthAnchor),
controller.view.heightAnchor.constraint(equalTo: view.heightAnchor),
controller.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
controller.view.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}

How do I show a SwiftUI Subview fullscreen?

Take a look at the screenshot:
I marked the "Top Navigation Bar" red, which I want to remove, as there is an unused top bar...
You have to know that I code using Storyboards, but this specific page is holding a subview of SwiftUI View!
This is the SwiftUI ContentView:
import SwiftUI
import UIKit
struct ContentView: View {
var body: some View {
NavigationView{
MasterView()
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
struct MasterView: View {
var body: some View {
Form {
Section(header: Text("Geplant")) {
Section {
NavigationLink(destination: UIKitView()) { Text("Berlin") }
}
}
}
.navigationBarTitle("Wohin gehts?")
}
}
struct UIKitView: UIViewControllerRepresentable {
typealias UIViewControllerType = SwipeViewController
func makeUIViewController(context: Context) -> SwipeViewController {
let sb = UIStoryboard(name: "Storyboard", bundle: nil)
let viewController = sb.instantiateViewController(identifier: "swipe") as! SwipeViewController
return viewController
}
func updateUIViewController(_ uiViewController: SwipeViewController, context: Context) {
}
}
And this is the UIViewController, which is holding the SwiftUI Subview:
import UIKit
import SwiftUI
class StartViewController: UIViewController {
#IBOutlet weak var btn: UIButton!
#IBOutlet weak var container: UIView!
let contentView = UIHostingController(rootView: ContentView())
override func viewDidLoad() {
super.viewDidLoad()
configureBackgroundGradient()
addChild(contentView)
view.addSubview(contentView.view)
setupContraints()
}
fileprivate func setupContraints(){
contentView.view.translatesAutoresizingMaskIntoConstraints = false
contentView.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
private func configureBackgroundGradient() {
let backgroundGray = UIColor(red: 244 / 255, green: 247 / 255, blue: 250 / 255, alpha: 1)
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.white.cgColor, backgroundGray.cgColor]
gradientLayer.frame = view.bounds
view.layer.insertSublayer(gradientLayer, at: 0) //Background Color
}
}
Can anyone can help? :))
Thank you! Feel free to ask me for more screenshots or code!
You can show a view in full screen with the SwiftUI view modifiere fullScreenCover. https://www.hackingwithswift.com/quick-start/swiftui/how-to-present-a-full-screen-modal-view-using-fullscreencover
Let us take simple FullScreenModalView struct that can dismiss itself, then presents it from ContentView when another button is pressed:
struct FullScreenModalView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Dismiss Modal") {
presentationMode.wrappedValue.dismiss()
}
}
}
And here is the code for ContentView -
struct ContentView: View {
#State private var isPresented = false
var body: some View {
Button("Present!") {
isPresented.toggle()
}
.fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init)
}
}
Happy to help.
Thanks.

Why does Injecting a Binding into UIViewControllerRepresentable cause a retain cycle?

So there seems to be a retain cycle when injecting a Binding that is a published property from an ObservableObject into UIViewControllerRepresentable.
It seems if you create a view inside another view in and that second view has an ObservableObject and injects it's published property into the UIViewControllerRepresentable and is then used in the coordinator, the ObservableObject is never released when the original view is refreshed.
Also it looks like the Binding gets completely broken and the UIViewControllerRepresentable no longer works
When looking at it, it makes sense that Coordinator(self) is bad, but Apple's own documentation says to do it this way. Am I missing something?
Here is a quick example:
struct ContentView: View {
#State var resetView: Bool = true
var body: some View {
VStack {
OtherView()
Text("\(resetView ? 1 : 0)")
// This button just changes the state to refresh the view
// Also after this is pressed the UIViewControllerRepresentable no longer works
Button(action: {resetView.toggle()}, label: {
Text("Refresh")
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct OtherView: View {
#ObservedObject var viewModel = OtherViewModel()
var body: some View {
VStack {
Text("Value: \(viewModel.value)")
Wrapper(value: $viewModel.value).frame(width: 100, height: 50, alignment: .center)
}
}
}
class OtherViewModel: ObservableObject {
#Published var value: Int
deinit {
print("OtherViewModel deinit") // this never gets called
}
init() {
self.value = 0
}
}
struct Wrapper: UIViewControllerRepresentable {
#Binding var value: Int
class Coordinator: NSObject, ViewControllerDelegate {
var parent: Wrapper
init(_ parent: Wrapper) {
self.parent = parent
}
func buttonTapped() {
// After the original view is refreshed this will no longer work
parent.value += 1
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> ViewController {
let vc = ViewController()
vc.delegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {}
}
protocol ViewControllerDelegate: class {
func buttonTapped()
}
class ViewController: UIViewController {
weak var delegate: ViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 20))
button.setTitle("increment", for: .normal)
button.setTitleColor(UIColor.blue, for: .normal)
button.addTarget(self, action: #selector(self.buttonTapped), for: .touchUpInside)
self.view.addSubview(button)
}
#objc func buttonTapped(sender : UIButton) {
delegate?.buttonTapped()
}
}

Resources