I want to get started with Core Data & SwiftUI and therefore created a new watchOS project using the latest Xcode 11.1 GM.
Then, I copied both persistentContainer & saveContext from a fresh iOS project (with Core Data enabled), to gain Core Data capabilities.
After that I modified the HostingController to return AnyView and set the variable in the environment.
class HostingController: WKHostingController<AnyView> {
override var body: AnyView {
let managedObjectContext = (WKExtension.shared().delegate as! ExtensionDelegate).persistentContainer.viewContext
return AnyView(ContentView().environment(\.managedObjectContext, managedObjectContext))
}
}
Now I can access the context inside the ContentView, but not in its sub views.
But thats not how it is intended to be? As far as I know, all sub views should inherit its environment from its super views, right?
Right now, to access it inside its sub views I simply set the environment variables again, like this:
ContentView.swift
NavigationLink(destination: ProjectsView().environment(\.managedObjectContext, managedObjectContext)) {
HStack {
Image(systemName: "folder.fill")
Text("Projects")
}
}
Once I remove the .environment() parameter inside ContentView, the App will crash, because there is no context loaded?!
The error message is Context in environment is not connected to a persistent store coordinator: <NSManagedObjectContext: 0x804795e0>.
ProjectsView.swift
struct ProjectsView: View {
#Environment(\.managedObjectContext) var managedObjectContext
[...]
}
But again, that can't be right? So, whats causing the error here?
I was able to solve this by fixing up HostingController and guaranteeing the CoreData stack was setup before view construction. First, let's make sure the CoreData stack is ready to go. In ExtensionDelegate:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
let persistentContainer = NSPersistentContainer(name: "Haha")
func applicationDidFinishLaunching() {
persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
// handle this
})
}
}
I had trouble when this property was lazy so I set it up explicitly. If you run into timing issues, make loadPersistentStores a synchronous call with a semaphore to debug and then figure out how to delay nib instantiation until the closure is called later.
Next, let's fix HostingController, by making a reference to a constant view context. WKHostingController is an object, not a struct. So now we have:
class HostingController: WKHostingController<AnyView> {
private(set) var context: NSManagedObjectContext!
override func awake(withContext context: Any?) {
self.context = (WKExtension.shared().delegate as! ExtensionDelegate).persistentContainer.viewContext
}
override var body: AnyView {
return AnyView(ContentView().environment(\.managedObjectContext, context))
}
}
Now, any subviews should have access to the MOC. The following now works for me:
struct ContentView: View {
#Environment(\.managedObjectContext) var moc: NSManagedObjectContext
var body: some View {
VStack {
Text("\(moc)")
SubView()
}
}
}
struct SubView: View {
#Environment(\.managedObjectContext) var moc: NSManagedObjectContext
var body: some View {
Text("\(moc)")
.foregroundColor(.red)
}
}
You should see the address of the MOC in white above and in red below, without calling .environment on the SubView.
In each view where you want to access your managedObjectContext you need to declare it like this:
#Environment(\.managedObjectContext) var context: NSManagedObjectContext
You don't set it on views, it gets passed around for you. And don't forget to import CoreData as well in those files.
Related
This question already has answers here:
SwiftUI - How to pass EnvironmentObject into View Model?
(7 answers)
Closed last year.
#EnviromentObject is passed down when the body is called, so it doesn't yet exist during the initialization phase of the View struct. Ok, that is clear. The question is, how do you solve the following problem?
struct MyCollectionScreen: View {
// Enviroment
#EnvironmentObject var viewContext: NSManagedObjectContext
// Internal dependencies
#ObservedObject private var provider: CoreDataProvider
init() {
provider = CoreDataProvider(viewContext: viewContext)
}
}
The previous doesn't compile and throws the error:
'self' used before all stored properties are initialized.
That is because I am trying to use the #EnviromentObject object before it is actually set.
For this, I am trying a couple of things but I am not super happy with any of them
1. Initialize my provider after the init() method
struct MyCollectionScreen: View {
// Enviroment
#EnvironmentObject var viewContext: NSManagedObjectContext
// Internal dependencies
#ObservedObject private var provider: CoreDataProvider
var body: some View {
}.onAppear {
loadProviders()
}
mutating func loadProviders() {
provider = CoreDataProvider(viewContext: viewContext)
}
}
but that, doesn't compile either and you get the following error within the onAppear block:
Cannot use mutating member on immutable value: 'self' is immutable
2. Pass the viewContext in the init method and forget about Environment objects:
This second solution works, but I don't like having to pass the viewContext to most of my views in the init methods, I kind of liked the Environment object idea.
struct MyCollectionScreen: View {
// Internal dependencies
#ObservedObject private var provider: CoreDataProvider
init(viewContext: NSManagedObjectContext) {
provider = CoreDataProvider(viewContext: viewContext)
}
}
You can make viewContext in CoreDataProvider an optional, and then set it onAppear
struct MyCollectionScreen: View {
#EnvironmentObject var viewContext: NSManagedObjectContext
#ObservedObject private var provider = CoreDataProvider()
var body: some View {
Text("Hello")
.onAppear {
provider.viewContext = viewContext
}
}
Downside is that you'll now have an optional.
I think that what I would do is to create CoreDataProvider at the same time viewContext is created, and pass it as an environment object as well.
When you start a new SwiftUI project in Xcode 12, and tick the 'Use Core Data' checkbox, you get some template code that'll set up a SwiftUI view with a managed object context injected into it through an environment value.
Like this:
struct CoreDataTestApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
In the above code, the persistenceController is a PersistenceController object (struct) that gives access to an NSManagedObjectContext, which is needed to work with Core Data in a SwiftUI view. Here's the relevant code:
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "CoreDataTest")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
...
})
}
}
This all works; it's the default/template code in Xcode 12. However, when you make the following change, it won't work anymore:
struct CoreDataTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
}
}
}
You'll get the following error: CoreData: error: No NSEntityDescriptions in any model claim the NSManagedObject subclass 'Item' so +entity is confused. Have you loaded your NSManagedObjectModel yet ?
Why?
I'm thinking that, since PersistenceController.shared is static and not weak, it's retained strongly, so memory management shouldn't be an issue. The persistenceController is merely a 'temporary' property – why can't you change one for the other? My guess is that it's got something to do with how the object is passed into the environment, but I'm not sure.
Any insights are greatly appreciated!
--
EDIT:
Core Data is used for the #FetchRequest in ContentView, here:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
List {
ForEach(items) { item in
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
.onDelete(perform: deleteItems)
}
...
Context
To performance operation on the Core Data object, the managed object context managedObjectContext is needed. The context is passed into View via the environment variable inside SceneDelegate when the project is generated with the "using Core Data" option checked (see below). A related question is Why does Core Data context object have to be passed via environment variable?
let contentView = MainView().environment(\.managedObjectContext, context)
However, when I try to pass the context into the View Model, it complains the following
Cannot use instance member 'context' within property initializer; property initializers run before 'self' is available
struct MainView: View {
#Environment(\.managedObjectContext) var context
// Computed property cannot be used because of the property wrapper
#ObservedObject var viewModel = ViewModel(context: context)
}
class ViewModel: ObservableObject {
var context: NSManagedObjectContext
}
Adding an init() to initialize the view model inside the view causes a different error which fails the build.
Failed to produce diagnostic for expression; please file a bug report
init() {
self.viewModel = ViewModel(context: context)
}
Question
So how can I use / get / pass the context inside the view model? What's the better way to get a context inside the view model?
Here is your scenario
let contentView = MainView(context: context) // << inject
.environment(\.managedObjectContext, context)
struct MainView: View {
#Environment(\.managedObjectContext) var context
#ObservedObject private var viewModel: ViewModel // << declare
init(context: NSManagedObjectContext) {
self.viewModel = ViewModel(context: context) // initialize
}
}
The CoreData context can get via the AppDelegate object.
import SwiftUI
class ViewModel: ObservableObject {
var context: NSManagedObjectContext
init() {
let app = UIApplication.shared.delegate as! AppDelegate
self.context = app.persistentContainer.viewContext
}
}
REFERENCE, https://kavsoft.dev/Swift/CoreData/
My data model property is declared in my table view controller, and the SwiftUI view is modally presented. I'd like the presented Form input to manipulate the data model. The resources I've found on data flow are just between SwiftUI views, and the resources I've found on UIKit integration are on embedding UIKit in SwiftUI rather than the other way around.
Furthermore, is there a good approach for a value type (in my case struct) data model, or would it be worth remodeling it as a class so that it's a reference type?
Let's analyse...
My data model property is declared in my table view controller and the SwiftUI view is modally presented.
So here is what you have now (probably simplified)
struct DataModel {
var value: String
}
class ViewController: UIViewController {
var dataModel: DataModel
// ... some other code
func showForm() {
let formView = FormView()
let controller = UIHostingController(rootView: formView)
self.present(controller, animating: true)
}
}
I'd like the presented Form input to manipulate the data model.
And here an update above with simple demo of passing value type data into SwiftUI view and get it back updated/modified/processed without any required refactoring of UIKit part.
The idea is simple - you pass current model into SwiftUI by value and return it back in completion callback updated and apply to local property (so if any observers are set they all work as expected)
Tested with Xcode 12 / iOS 14.
class ViewController: UIViewController {
var dataModel: DataModel
// ... some other code
func showForm() {
let formView = FormView(data: self.dataModel) { [weak self] newData in
self?.dismiss(animated: true) {
self?.dataModel = newData
}
}
let controller = UIHostingController(rootView: formView)
self.present(controller, animated: true)
}
}
struct FormView: View {
#State private var data: DataModel
private var completion: (DataModel) -> Void
init(data: DataModel, completion: #escaping (DataModel) -> Void) {
self._data = State(initialValue: data)
self.completion = completion
}
var body: some View {
Form {
TextField("", text: $data.value)
Button("Done") {
completion(data)
}
}
}
}
When it comes to organizing the UI code, best practices mandate to have 3 parts:
view (only visual structure, styling, animations)
model (your data and business logic)
a secret sauce that connects view and model
In UIKit we use MVP approach where a UIViewController subclass typically represents the secret sauce part.
In SwiftUI it is easier to use the MVVM approach due to the provided databinding facitilies. In MVVM the "ViewModel" is the secret sauce. It is a custom struct that holds the model data ready for your view to present, triggers view updates when the model data is updated, and forwards UI actions to do something with your model.
For example a form that edits a name could look like so:
struct MyForm: View {
let viewModel: MyFormViewModel
var body: some View {
Form {
TextField("Name", text: $viewModel.name)
Button("Submit", action: { self.viewModel.submit() })
}
}
}
class MyFormViewModel {
var name: String // implement a custom setter if needed
init(name: String) { this.name = name }
func submit() {
print("submitting: \(name)")
}
}
Having this, it is easy to forward the UI action to UIKit controller. One standard way is to use a delegate protocol:
protocol MyFormViewModelDelegate: class {
func didSubmit(viewModel: MyFormViewModel)
}
class MyFormViewModel {
weak var delegate: MyFormViewModelDelegate?
func submit() {
self.delegate?.didSubmit(viewModel: self)
}
...
Finally, your UIViewController can implement MyFormViewModelDelegate, create a MyFormViewModel instance, and subscribe to it by setting self as a delegate), and then pass the MyFormViewModel object to the MyForm view.
Improvements and other tips:
If this is too old-school for you, you can use Combine instead of the delegate to subscribe/publish a didSubmit event.
In this simple example the model is just a String. Feel free to use your custom model data type.
There's no guarantee that MyFormViewModel object stays alive when the view is destroyed, so probably it is wise to keep a strong reference somewhere if you want it survive for longer.
$viewModel.name syntax is a magic that creates a Binding<String> instance referring to the mutable name property of the MyFormViewModel.
I want to be able to pass a reference to a method on the UIViewRespresentable (or perhaps it’s Coordinator) to a parent View. The only way I can think to do this is by creating a field on the parent View struct with a class that I then pass to the child, which acts as a delegate for this behaviour. But it seems pretty verbose.
The use case here is to be a able to call a method from a standard SwiftUI Button that will zoom the the current location in a MKMapView that’s buried in a UIViewRepresentable elsewhere in the tree. I don’t want the current location to be a Binding as I want this action to be a one off and not reflected constantly in the UI.
TL;DR is there a standard way of having a parent get a reference to a child in SwiftUI, at least for UIViewRepresentables? (I understand this is probably not desirable in most cases and largely runs against the SwiftUI pattern).
I struggled with that myself, here's what worked using Combine and PassthroughSubject:
struct OuterView: View {
private var didChange = PassthroughSubject<String, Never>()
var body: some View {
VStack {
// send the PassthroughSubject over
Wrapper(didChange: didChange)
Button(action: {
self.didChange.send("customString")
})
}
}
}
// This is representable struct that acts as the bridge between UIKit <> SwiftUI
struct Wrapper: UIViewRepresentable {
var didChange: PassthroughSubject<String, Never>
#State var cancellable: AnyCancellable? = nil
func makeUIView(context: Context) → SomeView {
let someView = SomeView()
// ... perform some initializations here
// doing it in `main` thread is required to avoid the state being modified during
// a view update
DispatchQueue.main.async {
// very important to capture it as a variable, otherwise it'll be short lived.
self.cancellable = didChange.sink { (value) in
print("Received: \(value)")
// here you can do a switch case to know which method to call
// on your UIKit class, example:
if (value == "customString") {
// call your function!
someView.customFunction()
}
}
}
return someView
}
}
// This is your usual UIKit View
class SomeView: UIView {
func customFunction() {
// ...
}
}
I'm sure there are better ways, including using Combine and a PassthroughSubject. (But I never got that to work.) That said, if you're willing to "run against the SwiftUI pattern", why not just send a Notification? (That's what I do.)
In my model:
extension Notification.Name {
static let executeUIKitFunction = Notification.Name("ExecuteUIKitFunction")
}
final class Model : ObservableObject {
#Published var executeFuntionInUIKit = false {
willSet {
NotificationCenter.default.post(name: .executeUIKitFunction, object: nil, userInfo: nil)
}
}
}
And in my UIKit representable:
NotificationCenter.default.addObserver(self, selector: #selector(myUIKitFunction), name: .executeUIKitFunction, object: nil)
Place that in your init or viewDidLoad, depending on what kind of representable.
Again, this is not "pure" SwiftUI or Combine, but someone better than me can probably give you that - and you sound willing to get something that works. And trust me, this works.
EDIT: Of note, you need to do nothing extra in your representable - this simply works between your model and your UIKit view or view controller.
I was coming here to find a better answer, then the one I came up myself with, but maybe this does actually help someone?
It's pretty verbose though nevertheless and doesn't quite feel like the most idiomatic solution, so probably not exactly what the question author was looking for. But it does avoid polluting the global namespace and allows synchronous (and repeated) execution and returning values, unlike the NotificationCenter-based solution posted before.
An alternative considered was using a #StateObject instead, but I need to support iOS 13 currently where this is not available yet.
Excursion: Why would I want that? I need to handle a touch event, but I'm competing with another gesture defined in the SwiftUI world, which would take precedence over my UITapGestureRecognizer. (I hope this helps by giving some context for the brief sample code below.)
So what I came up with, was the following:
Add an optional closure as state (on FooView),
Pass it as a binding into the view representable (BarViewRepresentable),
Fill this from makeUIView,
So that this can call a method on BazUIView.
Note: It causes an undesired / unnecessary subsequent update of BarViewRepresentable, because setting the binding changes the state of the view representable though, but this is not really a problem in my case.
struct FooView: View {
#State private var closure: ((CGPoint) -> ())?
var body: some View {
BarViewRepresentable(closure: $closure)
.dragGesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onEnded { value in
self.closure?(value.location)
})
)
}
}
class BarViewRepresentable: UIViewRepresentable {
#Binding var closure: ((CGPoint) -> ())?
func makeUIView(context: UIViewRepresentableContext<BarViewRepresentable>) -> BazUIView {
let view = BazUIView(frame: .zero)
updateUIView(view: view, context: context)
return view
}
func updateUIView(view: BazUIView, context: UIViewRepresentableContext<BarViewRepresentable>) {
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else { return }
strongSelf.closure = { [weak view] point in
guard let strongView = view? else {
return
}
strongView.handleTap(at: point)
}
}
}
}
class BazUIView: UIView { /*...*/ }
This is how I accomplished it succesfully. I create the UIView as a constant property in the SwiftUI View. Then I pass that reference into the UIViewRepresentable initializer which I use inside the makeUI method. Then I can call any method (maybe in an extension to the UIView) from the SwiftUI View (for instance, when tapping a button). In code is something like:
SwiftUI View
struct MySwiftUIView: View {
let myUIView = MKMapView(...) // Whatever initializer you use
var body: some View {
VStack {
MyUIView(myUIView: myUIView)
Button(action: { myUIView.buttonTapped() }) {
Text("Call buttonTapped")
}
}
}
}
UIView
struct MyUIView: UIViewRepresentable {
let myUIView: MKMapView
func makeUIView(context: UIViewRepresentableContext<MyUIView>) -> MKMapView {
// Configure myUIView
return myUIView
}
func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MyUIView>) {
}
}
extension MKMapView {
func buttonTapped() {
print("The button was tapped!")
}
}