I have an array of dictionaries, [[String:AnyObject]], which is reduce+sorted as below successfully.
var arrUserList = [(key: String, value: [[String : Any]])]()
let result = self.arrJsonDict.reduce(into: [String: [[String:Any]]]()) { result, element in
let strName: String = (element as! NSDictionary).value(forKey: "name") as! String
if let firstLetter = strName.first {
let initial = String(describing: firstLetter).uppercased()
result[initial, default: [[String:Any]]() ].append(element as! [String : Any])
}}.sorted { return $0.key < $1.key }
self.arrUserList = result
Now I wanted to assign keys to table sections and values as table cell text from the array.
This is very cumbersome code.
You are highly encouraged to use a struct rather than a dictionary at least with a member name
struct Person {
let name : String
}
Declare and rename arrJsonDic (more descriptively) as
var people : [Person]()
and arrUserList as
var users = [String: [Person]]()
For the sections declare another array
var letters = [String]()
Group the array and populate letters simply with
users = Dictionary(grouping: people, by: { String($0.name.first!) })
letters = users.keys.sorted()
In the table view in numberOfSections return
return letters.count
and in numberOfRows return
let letter = letters[section]
return users[letter]!.count
In cellForRowAt assign a name to a label with
let letter = letters[indexPath.section]
let user = users[letter]![indexPath.row]
cell.nameLabel.text = user.name
------------------------------
To make it still swiftier declare a second struct Section
struct Section {
let index : String
let people : [Person]
}
delete
var letters = [String]()
and declare users
var users = [Section]()
The grouping is slightly different
let grouped = Dictionary(grouping: people, by: { String($0.name.first!) })
users = grouped.map({ Section(index: $0.0, people: $0.1) }).sorted{$0.index < $1.index}
The code in the three table view delegate methods are
return users.count
-
return users[section].people.count
-
let user = users[indexPath.section].people[indexPath.row]
cell.nameLabel.text = user.name
Related
I try to parse and assign data, which I am becoming from Firebase
The structure in Firebase looks like this:
I try to fetch data from database and assign it to instance of class Meal:
ref = Database.database().reference()
databaseHandle = ref.child("Meal").observe(.value, with: { (snapshot) in
var downloadedName : String!
var downloadedPhoto : String!
var downloadedRating : Int!
var downloadedSteps : Array <String>!
var downloadedIngredients : [Dictionary <String, String>]!
print(snapshot.value)
if let dict = snapshot.value as? Dictionary<String, Any>{
print("VALUES!!!")
for key in dict.keys {
if let values = dict[key] as? Dictionary<String, Any> {
print(values)
if let name = values["name"] as? String{
downloadedName = name
}
if let photo = values["photo"] as? String{
downloadedPhoto = photo
}
if let rating = values["rating"] as? Int{
downloadedRating = rating
}
if let steps = values["steps"] as? Array<String>{
downloadedSteps = steps
}
if let ingredients = values["ingredients"] as? [Dictionary <String, String>]{
downloadedIngredients = ingredients
}
let meal = Meal(name: downloadedName, photo: UIImage(named: downloadedPhoto), rating: downloadedRating, steps: downloadedSteps, ingredients: downloadedIngredients)
self.meals.append(meal!);
}
}
The Meal class itself looks like this:
class Meal {
var name: String
var photo: UIImage?
var rating: Int
var steps: Array<String>
var ingredients: [Dictionary<String, String>]}
I get the first print - the whole data, so the connection with DB is OK, but as i try to assign it - nothing happens, no errors, no data (the second print with message VALUES!!! is not shown at all, what am I doing wrong?
Here is also what I get by first print
Optional(<__NSArrayM 0x600002fdaa60>(
{
ingredients = (
{
amount = 100;
ingredient = milk;
measurement = ml;
},
{
amount = 120;
ingredient = milk;
measurement = ml;
}
);
name = "Caprese Salad";
photo = meal1;
rating = 4;
steps = (
test1,
test11
);
},
{
ingredients = (
{
amount = 100;
ingredient = milk;
measurement = ml;
},
{
amount = 120;
ingredient = milk;
measurement = ml;
}
);
name = "Chicken and Potatoes";
photo = meal2;
rating = 3;
steps = (
test2,
test22
);
},
{
ingredients = (
{
amount = 100;
ingredient = milk;
measurement = ml;
},
{
amount = 120;
ingredient = milk;
measurement = ml;
}
);
name = "Pasta with Meatballs";
photo = meal3;
rating = 2;
steps = (
test3,
test33
);
}
)
)
So, I assume, I retrieve the data in the false way at some point, how could i fix it?
You have:
print(snapshot.value)
if let dict = snapshot.value as? Dictionary<String, Any>{
print("VALUES!!!")
....
}
You say that print(snapshot.value) is called but not print("VALUES!!!").
Well, that means that snapshot.value or it isn't a Dictionary which keys are String objects and values are of type Any.
Now, let's see the output of snapshot.value:
Optional(<__NSArrayM 0x600002fdaa60> ...
NSArrayM => NSMutableArray, so snapshot.value is an Array, not a Dictionary. Of course then the as? Dictionary will fail!
So you need to treat it as an Array.
Quickly written:
if let array = snapshot.value as? [[String: Any]] {
for aValue in array {
let name = aValue["name"] as? String ?? "unnamed"
let photoName = aValue["photo"] as? String ?? "noPhoto"
let rating = aValue["rating"] as? Int ?? 0
let steps = aValue["steps"] as? [String] ?? []
let ingredients = aValue["ingredients"] as? [[String: String]] ?? [:]
let meal = Meal(name: name, photo: UIImage(named: photoName), rating: rating, steps: steps, ingredients: ingredients)
self.meals.append(meal)
}
}
What could also been wrong with your approach:
var downloadedName : String!
loop {
if let name = values["name"] as? String {
downloadedName = name
}
let meal = Meal(name: downloadedName, ...)
}
Well, if for the second value you didn't have a name (either because it's not a String or because the value doesn't exist), you downloadedName would have the value of the first one, but the rest of the values of the second one.
I used the approach:
let name = aValue["name"] as? String ?? "unnamed"
Meaning, that if it's nil, it gets a default value.
But you could decide to accept only valid one, by a guard let or if let potentially on all the subvalues.
For instance:
if let name = aValue["name"] as? String,
let let photoName = aValue["photo"] as? String,
... {
let meal = Meal(name: name, photo: UIImage(named: photoName)
self.meals.append(meal)
}
And since your Meal.init() seems to return an optional, you could add it also in the if let and avoid the force unwrap of meal.
In this I am getting data from server response after posting parameters and here I need to display it on table view and it should be displayed like shown below in the image 0 is the price for the particular shipping method
already i had written model class for server response data and here it is
struct ShippingMethod {
let carrierCode : String
let priceInclTax : Int
let priceExclTax : Int
let available : Any
let carrierTitle : String
let baseAmount : Int
let methodTitle : String
let amount : Int
let methodCode : String
let errorMessage : Any
init(dict : [String:Any]) {
self.carrierCode = dict["carrier_code"] as! String
self.priceInclTax = dict["price_incl_tax"]! as! Int
self.priceExclTax = dict["price_excl_tax"]! as! Int
self.available = dict["available"]!
self.carrierTitle = dict["carrier_title"] as! String
self.baseAmount = dict["base_amount"]! as! Int
self.methodTitle = dict["method_title"]! as! String
self.amount = dict["amount"]! as! Int
self.methodCode = dict["method_code"] as! String
self.errorMessage = (dict["error_message"] != nil)
}
}
by using this I had formed an array type like this by using code
var finalDict = [String: [String]]()
var responseData = [ShippingMethod]()
do
{
let array = try JSONSerialization.jsonObject(with: data, options: []) as? [[String : Any]]
for item in array! {
self.responseData.append(ShippingMethod.init(dict: item))
}
print(self.responseData)
}
catch let error
{
print("json error:", error)
}
print(self.responseData)
for item in self.responseData {
let dict = item
let carrierTitle = dict.carrierTitle
let methodTitle = dict.methodTitle
if self.finalDict[carrierTitle] == nil {
self.finalDict[carrierTitle] = [String]()
}
self.finalDict[carrierTitle]!.append(methodTitle)
}
print(self.finalDict)
the output of this finalDict is ["Flat Rate": ["Fixed"], "Best Way": ["Table Rate"]] in this carrier title key value pair should be displayed as section title and is Flat Rate and method title key value pair should be displayed as rows in section Fixed but the problem is I need amount key value pair with it also for corresponding method title can anyone help me how to get this ?
Why don't you create another struct for displaying row data:
struct CarrierInfo {
let name:String
let amount:Int
}
Change your finalDict to
var finalDict = [String: [CarrierInfo]]()
and create CarrierInfo instance and set it in finalDict
for item in self.responseData {
let dict = item
let carrierTitle = dict.carrierTitle
let methodTitle = dict.methodTitle
let amount = dict.amount
if self.finalDict[carrierTitle] == nil {
self.finalDict[carrierTitle] = [CarrierInfo]()
}
self.finalDict[carrierTitle]!.append(CarrierInfo(name: carrierTitle, amount: amount))
}
Likewise you can make other required changes. This would neatly wrap your row display data inside a structure.
PS: I have not tested the code in IDE so it may contain typos.
You can assign another dictionary with key as methodTitle and amount as value. i.e., ["fixed":"whatever_amount"]
OR
You can use finalDict differently, like ["Flat Rate": ["tilte":"Fixed","amount":"0"], "Best Way": ["title":"Table Rate","amount":"0"]]
If it is difficult for you to code this, you can revert back.
Edit
You can use the following code to create the array in the second solution I suggested above:
for item in self.responseData {
let dict = item
let carrierTitle = dict.carrierTitle
let methodTitle = dict.methodTitle
let amount = dict.amount
if self.finalDict[carrierTitle] == nil {
self.finalDict[carrierTitle] = [[String:String]]()
}
let innerDict = ["title":methodTitle,"amount":amount]
self.finalDict[carrierTitle]!.append(innerDict)
}
I'v created a struct and I want to populate it with my data.
My struct:
struct CrimeNameSection {
var firstChar: Character
var name: [String]
var detail: [String]
var time: [String]
init(firstLetter: Character, object1: [String], object2: [String], object3: [String]) {
firstChar = firstLetter // First letter of 'name'
name = object1
detail = object2
time = object3
}
The first value of my struct ('firstChar') should hold the first letter in 'name' to create an alphabetic sections in tableView, the rest ('name','detail','time') should hold the data from my database (three columns: name, detail, time).
My code:
var marrCrimesData : NSMutableArray! // Hold the database
func getSectionsFromData() -> [CrimeNameSection] {
guard marrCrimesData != nil else {
return []
}
var sectionDictionary = [CrimeNameSection]()
for crime in marrCrimesData {
let crime = crime as! CrimesInfo
let firstChar = CrimeNameSection(firstLetter: crime.name[crime.name.startIndex], object1: [crime.name], object2: [crime.detail], object3: [crime.time])
if var names = firstChar {
names.append(crime.name)
sectionDictionary[firstChar] = names
} else {
sectionDictionary[firstChar] = [crime.name]
}
}
let sections = sectionDictionary.map { (key, value) in
return CrimeNameSection(firstLetter: key, name: value)
}
let sortedSections = sections.sorted { $0.firstLetter < $1.firstLetter }
return sortedSections
}
I get errors all over the place, I need help with storing the data inside my struct and sort it alphabetically.
Thank you all
Consider
struct Crime {
let name: String
let detail: String
let time: String
}
let crimes = [
Crime(name: "Foo", detail: "detail 1", time: "9am"),
Crime(name: "Bar", detail: "detail 2", time: "10am"),
Crime(name: "Baz", detail: "detail 3", time: "11am"),
Crime(name: "Qux", detail: "detail 4", time: "12am")
]
One approach is to just build an dictionary indexed by the first character and then sort it:
var crimeIndex = [Character: [Crime]]()
for crime in crimes {
if let firstCharacter = crime.name.characters.first {
if crimeIndex[firstCharacter] == nil {
crimeIndex[firstCharacter] = [crime]
} else {
crimeIndex[firstCharacter]?.append(crime)
}
}
}
let sortedIndex = crimeIndex.sorted { $0.0 < $1.0 }
The advantage of the above is that we can use the dictionary to efficiently find the section. If you really want to use your custom "name section" structure, I'd first make it to use an array of Crime objects (having disjointed arrays of the properties of a Crime can be fragile, e.g. if you later decide to add sorting of the crimes). So it might look like:
struct CrimeNameSection {
let firstCharacter: Character
var crimes: [Crime]
}
And because we've lost some of the Dictionary efficiency for finding the index and have manually iterate through looking for the section, and I'll go ahead and do an insertion sort at the time, saving me from having to do a separate sort later:
var crimeSections = [CrimeNameSection]()
for crime in crimes {
if let firstCharacter = crime.name.characters.first {
var hasBeenAdded = false
for (index, crimeIndex) in crimeSections.enumerated() {
if firstCharacter == crimeIndex.firstCharacter { // if we found section, add to it
crimeSections[index].crimes.append(crime)
hasBeenAdded = true
break
}
if firstCharacter < crimeIndex.firstCharacter { // if we've passed where the section should have been, insert new section
crimeSections.insert(CrimeNameSection(firstCharacter: firstCharacter, crimes: [crime]), at: index)
hasBeenAdded = true
break
}
}
// if we've gotten to the end and still haven't found section, add new section to end
if !hasBeenAdded {
crimeSections.append(CrimeNameSection(firstCharacter: firstCharacter, crimes: [crime]))
}
}
}
First of all you could not instantiate an Array and map over it like a dictionary
var sectionDictionary = [CrimeNameSection]() // Here you are init an Array
For a dictionary you have also to specify the key, for instance if the key is a string:
var sectionDictionary = [String: CrimeNameSection]() // Dictionary init
But be aware that the key have to be unique so that the dict would work properly.
Another problem here is the constructor in your .map function because you have not created a constructor for your CrimeNameSection that only takes two parameters:
init(firstLetter: Character, object1: [String], object2: [String], object3: [String]) {
firstChar = firstLetter // First letter of 'name'
name = object1
detail = object2
time = object3
}
// Another constructor with 2 arguments
init(firstLetter: Character, object1: [String]) {
firstChar = firstLetter // First letter of 'name'
name = object1
detail = []()
time = []()
}
If you don't want to use another constructor then you have to provide default values to object2 and object3 in your initial constructor.
I want to sort an array of performers so that they are grouped by the first character of their first name. So for example 'A' in the following output, is for a collection of performers who's first name starts with 'A'.
[
"A"[Performer,Performer,Performer,Performer]
"B"[Performer,Performer,Performer]
"C"[Performer,Performer,Performer]
"D"[Performer,Performer,Performer]
"F"[Performer,Performer,Performer]
"M"[Performer,Performer,Performer]
... etc
]
I have achieved it but I'm hoping there is a more trivial way to do it. The following is how I achieved it.
class Performer {
let firstName: String
let lastName: String
let dateOfBirth: NSDate
init(firstName: String, lastName: String, dateOfBirth: NSDate) {
self.firstName = firstName
self.lastName = lastName
self.dateOfBirth = dateOfBirth
}
}
private var keyedPerformers: [[String: [Performer]]]?
init() {
super.init()
let performers = generatePerformers()
let sortedPerformers = performers.sort { $0.0.firstName < $0.1.firstName }
keyedPerformers = generateKeyedPerformers(sortedPerformers)
}
//'sortedPerformers' param must be sorted alphabetically by first name
private func generateKeyedPerformers(sortedPerformers: [Performer]) -> [[String: [Performer]]] {
var collectionKeyedPerformers = [[String: [Performer]]]()
var keyedPerformers = [String: [Performer]]()
var lastLetter: String?
var collection = [Performer]()
for performer in sortedPerformers {
let letter = String(performer.firstName.characters.first!)
if lastLetter == nil {
lastLetter = letter
}
if letter == lastLetter {
collection.append(performer)
} else {
keyedPerformers[lastLetter!] = collection
collectionKeyedPerformers.append(keyedPerformers)
keyedPerformers = [String: [Performer]]()
collection = [Performer]()
}
lastLetter = letter
}
return collectionKeyedPerformers.sort { $0.0.keys.first! < $0.1.keys.first! }
}
First of all, since you have an (sorted) array of section index titles it's not necessary to use an array of dictionaries. The most efficient way is to retrieve the array of Performers from the dictionary by key.
Add a lazy instantiated variable initialLetter in the Performer class. I added also the description property to get a more descriptive string representation.
class Performer : CustomStringConvertible{
let firstName: String
let lastName: String
let dateOfBirth: NSDate
init(firstName: String, lastName: String, dateOfBirth: NSDate) {
self.firstName = firstName
self.lastName = lastName
self.dateOfBirth = dateOfBirth
}
lazy var initialLetter : String = {
return self.firstName.substringToIndex(self.firstName.startIndex.successor())
}()
var description : String {
return "\(firstName) \(lastName)"
}
To create the performers use an array rather than a simple string. The "missing" letters are already omitted.
private let createIndexTitles = ["A","B","C","D","E","F","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
The function to create the dummy instances pre-sorts the arrays of performers by initialLetter and then by lastName
private func generatePerformers() -> [Performer] {
var performers = [Performer]()
for i in 0...99 {
let x = i % createIndexTitles.count
let letter = createIndexTitles[x]
performers.append(Performer(firstName: "\(letter)", lastName: "Doe", dateOfBirth: NSDate()))
}
return performers.sort({ (p1, p2) -> Bool in
if p1.initialLetter < p2.initialLetter { return true }
return p1.lastName < p2.lastName
})
}
Now create an empty array of strings for the section title indexes
private var sectionIndexTitles = [String]()
The function to create the keyed performers follows a simple algorithm : If the key for initialLetter doesn't exist, create it. Then append the performer to the array. At the end assign the sorted keys of the dictionary to sectionIndexTitles, the elements of the keyed arrays are already sorted.
private func generateKeyedPerformers(sortedPerformers: [Performer]) -> [String: [Performer]] {
var keyedPerformers = [String: [Performer]]()
for performer in sortedPerformers {
let letter = performer.initialLetter
var keyedPerformer = keyedPerformers[letter]
if keyedPerformer == nil {
keyedPerformer = [Performer]()
}
keyedPerformer!.append(performer)
keyedPerformers[letter] = keyedPerformer!
}
sectionIndexTitles = Array(keyedPerformers.keys).sort { $0 < $1 }
return keyedPerformers
}
Now test it
let performers = generatePerformers()
let keyedPerformers = generateKeyedPerformers(performers)
print(sectionIndexTitles , keyedPerformers)
In the (presumably) table view use sectionIndexTitles as the section array and get the performer arrays by key respectively.
There is an issue that i can't understand and can not find an answer to:
i have this method in a tableViewCOntroller that is calling another viewCOntroller with TableView inside it
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowBook" {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
let books = categoryStore.allCategories[selectedIndexPath.row].books
let destinationVC = segue.destinationViewController as! BookViewController
destinationVC.books = books
}
}
}
Then in the BookViewController i have this just for testing:
var books = [Book]()
override func viewDidLoad() {
super.viewDidLoad()
print(books[0].name)
}
I can see that the books var is an array that is holding books:
class Book {
var name: String = ""
var id: Int = -1
var categoryId: Int = -1
var bookAuthor: String = ""
var level1Name: String = ""
var level2Name: String = ""
var level3Name: String = ""
var level4Name: String = ""
var levels: Int = -1
var orderKey: Int = -1
var viewCount: Int = -1
var viewLevel: Int = -1
var chapters: [AnyObject] = []
}
so i am getting an array of books with 13 key/value pairs dictionary in the books var
when i'm trying to print any thing, lets say:
print(books[0].name)
I get the error:
fatal error: NSArray element failed to match the Swift Array Element type
and i can't understand why...
p.s The transition is working and i can see the next table but then getting the fatal error
Ok, let's start with your error.
fatal error: NSArray element failed to match the Swift Array Element
type
A swift array is complaining that one of it's elements is not what it's expecting. The NSArray element does not match the swift array element.
Now we know that a NSArray can store different types of elements, for example, NSNumber, NSDictionary, NSString... you get the picture. So clearly our issue here is type related.
Now looking at your segue code we can see that we do not actually state what type books is, we let swift's type inference work it out for us. This is where our first sign of the issue occurs. You are expecting an array of [Book] but for some reason you are getting a type of NSArray.
If you make the change from:
let books = categoryStore.allCategories[selectedIndexPath.row].books
let books : [Book] = categoryStore.allCategories[selectedIndexPath.row].books
You will now crash at this line, because you are now stating the type expected.
Full Method:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowBook" {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
let books : [Book] = categoryStore.allCategories[selectedIndexPath.row].books
let destinationVC = segue.destinationViewController as! BookViewController
destinationVC.books = books
}
}
}
This is backed up by your debugger console output, it says that you have an array of 13 NSDictionary types.
This means you need to investigate where you populate the [Book] array - i.e. categoryStore.allCategories
OK, This was a beginner problem that might help others.
The scenario: I have a complex JSON: root is object, for each key in the object there is a numeric value (deferent key but the value has the same structure) and the value is the Category Object!
For each category there are many key/Value pairs and one of them is the the Books KEY and the value is array or objects, each one is a book.
The problem: when i deserialised the JSON, i parsed the categories with an array of books, but the books as mentioned in the comments weren't really book instances more array of NSDictionary objects.
So after deserialising the Category i have to add an init method in The Book class to get an NSDictionary and parse it into a Book Object.
After that for each category i have to go over the the NSArray of books and for each one to create an Object go Book and return an Array of Books.
That did the trick and now every thing is working:
API:
// Getting NSArray of NSDictionary that represnets books and convert it into Books array
static func getBooksFromArr(booksArr: NSArray) -> [Book] {
var books = [Book]()
for book in booksArr {
let thisBook = Book(bookDict: book as! NSDictionary)
books.append(thisBook)
print(book)
}
return books
}
// Parsing the category object
private static func CategoryFromJSONObject(json: [String: AnyObject]) -> Category? {
guard let
id = json["Id"] as? Int,
name = json["Name"] as? String,
booksArr = json["Books"] as? NSArray else {
return nil
}
let books = getBooksFromArr(booksArr)
return Category(name: name, id: id, books: books)
}
Book Class:
init(bookDict: NSDictionary){
self.name = (bookDict["Name"] ?? "") as! String
self.id = (bookDict["Id"] ?? -1) as! Int
self.categoryId = (bookDict["CategoryId"] ?? -1) as! Int
self.bookAuthor = (bookDict["BookAuthor"] ?? "") as! String
self.level1Name = (bookDict["Level1Name"] ?? "") as! String
self.level2Name = (bookDict["Level2Name"] ?? "") as! String
self.level3Name = (bookDict["Level3Name"] ?? "") as! String
self.level4Name = (bookDict["Level4Name"] ?? "") as! String
self.levels = (bookDict["Levels"] ?? -1) as! Int
self.orderKey = (bookDict["OrderKey"] ?? -1) as! Int
self.viewCount = (bookDict["ViewCount"] ?? -1) as! Int
self.viewLevel = (bookDict["ViewLevel"] ?? -1) as! Int
self.chapters = (bookDict["Chapters"] ?? []) as! [AnyObject]
}
Now i will have to do the same for the chapters if i want to compose books to hold the chapters.
I didn't write the REST API so don't ask me why they did it as so, but now every thing is working and that is the point of it.
In my case, it was just casting to the wrong array type in the code above the problem...