Create a for loop to generate a range of ViewModel #Published properties in Swift to match the CoreData model - swift

The DataModel contains amongst other attributes 27 attributes (Int16) with names of qq1, qq2, ... qq27. It seems silly to initialise each one in the ViewModel individually if we could use a for loop. So, I tried an init(), but the #Published wrapper must be a property of a class. I tried calling a function from the initialisation with various permutations, but it doesn't seem that we can call a function from within the initialisation. It's no big deal - I could just list all 27 in the initialisation, but it's ugly and can't be efficient. Does anyone have a solution? The relevant code snippet showing both attempts, below:
class OpportunityViewModel: ObservableObject {
#Published var opportunityName = ""
#Published var opportunityDescription = ""
#Published var opportunityEstimatedValue: Int32 = 0
#Published var opportunityDead = false
#Published var opportunityCreated: Date = Date()
#Published var opportunityUpdated: Date = Date()
#Published var qualifier(qualifiers) = 0
#Published var qq1 = 0 // This should be the result, but for qq1...qq27
#Published var opportunityItem: OpportunityEntity!
init() {
var q = ""
for qualifier in 1...27 {
q = String("qq" + "\(qualifier)")
#Published var q = 0
}
}
func qualifiers () -> String {
for qualifier in 1...27 {
return String("qq" + "\(qualifier)")
}
}
}

Related

How to get #Published updates in Combine with a Nested ObservableObject Array (Replicate ValueType Behavior)

I'm not really sure how to properly title the question, so hopefully it is made clear here.
the use case is mostly hypothetical at the moment, but if I have an array of published objects where Child is itself an ObservableObject:
#Published var children: [Child]
then, if I update an individual Child's property which is also published, I'd like the publisher to fire.
(if we use value types, it triggers the entire array and functions easily, this is best for most solutions in my experience)
TestCase
import XCTest
import Combine
final class CombineTests: XCTestCase {
final class RefChild: ObservableObject {
#Published var name: String = ""
}
struct ValueChild {
var name: String = ""
}
final class Parent: ObservableObject {
#Published var refChildren: [RefChild] = []
#Published var refNames: String = ""
#Published var valueChildren: [ValueChild] = []
#Published var valueNames: String = ""
init() {
$refChildren.map { $0.map(\.name).joined(separator: ".") } .assign(to: &$refNames)
$valueChildren.map { $0.map(\.name).joined(separator: ".") } .assign(to: &$valueNames)
}
}
func testChildPublish() {
let parent = Parent()
parent.refChildren = .init(repeating: .init(), count: 5)
parent.valueChildren = .init(repeating: .init(), count: 5)
XCTAssertEqual(parent.refNames, "....")
XCTAssertEqual(parent.valueNames, "....")
parent.refChildren[0].name = "changed"
// FAILS
XCTAssertEqual(parent.refNames, "changed....")
parent.valueChildren[0].name = "changed"
// PASSES
XCTAssertEqual(parent.valueNames, "changed....")
}
}

How to mutate properties of a structure in Swift?

