Cannot convert value of type 'NSSet?' to expected argument type 'Range<Int>' (using CoreData) - core-data

Using CoreData I don't know how to solve that problem:
Error: Cannot convert value of type 'NSSet?' to expected argument type 'Range<Int>'
private func displayTopics(subject: Subject) -> some View {
NavigationView {
Form {
List {
ForEach(subject.topics) { topic in
NavigationLink(
destination: Text("Destination"),
label: {
Text("Navigate")
})
}
}
}
}
What should I do?

ForEach does not understand NSSet, you have to convert it in array:
if subject.topics != nil {
ForEach(Array(subject.topics! as Set), id: \.self) { topic in
NavigationLink(
destination: Text("Destination"),
label: {
Text("Navigate")
})
}
}

Related

How to use a function in another swift file that contains CoreData related properties?

clueless beginner here. Apologies if my question is formed poorly. This is the first time I ask a question of this scale and I have a hard time balancing between posting too much code for the good samaritans to read and too little to post an effective question. Huge thanks in advance!
I am trying to incorporate the textfieldalert in this post in my learner project. There are two Swift files in questions: File A (PosTextFieldAlertView) has an extension that needs to use two functions in File B (ListView).
These are the functions I need to use in File A.
func addPositive(){
let newPositive = PositiveEntity(context: viewContext)
newPositive.title = alertInput
save()
}
func save() {
do { try viewContext.save() } catch { print(error) }
}
I thought of/researched two methods: 1) duplicate the function in File A or 2) create instance of the view in File B that contains that functions according this post. However I ran into problems in both methods.
Duplicating the functions:
I copied the CoreData related properties in the PosTextFieldAlert struct. But now PosTextFieldAlert in the return part of the extension has the error of "Missing arguments for parameters [Core Data properties] in call". I don’t know how to set the property in the extension without referring or creating a different sets of Core Data entities.
Creating an instance of the relevant view
In the instance creation I would need to input the arguments but I don’t know how to refer to the same NSManagedObjectContext.
Code excerpts:
PosTextFieldAlertView
struct PosTextFieldAlert<Presenting>: View where Presenting: View {
var viewContext: NSManagedObjectContext
var positives: [PositiveEntity]
var targets: [TargetEntity]
#State private var alertInput = ""
// let listView = ListView(viewContext: NSManagedObjectContext, positives: PositiveEntity, negatives: NegativeEntity, targets: TargetEntity)
#Binding var isShowing: Bool
#Binding var text: String
let presenting: Presenting
let title: String
var body: some View {
GeometryReader { (deviceSize: GeometryProxy) in
ZStack {
self.presenting
.disabled(isShowing)
VStack {
Text(self.title)
TextField(self.title, text: self.$text)
Divider()
VStack{
HStack {
Button(action: {
withAnimation {
self.isShowing.toggle()
}
}) {
Text("+")
}.padding()
Button(action: {
withAnimation {
self.isShowing.toggle()
}
}) {
Text("-")
}.padding()
}
Button(action: {
withAnimation {
self.isShowing.toggle()
}
}) {
Text("Done")
}
}
}
.padding()
.background(Color.white)
.frame(
width: deviceSize.size.width*0.7,
height: deviceSize.size.height*0.7
)
.shadow(radius: 1)
.opacity(self.isShowing ? 1 : 0)
}
}
}
func addPositive(){
let newPositive = PositiveEntity(context: viewContext)
newPositive.title = alertInput
save()
}
func save() {
do { try viewContext.save() } catch { print(error) }
}
}
extension View {
func posTextFieldAlert(isShowing: Binding<Bool>,
text: Binding<String>,
title: String) -> some View {
PosTextFieldAlert(isShowing: isShowing,
text: text,
presenting: self,
title: title)
}
}
The code in ListView
struct ListView: View {
var viewContext: NSManagedObjectContext
var positives: [PositiveEntity]
var negatives: [NegativeEntity]
var targets: [TargetEntity]
//[layout of the project]
}
The Fetchrequests in ContentView:
#Environment(\.managedObjectContext) var viewContext
#FetchRequest(sortDescriptors: []) var targets: FetchedResults<TargetEntity>
#FetchRequest(sortDescriptors: []) var positives: FetchedResults<PositiveEntity>
#FetchRequest(sortDescriptors: []) var negatives: FetchedResults<NegativeEntity>

