How to preview Core Data data inside SwiftUI Previews - swift

Here is a demo of what I have (kind of a lot of code, but I hope someone can follow it).
I have one entity inside Core Data named Activity with one string field. For that I use this extension to display the data in the Previews:
extension Activity {
var _name: String {
name ?? ""
}
static var example: Activity {
let controller = DataController(inMemory: true)
let viewContext = controller.container.viewContext
let activity = Activity(context: viewContext)
activity.name = "running"
return activity
}
}
For setting up Core Data I use a DataController object:
class DataController: ObservableObject {
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Model")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { storeDescription, error in
if let _ = error {
fatalError("Fatal error loading store")
}
}
}
static var preview: DataController = {
let dataController = DataController(inMemory: true)
let viewContext = dataController.container.viewContext
do {
try dataController.createSampleData()
} catch {
fatalError("Fatal error creating preview")
}
return dataController
}()
func createSampleData() throws {
let viewContext = container.viewContext
for _ in 1...10 {
let activity = Activity(context: viewContext)
activity.name = "run"
}
try viewContext.save()
}
}
In the app file I do the following setup:
struct TestApp: App {
#StateObject var dataController: DataController
#Environment(\.managedObjectContext) var managedObjectContext
init() {
let dataController = DataController()
_dataController = StateObject(wrappedValue: dataController)
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
In my ContentView I display a list of this string from Core Data, which works correctly:
struct ContentView: View {
let activities: FetchRequest<Activity>
init() {
activities = FetchRequest<Activity>(entity: Activity.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Activity.name, ascending: false)], predicate: nil)
}
var body: some View {
List {
ForEach(activities.wrappedValue) { activity in
ActivityView(activity: activity)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var dataController = DataController.preview
static var previews: some View {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
.environmentObject(dataController)
}
}
But in my ActivityView where I display the string in a simple text field, previewing doesn't work.
struct ActivityView: View {
let activity: Activity
init(activity: Activity) {
self.activity = activity
}
var body: some View {
Text(activity._name)
}
}
struct ActivityView_Previews: PreviewProvider {
static var previews: some View {
ActivityView(activity: Activity.example)
}
}
I can see the string "run" in my list, 10 times, the way it is setup, but in the ActivityView screen I don't see anything displayed in the preview.
Not sure why is that, I hope someone has an idea.
edit:
I also tried this in the preview, but still doesn't work.
struct ActivityView_Previews: PreviewProvider {
static var dataController = DataController.preview
static var previews: some View {
ActivityView(activity: Activity(context: dataController.container.viewContext))
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}

In SwiftUI we use the View hierarchy to convert from the rich model types to simple types. So the best way to solve this is to redesign ActivityView to work with simple types rather than the model type then it would be previewable without creating a managed object. I recommend watching Structure your app for SwiftUI previews which covers this technique and offers a few others like protocols and generics.
Btw I also noticed this problem:
init() {
let dataController = DataController()
_dataController = StateObject(wrappedValue: dataController)
}
StateObject init uses #autoclosure, e.g.
#inlinable public init(wrappedValue thunk: #autoclosure #escaping () -> ObjectType)
This means the object init needs to be inside the brackets, e.g.
_dataController = StateObject(wrappedValue: DataController())
This is what prevents the object from being init over and over again every time SwiftUI recalculates the View hierarchy.

Related

How do I access a managed object context in the environment from a view model

I am trying to access a managed object context that has been placed into the environment from a view model. Below are 3 code snippets. 1) A data controller class where the persistent store is loaded, 2) Placing the view context into the environment, 3) Attempting to access the view context in the view model. In the view model the variable moc is nil.
Do not know what I am doing wrong.
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "Index Funds")
init() {
container.loadPersistentStores{ description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
} // end init
}
#main
struct CoreData4App: App {
#StateObject var dataController: DataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
class VOOViewModel: ObservableObject {
#Environment(\.managedObjectContext) var moc
After some additional research I discovered the solution. Below are code snippets from 4 sections of the program. 1) The main, 2) the content view 3) the view model and 4) a core data manager class:
I found the solution in the following Youtube video.
https://www.youtube.com/watch?v=gGM_Qn3CUfQ
#main
struct CoreData4App: App {
let viewContext = CoreDataManager.shared.persistentStoreContainer.viewContext
var body: some Scene {
WindowGroup {
ContentView(vooVM: VOOViewModel(moc: viewContext))
.environment(\.managedObjectContext, viewContext)
} // end window group
} // end body
}
struct ContentView: View {
private var vooVM: VOOViewModel
init(vooVM: VOOViewModel) {
self.vooVM = vooVM
}
class VOOViewModel: ObservableObject {
private (set) var moc: NSManagedObjectContext
init(moc: NSManagedObjectContext) {
self.moc = moc
class CoreDataManager: ObservableObject {
let persistentStoreContainer: NSPersistentContainer
static let shared = CoreDataManager()
init() {
persistentStoreContainer = NSPersistentContainer(name: "Index Funds")
persistentStoreContainer.loadPersistentStores{ description, error in
if let error = error {
fatalError("Unable to initialize Core Data \(error)")
}
}
} // end init
}

How to trigger automatic SwiftUI Updates with #ObservedObject using MVVM

I have a question regarding the combination of SwiftUI and MVVM.
Before we start, I have read some posts discussing whether the combination of SwiftUI and MVVM is necessary. But I don't want to discuss this here, as it has been covered elsewhere. I just want to know if it is possible and, if yes, how. :)
So here comes the code. I tried to add the ViewModel Layer in between the updated Object class that contains a number that should be updated when a button is pressed. The problem is that as soon as I put the ViewModel Layer in between, the UI does not automatically update when the button is pressed.
View:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#ObservedObject var numberStorage = NumberStorage()
var body: some View {
VStack {
// Text("\(viewModel.getNumberObject().number)")
// .padding()
// Button("IncreaseNumber") {
// viewModel.increaseNumber()
// }
Text("\(numberStorage.getNumberObject().number)")
.padding()
Button("IncreaseNumber") {
numberStorage.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published var number: NumberStorage
init() {
self.number = NumberStorage()
}
func increaseNumber() {
self.number.increaseNumber()
}
func getNumberObject() -> NumberObject {
self.number.getNumberObject()
}
}
Model:
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func getNumberObject() -> NumberObject {
return self.numberObject
}
public func increaseNumber() {
self.numberObject.number+=1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
} ```
Looking forward to your feedback!
I think your code is breaking MVVM, as you're exposing to the view a storage model. In MVVM, your ViewModel should hold only two things:
Values that your view should display. These values should be automatically updated using a binding system (in your case, Combine)
Events that the view may produce (in your case, a button tap)
Having that in mind, your ViewModel should wrap, adapt and encapsulate your model. We don't want model changes to affect the view. This is a clean approach that does that:
View:
struct ContentView: View {
#StateObject // When the view creates the object, it must be a state object, or else it'll be recreated every time the view is recreated
private var viewModel = ViewModel()
var body: some View {
VStack {
Text("\(viewModel.currentNumber)") // We don't want to use functions here, as that will create a new object , as SwiftUI needs the same reference in order to keep track of changes
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published
private(set) var currentNumber: Int = 0 // Private set indicates this should only be mutated by the viewmodel
private let numberStorage = NumberStorage()
init() {
numberStorage.currentNumber
.map { $0.number }
.assign(to: &$currentNumber) // Here we're binding the current number on the storage to the published var that the view is listening to.`&$` basically assigns it to the publishers address
}
func increaseNumber() {
self.numberStorage.increaseNumber()
}
}
Model:
class NumberStorage {
private let currentNumberSubject = CurrentValueSubject<NumberObject, Never>(NumberObject())
var currentNumber: AnyPublisher<NumberObject, Never> {
currentNumberSubject.eraseToAnyPublisher()
}
func increaseNumber() {
let currentNumber = currentNumberSubject.value.number
currentNumberSubject.send(.init(number: currentNumber + 1))
}
}
struct NumberObject: Identifiable { // I'd not use this, just send and int directly
let id = UUID()
var number = 0
}
It's a known problem. Nested observable objects are not supported yet in SwiftUI. I don't think you need ViewModel+Model here since ViewModel seems to be enough.
To make this work you have to trigger objectWillChange of your viewModel manually when objectWillChange of your model is triggered:
class ViewModel: ObservableObject {
init() {
number.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: &cancellables)
}
}
You better listen to only the object you care not the whole observable class if it is not needed.
Plus:
Since instead of injecting, you initialize your viewModel in your view, you better use StateObject instead of ObservedObject. See the reference from Apple docs: Managing model data in your app
One way you could handle this is to observe the publishers in your Storage class and send the objectWillChange publisher when it changes. I have done this in personal projects by adding a class that all my view models inherit from which provides a nice interface and handles the Combine stuff like this:
Parent ViewModel
import Combine
class ViewModel: ObservableObject {
private var cancellables: Set<AnyCancellable> = []
func publish<T>(on publisher: Published<T>.Publisher) {
publisher.sink { [weak self] _ in self?.objectWillChange.send() }
.store(in: &cancellables)
}
}
Specific ViewModel
class ContentViewModel: ViewModel {
private let numberStorage = NumberStorage()
var number: Int { numberStorage.numberObject.number }
override init() {
super.init()
publish(on: numberStorage.$numberObject)
}
func increaseNumber() {
numberStorage.increaseNumber()
}
}
View
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
VStack {
Text("\(viewModel.number)")
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
Model/Storage
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func increaseNumber() {
self.numberObject.number += 1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
}
This results in the view re-rendering any time Storage.numberObject changes.

How to preview Core Data objects in Canvas when using a CoreDataViewModel

The following code effectively uses the #StateObject and the #ObservedObject wrappers to capture changes from an ObservableObject view model. Everything is working as expected, except that I would like to be able to display dummy data in Canvas for designing purposes. As you can see from the code below, I'm able to create a Car object inMemory and effectively retrieve it in the Preview struct, my issue is when trying to display multiple cars, as you can see, to display multiple cars I had to duplicate the code for the List in the Preview struct for the CarsView. In the code below it doesn't seem like a big deal because I removed a lot of the code that makes the List but in my production code, I have a lot of code to customize the rows.
Is this really the best way to display data usually managed by a ViewModel?
Is there a way to display data without having to duplicate a lot of the SwiftUI code?
SwiftUI Views
Parent View / Content View
struct ContentView: View {
#StateObject var coreDataViewModel = CoreDataViewModel()
var body: some View {
TabView(selection: 1){
CarsView(coreDataViewModel: coreDataViewModel)
.tabItem {
Text("Cars")
}.tag(1)
// other tabs...
}
}
}
Second View / Cars View
My issue is that here, I'm duplicating the code from the main view in the Preview struct.
struct CarsView: View {
#ObservedObject var coreDataViewModel:CoreDataViewModel
var body: some View {
List {
ForEach(coreDataViewModel.cars) { car in
// custom row to display cars
}
}
}
}
struct CarsView_Previews: PreviewProvider {
#Environment(\.managedObjectContext) private var viewContext
static var previews: some View {
CarsView(coreDataViewModel: CoreDataViewModel())
let context = CoreDataManager.preview.container.viewContext
let requestCar: NSFetchRequest<Car> = Car.fetchRequest()
let fetchedCar = (try! context.fetch(requestCar).first)!
let cars = [fetchedCar]
// repeated code
List{
ForEach(cars) { car in
Text(car.make ?? "")
}
}
}
}
Core Data
Entities and Attributes
Car
make: String
model: String
Core Data Manager
class CoreDataManager{
static let instance = CoreDataManager()
static var preview: CoreDataManager = {
let result = CoreDataManager(inMemory: true)
let viewContext = result.container.viewContext
// new car
let car = Car(context: viewContext)
car.make = "Ford"
car.model = "Mustang"
do {
try viewContext.save()
} catch {
}
return result
}()
let container: NSPersistentContainer
let context: NSManagedObjectContext
init(inMemory: Bool = false){
container = NSPersistentContainer(name: "CoreDataContainer")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { (description, error) in
if let error = error{
print("Error loading Core Data. \(error)")
}
}
context = container.viewContext
}
func save(){
do{
try context.save()
print("Saved successfully!")
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
Core Data View Model
class CoreDataViewModel: ObservableObject{
let manager = CoreDataManager.instance
#Published var cars: [Car] = []
init(){
getCars()
}
// addCar, deleteCar, updateCar etc. methods...
func getCars(){
let request = NSFetchRequest<Car>(entityName: "Car")
let sort = NSSortDescriptor(keyPath: \Car.model, ascending: true)
request.sortDescriptors = [sort]
do{
cars = try manager.context.fetch(request)
}catch let error{
print("Error fetching businesses. \(error.localizedDescription)")
}
}
func save(){
self.manager.save()
}
}
The point of a preview is to use the ordinary view with some given data and not to duplicate the code from the actual view. Apart from that you seem to have much in place.
I would inject the Core Data manager class into the view model to start with:
class CoreDataViewModel: ObservableObject{
let manager: CoreDataManager
#Published var cars: [Car] = []
init(coreDataManager: CoreDataManager = .instance){
self.manager = coreDataManager
getCars()
}
Then I would simplify the preview to
struct CarsView_Previews: PreviewProvider {
static var previews: some View {
CarsView(coreDataViewModel: CoreDataViewModel(coreDataManager: .preview))
}
}
You are already creating one Car object in the preview so why not do that in a loop, here I have moved it into a separate function that can be called from inside the preview declaration or separately
#if DEBUG
extension CoreDataManager {
func createMockCars() {
for i in 1...5 {
let car = Car(context: self.context)
car.make = "Make \(i)"
car.model = "Model \(i)"
}
try! self.context.save()
}
}
#endif
As mentioned in one of the comments you should consider renaming your view model to something to do with cars like CarsViewModel or CarListViewModel.

PreviewProvider and ObservedObject properties

how can I set a property to my CoreData Object which has the type CDObject, it has a property called name: String
My issue is now that I do not know how to set the name property in the PreviewProvider
Here is the code:
struct MainView: View {
#ObservedObject var obj: CDObject
var body: some View {
Text("Hello, World!")
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView(obj: CDObject())
}
}
I would like to do something like, before passing it to the View:
let itm = CDObject()
itm.name = "Hello"
If you are using the standard PersistenceController that comes with Xcode when you start a new project with CoreData just add the below method so Xcode returns the .preview container when you are running in preview.
public static func previewAware() -> PersistenceController{
//Identifies if XCode is running for previews
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"{
return PersistenceController.preview
}else{
return PersistenceController.shared
}
}
As for the rest you can use something like this.
import SwiftUI
import CoreData
struct SamplePreviewView: View {
#ObservedObject var item: Item
var body: some View {
Text(item.timestamp?.description ?? "nil")
}
}
struct SamplePreviewView_Previews: PreviewProvider {
static let svc = CoreDataPersistenceService()
static var previews: some View {
SamplePreviewView(item: svc.addSample())
}
}
class CoreDataPersistenceService: NSObject {
var persistenceController: PersistenceController
init(isTest: Bool = false) {
if isTest{
self.persistenceController = PersistenceController.preview
}else{
self.persistenceController = PersistenceController.previewAware()
}
super.init()
}
func addSample() -> Item {
let object = createObject()
object.timestamp = Date()
return object
}
//MARK: CRUD methods
func createObject() -> Item {
let result = Item.init(context: persistenceController.container.viewContext)
return result
}
}

SwiftUI: Preview with data in ViewModel

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.