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.
Related
Context
I have a SwiftUI View which gets initialised with a ViewModel (Observable Object). This ViewModel itself has a Generic Type.
I am now trying to use the Generic Type of the ViewModel inside the View itself, however, can't get hold of it.
Code
class ComponentViewModel<C: Component>: ObservableObject { ... }
struct SomeView: View {
#ObservedObject var componentVM: ComponentViewModel
init(with componentVM: ComponentViewModel) {
componentVM = componentVM
}
var body: some View {
switch componentVM.C.self { ... } // This does not work.
}
Question
How can I use the Generic Type of Types Property, in this case of componentVM?
You need to make your View generic as well
struct SomeView<ModelType: Component>: View
and then you use ModelType in your code to refer to the generic type of the view model
struct SomeView<ModelType: Component>: View {
#ObservedObject var componentVM: ComponentViewModel<ModelType>
init(with componentVM: ComponentViewModel<ModelType>) {
self.componentVM = componentVM
}
var body: some View {
switch ModelType.self {
//...
}
}
}
You should declare your componentVM like that:
#ObservedObject var componentVM: ComponentViewModel<SomeSolideType>
SomeSolidType should be some class / struct conforming to your Component protocol.
I'm trying to implement my app architecture loading in the app struct an object containing the data to be shared in all the views of the app through an environment object:
#main
struct SMT_testingApp: App {
#StateObject private var dataManager = DataManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(dataManager)
}
}
}
Here's the Datamanager class publishing the var containing the data:
class DataManager: ObservableObject {
#Published var SMTItemList: [SMTItem] = [SMTItem(id: UUID(), itemDesc: "", itemCreaDate: Date(), itemUpdDate: Date(), itemTags: [], linkedItemsUID: [])]
var urlFile: URL {
getDocumentsDirectory().appendingPathComponent("/SMT.json")
}
init() { loadData() }
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
func loadData() {
//...
After here the View that contain the instance of his view model:
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
List(viewModel.dataManager.SMTItemList) { item in
SMTItemView(item: item)
}
}
}
struct SMTItemView: View {
var item : SMTItem
var body: some View {
Text("Item desc: \(item.itemDesc)")
}
}
And finally, the view model that contains the environment object with the data.
extension ContentView {
class ViewModel: ObservableObject{
#EnvironmentObject var dataManager: DataManager
}
}
Now, the code is built correctly but at runtime I obtain this error in the content View:
What I'm doing wrong? Is correct implementing an architecture that way (one enviroment object with the data and many views/view models) ?
Thanks
#EnvironmentObject must be in view scope, not view model class. Like
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
#EnvironmentObject var dataManager: DataManager // << here !!
var body: some View {
List(dataManager.SMTItemList) { item in
SMTItemView(item: item)
}
}
}
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 ObservableObject class called MyObjectModel that is passed to a struct like this:
var body: some View {
MyView(myObjectModel)
But on another context I do not have a model to pass, so I want to call MyView simply as
var body: some View {
MyView()
So I thought I could initialize MyView like
struct MyView: View {
#ObservedObject private var model: MyObjectModel?
init(model: MyObjectModel? = nil) {
self.model = model
}
But Xcode will not let me use #ObservedObject on an optional.
How do I do that?
The ObservedObject wrapper does not have constructor with optional for now (and we cannot extend Optional with conformance to ObservableObject, because it is not a class).
A possible approach is to have some specific mark that there is no model, as it would be nil.
Here is a demo:
class MyObjectModel: ObservableObject {
static let None = MyObjectModel() // Like NSNull.null
}
struct MyView: View {
#ObservedObject private var model: MyObjectModel
init(model: MyObjectModel? = nil) {
self.model = model ?? MyObjectModel.None
}
var body: some View {
if self.model === MyObjectModel.None {
Text("Same as it would be nil!") // << here !!
} else {
Text("For real model")
}
}
}
I load my data from a viewModel which is loading data from web. Problem: I want to set some preview sample data to have content in preview window. Currently my preview contains an empty list as I do not provide data.
How can I achieve this?
struct MovieListView: View {
#ObservedObject var viewModel = MovieViewModel()
var body: some View {
List{
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
.listRowInsets(EdgeInsets())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListView()
}
}
class MovieViewModel: ObservableObject{
private let provider = NetworkManager()
#Published var movies = [Movie]()
init() {
loadNewMovies()
}
func loadNewMovies(){
provider.getNewMovies(page: 1) {[weak self] movies in
print("\(movies.count) new movies loaded")
self?.movies.removeAll()
self?.movies.append(contentsOf: movies)}
}
}
Here is possible approach (based on dependency-injection of view model members instead of tight-coupling)
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
// create Movie to be previewed inline, say from bundled data
MovieListView(viewModel: MovieViewModel(provider: nil, movies: [Movie(...)]))
}
}
class MovieViewModel: ObservableObject {
private var provider: NetworkManager?
#Published var movies: [Movie]
// same as before by default, but allows to modify if/when needed explicitly
init(provider: NetworkManager? = NetworkManager(), movies: [Movie] = []) {
self.provider = provider
self.movies = movies
loadNewMovies()
}
func loadNewMovies(){
provider?.getNewMovies(page: 1) {[weak self] movies in
print("\(movies.count) new movies loaded")
self?.movies.removeAll()
self?.movies.append(contentsOf: movies)
}
}
}
This question was written before #StateObject was introduced at WWDC 2020. I believe these days you'd want to use #StateObject instead of #ObservedObject because otherwise your view model can be re-initialized numerous times (which would result in multiple network calls in this case).
I wanted to do the exact same thing as OP, but with #StateObject. Here's my solution that doesn't rely on any build configurations.
struct MovieListView: View {
#StateObject var viewModel = MovieViewModel()
var body: some View {
MovieListViewInternal(viewModel: viewModel)
}
}
private struct MovieListViewInternal<ViewModel: MovieViewModelable>: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
List {
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
}
}
.onAppear {
viewModel.fetchMovieRatings()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListViewInternal(viewModel: PreviewMovieViewModel())
}
}
The View model protocols and implementations:
protocol MovieViewModelable: ObservableObject {
var movies: [Movie] { get }
func fetchMovieRatings()
// Define vars or funcs for anything else your view accesses in your view model
}
class MovieViewModel: MovieViewModelable {
#Published var movies = [Movie]()
init() {
loadNewMovies()
}
private func loadNewMovies() {
// do the network call
}
func fetchMovieRatings() {
// do the network call
}
}
class PreviewMovieViewModel: MovieViewModelable {
#Published var movies = [fakeMovie1, fakeMovie2]
func fetchMovieRankings() {} // do nothing while in a Preview
}
This way your external interface to MovieListView is exactly the same, but for your previews you can use the internal view definition and override the view model type.
Further to the answer above, and if you want to keep your shipping codebase clean, I've found that extending the class captured in PreProcessor flags to add a convenience init works.
#if DEBUG
extension MovieViewModel{
convenience init(forPreview: Bool = true) {
self.init()
//Hard code your mock data for the preview here
self.movies = [Movie(...)]
}
}
#endif
Then modify your SwiftUI structs using preprocessor flags as well:
struct MovieListView: View {
#if DEBUG
let viewModel: MovieViewModel
init(viewModel: MovieViewModel = MovieViewModel()){
self.viewModel = viewModel
}
#else
#StateObject var viewModel = MovieViewModel()
#endif
var body: some View {
List{
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
.listRowInsets(EdgeInsets())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListView(viewModel: MovieViewModel(forPreview: true)
}
}
So while #Kramer's solution works, I hit a challenge with it in the sense that when I would debug the app on my device it would load the preview data and not other "development" data that I would be wanting to be using.
So I extended the solution a little by creating a new build configuration called "Preview" and then wrapped all the 'preview' related data into that build configuration.
That gives me the option then to preview dummy data in the Xcode preview, while still allowing me then to build and debug a development build with development data on my devices/simulators.
So my solution now looks like this..
class MovieViewModel: ObservableObject {
init() {
#if PREVIEW
//Hard code your mock data for the preview here
self.movies = [Movie(...)]
#else
// normal init stuff here
#endif
}
}
struct MovieListView: View {
#if PREVIEW
let viewModel: MovieViewModel
init(viewModel: MovieViewModel = MovieViewModel()){
self.viewModel = viewModel
}
#else
#StateObject var viewModel = MovieViewModel()
#endif
var body: some View {
List{
ForEach(viewModel.movies) { movie in
MovieRow(movie: movie)
.listRowInsets(EdgeInsets())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MovieListView(viewModel: MovieViewModel())
}
}
Might not be the best crack at this, but gave me the flexibility to manage my Preview dummy data separate to my development/Debug data and has so far proven to work well for my use cases so far. :)
I have been struggling with this as well and came up with the following simple solution.
//View
struct MyView: View {
#StateObject private var viewModel = ViewModel()
init(forPreview: Bool = false) {
guard forPreview else { return }
let viewModel = ViewModel()
viewModel.title = "Preview" // Call internal func to load sample data
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
Text(viewModel.title)
}
}
//View Model
extension MyView {
#MainActor class ViewModel: ObservableObject {
#Published var title: String = "Standard"
}
}
//Previews
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(forPreview: true)
}
}
This initialization of #StateObject is Apple approved.