Updating SwiftUI view from Core Data non scalar attribute

I have a Core Data NSManagedObject entity Person with a Bool attribute that I generate as a NSNumber. (the "Use Scalar Type" checkbox is not used, so the Bool attribute becomes an NSNumber)
I am trying to observe this attribute, employed, to control the UI.
#ObservedObject var person: Person
var body: some View {
List {
Section {
HStack {
Toggle(isOn: $person.employed) { <-- 1
Text("Showing employed content..")
}
}
}
if person.employed.boolValue {
Section { } etc
I get a warning at "1" saying: Cannot convert value of type 'Binding<NSNumber?>' to expected argument type 'Binding<Bool>'
How can I make use of the employed attribute as a bool without changing it to a scalar?
Note: $person.employed.boolValue would not work it seems, and I would also have to account for the optional part.
One possible way to do it is via a custom Binding:
Toggle("Showing employed content..", isOn: Binding<Bool>(get: {
person.employed?.boolValue == true
}, set: { value in
person.employed = NSNumber(value: value)
}))
Now, if you prefer this to be a computed property instead, you could do something like this:
var isEmployed: Binding<Bool> {
Binding<Bool>(get: {
person.employed?.boolValue == true
}, set: { value in
person.employed = NSNumber(value: value)
})
}
var body: some View {
Toggle("Showing employed content..", isOn: isEmployed)
}
Also, here is a possible implementation of an extension that handles optional NSNumber backed booleans (excuse my terible naming):
extension Binding where Value == NSNumber? {
var boolBinding: Binding<Bool> {
Binding<Bool>(get: {
self.wrappedValue == true
}, set: { value in
self.wrappedValue = NSNumber(value: value)
})
}
}
which can be used like this:
Toggle("Showing employed content..", isOn: $person.employed.boolBinding)

Why is my NSFetchRequest not updating my array as I expect? And does `shouldRefreshRefetchedObjects` make any difference?

I do not understand why this code does not work to update a list when navigating "back" from a DetailView(). As far as I can tell, I'm calling a new fetchRequest each time I want to update the list and it seems that request should always return object with current properties. But as others have said they are "stale", reflecting whatever was the property BEFORE the update was committed in the DetailView. And tapping a Navigation link from a "Stale" row, opens a DetailView with the current values of the properties, so I know they have been sacved to the context (haven't they?).
First I have a "dataservice" like this:
import CoreData
import SwiftUI
protocol CategoryDataServiceProtocol {
func getCategories() -> [Category]
func getCategoryById(id: NSManagedObjectID) -> Category?
func addCategory(name: String, color: String)
func updateCategory(_ category: Category)
func deleteCategory(_ category: Category)
}
class CategoryDataService: CategoryDataServiceProtocol {
var viewContext: NSManagedObjectContext = PersistenceController.shared.viewContext
///Shouldn't this next function always return an updated version of my list of categories?
func getCategories() -> [Category] {
let request: NSFetchRequest<Category> = Category.fetchRequest()
let sort: NSSortDescriptor = NSSortDescriptor(keyPath: \Category.name_, ascending: true)
request.sortDescriptors = [sort]
///This line appears to do nothing if I insert it:
request.shouldRefreshRefetchedObjects = true
do {
///A print statement here does run, so it's getting this far...
print("Inside get categories func")
return try viewContext.fetch(request)
} catch {
return []
}
}
func getCategoryById(id: NSManagedObjectID) -> Category? {
do {
return try viewContext.existingObject(with: id) as? Category
} catch {
return nil
}
}
func addCategory(name: String, color: String) {
let newCategory = Category(context: viewContext)
newCategory.name = name
newCategory.color = color
saveContext()
}
func updateCategory(_ category: Category) {
saveContext()
}
func deleteCategory(_ category: Category) {
viewContext.delete(category)
saveContext()
}
func saveContext() {
PersistenceController.shared.save()
}
}
class MockCategoryDataService: CategoryDataService {
override init() {
super .init()
self.viewContext = PersistenceController.preview.viewContext
print("MOCK INIT")
func addCategory(name: String, color: String) {
let newCategory = Category(context: viewContext)
newCategory.name = name
newCategory.color = color
saveContext()
}
}
}
And I have a viewModel like this:
import SwiftUI
extension CategoriesList {
class ViewModel: ObservableObject {
let dataService: CategoryDataServiceProtocol
#Published var categories: [Category] = []
init(dataService: CategoryDataServiceProtocol = CategoryDataService()) {
self.dataService = dataService
}
func getCategories() {
self.categories = dataService.getCategories()
}
func deleteCategories(at offsets: IndexSet) {
offsets.forEach { index in
let category = categories[index]
dataService.deleteCategory(category)
}
}
}
}
Then my view:
import SwiftUI
struct CategoriesList: View {
#StateObject private var viewModel: CategoriesList.ViewModel
init(viewModel: CategoriesList.ViewModel = .init()) {
_viewModel = StateObject(wrappedValue: viewModel)
}
#State private var isShowingSheet = false
var body: some View {
NavigationView {
List {
ForEach(viewModel.categories) { category in
NavigationLink(
destination: CategoryDetail(category: category)) {
CategoryRow(category: category)
.padding(0)
}
}
.onDelete(perform: { index in
viewModel.deleteCategories(at: index)
viewModel.getCategories()
})
}
.listStyle(PlainListStyle())
.onAppear(perform: {
viewModel.getCategories()
})
.navigationBarTitle(Text("Categories"))
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: { EditButton() })
ToolbarItem(placement: .navigationBarTrailing) {
Button(
action: {
isShowingSheet = true
viewModel.getCategories()
},
label: { Image(systemName: "plus.circle").font(.system(size: 20)) }
)
}
}
.sheet(isPresented: $isShowingSheet, onDismiss: {
viewModel.getCategories()
}, content: {
CategoryForm()
})
}
}
}
struct CategoriesList_Previews: PreviewProvider {
static var previews: some View {
let viewModel: CategoriesList.ViewModel = .init(dataService: MockCategoryDataService())
return CategoriesList(viewModel: viewModel)
}
}
So, when I navigate to the DetailView and change the name of the category, all is fine there. But then tapping the back button or swiping to return to the view - and the view still shows the old name.
I understand that the #Published array of [Category] is probably not looking at changes to objects inside the array, only if an object is removed or added, I guess.
But why is my list not updating anyways, since I am calling viewModel.getCategories() and that is triggering the fetch request in the dataservice getCategories function?
And if Combine is the answer, then how? Or what else am I missing? Does request.shouldRefreshRefetchedObjects = true offer anything? Or is it a bug as I read here: https://mjtsai.com/blog/2019/10/17/core-data-derived-attributes/