I'm new to swift. I have a structure called Class that represents a class a student would take, I have a method called updateTotalGrade that updates the total grade based on the grade of the assignments. What I'm noticing is that I can't change the value of totalGrade after I've set it initially.
This is the code from a function my structure classCreate that creates and returns a class, I set the total grade to .72 and then use my method to update it.
var cl = Class(id: UUID(), className: "Physics 109: Physics in the Arts (002)", totalGrade: 0.72, categories: categories)
cl.updateTotalGrade()
This is the Class structure and the updateTotalGrade method. Basically the issue is that it doesn't change totalGrade ever, totalGrade is always .72
struct Class: Equatable, Identifiable{
var id = UUID()
// Class name
#State var className:String
// Total grade
#State var totalGrade:Double
// List of categories
#State var categories = [Category]()
static func == (class1: Class, class2: Class) -> Bool{
return class1.className == class2.className
}
func updateTotalGrade(){
// In case a category is empty, increase the relative weight of other categories
var totalWeight = 0.0
for category in categories{
if (!category.assignments.isEmpty){
totalWeight += category.categoryWeight
}
}
totalGrade = 0.0
for category in categories{
totalGrade += category.getGrade() * (category.categoryWeight/totalWeight)
}
}
}
Please help, thanks.
If you are not using SwiftUI, don't set properties as #State. Instead, mark updateTotalGrade() function as mutating:
var totalGrade: Double // #State removed
...
mutating func updateTotalGrade() {
...

Published computed properties in SwiftUI model objects

Suppose I have a data model in my SwiftUI app that looks like the following:
class Tallies: Identifiable, ObservableObject {
let id = UUID()
#Published var count = 0
}
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
#Published var elements: [Tallies] = []
}
I want to add a computed property to GroupOfTallies that resembles the following:
// Returns the sum of counts of all elements in the group
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count }
}
However, I want SwiftUI to update views when the cumulativeCount changes. This would occur either when elements changes (the array gains or loses elements) or when the count field of any contained Tallies object changes.
I have looked into representing this as an AnyPublisher, but I don't think I have a good enough grasp on Combine to make it work properly. This was mentioned in this answer, but the AnyPublisher created from it is based on a published Double rather than a published Array. If I try to use the same approach without modification, cumulativeCount only updates when the elements array changes, but not when the count property of one of the elements changes.
There are multiple issues here to address.
First, it's important to understand that SwiftUI updates the view's body when it detects a change, either in a #State property, or from an ObservableObject (via #ObservedObject and #EnvironmentObject property wrappers).
In the latter case, this is done either via a #Published property, or manually with objectWillChange.send(). objectWillChange is an ObservableObjectPublisher publisher available on any ObservableObject.
This is a long way of saying that IF the change in a computed property is caused together with a change of any #Published property - for example, when another element is added from somewhere:
elements.append(Talies())
then there's no need to do anything else - SwiftUI will recompute the view that observes it, and will read the new value of the computed property cumulativeCount.
Of course, if the .count property of one of the Tallies objects changes, this would NOT cause a change in elements, because Tallies is a reference-type.
The best approach given your simplified example is actually to make it a value-type - a struct:
struct Tallies: Identifiable {
let id = UUID()
var count = 0
}
Now, a change in any of the Tallies objects would cause a change in elements, which will cause the view that "observes" it to get the now-new value of the computed property. Again, no extra work needed.
If you insist, however, that Tallies cannot be a value-type for whatever reason, then you'd need to listen to any changes in Tallies by subscribing to their .objectWillChange publishers:
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
#Published var elements: [Tallies] = [] {
didSet {
cancellables = [] // cancel the previous subscription
elements.publisher
.flatMap { $0.objectWillChange }
.sink(receiveValue: self.objectWillChange.send)
.store(in: &cancellables)
}
}
private var cancellables = Set<AnyCancellable>
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count } // no changes here
}
}
The above will subscribe a change in the elements array (to account for additions and removals) by:
converting the array into a Sequence publisher of each array element
then flatMap again each array element, which is a Tallies object, into its objectWillChange publisher
then for any output, call objectWillChange.send(), to notify of the view that observes it of its own changes.
This is similar to the last option of #New Devs answer, but a little shorter, essentially just passing the objectWillChange notification to the parent object:
import Combine
class Tallies: Identifiable, ObservableObject {
let id = UUID()
#Published var count = 0
func increase() {
count += 1
}
}
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
var sinks: [AnyCancellable] = []
#Published var elements: [Tallies] = [] {
didSet {
sinks = elements.map {
$0.objectWillChange.sink( receiveValue: objectWillChange.send)
}
}
}
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count }
}
}
SwiftUI Demo:
struct ContentView: View {
#ObservedObject
var group: GroupOfTallies
init() {
let group = GroupOfTallies()
group.elements.append(contentsOf: [Tallies(), Tallies()])
self.group = group
}
var body: some View {
VStack(spacing: 50) {
Text( "\(group.cumulativeCount)")
Button( action: group.elements.first!.increase) {
Text( "Increase first")
}
Button( action: group.elements.last!.increase) {
Text( "Increase last")
}
}
}
}
The simplest & fastest is to use value-type model.
Here is a simple demo. Tested & worked with Xcode 12 / iOS 14
struct TestTallies: View {
#StateObject private var group = GroupOfTallies() // SwiftUI 2.0
// #ObservedObject private var group = GroupOfTallies() // SwiftUI 1.0
var body: some View {
VStack {
Text("Cumulative: \(group.cumulativeCount)")
Divider()
Button("Add") { group.elements.append(Tallies(count: 1)) }
Button("Update") { group.elements[0].count = 5 }
}
}
}
struct Tallies: Identifiable { // << make struct !!
let id = UUID()
var count = 0
}
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
#Published var elements: [Tallies] = []
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count }
}
}

