How to mutate properties of a structure in Swift? - 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() {
...

Related

Update upcoming values in sink if needed

I'm currently trying to modify an upcoming value from a textField which is using a Binding<Double>, but haven't found any working solution yet. It's only been infinite loops (Like the example below) and other solutions which didn't work in the end anyway. So, for example, if an user inputs an amount which is too low, I would want to change the upcoming value to the minimum and vice verse if the value is higher than the maximum value.
I also want to present the modified value (if needed) for the user, so I can't just store it in another variable.
Any ideas on how to solve this?
Example
class ViewModel: ObservableObject {
#Published var amount: Double
private var subscriptions: Set<AnyCancellable> = []
private let minimum: Double = 10_000
private let maximum: Double = 100_000
init() {
$amount
.sink {
if $0 < self.minimum {
// Set minimum value
self.amount = self.minimum
} else if $0 > self.maximum {
// Set maximum value
self.amount = self.maximum
}
// If `Else` is implemented it will just be an infinite loop...
else {
self.amount = $0
}
}
.store(in: &subscriptions)
}
func prepareStuff() {
// Start preparing
let chosenAmount = amount
}
}
One way is to use a property wrapper to clamp the values.
Here is a very basic example of the issue, where we have an amount, that we can change to any value. The Stepper just makes it easy for input/testing:
struct ContentView: View {
#State private var amount = 0
var body: some View {
Form {
Stepper("Amount", value: $amount)
Text(String(amount))
}
}
}
The problem with this example is that amount isn't limited to a range. To fix this, create a Clamping property wrapper (partially from here):
#propertyWrapper
struct Clamping<Value: Comparable> {
private var value: Value
let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(range.lowerBound, newValue), range.upperBound) }
}
var clampedValue: Value {
get { wrappedValue }
set { wrappedValue = newValue }
}
init(wrappedValue value: Value, _ range: ClosedRange<Value>) {
precondition(range.contains(value))
self.value = value
self.range = range
}
}
And then we can chain property wrappers, and get a working example where amount is limited:
struct ContentView: View {
#State #Clamping(-5 ... 5) private var amount = 0
var body: some View {
Form {
Stepper("Amount", value: $amount.clampedValue)
Text(String(amount))
}
}
}
I know, this isn't the proper way to limit a Stepper's range. Instead you should use Stepper(_:value:in:). However, this is to instead demonstrate clamping a value - not how to clamp a Stepper.
What does this mean you need to do?
Well, first off change your #Published property to this:
#Published #Clamping(10_000 ... 100_000) var amount: Double
And now you can just access amount like normal to get the clamped value. Use $amount.clampedValue like I did in my solution to get your Binding<Double> binding.
If having troubles sometimes with compiling chained property wrappers (probably a bug), here is my example recreated using a Model object and #Published:
struct ContentView: View {
#StateObject private var model = Model(amount: 0)
var body: some View {
Form {
Stepper("Amount", value: $model.amount.clampedValue)
Text(String(model.amount.clampedValue))
}
}
}
class Model: ObservableObject {
#Published var amount: Clamping<Int>
init(amount: Int) {
_amount = Published(wrappedValue: Clamping(wrappedValue: amount, -5 ... 5))
}
}

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 to make deinit take effect in swift

I have a Car class. Let's say a car goes to the junkyard, this car should no longer be counted in the total population. I have the deinit function, but how do I systematically remove a car from the car population? In other words, how do I get the deinit to take effect?
I have a class variable isJunk but don't know how to use it to make this work.
class Car {
static var population: Int = 0
var isJunk: Bool = false
var color: String
var capacity: Int
var driver: Bool?
var carOn: Bool = false
init (carColor: String, carCapacity: Int) {
self.capacity = carCapacity
self.color = carColor
Car.population += 1
}
deinit {
Car.population -= 1
}
func startCar() {
self.carOn = true
}
}
class Car {
static var population: Int = 0
init() {
Car.population += 1
}
deinit {
Car.population -= 1
}
}
var cars: [Car] = [Car(), Car()]
print("Population:", Car.population) // "Population: 2"
// now the second car is removed from array and we have no other references to it
// it gets removed from memory and deinit is called
cars.removeLast()
print("Population:", Car.population) // "Population: 1"
However, the same can be achieved just by asking the number of items in cars array. And that's usually a better alternative than a private counter of instances.
To keep the items in memory you will always need some kind of register (e.g. an array) for them. And that register can keep them counted.
One possibility:
class CarPopulation {
var liveCars: [Car] = []
var junkCars: [Car] = []
}
Or you can keep them in one array and set junk on the car and count non-junk cars when needed:
class CarPopulation {
var cars: [Car] = []
func liveCars() -> Int {
return self.cars.filter { !$0.junk }.count
}
}
There are many possibilities but extracting the counters to some other class that owns the cars is probably a better solution.
The deinit is called when you deallocate your instance of Car (when you entirely get rid of the instance of the object). When you put a Car instance in the junkyard I don't think you want to get rid of the instance of Car, you really just want to change its location. I would suggest a different function to handle changing the location of Car.
Perhaps:
func changeLocation(newLocation: String) {
// Perhaps add an instance variable to 'remember' the location of the car
switch newLocation {
case "junkyard":
Car.population -= 1
default:
// Perhaps check whether previous location was Junkyard and increment
// counter if the Car is coming out of the Junkyard
print("Unrecognized location")
}
}

