I have an array of 1000+ items. Initially, the array elements have a state called initial.
At some point of time, I want to change the state of some items to updated.
If I changed the array items one by one, like iterating over the array and update their values if it met a condition, with every update I will get a callback for the array's didSet.
I don't want this. What I really want is to perform a batch update on the array, thus, I get a callback only once when I finish updating multiple items in the array.
Is that possible in Swift?
The simplest way to do this is to make a copy, modify it as you like, and then assign it back. For example:
var xs = [1,2,3,4] {
didSet {
print("SET")
}
}
var ys = xs
ys[0] = 0
ys[2] = 100
xs = ys
(Prints "SET" just one time.)
Before you ask: 1000 items is not a large array. Copying it this way infrequently is generally not a problem unless the stored items are themselves very large. But what if copying really does turn out to be a problem? Then you can go the unsafe route:
xs.withUnsafeMutableBufferPointer { (ptr) in
ptr[0] = 1000
ptr[1] = 2000
}
I'd kind of discounted map to solve this, but in trying to respond to Sulthan about why, I kind of talked myself out of it. You can definitely use map, especially given the kind of use case you were mentioning.
let updatedIndexes = [0, 2]
xs = xs.enumerated()
.map { (n, value) in
return updatedIndexes.contains(n) ? State.updated : value
}
Another way to batch update an array is to pass it as an inout variable to the function that updates it. Because of the copy in - copy out semantics of inout, the array will only be updated once:
class Foo {
var arr: [Int] = [1,2,3,4,5] {
didSet {
print("SET")
}
}
func batchUpdate(_ arr: inout [Int]) {
for idx in arr.indices {
arr[idx] *= 2
}
}
func test() {
batchUpdate(&arr)
print(arr)
}
}
Foo().test()
SET
[2, 4, 6, 8, 10]
Related
I know you shouldn't, I kind of know why. But I mean I don't understand my own code once I am trying really to think what's going on.
So I have an array with bunch of objects. I am iterating over it and once I find an object with specific type, I remove it from the array, and add another object into the array. So something like this:
var arr = parent.allchildren() //getting all the children in array
for ele in arr{
if(ele==somethingHere){
parent.remove(ele)
parent.add(new ele) //add new child into child array
}
}
If I have an array of 1,2,3,4,5, and I remove 3 and add a 6 while iterating, the actual array would be 1,2,4,5,6 but the array I am iterating would still be 1,2,3,4,5.
Which I think it would be fine, because at the end I still get what I want, which removed the element and added the element I need. However modifying the list while iterating it is bad and you shouldn't do that, but for my case I think it does what I need. What could be the potential issue in my case that I can't see?
One thing you may want to think about doing is making all of the changes at the end of the iteration. Instead of making the changes one by one, record the changes you want to make while iterating, and then actually make those changes once your loop is finished.
For example, you could make an array of elements to remove, and an array of elements to add.
//Our array where we record what we want to add
var elementsToAdd = [Any]()
//Our array of what elements we want to remove. We record the index at
//which we want to remove the element from the array
var indexesToRemoveAt = [Int]()
//Getting all the children in array
var arr = parent.allchildren()
//Enumerating an array allows us to access the index at which that
//element occurs. For example, the first element's index would be 0,
//the second element's index would be 1, the third would be 2, and so
//on
for (index,ele) in arr.enumerated() {
if(ele == somethingHere) {
indexesToRemoveAt.append(index)
elementsToAdd.append(newEle)
}
}
//Now that we have recorded the changes we want to make, we could make
//all of the changes at once
arr.remove(at: indexesToRemoveAt)
arr.append(contentsOf: elementsToAdd)
Note that removing array elements at multiple indexes would require the following extension to Array. If you wanted to avoid creating this extension, you could always just loop through the array of indexes and tell the array to remove at each individual index. All this extension function is really doing is looping through the indexes, and removing the array element at said index.
Array extension to remove elements at multiple indexes:
extension Array {
//Allows us to remove at multiple indexes instead of just one
mutating func remove(at indexes: [Int]) {
for index in indexes.sorted(by: >) {
if index <= count-1 {
remove(at: index)
}
}
}
}
I just tested in a playground with the following code:
var arr = ["hi", "bye", "guy", "fry", "sky"]
for a in arr {
if arr.count >= 3 {
arr.remove(at: 2)
}
print(a)
}
print(arr)
This prints:
hi
bye
guy
fry
sky
["hi", "bye"]
So it looks like when you use a for-in loop in Swift, the array is copied and changes you make to it will not affect the array you are iterating over. To answer your question, as long as you understand that this is the behavior, there's nothing wrong with doing this.
I have already read multiple posts and articles about how ArraySlice works with Array in `Swift.
But, what I couldn't find is how it works internally? What does ArraySlice are Views onto Arrays exactly mean?
var arr = [1, 2, 3, 4, 5]
let slice = arr[2...4]
arr.remove(at: 2)
print(slice.startIndex) //2
print(slice.endIndex) //5
slice[slice.startIndex] //3
In the code above, I've removed element at index-2 (i.e 3) from arr.
Index-2 is the startIndex of slice as well.
When I print slice[slice.startIndex] it still prints 3.
Since no extra storage is created for ArraySlice, then how come any changes in Array doesn't reflect in ArraySlice?
The articles/ posts can be found here:
https://dzone.com/articles/arrayslice-in-swift
https://marcosantadev.com/arrayslice-in-swift/
Both Array and ArraySlice are value types, which means that after
var array = [0, 1, 2, 3, 4, 5]
var slice = array[0..<2]
array and slice are independent values, and mutating one does not affect the other:
print(slice) // [0, 1]
array.remove(at: 0)
print(slice) // [0, 1]
How that is achieved is an implementation detail of the Swift standard library,
but one can inspect the source code to get some ideas: At
Array.swift#L1241
we find the implementation of Array.remove(at:):
public mutating func remove(at index: Int) -> Element {
_precondition(index < endIndex, "Index out of range")
_precondition(index >= startIndex, "Index out of range")
_makeUniqueAndReserveCapacityIfNotUnique()
// ...
}
which uses
#inlinable
#_semantics("array.make_mutable")
internal mutating func _makeUniqueAndReserveCapacityIfNotUnique() {
if _slowPath(!_buffer.isMutableAndUniquelyReferenced()) {
_copyToNewBuffer(oldCount: _buffer.count)
}
}
Following that trail, we find at ArrayBuffer.swift#L107
/// Returns `true` iff this buffer's storage is uniquely-referenced.
#inlinable
internal mutating func isUniquelyReferenced() -> Bool {
// ...
}
This isn't the full implementation yet, but (hopefully) already demonstrates that
the (mutating) remove(at:) method copies the element storage to a new
buffer if is was shared (with another array or an array slice).
We can also verify that by printing the element storage base address:
var array = [0, 1, 2, 3, 4, 5]
var slice = array[0..<2]
array.withUnsafeBytes { print($0.baseAddress!) } // 0x0000000101927190
slice.withUnsafeBytes { print($0.baseAddress!) } // 0x0000000101927190
array.remove(at: 0)
array.withUnsafeBytes { print($0.baseAddress!) } // 0x0000000101b05350
slice.withUnsafeBytes { print($0.baseAddress!) } // 0x0000000101927190
The same “copy-on-write” technique is used if arrays, dictionaries, or strings
are copied, or if String and Substring share storage.
So an array slice shares the element storage with its originating
array as long as neither of them is mutated.
That is still a useful feature. Here is a simple example:
let array = [1, 4, 2]
let diffs = zip(array, array[1...]).map(-)
print(diffs) // [-3, 2]
array[1...] is a view/slice of the given array, without actually copying
the elements.
A recursive binary search function where slices (of the left or right half) are passed down would be another application.
Explanation:
Array slice is a view to the underlying array and retains the array.
Probably (not 100% sure), when you remove an element from the array, it is still retained and is just marked as removed.
That is why when you print the array you don't see 3 but the slice still shows it.
Example:
class Car : CustomStringConvertible {
var description: String {
let objectIdentifier = ObjectIdentifier(self) //To get the memory address
return "car instance - \(objectIdentifier) "
}
deinit {
print("car deallocated")
}
}
var carSlice : ArraySlice<Car>?
var cars : [Car]? = [Car(), Car(), Car()]
carSlice = cars?[0...1]
print("Going to make cars nil")
cars = nil
print("cars = \(cars?.description ?? "nil")")
print("carSlice = \(carSlice?.description ?? "nil")")
print("----------------")
print("Going to make carSlice nil")
carSlice = nil
print("carSlice = \(carSlice?.description ?? "nil")")
Output:
Going to make cars nil
cars = nil
carSlice = [car instance - ObjectIdentifier(0x000060c00001e7b0) , car instance - ObjectIdentifier(0x000060c00001e7c0) ]
----------------
Going to make carSlice nil
carSlice = nil
car deallocated
car deallocated
car deallocated
Apple Documentation:
Long-term storage of ArraySlice instances is discouraged. A slice
holds a reference to the entire storage of a larger array, not just
to the portion it presents, even after the original array’s lifetime
ends. Long-term storage of a slice may therefore prolong the lifetime
of elements that are no longer otherwise accessible, which can appear
to be memory and object leakage.
Refer:
https://developer.apple.com/documentation/swift/arrayslice
I have a small bit of code that looks like this:
func initAllLabels() {
var scoreLabels:[[[SKLabelNode]]] = []
for (var x = 0; x < modesInGame; x++) {
for (var y = 0; y < levelsInMode; y++) {
for (z = 0; z < labelsInLevel; z++) {
scoreLabels[x][y][z] = SKLabelNode(fontNamed: "Font")
}
}
}
}
So what I am trying to do is store all my labels for every game mode. The reason I'm trying to use a multidimensional array is because I will have several labels per level (3-5) and I would like to access them like this:
updateText(scoreLabels[currentMode][currentLevel][thisLabel])
And accessing all the labels for the current label like this:
for label in labelsInLevel:
label.hidden = false
The problem is that when I try to create all my labels at the start of the game in initAllLabels, I get an "index out of range" error at the first run in the loop (index: 0). I think the problem is because I need to "append" to the array before setting its contents, is this right? How would I accomplish this in an array structure like mine?
You need to initialize the array to a given size before updating items at positions within it. It might help to start with the single-dimensional case:
var labels: [SKLabelNode] = [] // creates an empty array
// since the array is empty, this will generate an index out of range error:
labels[0] = SKLabelNode(fontNamed: "Font")
Instead you need to extend the array with the elements you want to add. For example,
for _ in 0..<labelsInLevel {
labels.append(SKLabelNode(fontNamed: "Font"))
}
(the _ means “I don’t care about the actual number of each iteration - normally if you wanted to know this was the ith time around the loop you’d write for i in 0..<n)
There are nicer ways to do this though. But be careful with one of them, the initializer for Array that takes a count and a repeatedValue:
let labels = Array(count: labelsInLevel, repeatedValue: SKLabelNode(fontNamed: "Font”))
SKLabelNode is a reference type. That means that a variable only refers to an instance, and assigning one variable to another only copies the reference. So for example:
let variableOne = SKLabelNode(fontNamed: "Foo")
let variableTwo = variableOne
// variableTwo now refers to the same instance of SKLabelNode
variableTwo.fontName = "Bar"
print(variableOne)
// fontName will now be “Bar” even though this was
// done via variableTwo
You get the same effect with the repeatedValue code above. The label is created once, and the same reference to it is inserted multiple times. Change a property on one label in the array, and you change them all. Note the for loop version does not have this problem. Every time around the loop, a new SKLabelNode instance will be created. They won’t be shared.
An alternative to the repeatedValue initializer, that creates without using a for loop is to use map:
(0..<labelsInLevel).map { _ in SKLabelNode(fontNamed: "Font") }
Here, just like in the for loop version, a new SKLabelNode instance is created every time.
(again, we use the _ to indicate we don’t care about the number of the iteration)
Finally, to create the nested multidimensional arrays with the loops inside, you can run map multiple times:
var scoreLabels =
(0..<modesInGame).map { _ in
(0..<levelsInMode).map { _ in
(0..<labelsInLevel).map { _ in
SKLabelNode(fontNamed: "Font")
}
}
}
You need to initialize the 3D array like this if you want all SKLabelNode to point at different SKLabelNodes :
func initAllLabels() {
var scoreLabels = [[[SKLabelNode]]](count: modesInGame, repeatedValue:
[[SKLabelNode]](count: levelsInMode, repeatedValue:
(0..< labelsInLevel).map { _ in SKLabelNode(fontNamed: "Font") }))
}
The repeatedValue is what you are storing in the cells (type or value) and the count represent the number of times you want to store it in your array.
PS: This answer was tested with Xcode Playground and works perfectly fine.
I edited my answer following Airspeed Velocity's comment.
I am making an app that has different game modes, and each game mode has a few scores. I am trying to store all the scores in a dictionary of arrays, where the dictionary's key is a game's id (a String), and the associated array has the list of scores for that game mode. But when I try to initialize the arrays' values to random values, Swift breaks, giving me the error below. This chunk of code will break in a playground. What am I doing wrong?
let modes = ["mode1", "mode2", "mode3"]
var dict = Dictionary<String, [Int]>()
for mode in modes
{
dict[mode] = Array<Int>()
for j in 1...5
{
dict[mode]?.append(j)
let array:[Int] = dict[mode]!
let value:Int = array[j] //breaks here
}
}
ERROR:
Execution was interrupted, reason: EXC_BAD_INSTRUCTION(code=EXC_I386_INVOP, subcode=0x0).
Your problem is array subscripts are zero-based. So when you write:
var a: [Int] = []
for i in 1...5 {
a.append(42)
println(a[i])
}
you will get a runtime error, because first time around the loop you are subscripting a[1] when there is only an a[0]. In your code, you either need to do for j in 0..<5 or let value = array[j-1].
By the way, even though it’s perfectly safe to do dict[mode]! (since you just added it), it’s a habit best avoided as one of these days your code won’t be as correct as you think, and that ! will explode in your face. There’s almost always a better way to write what you want without needing !.
Also, generally speaking, whenever you use array subscripts you are risking an accidental screw-up by accidentally addressing an out-of-bounds index like here. There are lots of alternatives that mean actually using a[i] is easy to avoid:
If you want the indices for a collection (like an array), instead of:
for i in 0..<a.count { }
you can write
for i in indices(a) { }
If you want to number the elements in an array, instead of
for i in indices(a) { println("item \(i) is \(a[i])" }
you can write
for (i, elem) in enumerate(a) { println("item \(i) is \(elem)") }
If the collection happens to have an Int for an index (such as Array), you can use i as an index, but if it doesn’t (such as String) an alternative to get the index and element is:
let s = "hello"
for (idx, char) in Zip2(indices(s),s) { }
If you want the first or last element of an array, instead of:
if a.count > 0 { let x = a[0] }
if a.count > 0 { let x = a[a.count - 1] }
you can write
if let first = a.first { let x = first }
if let last = a.last { let x = first }
Prefer map, filter and reduce to for loops in general (but don’t obsess over it, sometimes a for loop is better)
I've got an array of unsigned integers and I'd like to get a product of certain subsets.
For example, if my array was [2,2,1,5], I'd like the product of every two numbers (2 * 2 = 4 and 1 * 5 = 5).
So far, I've got:
var myArray:[UInt8] = [2,2,1,5]
var mySlice: Array<UInt8>
for (index,i) in enumerate(myArray) {
if (index % 2 == 1) {
mySlice = Array(myArray[(index - 1)...index])
println(mySlice.reduce(1,*))
mySlice.removeAll()
}
}
This seems like it would work (though ugly) but I get Execution was interrupted, reason: EXC_BAD_INSTRUCTION.
What's the best way to walk linearly down an array returning products (or computations) at certain intervals?
Thanks.
It looks like the problem is clearing out the slice while iterating the bigger array.
You should be able to work through this by adding the items to a separate array as you go, like this:
let myArray:[UInt8] = [2,2,1,5]
var result:[UInt8] = []
for (index,i) in enumerate(myArray) {
if (index % 2 == 1) {
let mySlice = Array(myArray[(index - 1)...index])
let tmp = mySlice.reduce(1,*)
result.append(tmp)
println(tmp)
}
}
println(result)
If you would like to put the results back into myArray, you can assign it after the loop.
Demo.
There are a few changes you can make that will make this much easier (and have much better performance) than your current attempt. First, use the global stride(from:to:interval:) function, which builds a sequence of indexes like the one you want -- no need to loop through all and skip the odd ones!
for i in stride(from: 0, to: myArray.count, by: 2) {
// ...
}
Second, you can use a slice's reduce method, so you don't have to convert a Slice back to an Array (doing so is unnecessary and inefficient):
let r = myArray[i ... i + 1].reduce(1, *)
So to bring it all together, your code could be:
var myArray: [UInt8] = [2, 2, 1, 5]
var result: [UInt8] = []
for i in stride(from: 0, to: myArray.count, by: 2) {
result.append( myArray[i...i + 1].reduce(1, *) )
}
// result is [4, 5]