How (or should?) I replace MVVM in this CoreData SwiftUI app with a Nested managedObjectContext?

I've written this small example of MVVM in a SwiftUI app using CoreData, but I wonder if there are better ways to do this such as using a nested viewcontext?
The object of the code is to not touch the CoreData entity until the user has updated all the fields needed and taps "Save". In other words, to not have to undo any fields if the user enters a lot of properties and then "Cancels". But how do I approach this in SwiftUI?
Currently, the viewModel has #Published vars which take their cue from the entity, but are not bound to its properties.
Here is the code:
ContentView
This view is pretty standard, but here is the NavigationLink in the List, and the Fetch:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Contact.lastName, ascending: true)],
animation: .default)
private var contacts: FetchedResults<Contact>
var body: some View { List {
ForEach(contacts) { contact in
NavigationLink (
destination: ContactProfile(contact: contact)) {
Text("\(contact.firstName ?? "") \(contact.lastName ?? "")")
}
}
.onDelete(perform: deleteItems)
} ///Etc...the rest of the code is standard
ContactProfile.swift in full:
import SwiftUI
struct ContactProfile: View {
#ObservedObject var contact: Contact
#ObservedObject var viewModel: ContactProfileViewModel
init(contact: Contact) {
self.contact = contact
self._viewModel = ObservedObject(initialValue: ContactProfileViewModel(contact: contact))
}
#State private var isEditing = false
#State private var errorAlertIsPresented = false
#State private var errorAlertTitle = ""
var body: some View {
VStack {
if !isEditing {
Text("\(contact.firstName ?? "") \(contact.lastName ?? "")")
.font(.largeTitle)
.padding(.top)
Spacer()
} else {
Form{
TextField("First Name", text: $viewModel.firstName)
TextField("First Name", text: $viewModel.lastName)
}
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarBackButtonHidden(isEditing ? true : false)
.navigationBarItems(leading:
Button (action: {
withAnimation {
self.isEditing = false
viewModel.reset() /// <- Is this necessary? I'm not sure it is, the code works
/// with or without it. I don't see a
/// difference in calling viewModel.reset()
}
}, label: {
Text(isEditing ? "Cancel" : "")
}),
trailing:
Button (action: {
if isEditing { saveContact() }
withAnimation {
if !errorAlertIsPresented {
self.isEditing.toggle()
}
}
}, label: {
Text(!isEditing ? "Edit" : "Done")
})
)
.alert(
isPresented: $errorAlertIsPresented,
content: { Alert(title: Text(errorAlertTitle)) }) }
private func saveContact() {
do {
try viewModel.saveContact()
} catch {
errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred"
errorAlertIsPresented = true
}
}
}
And the ContactProfileViewModel it uses:
import UIKit
import Combine
import CoreData
/// The view model that validates and saves an edited contact into the database.
///
final class ContactProfileViewModel: ObservableObject {
/// A validation error that prevents the contact from being 8saved into
/// the database.
enum ValidationError: LocalizedError {
case missingFirstName
case missingLastName
var errorDescription: String? {
switch self {
case .missingFirstName:
return "Please enter a first name for this contact."
case .missingLastName:
return "Please enter a last name for this contact."
}
}
}
#Published var firstName: String = ""
#Published var lastName: String = ""
/// WHAT ABOUT THIS NEXT LINE? Should I be making a ref here
/// or getting it from somewhere else?
private let moc = PersistenceController.shared.container.viewContext
var contact: Contact
init(contact: Contact) {
self.contact = contact
updateViewFromContact()
}
// MARK: - Manage the Contact Form
/// Validates and saves the contact into the database.
func saveContact() throws {
if firstName.isEmpty {
throw ValidationError.missingFirstName
}
if lastName.isEmpty {
throw ValidationError.missingLastName
}
contact.firstName = firstName
contact.lastName = lastName
try moc.save()
}
/// Resets form values to the original contact values.
func reset() {
updateViewFromContact()
}
// MARK: - Private
private func updateViewFromContact() {
self.firstName = contact.firstName ?? ""
self.lastName = contact.lastName ?? ""
}
}
Most of the viewmodel code is adapted from the GRDB Combine example. So, I wasn't always sure what to exclude. what to include.
I have opted to avoid a viewModel in this case after discovering:
moc.refresh(contact, mergeChanges: false)
Apple docs: https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext/1506224-refresh
So you can toss aside the ContactViewModel, keep the ContentView as is and use the following:
Contact Profile
The enum havae been made an extension to the ContactProfile view.
import SwiftUI
import CoreData
struct ContactProfile: View {
#Environment(\.managedObjectContext) private var moc
#ObservedObject var contact: Contact
#State private var isEditing = false
#State private var errorAlertIsPresented = false
#State private var errorAlertTitle = ""
var body: some View {
VStack {
if !isEditing {
Text("\(contact.firstName ?? "") \(contact.lastName ?? "")")
.font(.largeTitle)
.padding(.top)
Spacer()
} else {
Form{
TextField("First Name", text: $contact.firstName ?? "")
TextField("First Name", text: $contact.lastName ?? "")
}
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarBackButtonHidden(isEditing ? true : false)
.navigationBarItems(leading:
Button (action: {
/// This is the key change:
moc.refresh(contact, mergeChanges: false)
withAnimation {
self.isEditing = false
}
}, label: {
Text(isEditing ? "Cancel" : "")
}),
trailing:
Button (action: {
if isEditing { saveContact() }
withAnimation {
if !errorAlertIsPresented {
self.isEditing.toggle()
}
}
}, label: {
Text(!isEditing ? "Edit" : "Done")
})
)
.alert(
isPresented: $errorAlertIsPresented,
content: { Alert(title: Text(errorAlertTitle)) }) }
private func saveContact() {
do {
if contact.firstName!.isEmpty {
throw ValidationError.missingFirstName
}
if contact.lastName!.isEmpty {
throw ValidationError.missingLastName
}
try moc.save()
} catch {
errorAlertTitle = (error as? LocalizedError)?.errorDescription ?? "An error occurred"
errorAlertIsPresented = true
}
}
}
extension ContactProfile {
enum ValidationError: LocalizedError {
case missingFirstName
case missingLastName
var errorDescription: String? {
switch self {
case .missingFirstName:
return "Please enter a first name for this contact."
case .missingLastName:
return "Please enter a last name for this contact."
}
}
}
}
This also requires the code below that can be found at this link:
SwiftUI Optional TextField
import SwiftUI
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
Binding(
get: { lhs.wrappedValue ?? rhs },
set: { lhs.wrappedValue = $0 }
)
}

Swift UI, removing item from array, while looping in it throws Fatal Error: Index out of range [duplicate]

I am trying to remove rows inside a ForEach. Removing the last row always throws an index out of range exception. Removing any other row does not.
ForEach(Array(player.scores.enumerated()), id: \.element) { index, score in
HStack {
if self.isEditSelected {
Button(action: {
self.player.scores.remove(at: index)
}, label: {
Image("delete")
})
}
TextField("\(score)", value: self.$player.scores[index], formatter: NumberFormatter())
}
}
I have tried using ForEach(player.indices...) & ForEach(player.scores...), but see the same problem.
Looks to me like the crash happens here self.$player.scores[index], as hardcoding the index to any value other that the last row is working.
Does anyone know how to fix this? Or if there is a better approach.
Here is fix
ForEach(Array(player.scores.enumerated()), id: \.element) { index, score in
HStack {
if self.isEditSelected {
Button(action: {
self.player.scores.remove(at: index)
}, label: {
Image("delete")
})
}
TextField("\(score)", value: Binding( // << use proxy binding !!
get: { self.player.scores[index] },
set: { self.player.scores[index] = $0 }),
formatter: NumberFormatter())
}
}
Based on #Asperi answer
public extension Binding where Value: Equatable {
static func proxy(_ source: Binding<Value>) -> Binding<Value> {
self.init(
get: { source.wrappedValue },
set: { source.wrappedValue = $0 }
)
}
}
You can use this as follows:
TextField("Name", text: .proxy($variable))
Xcode 13.0 beta introduced a new way to establish two-way-bindings between the elements of a collection and the views built by ForEach / List.
This method fixes the crash related to deleting the last row.
struct Score: Identifiable {
let id = UUID()
var value: Int
}
struct Player {
var scores: [Score] = (1...10).map {_ in .init(value: Int.random(in: 0...25))}
}
struct BindingTest: View {
#State private var player = Player()
var body: some View {
List {
ForEach($player.scores) { $score in
HStack {
TextField("\(score.value)", value: $score.value,
formatter: NumberFormatter())
}
}
.onDelete { player.scores.remove(atOffsets: $0)}
}
}
}

Resources