How can I properly map RealmDB Results objects to SwiftUI Lists?

I am trying to display results of a realmdb query in a SwiftUI list but have trouble when deleting database objects.
I am trying to use something like this:
final class DBData: ObservableObject{
let didChange = PassthroughSubject<DBData, Never>()
private var notificationTokens: [NotificationToken] = []
var events = try! Realm().objects(ADMEvent.self)
#Published var events: [ADMEvent] = []
init() {
// Observe changes in the underlying model
self.notificationTokens.append(posts.observe { _ in
self.events = Array(self.posts)
self.didChange.send(self)
})
}
}
Which works if I display items In a list but the moment I use realm.deleteAll() the app crashes because it looks like Swift UI's list implementation is trying to diff the list, accessing the now invalidated realm db objects.
There are like 3 or 4 similar questions on stack overflow but they are all out of date in one way or another, or work but still have this issue when it comes to deletion.
Thanks!
Realm objects are live an autoupdating this is why they crash when you try to hold onto a deleted object. Instead of giving your publish subject the Realm.Object map it to a struct that has only the fields you need to use and use that array to drive SwiftUI.
struct Event: Identifiable {
var id: String
var name: String
var date: Date
}
final class DBData: ObservableObject {
private var notificationTokens: [NotificationToken] = []
var events = try! Realm().objects(ADMEvent.self)
#Published var publishedEvents: [ADMEvent] = []
init() {
// Observe changes in the underlying model
self.notificationTokens.append(posts.observe { _ in
self.publishedEvents = events.map { Event(id: $0.id, name: $0.name, date: $0.date)}
})
}
}
I love this approach! I just want to put this out there because the accepted answer won't compile and has more than one issue:
#Published var publishedEvents: [ADMEvent] = []
should be:
#Published var publishedEvents: [Event] = []
and
self.notificationTokens.append(posts.observe { _ in
should be:
self.notificationTokens.append(events.observe { _ in
so
final class DBData: ObservableObject {
private var notificationTokens: [NotificationToken] = []
var events = try! Realm().objects(ADMEvent.self)
#Published var publishedEvents: [Event] = []
init() {
// Observe changes in the underlying model
self.notificationTokens.append(events.observe { _ in
self.publishedEvents = events.map { Event(id: $0.id, name: $0.name, date: $0.date)}
})
}
}

RealmSwift replacement for RLMobject

Whats the correct way todo this with RealmSwift, it used to be RLMobject
var stream:Results<streams>
stream = Realm().objects(streams)
this first one lives on my class as a global the second line in my viewdidload
this is what i try todo: https://dpaste.de/AKKJ
class tabelviewcontroller has no initializers
the model
class streams: Object {
dynamic var br = ""
dynamic var categorie = 0
dynamic var ct = ""
dynamic var lc = ""
dynamic var ml = ""
dynamic var mt = ""
dynamic var name = ""
dynamic var shoutcatid = 0
dynamic var stationid = 0
override static func primaryKey() -> String? {
return "stationid"
}
}
A few quick notes, its good to keep class names singular, my model would look like this:
class Stream: Object {
...
}
If you want to get all objects from streams you can just do this:
let results = Realm().objects(Stream)
var stream:Results<streams>!
solved it