Swift iOS -How come I can loop through an array of class objects and make property changes but not structs [duplicate] - swift

This question already has answers here:
Is Swift Pass By Value or Pass By Reference
(10 answers)
Swift can change struct declared with let if using an index but not if using a loop
(3 answers)
Closed 3 years ago.
If I loop through an array of Class objects I can make changes to a property on it
class Country {
var name: String?
var region: String?
init(name: String?, region: String?) {
self.name = name
self.region = region
}
}
let canada = Country(name: "Canada", region: "North America")
let mexico = Country(name: "Mexico", region: "North Ameria")
let france = Country(name: "France", region: "Europe")
let korea = Country(name: "Korea", region: "Asia")
var countryArr = [canada, mexico, france, korea]
// this works fine
let transformed = countryArr.map { $0.name = "Random" }
But if I try this with Struct objects I get
Cannot assign to property: '$0' is immutable
struct Country {
var name: String?
var region: String?
}
var canada = Country(name: "Canada", region: "North America")
var mexico = Country(name: "Mexico", region: "North Ameria")
var france = Country(name: "France", region: "Europe")
var korea = Country(name: "Korea", region: "Asia")
var countryArr = [canada, mexico, france, korea]
// this gets an error
let transformed = countryArr.map { $0.name = "Random" }

The issue is caused by the fact that structs are value types, so mutating any properties of the struct mutates the struct instance itself as well and the closure input arguments in map are immutable. So when you try to mutate a property $0 in the closure of map, you are trying to mutate $0 itself in case map is called on a collection of value types.
On the other hand, classes are reference types, so mutating a property of a class instance doesn't mutate the instance itself.
A solution for your problem is to create a mutable copy of the struct instance in the map, mutate its name property and return that. There are two solutions, if you have a small number of properties on your type, calling its memberwise initialiser is easier, but if you have a lot of properties and only want to mutate a few, copying the struct and then modifying the necessary properties is the better choice.
let transformed = countryArr.map { Country(name: "Random", region: $0.region) }
let transformed2 = countryArr.map { country->Country in
var copy = country
copy.name = "Random"
return copy
}

Related

Why is this showing 'Expected 'func' keyword in instance method declaration'

I'm new to coding and using SwiftUI on Xcode, and I don't see what's wrong with this code:
class NormalSpace {
var name = ""
var value = 0
var rent = 0
var owned = false
}
var newRoad = NormalSpace()
newRoad.name = "New Road"
newRoad.value = 600
newRoad.rent = 25
newRoad.owned = false
the error 'Expected 'func' keyword in instance method declaration' only shows on the newRoad.name line. The same line also has the error: Invalid redeclaration of 'newRoad'.
What have I done wrong?
In an normal project, this is not valid:
class NormalSpace {
var name = ""
var value = 0
var rent = 0
var owned = false
}
var newRoad = NormalSpace()
newRoad.name = "New Road"
newRoad.value = 600
newRoad.rent = 25
newRoad.owned = false
You can do that in a playground (where it just runs this code directly), but in an app, code (such as the setting of the properties) belongs in a function or initializer.
That initialization code needs to be placed within some context. Let us imagine that it is inside a struct. But this still is not valid:
class NormalSpace {
var name = ""
var value = 0
var rent = 0
var owned = false
}
struct Foo {
var newRoad = NormalSpace()
newRoad.name = "New Road"
newRoad.value = 600
newRoad.rent = 25
newRoad.owned = false
}
The property, newRoad is fine, but the values are not. You need to wrap it inside a func (hence the error) or an init. E.g., this initializes newRoad during the init of the struct:
struct Foo {
let newRoad: NormalSpace
init() {
newRoad = NormalSpace()
newRoad.name = "New Road"
newRoad.value = 600
newRoad.rent = 25
newRoad.owned = false
}
}
Or you might initialize it in a func:
struct Foo {
var newRoad: NormalSpace?
mutating func bar() {
let road = NormalSpace()
road.name = "New Road"
road.value = 600
road.rent = 25
road.owned = false
newRoad = road
}
}
Or, alternatively, you can initialize this property with a closure (note the extra () at the end):
struct Foo {
let newRoad: NormalSpace = {
let road = NormalSpace()
road.name = "New Road"
road.value = 600
road.rent = 25
road.owned = false
return road
}()
}
But the code where you initialize the properties must be placed within some context, so that the compiler knows when those lines of code should be run.
Note, we would generally give NormalSpace a “memberwise initializer”, e.g.:
class NormalSpace {
let name: String
let value: Int
let rent: Int
let owned: Bool
init(name: String, value: Int, rent: Int, owned: Bool) {
self.name = name
self.value = value
self.rent = rent
self.owned = owned
}
}
Or, if a struct (and we would generally prefer to make our model objects struct value-types rather than class reference-types), this memberwise initializer would be created for you:
struct NormalSpace {
let name: String
let value: Int
let rent: Int
let owned: Bool
}
Either way, you can then provide all the desired values during initialization, e.g.:
struct Foo {
let newRoad = NormalSpace(name: "New Road", value: 600, rent: 25, owned: false)
}
Note, that I've removed the “default” values because those really are not appropriate. If you wanted to say that they do not need to be provided, then you would make them “optionals”. But there is generally a big difference between, say, a rent of zero (i.e. it is my grandmother’s house and she's not charging me) and that no rent has been specified. In Swift, we generally avoid using “sentinel” values like "" or 0 for “no value provided”.
Also, now that we have a memberwise initializer, I have also made the properties immutable (let rather than var). If you need to make them mutable (e.g. to let someone change the rent later), fine, revert back to var. But only make properties mutable if you really need to change them later on.