Swift - A function with no parameters with return value

I am learning Swift and am writing a basic card game init function where I want to use a function to setup some decks.
Swift keeps complaining that I'm missing an argument in parameter #1, but there aren't any parameters, nor am I wanting any.
Game class is as follows
class Game
{
// MARK: ** Private vars **
private var gameState: GameState?
private var playerOnTurn: Player?
private var seedCash:Int?
// MARK: ** Public vars **
lazy var players = [Player]()
var chequesDeck:Deck = Deck()
var propertiesDeck:Deck = Deck()
init()
{
self.gameState = .Initialize
self.playerOnTurn = nil // No player on turn when game is initialized
self.seedCash = kInitialSeedCash
}
func setup(numberOfPlayers:Int)
{
// Create decks of properties and cheques
self.propertiesDeck = Deck.createProperties()
self.chequesDeck = Deck.createCheques()
}
}
Deck class is as follows
// Deck of cards
// Two deck types in the game - (1) Properties & (2) Cheques
class Deck
{
private var cards:[Card] = [] // Empty Array
// #return: An array of cards
func createProperties() -> [Card]
{
var propertyDeck:[Card] = []
// TODO: - Needs Local JSON reader
let prop1 = Card.init(name:"Cardboard box", value:1)
propertyDeck.append(prop1)
let prop2 = Card.init(name:"Outhouse", value:2)
propertyDeck.append(prop2)
let prop3 = Card.init(name:"Outhouse", value:3)
propertyDeck.append(prop3)
return propertyDeck
}
// #return: An array of cards
func createCheques() -> [Card]
{
var chequeDeck:[Card] = []
// create 2 copies of each card, but skip 1s
for var i:Int = 0; i<=15; i++
{
if (i != 1)
{
let chequeCard = Card.init(name: "Cheque", value: i * 1000)
chequeDeck.append(chequeCard)
}
}
return chequeDeck
}
func addCard()
{
}
func shuffle()
{
}
}
Deck() is a class
func setup() {
var propertiesDeck:Deck = Deck()
// Create property deck
self.propertiesDeck = Deck.createProperties()
}
// Deck.createProperties file
// #return: An array of cards
func createProperties() -> [Card]
{
var propertyDeck:[Card] = []
let prop1 = Card.init(name:"Penthouse", value:1)
propertyDeck.append(prop1)
return propertyDeck
}
But Swift keeps complaining that;
Missing argument for parameter #1 in call
But there aren't any arguments or parameters.
Perhaps I'm doing something wrong/silly?
This error would generally say that Deck is expecting some constructor parameters. Could you please post your Deck class so I can see if there are any?
Also some more suggestions. You seem to be creating the Deck variable propertiesDeck, but then statically accessing createProperties by stating Deck.createProperties(). Should you not be calling propertiesDeck.createProperties()? Also createProperties is returning an Array of the Card object, but propertiesDeck is a Deck class.
Since you're accessing your function like this:
Deck.createProperties()
you probably want a static method instead:
static func createProperties() -> [Card] {
...
}
Which shouldn't give you an error anymore.
Another way to make it work is by calling createProperties() on your already defined Deck like this (not recommended):
self.propertiesDeck = propertiesDeck.createProperties()
The reason for the missing parameter comes from the fact that methods take the class instance as their first parameter, so you could actually call it like Deck.createProperties(propertiesDeck)().
I believe you've got some other flaws in your code, I will try to make a better example for you:
struct Card {
let name : String
let value : Int
}
class Deck {
var cards : [Card]
init() {
cards = [
Card(name: "Penthouse", value: 1)
]
}
}
Try This,
func createProperties() -> [Card]
{
var propertyDeck:[Card] = []
let prop1 = Card(name:"Penthouse", value:1)
propertyDeck.append(prop1)
return propertyDeck
}
If this does not works then show the code for the class Deck and Card.
We can't answer your question without the complete code. We need the code of your Deck class.
But you can try class funcs...
public class Cards {
class func createProperties() -> [Card]
{
var propertyDeck:[Card] = []
let prop1 = Card.init(name:"Penthouse", value:1)
propertyDeck.append(prop1)
return propertyDeck
}
} 
You can call the function with:
Cards.createProperties()

