I am using a List to display a CoreData one-to-many relationship models. The list displayed the computed property. When the NSManagedObject was changed, the computed property doesn't know about it.
Let's say People has many Books. Like the code below:
People+CoreDataProperties.swift
extension People {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Contract> {
return NSFetchRequest<People>(entityName: "People")
}
#NSManaged public var peopleID: String?
#NSManaged public var name: String?
#NSManaged public var books: Set<Book>?
#NSManaged public var bookOrders: [String]
}
The bookOrders is the ordered id of the books.
BookListView.swift
struct BookListView: View {
#state var people: People //this is from #FetchRequest
List {
ForEach(people.sortedBooks.indices, id:\.self) { index in
BookRowView(bookListItem: viewModel.books[index])
}
}
}
Here is the extension of People:
extension People {
var sortedBooks: [Book] {
let contractMapping = books.reduce(into: [String: Book]()) { (result, book) in
result[book.bookID] = book
}
return bookOrders.map {
contractMapping[$0]
}.compactMap { $0 }
}
So when I reorder the books, which say update the bookOrders or there's a background request update of adding the new books then how can I notify BookListView update the view?
Inside the BookListView declare your People as ObservedObject
struct BookListView: View {
#ObservedObject var people: People // << now View updates for any changes here
Related
Basically, I have this list view that can be sorted by PPD or CPD and based on the toggle, I want it to show one or the other. The problem is, when I click the toggle, the array is sorted but the list does not update properly. It's almost as if its late or something, When I initially click the toggle to sort list by PPD - no change occurs, then when i switch it back to sort by CPD, it is now sorted by PPD. The actual array is sorted in the correct order but the list itself is displaying the previous sort. Code below:
import SwiftUI
struct RestaurantMenu: View {
#ObservedObject var restaurant: Restaurant
//when Restaurant's #Published variable (sortedMenu) changes, we want list to update.
//it fucking should since restaurant is observedobject and menu is published, but it is either
//updating late, or only sometimes, out of sync, i got no clue
#State private var sortByPPD: Bool = false
var body: some View {
VStack{
Toggle(isOn: self.$sortByPPD) {
Text("Sort by PPD")
}.onChange(of: sortByPPD) { _ in
sortMenu(restaurant: restaurant, sortByCPD: !sortByPPD)
}
if(sortByPPD) {
List {
ForEach(restaurant.sortedMenu) { anItem in
PPDMenuItem(item: anItem)
}
}
.navigationBarTitle(Text("Menu"), displayMode: .inline)
}
else {
List {
ForEach(restaurant.sortedMenu) { anItem in
CPDMenuItem(item: anItem)
}
}
.navigationBarTitle(Text("Menu"), displayMode: .inline)
}
}
}
}
This is the class with the list that wont sort correctly ^
import Foundation
import CoreData
import SwiftUI
public class Restaurant: NSManagedObject, Identifiable {
#NSManaged public var name: String?
#NSManaged public var menu: NSSet?
#Published public var sortedMenu = [FoodItem]()
}
extension Restaurant {
static func allRestaurantsFetchRequest() -> NSFetchRequest<Restaurant> {
let fetchRequest = NSFetchRequest<Restaurant>(entityName: "Restaurant")
fetchRequest.sortDescriptors =
[NSSortDescriptor(key: "name", ascending: true)]
return fetchRequest
}
}
This is the Restaurant class (Note that Restaurant is a core data entity but the sortedMenu is not stored, this is intentional and the array is created on app launch and functioning fine.
//given a restaurant entity, return an array of fooditems sorted by cpd
public func sortMenu(restaurant: Restaurant, sortByCPD: Bool) {
if(sortByCPD) {
restaurant.sortedMenu.sort(by: { $0.cpd?.doubleValue ?? 0.0 > $1.cpd?.doubleValue ?? 0.0})
} else {
restaurant.sortedMenu.sort(by: { $0.ppd?.doubleValue ?? 0.0 > $1.ppd?.doubleValue ?? 0.0})
}
}
This is the sortMenu function that gets called when the toggle is switched.
How do I pass a bindable object into a view inside a ForEach loop?
Minimum reproducible code below.
class Person: Identifiable, ObservableObject {
let id: UUID = UUID()
#Published var healthy: Bool = true
}
class GroupOfPeople {
let people: [Person] = [Person(), Person(), Person()]
}
public struct GroupListView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
//MARK: Other properties
let group: GroupOfPeople = GroupOfPeople()
//MARK: Body
public var body: some View {
ForEach(group.people) { person in
//ERROR: Cannot find '$person' in scope
PersonView(person: $person)
}
}
//MARK: Init
}
public struct PersonView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
#Binding var person: Person
//MARK: Other properties
//MARK: Body
public var body: some View {
switch person.healthy {
case true:
Text("Healthy")
case false:
Text("Not Healthy")
}
}
//MARK: Init
init(person: Binding<Person>) {
self._person = person
}
}
The error I get is Cannot find '$person' in scope. I understand that the #Binding part of the variable is not in scope while the ForEach loop is executing. I'm looking for advice on a different pattern to accomplish #Binding objects to views in a List in SwiftUI.
The SwiftUI way would be something like this:
// struct instead of class
struct Person: Identifiable {
let id: UUID = UUID()
var healthy: Bool = true
}
// class publishing an array of Person
class GroupOfPeople: ObservableObject {
#Published var people: [Person] = [
Person(), Person(), Person()
]
}
struct GroupListView: View {
// instantiating the class
#StateObject var group: GroupOfPeople = GroupOfPeople()
var body: some View {
List {
// now you can use the $ init of ForEach
ForEach($group.people) { $person in
PersonView(person: $person)
}
}
}
}
struct PersonView: View {
#Binding var person: Person
var body: some View {
HStack {
// ternary instead of switch
Text(person.healthy ? "Healthy" : "Not Healthy")
Spacer()
// Button to change, so Binding makes some sense :)
Button("change") {
person.healthy.toggle()
}
}
}
}
You don't need Binding. You need ObservedObject.
for anyone still wondering... it looks like this has been added
.onContinuousHover(perform: { phase in
switch phase {
case .active(let location):
print(location.x)
case .ended:
print("ended")
}
})
I have the following Realm schema where a Race is done on a Track:
final class Race: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var track: Track?
#Persisted var duration: Int = 45
}
final class Track: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var name: String = "Imola"
#Persisted var country: String = "🇮🇹"
#Persisted(originProperty: "tracks") var group: LinkingObjects<TrackGroup>
}
final class TrackGroup: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var tracks = RealmSwift.List<Track>()
}
In my ContentView I have an Add Button that opens a sheet (AddRaceView). The new Race is already created when the sheet appears. Now, I want to use a Picker for the Track selection for our newly created Race.
The following code does not update the Track for the editable Race, and I do not understand why:
struct AddRaceView: View {
#ObservedRealmObject var race: Race
#ObservedRealmObject var trackGroup: TrackGroup
var body: some View {
Form {
chooseTrackSection
raceDurationSection
}
}
#State private var trackPickerVisible = false
var chooseTrackSection: some View {
Section(header: Text("Track")) {
Button {
withAnimation(.easeIn) {
self.trackPickerVisible.toggle()
}
} label: {
HStack {
Text(race.track?.name ?? "")
Spacer()
Image(systemName: "arrow.turn.right.down")
}
}
if trackPickerVisible {
// HERE: Selection is not processed.
Picker(selection: $race.track, label: Text("Track")) {
ForEach(trackGroup.tracks) {
Text($0.name)
}
}
.pickerStyle(.wheel)
}
}
}
Updating other values in Race (like duration) does work! When Track is a String for example, I can use the Picker to make a selection. The problem must be connected to the fact that I'm trying to change a Realm object/relationship.
There are three things that need to be taken into account:
Picker needs to know where to find the values. The value can be specified manually by adding .tag(value) to the elements. While ForEach provides implicit tags for objects that conform to Identifiable, the type doesn't match in your case (needs to be Optional<Track> instead of Track).
The Picker compares all tag values to the selection to find out which item is currently selected. The comparison fails if the objects are not from the same Realm instance. Unfortunately, there isn't currently any way to specify a Realm for ObservedResults or an ObservedRealmObject.
Referencing objects from a frozen Realm doesn't work, so they (or their Realm) have to be thawed first.
Code:
// Fetch the Tracks from the Race's Realm by finding the TrackGroup by primaryKey
if let tracks = race.realm?.thaw().object(ofType: TrackGroup.self, forPrimaryKey: trackGroup._id)?.tracks {
Picker("Track", selection: $race.track) {
ForEach(tracks) { track in
Text(track.name)
.tag(Optional(track)) // Specify the value, making it optional to exactly match the expected type
}
}
}
protocol Identifiable {
var id: String { get }
}
struct ModelA: Identifiable {
var id: String
}
struct ModelB: Identifiable {
var id: String
}
protocol ViewModelable {
associatedtype model: Identifiable
}
struct ViewModelA: ViewModelable {
typealias model = ModelA
}
class ViewA: UIView {
var viewModel: ViewModelA
}
struct ViewModelB: ViewModelable {
typealias model = ModelB
}
class ViewB: UIView {
var viewModel: ViewModelB
}
class CustomListView: UIView {
var viewModels<T: ViewModelable>: [T]?
var viewmodels:[ViewModelable]
}
I need to enforce a rule on the models which my viewmodel will hold. But the syntax in View1 is throwing a compilation error. (Protocol 'ViewModelable' can only be used as a generic constraint because it has Self or associated type requirements). So is there any other way i can enforce this?
My use case:
Custom List View is kind of container view which will have a tableview and render the views (of viewmodelA and viewmodelB)
You can enforce it during type definition.
class View1<T: ViewModelable>: UIView {
var viewmodels: [T] = []
}
Many standard APIs do this.
Array<Element>
Dictionary<Key: Hashable etc.
UPDATE
For accessing your properties through all these protocols back to id, you can do this.
protocol ViewModelable {
associatedtype Model: Identifiable
var model: Model { get }
}
struct ViewModelA: ViewModelable {
typealias Model = ModelA
var model: Model
}
struct ViewModelB: ViewModelable {
typealias Model = ModelB
var model: Model
}
class View1<T: ViewModelable>: UIView {
var viewModels: [T] = []
func doSomething() {
let firstModelID = viewModels.first?.model.id
}
}
UPDATE 2
Custom List View is kind of container view which will have a tableview and render the views (of viewmodelA and viewmodelB)
class CustomListView1<A: ViewModelable, B: ViewModelable>: UIView {
var viewModelAs: [A] = []
var viewModelBs: [B] = []
}
/// You can go one step further as well
class CustomListView2<A: ViewModelable, B: ViewModelable>: UIView
where A.Model == ModelA, B.Model == ModelB {
var viewModelAs: [A] = []
var viewModelBs: [B] = []
}
UPDATE 3
There are 5+ viewmodels and all of it are rendered in a tableview, so it cannot be held in separate arrays.
class View2: UIView {
enum ViewModel {
case viewModelA(ViewModelA)
case viewModelB(ViewModelB)
// Add as many variants as you want
var model: Identifiable {
switch self {
case .viewModelA(let vma): return vma.model
case .viewModelB(let vmb): return vmb.model
}
}
}
var viewModels: [ViewModel] = []
func doSomething() {
let firstModelID = viewModels.first?.model.id
}
}
I have a DataBase handled by DataCore. I am trying to retrieve any object of "assignment" and insert it to a View List. The assignment class itself is identifiable but I am getting an error while trying to create the List in the View :
Initializer 'init(_:id:rowContent:)' requires that 'Set<NSManagedObject>' conform to 'RandomAccessCollection'
Is the set itself is not identifiable even though the objects are identifiable ? How can I present all the objects in the set inside the View's list ?
The view:
import SwiftUI
struct AssignmentList: View {
#ObservedObject var assignmentViewModel = assignmentViewModel()
var body: some View {
VStack {
NavigationView {
//**The error is in the following line : **
List(assignmentViewModel.allAssignments, id: \.self) { assignment in
AssignmentRow(assignmentName: assignment.assignmentName, notes: assignment.notes) //This view works by itself and just present the data as text under HStack
}
.navigationBarTitle(Text("Assignments"))
}
Button(action: {
self.assignmentViewModel.retrieveAllAssignments()
}) {
Text("Retrieve")
}
}
}
}
This is the assignment class:
import Foundation
import CoreData
extension Assignment: Identifiable {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Assignment> {
return NSFetchRequest<Assignment>(entityName: "Assignment")
}
#NSManaged public var id: UUID?
#NSManaged public var assignmentName: String?
#NSManaged public var notes: String?
}
This is the ViewModel that connects to the view using binding:
class AssignmentViewModel : ObservableObject
{
private var assignmentModel = AssignmentModel()
/*AssignmentModel is a different class, we assume all methods working correctly and it's not a part of the question.*/
#Published var allAssignments : Set<Assignment>
init()
{
allAssignments=[]
}
func retrieveAllAssignment()
{
allAssignments=assignmentModel.retrieveAllAssignments()
}
}
Try the following
List(Array(assignmentViewModel.allAssignments), id: \.self) { assignment in