How can I refer to properties from a struct within a struct/

I'm trying to get the hang of how to make my code the most efficient using Structs / Classes, and I'm trying to understand it via the following example.
I'd be really grateful if someone could correct me or guide me about the same:
Just as an example, I'll use Harry Potter. There are four houses, and each house has certain characteristics.
So now I have a struct for 2 of them:
struct Gryffindor {
let name = "Gryffindor"
let characteristic = "Brave"
let image = Image("Lion")
}
struct Slytherin {
let name = "Slytherin"
let characteristic = "Cunning"
let image = Image("Snake")
}
Now if I wish to have a wizard struct as follows, but I don't know how to include a House property within, such that when I try to create an instance of a wizard, I can call the properties from their respective houses.
struct Wizard {
let name: String
var house: ?
}
let harryPotter = Wizard(name: "Harry", house: Gryffindor)
Basically, I wish to be able to refer to harry's house using the harryPotter instance, as such:
print(harryPotter.characteristic) //should print "Brave"
Is what I'm trying to achieve even possible?
First of all you are mixing types with objects so you should have a type House
struct House {
let name: String
let characteristic: String
let image: Image
}
And then use that in the Wizard struct
struct Wizard {
let name: String
var house: House
}
And now you create first a House object for the Wizard and then the Wizard object
let gryffindor = House(name: "Gryffindor", characteristic: "Brave", image: Image("Lion"))
let harryPotter = Wizard(name: "Harry", house: gryffindor)
or all in one call
let harryPotter = Wizard(name: "Harry",
house: House(name: "Gryffindor", characteristic: "Brave", image: Image("Lion")))
Use protocol & generics, like below. Tested with Xcode 11.4.
protocol House {
var name: String { get }
var characteristic: String { get }
var image: Image { get }
}
struct Gryffindor: House {
let name = "Gryffindor"
let characteristic = "Brave"
let image = Image("Lion")
}
struct Wizard<H: House> {
let name: String
var house: H
}
let harryPotter = Wizard(name: "Harry", house: Gryffindor())

Whats wrong with my convenience initializer when I attempt to call self.init?

