SwiftUI: How to pass NSManagedObjectContext into several viewmodels - swiftui

I'm trying to inject my core data viewcontext into several view models, so I don't need to pass one big view model throughout the whole app.
I'm using the SwiftUI lifecycle, so the NSManagedObjectContext is generated here:
#main
struct Core_Data_VM_TestApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
Following along this answer I didn't succeed, also after playing along with different initializers.
What I want to do is something like this, which isn't working ('Cannot use instance member 'viewContext' within property initializer; property initializers run before 'self' is available')
Content View
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#StateObject var itemVM = ItemViewModel(viewContext: viewContext)
(...)
Viewmodel:
class ItemViewModel: ObservableObject {
var viewContext: NSManagedObjectContext
init(viewContext: NSManagedObjectContext) {
self.viewContext = viewContext
}
}
Thanks for any help!

Just get it from your controller. The variable in the ViewModel will look something like
var viewContext: NSManagedObjectContext = PersistenceController.shared.container.viewContext
There is no need to pass it in the init.
Also, as a best practice if several anything are using something you should create an object for that something and put the common code there.
All the ViewModels can reference this single object. It will make changing things and debugging a million times easier as you app grows.

One approach is to not instantiate the #StateObject in the declaration, but in the view's init method. Note that the StateObject struct that wraps the itemVM property is stored as _itemVM:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#StateObject var itemVM: ItemViewModel
init() {
_itemVM = StateObject(wrappedValue: ItemViewModel(viewContext: viewContext))
}
// ...
}

Related

EnvironmentValues getter is being called on app launch without access

I have a custom EnvironmentValue defined as:
private struct DataProviderKey: EnvironmentKey {
static let defaultValue: DataProvider = {
fatalError()
}()
}
extension EnvironmentValues {
var dataProvider: DataProvider {
get {
self[DataProviderKey.self]
}
set {
self[DataProviderKey.self] = newValue
}
}
}
And main function looks like this:
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
MyView()
.environment(\.dataProvider, MyDataProvider())
}
}
}
My expectation is that the defaultValue won't be accessed as I inject the environment on App Launch. But before that happens, there's another call to the var dataProvider: DataProvider getter that I am not invoking.
Stack trace doesn't reveal much information as well:
So my question is why is the getter being invoked by the system? Even if I remove the access with keyPath .environment(\.dataProvider, MyDataProvider()) it still gets called at the same point in time. I don't want to provide a default value because I want to know whenever such value is not injected immediately.

Understanding SwiftUI syntax: What is actually defined inside var body?

Starting to learn SwiftUI I am a bit confused on how Viewss are implemented. View is actually not a type but a protocol which requires a var body of type View.
Problem 1: So the protocol requires it self. Is this not an infinite, recursive loop? Implementation of View requires a var body that implements View which requires a var body... How does this work?
Problem 2: var body is usually implemented as computed property. This is nothing unusual. However, the implementation does not return anything but only "creates" subviews which are not explicitly added to their parent view. Initializing the sub views is enough.
This is not only the case with the body var but with all other views which include other views like HStack, VStack, etc.
struct SomeView: View {
var body: some View {
ViewA()
OtherView()
VStack {
Sub1()
Sub2()
}
}
}
How can this work? Is this valid Swift syntax? I mean a "normal" computed property would look like this, wouldn't it?
var someValue: Int {
let value1 = getValue()
let value2 = SomeOtherValue()
return value1 + value2
}
And not like this:
var someValue: Int {
getValue()
SomeOtherValue()
}

Generic Struct 'ObservedObject' Requires That Conform To 'ObservableObject

