Swift - issue with initializing and appending to arrays in dictionary - arrays

Here's what I am trying to do:
I am starting with an array of ArticleItem objects. Those objects have a property on them named 'category' which is a string. I'm trying to loop through all of my ArticleItem objects and group items with like categories in a dictionary. I'm using the category name as my key. The issue I am having is that my dictionary keys are hold arrays that never contain more than 1 object. I definitely have more than 3 objects with the same category name. Here is the relevant code from my class. I'd love to understand the right way to do this..
private var _articlesDict:[String:[ArticleItem]]
init(articles:[ArticleItem]) {
_articlesDict = [String:[ArticleItem]]()
for item:ArticleItem in articles {
var optionalCatArray:[ArticleItem]? = _articlesDict[item.category]
if let catArray = optionalCatArray {
optionalCatArray!.append(item) //why can't I do catArray.append(item)?
} else {
var arr:[ArticleItem] = [ArticleItem]()
arr.append(item)
_articlesDict[item.category] = arr
}
}
}

The problem is that arrays are value types, so they are passed by value and not by reference. That means that every time you assign a variable holding an array to another variable (or array, or dictionary) you actually create a copy of it. But there's more.
1st problem
This line of code:
if let catArray = optionalCatArray {
creates an immutable copy of optionalCatArray, so it cannot be modified. Use this instead:
if optionalCatArray != nil {
2nd problem
This line of code:
var optionalCatArray:[ArticleItem]? = _articlesDict[item.category]
creates a copy of the array stored in the dictionary - here:
if optionalCatArray != nil {
optionalCatArray!.append(item)
assign a new item to the array, but remember: this is a copy, so you are not modifying the array contained in the dictionary. What's missing is setting it back into the dictionary:
if optionalCatArray != nil {
optionalCatArray!.append(item)
_articlesDict[item.category] = optionalCatArray!
}
Probably this code can be improved by avoiding the array copy like this:
if _articlesDict[item.category] != nil {
_articlesDict[item.category]!.append(item)
} else {
_articlesDict[item.category] = [item]
}
I haven't tested it, but conceptually it should work. Also note how I shortened the else branch, easier to read.

You cannot edit an array in a dictionary directly. You append to a local copy only.
See https://stackoverflow.com/a/24251066/1183577 for more info.

As an alternative to the native-array solutions in other answers, you could do this:
var _articlesDict = [String:NSMutableArray]()
init(articles:[ArticleItem]) {
for item:ArticleItem in articles {
if let array = _articlesDict[item.category] {
array.addObject(item)
} else {
_articlesDict[item.category] = NSMutableArray(object:item)
}
}
}
But keep in mind you'll have to cast to ArticleItem when you extract items from the arrays in the dictionary.

Related

Find reference of item from array - swift

is there a shorter version of the code below?
Also, when accessed with "for var", it is not append because it is "pass by value". Any suggestions?
struct Food {
var fruits = [Fruit]()
var category = "summer fruits"
}
struct Fruit {
var name = ""
}
var foods = Food()
let fruit = Fruit()
fruit.name == "banana"
for var food in foods {
if food.category == "summer fruits" {
food.fruits.append(fruit)
}
}
Using var with arrays is tricky because they still act like values when being passed around (you get a copy, not a reference).
var foods = [Food()]
var food = foods[0]
food.fruits.append(Fruit(name: "banana"))
This does not work if you want to modify the struct inside the foods array. As soon as you do var food = foods[0] you get a mutable copy of foods[0]. The same principle is what causes your example to not work. You are getting a mutable copy of each element in your for loop.
Don't think about it as modifying the struct inside the array, instead think about modifying the array itself. Arrays in Swift are also structs, so changing any element in the array should be thought of as changing the array itself.
foods[0].fruits.append(Fruit(name: "banana"))
There is nothing creating a copy of a value here. You modify the element of the array directly. To do this in a loop, you must use an index:
for i in 0..<foods.count {
foods[i].fruits.append(...)
}
If you want to just modify the first element matching your criterion there is a method for finding a particular index
if let index = foods.firstIndex(where: { $0.category == "summer fruits" }) {
foods[index].fruits.append(fruit)
}
On another note, your code would really benefit from avoiding var and instead using let as much as possible.
let fruit = Fruit(name: "banana")
is much shorter and clearer than your code, and also would help you avoid bugs like using == instead of = for assignment.

Delete struct from array of structs if struct string element is nil or empty swift

Lets say I have a simple struct as such:
struct Model: Codable {
var myVariable: String
var myVariable2: String?
}
And lets say sometimes, myVariable2 is ether a nil/null value ( from json) or a literal empty string as such "".
How would I filter out the empty nil/null and remove that particular struct from the array of structs?
I have tried:
Model.compactMap{ $0.myVaraible2 }.flatMap { $0 }
to filter out the specific string. But obviously I don't get my desired result because It's wrong. so, to first filter out the nil/empty value and then remove that struct from the array, it should be pretty straight forward yes?
Can I filter it out right after I call JSONDecoder?
let model = try JSONDecoder().decode([Model].self, from: data)
something like : I know this next line of code isn't good. =)
for element in model {
if element.myVariable2.isEmpty || element.myVariable2 == "" {
model.remove(at: what to put here ? )
}
I know the for loop is BAD !!! but how would I fix it or do something more swifty ?
Thanks!
The tool you want is filter:
let filteredModels = model.filter { $0.myVariable2 != nil }
This removes all elements which have a nil for this property. The property is still Optional, however, since you defined it that way. If you also want to check for "" you can add that to the predicate:
let filteredModels = model.filter { $0.myVariable2 != nil && $0.myVariable2 != "" }

Loop over array and return TRUE if value exists

I would like to iterate over an array and if a value exists, I would like to return TRUE.
struct Loops {
var loopStep: LoopStep
}
struct LoopStep {
var template: [Template]
}
struct Template {
var stepType: String
}
let templates: [Template] = [Template(stepType: "FORM_ONE"), Template(stepType: "FORM_TWO")]
let loopStep = LoopStep(template: templates)
let incompleteSteps = [Loops(loopStep: loopStep)]
I have tried this using reduce however cannot make this syntax work
let result = incompleteSteps.reduce(true, $0.loopStep.template.stepType == "FORM_ONE" )
You simply need to use contains(where:) to get a bool return value indicating whether an element matching a closure exists in the collection or not. Since template is an Array itself as well, you actually need to nest two contains(where:) calls if you want to find out if an array of Loops contains any Loops whose template array contains a Template with the matching requirement.
let result = incompleteSteps.contains(where: {$0.loopStep.template.contains(where: {$0.stepType == "FORM_ONE"})})

How to update an array with WritableKeyPath in Swift 4.0

In Swift 4.0, I have an array of structs. Is there a way to use keyPaths to update all items in the array without using manually iterating like map or forEach? Something similar to objc [people makeObjectsPerformSelector: #selector(setName:) withObject: #"updated"];
struct Person {
var name: String? = "Empty"
}
var people = [Person(), Person()]
//This only updates one person:
people[keyPath: \[Person].[0].name] = "single update"
//I'm looking to accomplish something like this without a map
let updatedPeople = people.map { (person: Person) -> Person in
var copy = person
copy[keyPath: \Person.name] = "updated"
return copy
}
something like
people[keyPath: \[People].all.name] = "update all without manually iterating"
Mutating into a member of an array requires an l-value. Swift's mechanism for l-values is the subscript, so we can use that:
for i in people.indices {
people[i][keyPath: \Person.name] = updated
// or more simply, just:
// people[i].name = "updated"
// This even works too, but I can't see any reason why it would be desirable
// over the other 2 approaches:
// people[keyPath: \[Person].[i].name] = "update"
}
You could also use forEach, but I generally only recommend that over for in cases where you have an existing closure/function to pass in which has type (Index) -> Void:
// meh
people.indices.forEach {
people[$0][keyPath: \Person.name] = "updated"
}
EDIT Responding to your now edited question asking where is the Swift equivalent of [people makeObjectsPerformSelector: #selector(setName:) withObject: #"updated"], the simple answer is that map, which you for some reason reject in your question, is that equivalent. Of course, to do what Objective-C does, we have to use the Objective-C style of object type, namely a class:
class Person {
var name: String? = "Empty"
}
var people = [Person(), Person()]
people = people.map {$0.name = "updated"; return $0} // *
The starred line is how you make the objects in the array perform the "selector".
A struct is a value type, so as you rightly said in your question, we have to insert a temp variable with a var reference:
struct Person {
var name: String? = "Empty"
}
var people = [Person(), Person()]
people = people.map {var p = $0; p.name = "updated"; return p}
[Original answer:]
The use of key paths in your question seems to be a red herring. You're just asking how to set a property of all the structs in an array.
map is just a way of cycling through the array. You cannot magically do this without cycling through the array; if you don't do it explicitly, you have to do it implicitly.
Here's an explicit way of doing it:
struct Person {
var name: String? = "Empty"
}
var people = [Person(), Person()]
let kp = \Person.name
for i in 0..<people.count {
people[i][keyPath:kp] = "updated"
}
That's not actually any more efficient than using map, though, as far as I know; structs are not mutable in place, so we are still filling the array with entirely new Person objects, exactly as we would have done if using map.

How to populate array of custom objects

class abc{
var name = String()
init(name:String){
self.name = name
}
}
var obj = [abc]()
obj[0] = abc(name:"sun")
obj[1] = abc(name:"sharma")
The issue is that you are trying to access elements of the array by subscript that don't exist by the time you're trying to access them.
var obj = [abc]() just initializes an empty array, so you cannot access its elements by subscript, since it doesn't have any elements yet.
You should use Array.append to add new elements to the end of your array.
var obj = [Abc]()
obj.append(Abc(name:"sun"))
obj.append(Abc(name:"sharma"))
You can also create the array straight away with the elements you want to store in it:
var obj = [Abc(name:"sun"),Abc(name:"sharma")]
After you have populate the array, you can access its elements in several ways. If you want to iterate through the array, you'll usually want to use the for...in.. loop:
for object in obj {
print(object.name)
}
If you want to access a single element, you can do that using array subscripts, but make sure the index doesn't exceed the array's size before using indexing.
let index = 1
if index < array.count { //safe to use index as subscript
print(obj[index])
}
You should also conform to the Swift naming convention, which is upperCamelCase for types (Abc instead of abc).
I think you are asking about creating an array of objects..
It is really easy
Consider you have two classes, Class A and Class B
Class A{
var name:String
init(localName: String){
self.name = localName
}
}
Class B{
//here you can create an array of objects of class A
//this will create an empty array of objects of class A
var objList = [A]()
//to add data into this array u can use append
func addData(objA: A){
self.objList.append(objA)
}
func displayData(list : [A]){
for entry in list{
print(entry.name)
}
}
}

Resources