I wrote the code below. The error I'm getting is at the end of my convenience initializer when I attempt to call self.init. What's wrong with my logic or syntax? Or how would I debug this? The error Xcode is giving is "cannot invoke with an argument list of type".
Thank you for any help on this
import Foundation
import UIKit
class Item: NSObject {
var name: String
var valueInDollars: Int
var serialNumber: String?
let dateCreated: NSDate
var stolen: Bool?
init(name: String, valueInDollars: Int, serialNumber: String?, dateCreated: NSDate, stolen: Bool?) {
self.name = name
self.valueInDollars = valueInDollars
self.serialNumber = serialNumber
self.dateCreated = NSDate()
self.stolen = stolen
//below why did I have to call super.init for this custom class that inherits from NSObject? Doesn't it automatically get called anyway once the custom object is initialized since it inherits from NSObject? It just seems like more work on my behalf which isn't fair. it should do it automatically. Why wouldn't it do it automatically if it inherits from NSObject?
super.init()
}
convenience init(random: Bool = false) {
if random {
let adjectives = ["Fluffy", "Rusty", "Shiny"]
let nouns = ["MacBook Pro", "Red Tribe Bike", "Vegan Pizzas"]
//take a variable that's random; the highest value for this random number will be the number of ojbects in the adjectives array
var idx = arc4random_uniform(UInt32(adjectives.count))
//now use this random variable and let it be the index of the adjectives array...so basically it'll be a random object from the adjectives array
let randomAdjective = adjectives[Int(idx)]
//AWESOME!! Now that the random adjective is stored in the randomAdjective constant, let's re-use the idx variable...Ayyyyeeeee re-use!
//we'll re-use it by doing the same process or close to the same process for nouns
idx = arc4random_uniform(UInt32(nouns.count))
let randomNoun = nouns[Int(idx)]
//now let's concatenate these two clever words, shall we!!
let randomName = "\(randomAdjective) \(randomNoun)"
//yayyy we're programmmminnngg!
//now let's ....whad de fuk....
let randomValue = Int(arc4random_uniform(100))
let randomSerialNumber = NSUUID().uuidString.components(separatedBy: "-").first!
let betterNotBeStolen: Bool = false
self.init(name: randomName, valueInDollars: randomValue, serialNumber: randomSerialNumber, stolen: betterNotBeStolen)
}
}
}
You got the error
"Cannot invoke 'Item.init' with an argument list of type '(name:
String, valueInDollars: Int, serialNumber: String, stolen: Bool)'"
because you missed the the dateCreated param in the self.init(params...).
So you need to replace this line
self.init(name: randomName, valueInDollars: randomValue, serialNumber: randomSerialNumber, stolen: betterNotBeStolen)
with this one
self.init(name: randomName, valueInDollars: randomValue, serialNumber: randomSerialNumber,dateCreated: NSDate(), stolen: betterNotBeStolen)
The next error which you will see after is
Self.init isn't called on all paths before returning from initializer
So you need to add else statement because the initializer don't know what to do when the random param is false.
convenience init(random: Bool = false) {
if random {
let adjectives = ["Fluffy", "Rusty", "Shiny"]
let nouns = ["MacBook Pro", "Red Tribe Bike", "Vegan Pizzas"]
//take a variable that's random; the highest value for this random number will be the number of ojbects in the adjectives array
var idx = arc4random_uniform(UInt32(adjectives.count))
//now use this random variable and let it be the index of the adjectives array...so basically it'll be a random object from the adjectives array
let randomAdjective = adjectives[Int(idx)]
//AWESOME!! Now that the random adjective is stored in the randomAdjective constant, let's re-use the idx variable...Ayyyyeeeee re-use!
//we'll re-use it by doing the same process or close to the same process for nouns
idx = arc4random_uniform(UInt32(nouns.count))
let randomNoun = nouns[Int(idx)]
//now let's concatenate these two clever words, shall we!!
let randomName = "\(randomAdjective) \(randomNoun)"
//yayyy we're programmmminnngg!
//now let's ....whad de fuk....
let randomValue = Int(arc4random_uniform(100))
let randomSerialNumber = NSUUID().uuidString.components(separatedBy: "-").first!
let betterNotBeStolen: Bool = false
self.init(name: randomName, valueInDollars: randomValue, serialNumber: randomSerialNumber,dateCreated: NSDate(), stolen: betterNotBeStolen)
} else {
self.init(name: "SomeName", valueInDollars: 3, serialNumber: "123", dateCreated: NSDate(), stolen: true)
}
}

