Swift Can Change Struct Declared with Let If Using an Index But Not If Using a Loop

Swift can change struct declared with let if using an index but not if using a loop

How the struct is "declared" before you put into an array is not really relevant. Let's talk about how things are accessed from an array.

I posit the following test situation:

struct Card {
var property : String
}
var cards = [Card(property:"hello")]

We try to say

for card in cards {
card.property = "goodbye"
}

but we cannot, because card is implicitly declared with let and its properties cannot be mutated. So let's try to work around that with a var reassignment:

for card in cards {
var card = card
card.property = "goodbye"
}

Now our code compiles and runs, but guess what? The array itself is unaffected! That's because card is a copy of the struct sitting in the array; both parameter passing and assignment make a copy. Actually we could condense that by insisting on a var reference up front:

for var card in cards {
card.property = "goodbye"
}

But we gain nothing; card is still a copy, so the array contents are still unaffected.

So now let's try it through indexing, as did in your experiments:

for ix in cards.indices {
cards[ix].property = "goodbye"
}

Bingo! It compiles and runs and changes the contents of the cards array. That's because we are accessing each card directly within the array. It is exactly as if we had said:

for ix in cards.indices {
var card = cards[ix]
card.property = "goodbye"
cards[ix] = card
}

Yes, we're still making a copy, but we are reassigning that copy back into the same place in the array. The index access is a shorthand for doing that.

However, we are still actually pulling out a copy, mutating it, and reinserting it. We can try to work around that, at the cost of some more elaborate planning, by using inout, like this:

func mutate(card: inout Card) {
card.property = "goodbye" // legal!
}
for ix in cards.indices {
mutate(card: &cards[ix])
}

As you can see, we are now allowed to set card.property, because with inout the parameter is implicitly a var. However, the irony is that we are still doing a copy and replacement, because a struct is a value type — it cannot really be mutated in place, even though the assignment thru a var reference gives the illusion that we are doing so.

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

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
}

Swift - How to mutate a struct object when iterating over it

struct are value types, thus in the for loop you are dealing with a copy.

Just as a test you might try this:

Swift 3:

struct Options {
var backgroundColor = UIColor.black
}

var arrayOfMyStruct = [Options]()

for (index, _) in arrayOfMyStruct.enumerated() {
arrayOfMyStruct[index].backgroundColor = UIColor.red
}

Swift 2:

struct Options {
var backgroundColor = UIColor.blackColor()
}

var arrayOfMyStruct = [Options]()

for (index, _) in enumerate(arrayOfMyStruct) {
arrayOfMyStruct[index].backgroundColor = UIColor.redColor()
}

Here you just enumerate the index, and access directly the value stored in the array.

Hope this helps.

Passing calculated variable to another View

Since none of the answers worked, and only one worked half, I solved the problem myself. You need to generate a Binding, and then just pass it

var videos: some View {
GeometryReader { geo in
let columnCount = Int((geo.size.width / 250).rounded(.down))
let rowsCount = (CGFloat((CDNresponse.data?.first?.translations ?? []).count) / CGFloat(columnCount)).rounded(.up)
LazyVStack {
ForEach(0..<Int(rowsCount), id: \.self) { row in // create number of rows
HStack {
ForEach(0..<columnCount, id: \.self) { column in // create 3 columns
let index = row * columnCount + column
if index < ((CDNresponse.data?.first?.translations ?? []).count) {
MovieCellView(translation: trBinding(for: index), qualities: qualityBinding(for: (CDNresponse.data?.first?.translations ?? [])[index]))
}
}
}
}
}
}
}
private func qualityBinding(for key: String) -> Binding<[Int : URL]> {
return .init(
get: { self.qualities[key, default: [:]] },
set: { self.qualities[key] = $0 })
}
private func trBinding(for key: Int) -> Binding<String> {
return .init(
get: { (self.CDNresponse.data?.first?.translations ?? [])[key] },
set: { _ in return })
}

struct MovieCellView: View {
@State var showError: Bool = false
@State var detailed: Bool = false
@Binding var translation: String

@Binding var qualities: [Int : URL]

var body: some View {
...
}

}

Swift and mutating struct

The mutability attribute is marked on a storage (constant or variable), not a type. You can think struct has two modes: mutable and immutable. If you assign a struct value to an immutable storage (we call it let or constant in Swift) the value becomes immutable mode, and you cannot change any state in the value. (including calling any mutating method)

If the value is assigned to a mutable storage (we call it var or variable in Swift), you're free to modify the state of them, and calling of mutating method is allowed.

In addition, classes don't have this immutable/mutable mode. IMO, this is because classes are usually used to represent reference-able entity. And reference-able entity is usually mutable because it's very hard to make and manage reference graphs of entities in immutable manner with proper performance. They may add this feature later, but not now at least.

For Objective-C programmers, mutable/immutable concepts are very familiar. In Objective-C we had two separated classes for each concept, but in Swift, you can do this with one struct. Half work.

For C/C++ programmers, this is also very familiar concept. This is exactly what const keyword do in C/C++.

Also, immutable value can be very nicely optimised. In theory, Swift compiler (or LLVM) can perform copy-elision on values passed by let, just like in C++. If you use immutable struct wisely, it will outperform refcounted classes.

Update

As @Joseph claimed this doesn't provide why, I am adding a little more.

Structs have two kind of methods. plain and mutating methods. Plain method implies immutable (or non-mutating). This separation exists only to support immutable semantics. An object in immutable mode shouldn't change its state at all.

Then, immutable methods must guarantee this semantic immutability. Which means it shouldn't change any internal value. So compiler disallows any state changes of itself in a immutable method. In contrast, mutating methods are free to modify states.

And then, you may have a question of why immutable is the default? That's because it's very hard to predict the future state of mutating values, and that usually becomes the main source of headaches and bugs. Many people agreed that the solution is avoiding mutable stuffs, and then immutable by default was on top of wish list for decades in C/C++ family languages and its derivations.

See purely functional style for more details. Anyway, we still need mutable stuffs because immutable stuffs have some weaknesses, and discussing about them seems to be out of topic.

How to iterate a loop with index and element in Swift

Yes. As of Swift 3.0, if you need the index for each element along with its value, you can use the enumerated() method to iterate over the array. It returns a sequence of pairs composed of the index and the value for each item in the array. For example:

for (index, element) in list.enumerated() {
print("Item \(index): \(element)")
}

Before Swift 3.0 and after Swift 2.0, the function was called enumerate():

for (index, element) in list.enumerate() {
print("Item \(index): \(element)")
}

Prior to Swift 2.0, enumerate was a global function.

for (index, element) in enumerate(list) {
println("Item \(index): \(element)")
}


Related Topics



Leave a reply



Submit