Change the value that is being set in variable's willSet block

I'm trying to sort the array that is being set before setting it but the argument of willSet is immutable and sort mutates the value. How can I overcome this limit?
var files:[File]! = [File]() {
willSet(newFiles) {
newFiles.sort { (a:File, b:File) -> Bool in
return a.created_at > b.created_at
}
}
}
To put this question out of my own project context, I made this gist:
class Person {
var name:String!
var age:Int!
init(name:String, age:Int) {
self.name = name
self.age = age
}
}
let scott = Person(name: "Scott", age: 28)
let will = Person(name: "Will", age: 27)
let john = Person(name: "John", age: 32)
let noah = Person(name: "Noah", age: 15)
var sample = [scott,will,john,noah]
var people:[Person] = [Person]() {
willSet(newPeople) {
newPeople.sort({ (a:Person, b:Person) -> Bool in
return a.age > b.age
})
}
}
people = sample
people[0]
I get the error stating that newPeople is not mutable and sort is trying to mutate it.
It's not possible to mutate the value inside willSet. If you implement a willSet observer, it is passed the new property value as a constant parameter.
What about modifying it to use didSet?
var people:[Person] = [Person]()
{
didSet
{
people.sort({ (a:Person, b:Person) -> Bool in
return a.age > b.age
})
}
}
willSet is called just before the value is stored.
didSet is called immediately after the new value is stored.
You can read more about property observers here
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html
You can also write a custom getter and setter like below. But didSet seems more convenient.
var _people = [Person]()
var people: [Person] {
get {
return _people
}
set(newPeople) {
_people = newPeople.sorted({ (a:Person, b:Person) -> Bool in
return a.age > b.age
})
}
}
It is not possible to change value types (including arrays) before they are set inside of willSet. You will need to instead use a computed property and backing storage like so:
var _people = [Person]()
var people: [Person] {
get {
return _people
}
set(newPeople) {
_people = newPeople.sorted { $0.age > $1.age }
}
}
Another solution for people who like abstracting away behavior like this (especially those who are used to features like C#'s custom attributes) is to use a Property Wrapper, available since Swift 5.1 (Xcode 11.0).
First, create a new property wrapper struct that can sort Comparable elements:
#propertyWrapper
public struct Sorting<V : MutableCollection & RandomAccessCollection>
where V.Element : Comparable
{
var value: V
public init(wrappedValue: V) {
value = wrappedValue
value.sort()
}
public var wrappedValue: V {
get { value }
set {
value = newValue
value.sort()
}
}
}
and then assuming you implement Comparable-conformance for Person:
extension Person : Comparable {
static func < (lhs: Person, rhs: Person) -> Bool {
lhs.age < lhs.age
}
static func == (lhs: Person, rhs: Person) -> Bool {
lhs.age == lhs.age
}
}
you can declare your property like this and it will be auto-sorted on init or set:
struct SomeStructOrClass
{
#Sorting var people: [Person]
}
// … (given `someStructOrClass` is an instance of `SomeStructOrClass`)
someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people.last
Caveat: Property wrappers are not allowed (as of time of writing, Swift 5.7.1) in top-level code— they need to be applied to a property var in a struct, class, or enum.
To more literally follow your sample code, you could easily also create a ReverseSorting property wrapper:
#propertyWrapper
public struct ReverseSorting<V : MutableCollection & RandomAccessCollection & BidirectionalCollection>
where V.Element : Comparable
{
// Implementation is almost the same, except you'll want to also call `value.reverse()`:
// value = …
// value.sort()
// value.reverse()
}
and then the oldest person will be at the first element:
// …
#Sorting var people: [Person]
// …
someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people[0]
And even more directly, if your use-case demands using a comparison closure via sort(by:…) instead of implementing Comparable conformance, you can do that to:
#propertyWrapper
public struct SortingBy<V : MutableCollection & RandomAccessCollection>
{
var value: V
private var _areInIncreasingOrder: (V.Element, V.Element) -> Bool
public init(wrappedValue: V, by areInIncreasingOrder: #escaping (V.Element, V.Element) -> Bool) {
_areInIncreasingOrder = areInIncreasingOrder
value = wrappedValue
value.sort(by: _areInIncreasingOrder)
}
public var wrappedValue: V {
get { value }
set {
value = newValue
value.sort(by: _areInIncreasingOrder)
}
}
}
// …
#SortingBy(by: { a, b in a.age > b.age }) var people: [Person] = []
// …
someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people[0]
Caveat: The way SortingBy's init currently works, you'll need to specify an initial value ([]). You can remove this requirement with an additional init (see Swift docs), but that approach is much less complicated when your property wrapper works on a concrete type (e.g. if you wrote a non-generic PersonArraySortingBy property wrapper), as opposed to a generic-on-protocols property wrapper.