How to pass data between UIViewController and struct ContentView?
I tried with ObservableObject but I can't get the data up to date.
To pass data from a UIViewController to an SwiftUI-Struct inside an UIHostingController you can attach an environmentObject to the SwiftUI rootView:
let vc = UIHostingController(rootView: YourContentView().environmentObject(yourEnvironmentObject))
Of course you'll need to create an ObservableObject and add it to your SwiftUI-Struct.
Create the ObservableObject:
class TypeOfEnvironmentObject: ObservableObject {
#Published var data = "myData"
}
Add it to your struct:
#EnvironmentObject var yourEnvironmentObject: TypeOfEnvironmentObject
I found the existing answers confusing/incomplete, perhaps something changed around generic inference in Swift 5.3 etc. While you can add an environment object to the UIHostingController's view this seems to conflict with the types (i.e. the generic parameter to UIHostingController needs a concrete type). Adding AnyView resolves this:
import UIKit
import SwiftUI
struct TutorialView: View {
#EnvironmentObject private var integration: TutorialIntegrationService
var body: some View {
Text("Hi").navigationBarTitle("test: \(integration.id)")
}
}
class TutorialIntegrationService: ObservableObject {
#Published var id: Int = 0
}
class TutorialViewController: UIHostingController<AnyView> {
let integration = TutorialIntegrationService()
required init?(coder: NSCoder) {
super.init(coder: coder,rootView: AnyView(TutorialView().environmentObject(integration)));
}
}
Add class myclassname: ObservableObject
In the class create a variable with #Published var myvar and add:
init(myvar: type) {
self.myvar = myvar
}
In UIViewController add:
private var model = myclassname(myvar: XXX)`
and in viewWillAppear add:
let vc = myclassname(myvar: myvar)
let childView = UIHostingController(rootView: ContentView(model: vc))
In the struct add:
#ObservedObject var model: myclassname
Related
We can use an #EnvironmentObject in SwiftUI to hold an instance of an object available for multiple views:
class MyObject: #ObservableObject {
var state = ""
func doSomethingWithState() {
//
}
}
struct MyView {
#EnvironmentObject var myObject: MyObject
}
However, we need to take care of this by adding .environment both to the main class and to every individual preview so that they don't crash:
struct MyApp: App {
var myObject = MyObject()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(myObject)
}
}
struct MyView: View {
var body: some View {
MyView()
.environmentObject(MyObject())
}
}
In addition to that there is no easy way to access one environment object from another:
class MySecondClass: #ObservableObject {
#EnvironmentObject MyObject myObject; // cannot do this
}
I came across to a better solution using Singletons with static let shared:
class MyObject: #ObservableObject {
static let shared = MyObject()
}
This was I can:
Use this object just like an #EnvironmentObject in any of my views:
struct MyView: View {
#ObservedObject var myObject = MyObject.shared
}
I don't need to add any .environment(MyObject()) to any of my views because the declaration in 1. takes care of it all.
I can easily use any of my singleton objects from another singleton objects:
class MySecondObject: #ObservableObject {
func doSomethingWithMyObject() {
let myVar = MyObject.shared.state
}
}
It seems to me better in every aspect. My question is: is there any advantage in using #EnvironmentObject over a Singleton as shown above?
The correct way is to set the environmentObject using a singleton.
struct MyApp: App {
var myObject = MyObject.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(myObject)
}
}
The reason is we must not init objects inside SwiftUI structs because these structs are recreated all the time so many objects being created is a memory leak.
The other advantage to this approach is you can use a different singleton for previews:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(MyObject.preview)
}
}
FYI #StateObject are not init during previewing.
I want to put the logic of all my #Published in a model class, however when I try to separate it, it doesn't update. I recreated a little example:
The code below works, it increases every time the button is clicked:
struct ContentView: View {
#StateObject var myClass = MyClass()
var body: some View {
Button(action: {
myClass.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
func doStuff(numb: Int) {
people += numb
}
}
However, once I split the logic and try to have my #Published in a separate class to have it more clean, it doesn't update, see below:
struct ContentView: View {
#StateObject var myClass = MyClass()
let modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
var myClass = MyClass()
func doStuff(numb: Int) {
myClass.people += numb
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
}
I think it's because there are two different instances in the view right? Anyway, how can I separate the #Publish correctly have it updated?
Thanks
Your first form is absolutely fine! You may, though, consider your ContentView using a #ObservedObject instead a #StateObject.
Your second form is flawed, for several reasons:
don't move logic into a view
don't use class variables to keep "state".
The first statement is due to a sane design that keeps your models and views nicely separated.
The second statement is due to how SwiftUI works. If you need to have some "state" in your views, use #State where the value is a struct.
Using #State ensures, that it's value is bound to the actual life time of the "conceptual view", i.e. the thing we human perceive as the view. And this "conceptual view" (managed as some data object by SwiftUI) is distinct from the struct View, which is merely a means to describe how to create and modify this conceptual view - that is, struct view is rather a function that will be used to initially create the "conceptual view" and modify it. Once this is done, it gets destroyed, and gets recreated when the conceptual view needs to be modified. That also means, the life time of this struct is not bound to the life time of its "state". The state's life time is bound to the conceptual view, and thus has usually longer life time than the struct view, where the struct view can be created and destroyed several times.
Now, imagine what happens when you always execute let modify = Modify() whenever the (conceptual) view or its content view is modified and needs to be recalculated and rendered by creating a struct view and then - after it has been rendered - destroying it again.
Also, this "state" is considered private for the view, means it is considered an implementation detail for the view. If you want to exchange data from "outside" and "inside" use a #ObservedObject or a Binding.
The problem is that you have 2 separate instances of MyClass:
#StateObject var myClass = MyClass()
var myClass = MyClass()
You are updating the myClass in Modify, which you aren't receiving updates from. A way to fix this is by having one instance of MyClass, passed into Modify during initialization:
struct ContentView: View {
#StateObject var myClass: MyClass
let modify: Modify
init() {
let temp = MyClass()
_myClass = StateObject(wrappedValue: temp)
modify = Modify(myClass: temp)
}
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
let myClass: MyClass
init(myClass: MyClass) {
self.myClass = myClass
}
func doStuff(numb: Int) {
myClass.people += numb
}
}
Another method is to have a #Published property in Modify to observe the changes of MyClass:
struct ContentView: View {
#StateObject var modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(modify.myClass.people)")
}
}
}
class Modify: ObservableObject {
#Published var myClass = MyClass()
private var anyCancellable: AnyCancellable?
init() {
anyCancellable = myClass.objectWillChange.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
func doStuff(numb: Int) {
myClass.people += numb
}
}
you could try this approach using a singleton. Works well for me:
struct ContentView: View {
#StateObject var myClass = MyClass.shared // <--- here
let modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
var myClass = MyClass.shared // <--- here
func doStuff(numb: Int) {
myClass.people += numb
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
static let shared = MyClass() // <--- here
}
I have a SettingsManager singleton for my entire app that holds a bunch of user settings. And I've got several ViewModels that reference and can edit the SettingsManager.
The app basically looks like this...
import PlaygroundSupport
import Combine
import SwiftUI
class SettingsManager: ObservableObject {
static let shared = SettingsManager()
#AppStorage("COUNT") var count = 10
}
class ViewModel: ObservableObject {
#Published var settings = SettingsManager.shared
func plus1() {
settings.count += 1
objectWillChange.send()
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(action: viewModel.plus1) {
Text("\(viewModel.settings.count)")
}
}
}
}
let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController
Frustratingly, it works about 85% of the time. But 15% of the time, the values don't update until navigating away from the view and then back.
How can I get #AppStorage to play nice with my View Model / MVVM framework?!
Came across this question researching this exact issue. I came down on the side of letting SwiftUI do the heavy lifting for me. For example:
// Use this in any view model you need to update the value
extension UserDefaults {
static func setAwesomeValue(with value: Int) {
UserDefaults.standard.set(value, forKey: "awesomeValue")
}
static func getAwesomeValue() -> Int {
return UserDefaults.standard.bool(forKey: "awesomeValue")
}
}
// In any view you need this value
struct CouldBeAnyView: some View {
#AppStorage("awesomeValue") var awesomeValue = 0
}
AppStorage is just a wrapper for UserDefaults. Whenever the view model updates the value of "awesomeValue", AppStorage will automatically pick it up. The important thing is to pass the same key when declaring #AppStorage. Probably shouldn't use a string literal but a constant would be easier to keep track of?
This SettingsManager in a cancellables set solution adapted from the Open Source ACHN App:
import PlaygroundSupport
import Combine
import SwiftUI
class SettingsManager: ObservableObject {
static let shared = SettingsManager()
#AppStorage("COUNT") var count = 10 {
willSet { objectWillChange.send() }
}
}
class ViewModel: ObservableObject {
#Published var settings = SettingsManager.shared
var cancellables = Set<AnyCancellable>()
init() {
settings.objectWillChange
.sink { [weak self] _ in
self?.objectWillChange.send()
}
.store(in: &cancellables)
}
func plus1() {
settings.count += 1
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(action: viewModel.plus1) {
Text(" \(viewModel.settings.count) ")
}
}
}
}
let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController
Seems to be slightly less glitchy, but still isn't 100% rock-solid consistent :(
Leaving this here to hopefully inspire someone with my attempt
I have the following data model:
class MyImage: : Identifiable, Equatable, ObservableObject {
let id = UUID()
var path: String
#Published var coordinate: CLLocationCoordinate2D?
}
class MyImageCollection : ObservableObject {
#Published var images: [MyImage] = []
#Published var selection: Set<UUID> = []
}
extension Array where Element == MyImage {
func haveCoordinates() -> Array<MyImage> {
return filter { (image) -> Bool in
return image.coordinate != nil
}
}
}
I use the collection in the views as follows:
# Top View
#StateObject var imageModel: MyImageCollection = MyImageCollection()
# Dependend Views
#ObservedObject var imageModel: MyImageCollection
So in my SwiftUI, whenever I add a new instance of MyImage via imageCollection.images.append(anImage) everything works perfectly and any View is updated accordingly, also any View using imageCollection.haveCoordinates() is updated. But I also want to have any views updated, when I change a property of an image like imageCollection.images[0].coordinate = someCoordinate. That does not work currently.
Any hints?
Your subviews need to directly observe your 'MyImage' class to be updated accordingly. Pass in your 'MyImage' instances directly into an observed object variable. Here's what that may look like...
ForEach(collection.images) { myImage in
YourSubView(image: myImage)
}
Where the image parameter is passed to an observed object property in your subview.
Switch the #ObservedObject to #EnvironmentObject in the DependantView and initialize DependentView().environmentObject(imageModel). Apple Documentation.This connects the two instances.
Also, If you want to Observe each MyImage you have to create an MyImageView that observes it directly
struct MyImage: View{
#ObservedObject var myImage: MyImage
//Rest of code
}
Thanks for your replies, very appreciated. I was trying to create a Subview with a reference to the object as ObservableObject too, but failed because I was using MapAnnotation and that is initialized with the coordinate property. Need to figure that out in detail what the difference is here, the documentation from Apple does not help very much to see any difference at a glance. I'm using some kind of workaround now by "invalidating" the MyImage instance with setting a new UUID:
class MyImage: : Identifiable, Equatable, ObservableObject {
var id = UUID()
var path: String
#Published var coordinate: CLLocationCoordinate2D? {
didSet {
id = UUID()
}
}
}
I am trying to use environmentObject in a watchOS6 app to bind my data model to my view.
I have created a simple, stand-alone Watch app in Xcode 11.
I created a new DataModel class
import Combine
import Foundation
import SwiftUI
final class DataModel: BindableObject {
let didChange = PassthroughSubject<DataModel,Never>()
var aString: String = "" {
didSet {
didChange.send(self)
}
}
}
In my ContentView struct I bind this class using #EnvironmentObject -
struct ContentView : View {
#EnvironmentObject private var dataModel: DataModel
var body: some View {
Text($dataModel.aString.value)
}
}
Finally, I attempt to inject an instance of the DataModel into the environment in the HostingController class -
class HostingController : WKHostingController<ContentView> {
override var body: ContentView {
return ContentView().environmentObject(DataModel())
}
}
But, I get an error:
Cannot convert return expression of type '_ModifiedContent<ContentView, _EnvironmentKeyWritingModifier<DataModel?>>' to return type 'ContentView'
The error is because the WKHostingController is a generic that needs a concrete type - WKHostingController<ContentView> in this case.
A similar approach works perfectly with UIHostingController in an iOS app because UIHostingController isn't a generic class.
Is there some other way to inject the environment to a watchOS view?
You can use type erasure, AnyView in the case of SwiftUI View.
I would refactor WKHostingController to return AnyView.
This seems to compile fine on my end.
class HostingController : WKHostingController<AnyView> {
override var body: AnyView {
return AnyView(ContentView().environmentObject(DataModel()))
}
}
For anyone like Brett (in the comments) who was getting
"Property 'body' with type 'AnyView' cannot override a property with type 'ContentView'"
I got the same error because I hadn't replaced the return value and wrapped the ContentView being returned.
ie. this is what my first attempt looked like.. notice the
WKHostingController<ContentView>
that should be
WKHostingController<AnyView>
class HostingController : WKHostingController<ContentView> {
override var body: AnyView {
return AnyView(ContentView().environmentObject(DataModel()))
}
}
Adding to Matteo's awesome answer,
If you want to use delegate then use like this:
class HostingController : WKHostingController<AnyView> {
override var body: AnyView {
var contentView = ContentView()
contentView.environmentObject(DataModel())
contentView.delegate = self
let contentWrapperView = AnyView(contentView)
return contentWrapperView
}
}