Elegant way to combine [[(Int, String)]] into [(Int, String)]? - ios

I am using Swift 4, Xcode 9.
Specifically, I have an array of arrays [[(Int, String)]] where Int is a rank and String is a Name + Items... that are combined using .joined(separator: ";").
The data might look like this:
[[1,"My Name;Item1;Item2"], [(5,"My Name;Item2;Item3"), (3,"My Second Name;Item1")]]
I want to combine the inner arrays so that:
Int adds for matching items based on the name (up to ";")
Strings add subsequent items if not already present
Combining my above example should lead to this:
[(6,"My Name;Item1;Item2;Item3"), (3,"My Second Name;Item1")]
I.e. input is [[(Int, String)]] and output is [(Int, String)]
Currently, I can achieve this through a fairly complex set of loops. With a large dataset, this results in a noticeable performance drop. Is there an elegant/simple way to combine these arrays as I am requesting?
Thank you for any guidance!

(Generally I would make this a comment because it doesn't answer the question, but it feels worth the trouble to explain exactly how you should change this.)
This is certainly possible, but don't. The answer is to replace this with an array of structs. Based on your data description:
struct Element {
let rank: Int
let name: String
let items: Set<String> // Since you seem to want them to be unique and unordered
}
let elements: [[Element]] =
[[Element(rank: 1, name: "My Name", items: ["Item1", "Item2"])],
[Element(rank: 5, name: "My Name", items: ["Item2", "Item3"]),
Element(rank: 3, name: "My Second Name", items: ["Item1"])]]
// You want to manage these by name, so let's make key/value pairs of all the elements
// as (Name, Element)
let namedElements = elements.joined().map { ($0.name, $0) }
// Now combine them as you describe. Add the ranks, and merge the items
let uniqueElements =
Dictionary<String, Element>(namedElements,
uniquingKeysWith: { (lhs, rhs) -> Element in
return Element(rank: lhs.rank + rhs.rank,
name: lhs.name,
items: lhs.items.union(rhs.items))
})
// The result is the values of the dictionary
let result = uniqueElements.values
// Element(rank: 6, name: "My Name", items: Set(["Item3", "Item2", "Item1"]))
// Element(rank: 3, name: "My Second Name", items: Set(["Item1"]))

Related

ForEach on a dictionary

I need my app to display a table of data. The data looks like ["body": Optional("Go Shopping"), "isDeleted": Optional(false), "_id": Optional("63333b1600ce507b0097e3b3"), "isCompleted": Optional(false)] The column headers for the table would be the keys body, isDeleted, isCompleted, _id. I will have multiple instances of this data that will have the same keys, but different values. I will need to display the values for each data instance under the respective header and each row will belong to one data instance.
Example:
I'm struggling because the only way I can think of doing this is with a dictionary, but run into a lot of problems when using a dictionary in the View.
*** Important Note:
The app allows a user to select a certain collection and then the app will load all the data for that collection. Each collection has different keys in its data, so I cannot create a specific struct since I won't actually know the keys/values in the data. The model will have to be dynamic in the sense that I don't know what key/value types will be used in each collection and the table will need to redraw when a different collection is selected.
What I Tried
A document class that would hold a 'value: [String: Any?]` the string would be the key and the Any is the value from the data instance
class Document{
let value: [String:Any?]
init(value:[String:Any?]) {
self.value = value
}
}
in my ViewModel I have a call to a database that uses the selected collection name to return an array of all the documents from that collection. I loop through the array and create a Document obj with the value of the Document looking like ["body": Optional("Go Shopping"), "isDeleted": Optional(false), "_id": Optional("63333b1600ce507b0097e3b3"), "isCompleted": Optional(false)] and I add each Document to an array of Document's
class DocumentsViewModel : ObservableObject {
#Published var docKeys: [String]?
#Published var docsList: [Document]?
func getDocs() {
... //Database call to get docs from collection
for doc in docs {
// add doc keys to array (used for table header)
self.docKeys = doc.value.keys.map{$0}
self.docsList?.append(Document(value: doc.value))
}
Then in my View I tried to first display a header from the docKeys and then use that key to loop through the array of [Document] and access the value var and use the key to get the correct value to display under the header for that document
var body: some View {
Text(viewModel.collectionName)
HStack {
ForEach(viewModel.docKeys ?? [], id: \.description) {key in
Text(key.name)
VStack {
ForEach(viewModel.docsList ?? [], id: \.value) { doc in
Text(doc.value[property.name])
}
}
}
}
}
After doing research I understand why I can't ForEach over an unsorted dictionary.
I will accept any help/guidance on how I can display this table. Also, if there is any other suggestions besides using a dictionary? THANK YOU!
**Update
I was able to get it working with an ordered collection
class Document : Hashable, Equatable{
static func == (lhs: Document, rhs: Document) -> Bool {
lhs.id.description == rhs.id.description
}
let id: String
let value: OrderedDictionary<String,Any?>
init(id: String, value: OrderedDictionary<String,Any?>) {
self.id = id
self.value = value
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
Since it is ordered this allowed me to iterate of the dictionary in the View
Just a a few hints:
If you need "dictionary with order" you can try to use a Key-ValuePairs object which is essentially an array of tuples with labels key and value
let values: KeyValuePairs = ["key1": "value1", "key2": "value2"]
when you print this to the console you'll realize that this is just a tuple!
print(values[0]) will display (key: "key1", value: "value1")
please take a look at OrderedDictionary from The Swift Collections https://www.kodeco.com/24803770-getting-started-with-the-swift-collections-package
have you consider to use an array of simple structs instead?
struct Document {
let body: String?
let isDeleted: Bool?
let id: String?
let isCompleted: Bool?
...
}

same value in dictionary in swift

I m mapping data that come from service with using dictionary [String: String]. I collect them dictionary array. For example, if their parent ids are the same, I want to add their values by array value.
["ParentId": "1","Value": "["Giyim","Aksesuar","Ayakkabı"]"]
It is also the reason I don't know parent id sometimes on the left sometimes on the right in photo
Here is my code and its output.
struct Categories {
let parentId: String
let values: [String]
}
for result in results {
if result?.parentCategoryId != "" {
for docId in self.docIds {
if result?.parentCategoryId == docId {
//print(result?.name)
var values = [String]()
values.append(result?.name ?? "")
self.newCat.append(Categories(parentId: docId, values: values))
}
}
}
}
Problem
As far as I understand from the description you want to map some service data structure to a dictionary where key is parentId and value is an array of some items referred to parentId.
I believe your problem comes from a misunderstanding of the concept of dictionary as a data structure.
[String: String] is dictionary where keys and their associated values are of String type. For example:
["firstKey": "firsthValue", "secondKey": "secondValue", ...]
That means you cannot store associated values of String and Array types in the same dictionary, as you already told the compiler you would like to store only strings.
It is also the reason I don't know parent id sometimes on the left sometimes on the right in photo
This is because key-value pairs are stored in the dictionary without order. This is how dictionaries work :) I'd strongly recommend reading some short official materials to get used to them.
New Swift 5.4 version has a new OrderedDictionary data structure, where keys are ordered, but there is absolutely 100500% no reason to use it for your problem*
Possible solutions
In your case i would suggest either use some struct:
struct SomeData {
let parentID: Int
var values: [String]
}
var storage: [SomeData] // alternative to emptyDic
// Filling storage with data from service
for result in inputData {
// search for SomeData with required id, add value
// OR create SomeData if there is no such id in array yet
}
OR [personally this appeals to me more]
Store data in [String: [String]] dictionary, where the key is parentID and the associated value is an array of values.
The algorithm of filling this dictionary is pretty the same:
You add new key-value pair for every new parentID
You append new values for parentIDs that are already in the dictionary.
Using the struct approach, you could do something like this (you'll need to adapt it to your code, but that should be straightforward):
struct Categories {
let parentId: String
var values: [String] //notice this needs to be a var, not a let
}
func addItem(categories : inout [Categories], docId: String, name: String) {
if let index = categories.firstIndex(where: { $0.parentId == docId }) {
categories[index].values.append(name)
} else {
categories.append(Categories(parentId: docId, values: [name]))
}
}
func addValues() {
var categories = [Categories]()
addItem(categories: &categories, docId: "4", name: "Test1")
addItem(categories: &categories, docId: "1", name: "Test")
addItem(categories: &categories, docId: "4", name: "Test2")
addItem(categories: &categories, docId: "4", name: "Test3")
print(categories)
//in your code, it'll look more like:
// addItem(categories: &self.newCat, docId: docId, name: result?.name ?? "")
}
Which yields this:
[
StackOverflowPlayground.Categories(parentId: "4", values: ["Test1", "Test2", "Test3"]),
StackOverflowPlayground.Categories(parentId: "1", values: ["Test"])
]
I still wonder whether you maybe just want a Dictionary that is keyed by the parentId, but not knowing your use case, it's hard to say.

Not correctly referencing random number in Swift object

I have an array of objects where each object has an Exercise Name and a random number of reps.
I then have a function to generate a random workout (with between 3 - 6 exercises in it)
However, when I print it the reps are almost always 1, 2 or occasionally 14, despite loading it 30 times or so.
Am I doing something wrong here?
Here's my objects and struct :
struct exerciseInWorkout {
let name : String
let reps : Int
}
let exerciseBankArray = [
exerciseInWorkout(name: "Squat", reps: (Int(arc4random_uniform(10)))),
exerciseInWorkout(name: "Push Ups", reps: (Int(arc4random_uniform(5)))),
exerciseInWorkout(name: "Viking Press", reps: (Int(arc4random_uniform(20)))),
]
and here's my function :
func generateWorkout(){
let possibleExercises = exerciseBankArray
let numberOfExercisesKey = Int(arc4random_uniform(4) + 3)
let workoutSet : [exerciseInWorkout] = (1...numberOfExercisesKey).map { _ in
let randomKey = Int(arc4random_uniform(UInt32(possibleExercises.count)))
return exerciseInWorkout(name: exerciseBankArray[randomKey].name, reps: exerciseBankArray[randomKey].reps)}
print (workoutSet)
}
}
Also, is there a way to create a set of these to avoid the same exercise coming up twice? I tried using Set but didn't seem to work at all.
Lastly, when I print it, each object is prepended with "project.exerciseInWorkout... is there any way to just print/return the clean array i.e. [["name: "press ups", reps: 12], [name:xyz, reps: 30]]?
Reason being I want to pass this to a new VC to put in a table view next and presume I need a clean array to do that.
Short Answer
It looks like your array exerciseBankArray is stored globally, which means exerciseInWorkout components are initialized once for the whole app. Having said that, it is then normal that the number of reps is always the same, the arc4random_uniform is only executed on the first access.
Quick Fix
If you want to keep the same structure, I recommend this
remove the arc4random_uniform from exerciseBankArray and just write the maximum amount of reps you want
let exerciseBankArray = [
exerciseInWorkout(name: "Squat", maxReps: 10),
exerciseInWorkout(name: "Push Ups", maxReps: 5),
exerciseInWorkout(name: "Viking Press", maxReps: 20)
]
Call the random function inside generateWorkout() like this
func generateWorkout(){
let possibleExercises = exerciseBankArray
let numberOfExercisesKey = Int(arc4random_uniform(4) + 3)
let workoutSet : [exerciseInWorkout] = (1...numberOfExercisesKey).map { _ in
let randomKey = Int(arc4random_uniform(UInt32(possibleExercises.count)))
return exerciseInWorkout(
name: exerciseBankArray[randomKey].name,
reps: Int(arc4random_uniform(exerciseBankArray[randomKey].maxReps))
)
}
}
Longer Improvements
If you're open to making a better architecture for your code, here are some suggestions
Remove global variables
Split your model for exercise into two classes / structs:
One that represents an actual exercise
struct WorkoutExercise {
let name: String
let reps: Int
}
One that represents a generator of exercise
struct WorkoutExerciseGenerator {
let name: String
let maxReps: Int
// Add any other parameter you need to generate a good exercise
func generate() -> WorkoutExercise {
return WorkoutExercise(
name: name,
reps: Int(arc4random_uniform(maxReps))
)
}
}
More Improvements / Q&A
When you say remove global variables do you mean store the array of exercises in each VC that needs them? I just thought that would be “repeating myself” (from DRY principles etc?)
I totally agree with the DRY guidelines, but there are many ways to not repeat yourself. The issue with global variables (a variable that is not inside any class, just free-floating) are numerous:
It gets awkward when you want to include it in different targets
It's not part of any namespace, so it might overload another one from another library of file, it messes up the auto-completion, etc...
etc... you can find more documentation in this thread
Also, if I change to the 2nd example above, how would I then call the
right amount of those? Just replace “return exerciseInWorkout” with
the new function? Or would it be unchanged because the func is
contained in the struct?
So I understand correctly, what you actually want to create is a set of default generators for exercises that have a name and a max count of reps, and these should be available in the whole project (hence why you used global variables).
A good way to improve this code is by defining static generators, for instance you can update WorkoutExerciseGenerator to be
struct WorkoutExerciseGenerator {
let name: String
let maxReps: Int
// Add any other parameter you need to generate a good exercise
func generate() -> WorkoutExercise {
return WorkoutExercise(
name: name,
reps: Int(arc4random_uniform(maxReps))
)
}
// Generates a "Squat" workout
static var squat {
return WorkoutExerciseGenerator(name: "Squat", maxReps: 10)
}
// Generates a "Push Up" workout
static var pushUp {
return WorkoutExerciseGenerator(name: "Push Ups", maxReps: 5)
}
// Generates a "Viking Press" workout
static var vikingPress {
return WorkoutExerciseGenerator(name: "Viking Press", maxReps: 20)
}
}
Now that you have these specific generators, it looks like you also want to have a way to generate a whole workout. If that's the case, then you can simply create, in addition to what I wrote about, some objects to represent a workout and a workout generator.
/// This represents a whole workout that contains
/// multiple exercises
struct Workout {
let exercises: [WorkoutExercise]
}
/// This allows to dynamically creates a Workout
struct WorkoutGenerator {
// This is the "pool" of exercises from
// which it generates a workout (similar to your
// `exerciseBankArray`)
let exercisePool: [ExerciseGenerators]
// Min and max amount of workouts
let minCount: Int
let maxCount: Int
// Generates a workout from the generator
func generate() -> WorkoutGenerator {
let amount = Int(arc4random_uniform(maxCount - minCount)) + minCount
let exercises = (0..<amount).map { _ in
// Selects an exercise generator at random
let index = Int(arc4random_uniform(exercisePool.count))
// Generates a random workout exercise from this generator
return exercisePool[index].generate()
}
return Workout(exercises: exercises)
}
// Same thing here, you can use a static variable to create
// a "default" workout generator that contains the exercises
// you had inside your `exerciseBankArray`
static var default: WorkoutGenerator {
return WorkoutGenerator(
exercisePool: [.squat, .pushUp, .vikingPress],
minCount: 3,
maxCount: 6
)
}
}
Now that you have all of this, the only thing you need to do to create a totally random work-out according to your requirements is
let myWorkout = WorkoutGenerator.default.generate()
If you want to add more exercise types, just create more static ExerciseGenerator, and if you want to create different types of workouts (maybe with different exercise pools, some hard or some easy), just create additional static WorkoutGenerator. (Note that you don't need static, you can also just create the object directly in your VC).
Hope that helps!

Swift: Associative Arrays in UITableView

Pretty new so apologies if this is noobish.
I'm trying to get the key and the value from an associative array to print out as the label text in a cell.
At the moment I have the array:
let users = ["John","James","Liam"]
and I am getting the value for the cell text like so:
cell.textLabel!.text = self.users[indexPath.row]
Which will give me rows of the names in. I am struggling when I add in user ages like so
lets users = ["John" : 36, "James": 12, "Liam": 30]
I get this error:
Ambiguous reference to member 'subscript'
How do I get the cell text to display both the name and age?
If you really need to refer to the data via an array, there are at least two ways you can easily accomplish this. Create a Person object like in the comments, or you can quickly use a tuple structure. The following code works in Xcode Playground (Swift 3, 4).
// - First Approach
struct Person {
var name: String
var age: Int
}
let users = [Person(name: "John", age: 36), Person(name: "James", age: 12), Person(name: "Liam", age: 30)]
// say indexPath.row = 0
print("name: \(users[0].name), age: \(users[0].age)") // name: John, age: 36
// - Second Approach
let usersTuple = [("John", 36), ("James", 12), ("Liam", 30)]
print("name: \(usersTuple[0].0), age: \(usersTuple[0].1)") // name: John, age: 36
You need to define a dictionary and with ages and names and in each cell get from the dictionary the age with the name as key, or you can define a model with name and age like Person and put and use as your datasource Array, which I think is better
class Person{
var age : Int = 0
var name : String = ""
init(name:String,age:Int){
self.name = name
self.age = age
}
}
declare an array of Person in your ViewController
var users = [Person(name: "John", age: 36),Person(name: "James", age: 12),Person(name: "Liam", age: 30)]
In the cellForRow method
let currUser = self.users[indexPath.row]
cell.textLabel!.text = currUser.name + "Age: \(currUser.age)"
If you're coming from a PHP style background, it can be confusing that dictionaries look like associated arrays but there's no way to reference them by index like in PHP. You'll need to reference by the key (the string) or convert them into something which you can retrieve later.
Luckily in Swift you can just use something called a tuple, which looks like this:
let user = ("John", 36)
And you reference the values like this:
let name = user.0
let age = user.1
You can store tuples in arrays, so you can do this:
let users = [("John", 36), ("James", 12)]
let johnAge = users[0].1
You can also give the tuple named parameters but by this point you might as well create a struct. Here's what it looks like anyway:
let user = (name: "John", age: 36)
let johnAge = user.age
Another neat trick with tuples is that if you typealias it you can create it the same way you would a struct, without having to device the properties individually, like this:
typealias User = (name: String, age: Int)
let john = User(name: "John", age: 36)
By this point it's just a case of saving a few lines of code but it really depends on how lightweight you need the user object to be. You wouldn't be able to make it conform to anything in the future, which is why a struct can be a better option. If all you ever need is a lightweight object, tuples are great.

Compare objects and edit array [duplicate]

This question already has answers here:
How to group by the elements of an array in Swift
(16 answers)
Closed 6 years ago.
I want to create array of unique elements by specific property.
Ex:
I have array of objects (Person) :
struct Person {
var name: String?
var secondName: String?
init (name: String, secondName: String) {
self.name = name
self.secondName = secondName
}
}
let person1 = Person(name: "name1", secondName: "secondName1")
let person2 = Person(name: "name2", secondName: "secondName2")
let person3 = Person(name: "name1", secondName: "secondName3")
let personsArray = [person1, person2, person3]
I want to get new array, that will contain person objects with unique name
something like this $0.name == $1.name
What is the best way to achieve that ?
Result should be arrays of objects with unique name param = [[person1, person3], [person2]]
This is my personal interpretation of your question
Given an array of Person(s) you want in output several dictionaries where the key is the name of a person and the value is a list of persons with that name.
Here's the code
let dict = persons.reduce([String:[Person]]()) { (dict, person) -> [String:[Person]] in
var dict = dict
dict[person.name] = (dict[person.name] ?? []) + [person]
return dict
}
One approach: You could add them one by one to a dictionary where "name" is the key (consider using lowercase for it), and "array of Persons" is the value. When done, the keys array will have all your unique "name" values, and each key's value will be the array of Persons with that "name". You could then "trim" your dictionary by removing any key with an array that has a count less than 2.
Alternative: Sort the array by "name", then you can easily remove any that don't appear twice (if an element doesn't match one of it's neighbors, then remove it).

Resources