Preview in Canvas stops working as soon as I use an object passed by another view in SwiftUI - swift

I have an app that uses Core Data, everything works fine when I compile to the simulator or a physical device, the issue is that for some reason the Preview in Canvas doesn't work. In the code below I'm passing an Item from ItemsView to ItemView through a NavigationLink, the issue starts as soon as I use the passed item anywhere in ItemView. Again, the issue is only in Canvas. I know it's not a big deal since it compiles fine but I got used to seeing the preview when designing the interface.
Error message:
ItemsApp crashed due to an uncaught exception NSInvalidArgumentException. Reason: - [Item name]: unrecognized selector sent to instance 0x600001d6c080.: The preview process appears to have crashed.
Items View: Preview works fine.
import SwiftUI
struct ItemsView: View {
#ObservedObject var coreDataViewModel:CoreDataViewModel
var body: some View {
NavigationView{
VStack{
List {
ForEach(coreDataViewModel.items) { item in
HStack{
VStack(alignment:.leading){
Text(item.name ?? "")
Text(item.price ?? "")
}
NavigationLink(destination: ItemView(coreDataViewModel: coreDataViewModel, selectedItem: item)){
}
}
}
}
}
}
}
}
Item View: Preview doesn't work. The issue starts when I call Text(selectedItem.name ?? "--")
import SwiftUI
struct ItemView: View {
#ObservedObject var coreDataViewModel: CoreDataViewModel
#State var selectedItem: Item
var body: some View {
VStack{
HStack{
Text(selectedItem.name ?? "--") // this causes the issue
}
}
.onAppear{
Text(selectedItem.name ?? "--") // this causes the issue
}
}
}
struct ItemView_Previews: PreviewProvider {
static var previews: some View {
ItemView(coreDataViewModel: CoreDataViewModel(), selectedItem: Item())
}
}
Any idea what could be wrong?
Am I passing the item correctly?
Thanks
EDIT:
Corrected view name from ServicesView to ItemView in NavigationLink and Previews. Also added the error message.
EDIT:
Added CoreDataManager and CoreDataViewModel
CoreDataManager
class CoreDataManager{
static let instance = CoreDataManager()
let container: NSPersistentContainer
let context: NSManagedObjectContext
init(){
container = NSPersistentContainer(name: "CoreDataContainer")
container.loadPersistentStores { (description, error) in
if let error = error{
print("Error loading Core Data. \(error)")
}
}
context = container.viewContext
}
func save(){
do{
try context.save()
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
CoreDataViewModel
class CoreDataViewModel: ObservableObject{
let manager = CoreDataManager.instance
#Published var items: [Item] = []
init(){
getItems()
}
func addItem(name: String, price: String){
let item = Item(context: manager.context)
item.name = name
item.price = price
save()
getItems()
}
func getItems(){
let request = NSFetchRequest<Item>(entityName: "Item")
let sort = NSSortDescriptor(keyPath: \Item.name, ascending: true)
request.sortDescriptors = [sort]
do{
items = try manager.context.fetch(request)
}catch let error{
print("Error fetching businesses. \(error.localizedDescription)")
}
}
func save(){
self.manager.save()
}
}

Here are the steps to follow:
In your Persistance struct declare a variable preview with your preview Items:
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let newItem = Item(context: viewContext)
newItem.yourProperty = yourValue
do {
try viewContext.save()
} catch {
// error handling
}
return result
}()
Create item from your viewContext and pass it to preview:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
let context = PersistenceController.preview.container.viewContext
let request: NSFetchRequest<Item> = Item.fetchRequest()
let fetchedItem = (try! context.fetch(request).first)!
YourView(item: fetchedItem)
}
}
Here is Persistence struct created by Xcode at the moment of the project initialization:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let item = Item(context: viewContext)
item.property = yourProperty
do {
try viewContext.save()
} catch {
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}

Related

Unable to show image form Core data on offline Mode In swift UI