I am having trouble with understanding why the code below will not compile. I'm getting an error stating that I must conform to ObservableObject and I cannot see why I wouldn't be.
I've simplified to show that I am seeing. I have two classes. The second observes the first and then the view observes the second.
First Class
import Foundation
import SwiftUI
import CoreBluetooth
class BLEPeripheralDevice: NSObject, ObservableObject {
#Published var bodySesnorLocation: String = ""
}
Second Class
import Foundation
import SwiftUI
import CoreBluetooth
class BLEManager: NSObject, ObservableObject {
#ObservedObject var blePeripheralDevice: BLEPeripheralDevice!
#Published var blePeripheralName: String = ""
}
View
import SwiftUI
struct BluetoothDeviceView: View {
#ObservedObject var bleManager = BLEManager()
var body: some View {
VStack (spacing: 10) {
Text("Bluetooth Devices")
}
}
When I compile this code I am getting an error in the second class on the following line.
#ObservedObject var blePeripheralDevice: BLEPeripheralDevice!
Generic struct 'ObservedObject' requires that 'BLEPeripheralDevice?'
conform to 'ObservableObject'
I don't understand why this would be. Any help is appreciated.
ObservedObject is a property wrapper mainly for Views. Use Published instead..
#Published var blePeripheralDevice: BLEPeripheralDevice!

Can I use #EnvironmentObject in SwiftUI for all shared data?

There are #State, #ObservedObject and #EnvironmentObject bindings in SwfitUI to share data between views and other objects. Each has its designated usage but #EnvironmentObject seems to be the most powerful and easiest to use. So, can I use it for all state variables and shared data? Are there any downsides to this?
First, #EnvironmentObject is for classes. So if you want to bind primitive type like Int - you can only use Binding.
Second, I think it will couse a problems when you try to define more then one #EnvironmentObject of same type. So, when you can use Binding - you should do that. Thats only my appinion.
class SomeClass: ObservableObject{
#Published var value: Int
init(value: Int){
self.value = value
}
}
struct ContentView: View {
#State var one: SomeClass = SomeClass(value: 1)
#State var two: SomeClass = SomeClass(value: 2)
var body: some View {
Adss().environmentObject(one).environmentObject(two)
}
}
struct Adss: View{
#EnvironmentObject var two: SomeClass
var body: some View{
Text("there must be two: \(two.value)")//prints "1"
}
}
you will have to define all the objects of needed type in straight order even if you don't need them

How to initialize an ObservedObject from a view created with a NavigationLink?

My apologies if this is not the right question to ask, as I am completely new to SwiftUI and iOS programming in general. The question indicates what I want to do, and the error I'm getting I believe is a red herring because of the SwiftUI compiler. It's likely that I am taking the incorrect approach to solving this problem altogether.
I am using XCode Version 11.2.1 (11B500)
View utilizing the ObservedObject:
struct Results: View {
var jobId: String
#ObservedObject var jobDetailService: JobDetailService
init(jobId: String) {
self.jobId = jobId
jobDetailService = JobDetailService(jobId: jobId)
}
var body: some View {
//... view code here
}
}
And it is within this view that I am getting the error (at the ZStack line) "Generic parameter 'C0' could not be inferred". When I comment out the NavigationLink block, the error goes away. Further, when the Results view does not depend on the jobId parameter (and we construct JobDetailService inline with #ObservedObject var jobDetailService = JobDetailService(), this all works. However, I need to be able to pass the jobId parameter to the JobDetailService in order to make the network call to fetch and publish the data.
struct JobList: View {
#ObservedObject var jobListService = JobListService()
var body: some View {
NavigationView {
List(jobListService.jobs) {job in
ZStack {
JobCard(name: job.fullName, date: job.lastUpdated)
NavigationLink(destination: Results(jobId: job.jobId)) {
EmptyView()
}
}
}
}
}
}
After reading this article, and thinking about Asperi's advice on not solely relying on initialization, I opted to do the following:
Remove custom initializer from JobDetailService and instead instantiate the service inside my Results view. Then in an .onAppear method on the Results view, call the getJobDetail method from JobDetailService which in turn makes the network call that populates the #ObservedObject. This allows me to pass in the parameters I need and control when the network call is made. Maybe not the right pattern for this problem but it works for my use case for now.
I assume the following should help you:
Declaration...
struct Results: View {
#ObservedObject var jobDetailService: JobDetailService
var body: some View {
//... view code here
}
}
... and usage
NavigationLink(destination: Results(jobDetailService: JobDetailService(jobId: jobId))) {
EmptyView()
}