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)
])
}
}
Related
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
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?
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.
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()
}
}
I'm trying to call a local ViewController function from ContentView. The function uses some local variables and cannot be moved outside the ViewController.
class ViewController: UIViewController {
func doSomething() {...}
}
extension ViewController : LinkViewDelegate {...}
located on a different file:
struct ContentView: View {
init() {
viewController = .init(nibName:nil, bundle:nil)
}
var viewController: viewController
var body: some View {
Button(action: {self.viewController.doSomething()}) {
Text("Link Account")
}
}
}
UIViewController cannot be changed to something like UIViewRepresentable because LinkViewDelegate can only extend UIViewController.
So you need to create a simple bool binding in SwiftUI, flip it to true to trigger the function call in the UIKit viewController, and then set it back to false until the next time the swiftUI button is pressed. (As for LinkViewDelegate preventing something like UIViewControllerRepresentable that shouldn't stop you, use a Coordinator to handle the delegate calls.)
struct ContentView: View {
#State var willCallFunc = false
var body: some View {
ViewControllerView(isCallingFunc: $willCallFunc)
Button("buttonTitle") {
self.willCallFunc = true
}
}
}
struct ViewControllerView: UIViewControllerRepresentable {
#Binding var isCallingFunc: Bool
func makeUIViewController(context: Context) -> YourViewController {
makeViewController(context: context) //instantiate vc etc.
}
func updateUIViewController(_ uiViewController: YourViewController, context: Context) {
if isCallingFunc {
uiViewController.doSomething()
isCallingFunc = false
}
}
}
Here is a way that I've come up with which doesn't result in the "Modifying state during view update, this will cause undefined behavior" problem. The trick is to pass a reference of your ViewModel into the ViewController itself and then reset the boolean that calls your function there, not in your UIViewControllerRepresentable.
public class MyViewModel: ObservableObject {
#Published public var doSomething: Bool = false
}
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
var body: some View {
MyView(viewModel: viewModel)
Button("Do Something") {
viewModel.doSomething = true
}
}
}
struct MyView: UIViewControllerRepresentable {
#ObservedObject var viewModel: MyViewModel
func makeUIViewController(context: Context) -> MyViewController {
return MyViewController(viewModel)
}
func updateUIViewController(_ viewController: MyViewController, context: Context) {
if viewModel.doSomething {
viewController.doSomething()
// boolean will be reset in viewController
}
}
}
class MyViewController: UIViewController {
var viewModel: MyViewModel
public init(_ viewModel: MyViewModel) {
self.viewModel = viewModel
}
public func doSomething() {
// do something, then reset the flag
viewModel.doSomething = false
}
}
You could pass the instance of ViewController as a parameter to ContentView:
struct ContentView: View {
var viewController: ViewController // first v lowercase, second one Uppercase
var body: some View {
Button(action: { viewController.doSomething() }) { // Lowercase viewController
Text("Link Account")
}
}
init() {
self.viewController = .init(nibName:nil, bundle:nil) // Lowercase viewController
}
}
// Use it for the UIHostingController in SceneDelegate.swift
window.rootViewController = UIHostingController(rootView: ContentView()) // Uppercase ContentView
Updated answer to better fit the question.