I am using AsyncImage to download the image and display it into view. I have created the respective properties into core data including image type is string to save the data locally . I am trying to load the into offline mode , it was able to show the rest of the properties without Image and it showing blank ..
Here is the code for Core Data manager .
class CoreDataManager {
let persistentContainer: NSPersistentContainer
static let shared: CoreDataManager = CoreDataManager()
private init() {
persistentContainer = NSPersistentContainer(name: "ReditModel")
persistentContainer.loadPersistentStores { description, error in
if let error = error {
// fatalError("Unable to initialize Core Data \(error)")
print("Unable to save the data :\(error.localizedDescription)")
}
}
}
}
Here is the code for main ..
#main
struct CoreDataDemoApp: App {
#StateObject private var viewModel = RedditViewModel()
let persistentContainer = CoreDataManager.shared.persistentContainer
var body: some Scene {
WindowGroup {
ContentView().environment(\.managedObjectContext, persistentContainer.viewContext).environmentObject(viewModel)
}
}
}
Here is the view model..
#MainActor
class RedditViewModel: ObservableObject {
#Published private(set) var stories = [Story]()
private var redditService: RedditService
init(redditService: RedditService = RedditService()) {
self.redditService = redditService
}
// Swift 5.5
func fetchData(viewContext: NSManagedObjectContext) async {
let url = NetworkURLs.urlBase
do {
let response = try await redditService.getModel(from: url)
let stories = response.data.children.map { $0.data }
self.stories = stories
saveRecord(viewContext: viewContext)
} catch (let error) {
print(error)
}
}
public func saveRecord(viewContext: NSManagedObjectContext) {
do {
stories.forEach { story in
let redit = ReditEntity(context: viewContext)
redit.title = story.title
redit.numComments = Int64(story.numComments)
redit.score = Int64(story.score)
redit.urlImage = story.thumbnail
}
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
Here is the code for Row view .
struct RowView: View {
#EnvironmentObject var viewModel: RedditViewModel
let title: String
let comments: String
let score: String
let urlImage: String?
var body: some View {
VStack(alignment: .leading) {
HStack {
if let urlImage = urlImage, urlImage.contains("https"), let url = URL(string: urlImage) {
AsyncImage(url: url)
}
VStack(alignment: .leading) {
HeadTitleView(title: title)
Text("Comments: \(comments)")
Text("Score: \(score)")
Spacer()
}
}
}
}
}
Here is the content view ..
struct ContentView: View {
#EnvironmentObject private var viewModel: RedditViewModel
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: ReditEntity.entity(), sortDescriptors: [])
private var dbStories: FetchedResults<ReditEntity>
var dbFatchReditRecord: NSFetchRequest<ReditEntity> = ReditEntity.fetchRequest()
var body: some View {
VStack {
Text("Reddit Service")
.font(.largeTitle)
List {
ForEach(dbStories) { story in
// custom cell
RowView(title: story.title ?? "", comments: "\(story.numComments)", score: "\(story.score)", urlImage: story.urlImage)
}
}
}
.onAppear {
if dbStories.isEmpty {
Task {
await viewModel.fetchData(viewContext: viewContext)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let persistedContainer = CoreDataManager.shared.persistentContainer
ContentView().environment(\.managedObjectContext, persistedContainer.viewContext) }
}
Here is the screenshot when I run the app on Offline model ..
Out of curiosity: How are you supposed to be able to load a https url while being offline?
-> urlImage.contains("https")
You should download the image and store it on your local file system. For example give your data model 2 attributes: offlineUrlString and onlineUrlString. Then as a fallback if the online url results in an empty image, then use the offline image. Once you are online again, update the offline image again.

Error only when the app is launched in an iPhone. Thread 1: EXC_BAD_ACCESS (code=1, address=0x4052c00000000000)

I am writing a program that uses data from coredata and creates a graph out of it. When I run it, it crashes with the following error Thread 1: EXC_BAD_ACCESS (code=1, address=0x4052c00000000000). The code starts from ContentView as follows
struct ContentView: View {
#State private var name = ""
#State private var score = ""
var reviewItem: ReviewItem
#State private var displayNewView = false
var body: some View {
NavigationView{
VStack {
NavigationLink(destination: ReviewView(reviewItem: ReviewItem()), isActive: self.$displayNewView) { EmptyView() }
Form {
TextField("Name", text: $name)
TextField("Score", text: $score)
}
.toolbar {
Button("add") {
reviewItem.saveReviewData(name: name, score: Int32(score)!)
self.displayNewView = true
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(reviewItem: ReviewItem())
}
}
This is what the code for reviewView looks like
struct GradeData: Identifiable {
var id = UUID()
var averageGrade: Int32
var condition: String
}
struct ReviewView: View {
var gradeVShours: [GradeData]
init(reviewItem: ReviewItem) {
gradeVShours = [
GradeData(averageGrade: reviewItem.getScore(scoreType: "high"), condition: "Well"), GradeData(averageGrade: reviewItem.getScore(scoreType: "low"), condition: "Not Well")]
}
var body: some View {
if #available(iOS 16.0, *) {
Chart {
ForEach(self.gradeVShours) { shape in
BarMark (
x: .value("Total Grade", shape.averageGrade),
y: .value("Grade Condition", shape.condition)
)
}
}
} else {
Text("Upgrade to ios version 16.0 or higher")
}
}
}
This is what the code for ReviewItem looks like
class ReviewItem: NSObject, ObservableObject {
let coreDM: DataController = DataController.shared
override init() {
super.init()
}
func getListScore(scoreType: String) -> [Int32] {
let score = coreDM.getScoreFromCoreData(scoreType: scoreType)
return score
}
func getScore(scoreType: String) -> Int32 {
var listScore = getListScore(scoreType: scoreType)
let totalCount = scoreType.count
let totalSum = listScore.reduce(0, +)
return totalSum/Int32(totalCount)
}
func saveReviewData(name: String, score: Int32) {
coreDM.saveToCoreData(name: name, score: score)
}
}
This is what the code in DataController looks like. It is used to fetch the data from coredata. And this is where the error is thrown after the app crashes. I have noted the line where the error is thrown.
class DataController: ObservableObject {
static var shared = DataController()
let container = NSPersistentContainer(name: "StudentData")
init() {
container.loadPersistentStores(completionHandler: { description, error in
if let error = error {
print("Core data failed to laod: \(error.localizedDescription)")
}
})
}
func getScoreFromCoreData(scoreType: String) -> [Int32] {
// let filter: NSPredicate
var scoreData: [GradeTable]
var listScore: [Int32] = []
let fetchRequest: NSFetchRequest<GradeTable> = GradeTable.fetchRequest()
fetchRequest.returnsObjectsAsFaults = false
if scoreType == "high" {
let filter = NSPredicate(format: "score >= %#", 85) //Error thrown here
fetchRequest.predicate = filter
}
else {
let filter = NSPredicate(format: "score <= %#", 85)
fetchRequest.predicate = filter
}
do {
scoreData = try container.viewContext.fetch(fetchRequest)
} catch let error {
print("ERROR while fetching data from db array \(error.localizedDescription)")
return []
}
for item in scoreData {
listScore.append(item.score)
}
return listScore
}
func saveToCoreData(name: String, score: Int32) {
let gradeTable = GradeTable(context: container.viewContext)
gradeTable.uuid = UUID()
gradeTable.name = name
gradeTable.score = score
do {
try container.viewContext.save()
print("Saved")
} catch let error {
print("Error: \(error.localizedDescription)")
}
}
}
Note that StudentData has an entity named GradeTable which has attributes named score, name and uuid which is of data type Int32, string and UUID. The app launches fine and works well in simulator, but it crashes with the following error when I launch it in my phone.
I can't figure out what's going on. Using breakpoints didn't help much. Will you please tell me what's going on and how to fix it?
The problem is that your DataController instance will load the persistent store in an asynchronous operation, while the view immediately creates the fetch request before the persistent store is loaded completely.
You would want to move the initialization way before the view creation, such as in the SceneDelegate.
If you are having this issue, make sure to check the data type of the filter in Nspredicate. In my case, I was using an integer as a filter, and it was the reason behind the error. Using a string instead of an integer fixed it.
This doesn't work because the filter(85) is an integer.
let filter = NSPredicate(format: "score >= %#", 85) //Error thrown here
This works because the filter("85") is a string.
let filter = NSPredicate(format: "score >= %#", "85")

Error fetching abstract entity in preview

I have an abstract entity 'Animal' with a name attribute which is parent to two other entities 'Cat' and 'Dog'. I get the broken preview message in the canvas the moment I set fetchrequest to 'Animal' (works without problems setting any other entity or sub-entity)
Here's how Core Data stack is set up:
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let newCat = Cat(context: viewContext)
newCat.name = "Cat1"
let newDog = Dog(context: viewContext)
newDog.name = "Dog1"
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Zoo")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error {
print(error.localizedDescription)
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
Main app file:
struct ZooApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
And here's ContentView:
struct ContentView: View {
#Environment(\.managedObjectContext) var viewContext
#FetchRequest(sortDescriptors: []) var animals: FetchedResults<Animal>
var body: some View {
VStack {
ForEach(animals) { animal in
Text(animal.name ?? "")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let persistenceController = PersistenceController.preview
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
I tried every possible answer I could find on the web (manually generating the class files and adding #objc(Entity) wrapper, setting module to current product module, changing the previews in every way, making the 'Animal' class non-abstract). Still, the app crashes in the previews. I found that this only happens in the preview canvas, works fine in the simulator and on my iPhone. I am currently using the latest Xcode build, but I had the same issue in Xcode 13.
Any ideas why this is happening?

SwiftUI. Preview with CoreData – populating, singleton and weird behaviour

I have a small app which can add and save arbitrary Events to CoreData.
And I'm struggling with preparing of preview data.
Correct behaviour
In preview window I see one text with content "Test event"
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
//init coredata
let container = NSPersistentContainer(name: "Model")
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
container.loadPersistentStores() { description, error in
if let error = error {
print("Can't load core data \(error.localizedDescription)")
}
}
//adding event
let event = Event(context: container.viewContext)
event.id = UUID()
event.name = "Test"
try! container.viewContext.save()
//getting events
let request = Event.fetchRequest()
let events = try! container.viewContext.fetch(request)
//view
return VStack {
ForEach(events, id: \.id) { event in
Text("\(event.id?.uuidString ?? "") \(event.name ?? "") fs")
}
}
}
But if I try to move logic to DataController, I get 7 Text blocks with the same name, but different UUIDs
class DataController {
static let preview = DataController()
private(set) var container: NSPersistentContainer
private init(inMemory: Bool = false) {
self.container = NSPersistentContainer(name: "Model")
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
container.loadPersistentStores() { description, error in
if let error = error {
fatalError("Can't load core data \(error.localizedDescription)")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static let dataController = DataController.preview
static var previews: some View {
//adding event
let event = Event(context: dataController.container.viewContext)
event.id = UUID()
event.name = "Test event"
try! dataController.container.viewContext.save()
//getting events
let request = Event.fetchRequest()
let events = try! dataController.container.viewContext.fetch(request)
//view
return VStack {
ForEach(events, id: \.id) { event in
Text("\(event.id?.uuidString ?? "") \(event.name ?? "")")
}
}
}
}
What can cause such behaviour?
Model for Event was manually generated. The only thing I changed – commenting #objc(Event), because with this statement preview doesn't work at all.

Value in SwiftUI not being passed

I am trying to pass that a values to let my item know what group it should be added to in Core Data.
Here are some pictures of my App.
Please note that itemsInGroup fetches all of the items that should be in the group.
After adding breakpoints in my app, the value of the group where the Item entity is being add is equal to nil. This should have a value (which is set when the add item button is pressed).
Thank you for your help in advance.
Main part of the code
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
enum ActiveSheet: Identifiable {
case first, second
var id: Int {
hashValue
}
}
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Group.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Group>
#State var activeSheet: ActiveSheet?
#State private var selectedGroup: Group? = nil
var body: some View {
NavigationView {
List {
ForEach(items) { group in
// Text("Item at \(item.timestamp!, formatter: itemFormatter)")
Text(group.title ?? "New group")
ForEach(group.itemsInGroup) { item in
Text(item.title ?? "New Item")
}
Button(action: {
selectedGroup = group
activeSheet = .second
}, label: {
Text("Add Item")
})
}
.onDelete(perform: deleteItems)
}
.toolbar {
Button(action: {
activeSheet = .first
}) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(item: $activeSheet) { item in
switch(item) {
case .first: AddGroupName()
case .second: AddItemView(group: selectedGroup)
}
}
}
private func addItem() {
withAnimation {
let newItem = Group(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
Where I add a new item
struct AddItemView: View {
#Environment(\.managedObjectContext) private var viewContext
let group: Group?
#State var title = ""
var body: some View {
VStack {
Form {
TextField("Title", text: $title)
Button(action: {
let item = Item(context: viewContext)
item.group = group
item.title = title
try? viewContext.save()
}, label: {
Text("Save")
})
}
}
}
}
Where I add a new group
struct AddGroupName: View {
#Environment(\.managedObjectContext) private var viewContext
#State var title = ""
var body: some View {
VStack {
Form {
TextField("Title", text: $title)
Button(action: {
let item = Group(context: viewContext)
item.title = title
item.timestamp = Date()
try? viewContext.save()
}, label: {
Text("Save")
})
}
}
}
}
My Core Data Model
Why is the group value not being passed and saved correctly? It should be saved in the selectedGroup variable in the main part of the code.
When I try and add an item and save it to the core data database I get this error "Illegal attempt to establish a relationship 'group' between objects in different contexts"
Please note that I have tried setting selectedGroup equal to a value other than nil, but then this initial value is used when trying to add an item.
You should declare your group property using the #Binding property wrapper in AddItemView
#Binding var group: Group
This way the #State property selectedGroup will be updated when the AddItemView is dismissed