How to create an variable property based on the other property in the class

class someClass
{
// This is a list of restaurant names
var restaurantNames = ["Cafe Deadend", "Homei", "Teakha", "Cafe Loisl", "Petite Oyster", "For Kee Restaurant", "Po's Atelier", "Bourke Street Bakery", "Haigh's Chocolate", "Palomino Espresso", "Upstate", "Traif", "Graham Avenue Meats", "Waffle & Wolf", "Five Leaves", "Cafe Lore", "Confessional", "Barrafina", "Donostia", "Royal Oak", "Thai Cafe"]
// This is an array to record the restaurants I have visited
var restaurantIsVisited:[Bool] = [Bool](count: 21, repeatedValue: false)
// This is a function to set the restaurants I have visited
func setTheVisitedRestaurants(somenumber:Int)
{
self.restaurantIsVisited[somenumber] = true
}
}
let thisVisit = someClass()
thisVisit.setTheVisitedRestaurants(1)
let somearray = thisVisit.restaurantIsVisited
Above code has no error in playground. But what if I don't know the number of the total restaurants is 21 (It could be hundreds and I don't want to count). I tried the following code to create an immutable computed property. But it won't work because later on the property will be changed by function setTheVisitedRestaurants and it will return an error.
// var restaurantIsVisited:[Bool]{
// return [Bool](count: restaurantNames.count, repeatedValue: false)
// }
In a word, the question is like how to create an variable property based on the other property in the class. I am a beginner and I really tried. Please help!
Declare restaurantIsVisited with the lazy keyword. This will insure that it isn't created until it is accessed the first time, and by that time you will be able to ask restaurantNames for its count:
class someClass
{
// This is a list of restaurant names
var restaurantNames = ["Cafe Deadend", "Homei", "Teakha", "Cafe Loisl", "Petite Oyster", "For Kee Restaurant", "Po's Atelier", "Bourke Street Bakery", "Haigh's Chocolate", "Palomino Espresso", "Upstate", "Traif", "Graham Avenue Meats", "Waffle & Wolf", "Five Leaves", "Cafe Lore", "Confessional", "Barrafina", "Donostia", "Royal Oak", "Thai Cafe"]
// This is an array to record the restaurants I have visited
lazy var restaurantIsVisited:[Bool] = [Bool](count: self.restaurantNames.count, repeatedValue: false)
// This is a function to set the restaurants I have visited
func setTheVisitedRestaurants(somenumber:Int)
{
self.restaurantIsVisited[somenumber] = true
}
}
let thisVisit = someClass()
thisVisit.setTheVisitedRestaurants(1)
let somearray = thisVisit.restaurantIsVisited
println(somearray.count) // "21"
Why not use a struct to represent a restaurant, for example:
struct Restaurant {
let name: String
var visited: Bool
init(name: String, visited: Bool = false) {
self.name = name
self.visited = visited
}
}
Now it's much easier to keep track of which restaurants have been visited and which ones haven't because you haven't got to worry about information on each restaurant being separated into two arrays.
struct SomeStruct {
var restaurants = [Restaurant(name: "Cafe Deadend"),
Restaurant(name: "Homei"),
Restaurant(name: "Cafe Loisl")]
}
Usage:
var thisVisit = SomeStruct()
thisVisit.restaurants[0].visited = true

Extract value from struct that meet condition

I have a struct for different animals, and values for these animals. Im adding animals to it.
struct Animal {
var type: String
var weight: String
var cost: String
}
var animals = [Animal]()
func addAnimal(type: String, weight: String, cost: String){
animals.append(Animal(type: type, weight: weight, cost: cost))
}
addAnimal("monkey", "80", "300")
addAnimal("zebra", "200", "500")
addAnimal("monkey", "50", "250")
I want to say, if type == "monkey" then return all weights for monkeys. In this example I would want the code to return values "80" and "50".
I'm new to coding so any advice on this would be helpful. Thank you
You can combine filter and map to accomplish what you want as follow:
let monkeysWeights = animals.filter{$0.type == "monkey"}.map{$0.weight}
println(monkeysWeights) // ["80", "50"]