1

I have Dictionary which contains String keys and Array of Objects as value. These values are added from the sorted Array of objects into Dictionary using append method. The values are categorized into keys based on the first letter of object property. But returns unsorted Dictionary.

The dictionaries are declared:

var namesDic = [String: [Name]]()

var filteredNames = [String: [Name]]()

And iterating through array and appending into Dictionary:

for name in names {
        let letterIndex = name.getName().index(name.getName().startIndex, offsetBy: 0)

        let letter = name.getName()[letterIndex]

        if namesDic[String(letter)] != nil {
            namesDic[String(letter)]?.append(name)
        } else {
            namesDic[String(letter)] = [name]
        }
    }
    filteredNames = namesDic

}

Name structure:

struct Name {
    var id: Int!
    var name: String!
    var native: String!
    var meaning: String!
    var origin: String!
    var isFavorite: Bool
    var gender: String!

    init(id: Int, name: String, native: String, meaning: String, origin: String, isFavorite: Int, gender: String) {
        self.id = id
        self.name = name
        self.native = native
        self.meaning = meaning
        self.origin = origin
        if isFavorite == 0 {
            self.isFavorite = false
        } else { self.isFavorite = true }
        self.gender = gender
    }
}

I found in debugging that they are unsorted when they are appended to dictionary. I understand sort on Swift Dictionary is not working but I want a work around to sort Dictionary by key to pass it to TableView.

I went through many questions/answers here but they are all for [String: String] not Array of Objects.

Maihan Nijat
  • 9,054
  • 11
  • 62
  • 110
  • https://stackoverflow.com/a/46940363/2303865 If your names array input it is unsorted just sort it before creating your dictionary. Btw you should post your `Name` object structure/class – Leo Dabus Nov 07 '17 at 01:59
  • @LeoDabus I get the following error after sorting and assigning: `Cannot assign value of type '[(key: String, value: [Name])]' to type '[String : [Name]]'` – Maihan Nijat Nov 07 '17 at 02:06
  • post your Name structure/class declaration – Leo Dabus Nov 07 '17 at 02:07
  • @LeoDabus Added in the question. Thanks – Maihan Nijat Nov 07 '17 at 02:08
  • first remove all the implicitly unwrapped optionals and make the properties constants – Leo Dabus Nov 07 '17 at 02:08
  • and change no need to use an Int to initialize your Bool. Btw structures provide its own initializers you don't need to create one most of the time `struct Name { let id: Int let name: String let native: String let meaning: String let origin: String let isFavorite: Bool let gender: String }` – Leo Dabus Nov 07 '17 at 02:10
  • @LeoDabus Thanks I changed this. – Maihan Nijat Nov 07 '17 at 02:12
  • @MaihamNijat check my post – Leo Dabus Nov 07 '17 at 02:20
  • Btw if isFavorite is coming from a JSON just cast it to Bool instead of casting it to Int – Leo Dabus Nov 07 '17 at 02:27
  • 1
    All var, all implicitly unwrapped optionals. That's a huge red flag. – Alexander Nov 07 '17 at 02:44
  • @Alexander Actually IMO `var` is the correct default for structure properties (but `let` is indeed the correct default in every other case). It delegates the mutability to the user of the structure (whether they use `let` or `var`). `let` should only really be used if there are internal invariants that need to be maintained, which doesn't appear to be the case here. – Hamish Nov 07 '17 at 11:05
  • Good thread on the matter: https://twitter.com/UINT_MIN/status/921557907719598080 – Hamish Nov 07 '17 at 11:18
  • 2
    Note that the types of some of your properties can be improved here – `gender` should almost certainly be an enumeration, and `id` probably shouldn't be an `Int` (does it make sense to add two ids together? should a name ID be the same type as other ID types in your program?). [Rob's advise here is great](https://www.youtube.com/watch?v=_S6UOrwS-Tg&feature=youtu.be&t=17m14s) – create a nested `ID` type. – Hamish Nov 07 '17 at 11:34
  • @Hamish Sorry, could you elaborate why `var` is preferable? I don't understand "It delegates the mutability to the user of the structure". In this case, I would have preferred `let`, because changing any one of these values would necessarily require the others to change as well, at which point it's better just instantiate a new struct, instead. – Alexander Nov 07 '17 at 15:44
  • @Alexander It's a little difficult to say what some of the properties in OP's structure actually represent (e.g `meaning` and `native`), but for the most part, I'd say that it probably makes perfect sense for a single property to be mutated without changing others (e.g `isFavorite`). But consider what changing them to `let` would actually get you – well, you wouldn't be able to enforce any additional preconditions, because, well the structure has an initialiser that accepts all values! You just get a (quite a bit) more verbosity if you want to perform a mutation.... – Hamish Nov 07 '17 at 16:08
  • ...And of course, because we're dealing with value types, we don't have to worry about shared mutable state, so I really don't see the argument that it's preferable to go through the initialiser to make one simple mutation (or even mutations to two or three properties). By using `var` as the default, you get the most flexibility – you leave it up to the consumer of the type to decide if they want a given value of that type to be constant or mutable.... – Hamish Nov 07 '17 at 16:08
  • ...But of course, `let` still has its place in structures; you wouldn't be able to enforce certain preconditions without it – but in quite a lot of cases (including this one), structures don't come with preconditions, they're just a bag of values. – Hamish Nov 07 '17 at 16:08
  • @Hamish Oh yes actually, I agree that `isFavorite` should be a `var`. My prior comment was relating to something like `name`, which gives this entire struct its identity. If you change it, none of the other fields would make sense. `well, you wouldn't be able to enforce any additional preconditions, because, well the structure has an initialiser that accepts all values!` Could you elaborate on this? I don't see why immutability prohibits you from enforcing preconditions. That seems like the perfect use-case for a failable initializer. – Alexander Nov 07 '17 at 16:24
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/158433/discussion-between-hamish-and-alexander). – Hamish Nov 07 '17 at 16:29

2 Answers2

4
struct Name: CustomStringConvertible {
    let id: Int
    let name: String
    let native: String
    let meaning: String
    let origin: String
    let isFavorite: Bool
    let gender: String
    var description: String {
        return "Id: " + String(id) + " - Name: " + name 
    }
}

let name1 = Name(id: 1, name: "Tim Cook", native: "native", meaning: "meaning", origin: "origin", isFavorite: true, gender: "Male")
let name2 = Name(id: 2, name: "Steve Jobs", native: "native", meaning: "meaning", origin: "origin", isFavorite: true, gender: "Male")
let name3 = Name(id: 3, name: "Tiger Woods", native: "native", meaning: "meaning", origin: "origin", isFavorite: true, gender: "Male")
let name4 = Name(id: 4, name: "Socrates", native: "native", meaning: "meaning", origin: "origin", isFavorite: true, gender: "Male")

let names = [name1, name2, name3, name4]


let dictionary = names.sorted(by: {$0.name < $1.name }).reduce(into: [String: [Name]]()) { result, element in
    // make sure there is at least one letter in your string else return
    guard let first = element.name.first else { return }
    // create a string with that initial
    let initial = String(first)
    // initialize an array with one element or add another element to the existing value
    result[initial, default: []].append(element)
}

let sorted = dictionary.sorted {$0.key < $1.key}
print(sorted)   // "[(key: "S", value: [Id: 4 - Name: Socrates, Id: 2 - Name: Steve Jobs]), (key: "T", value: [Id: 3 - Name: Tiger Woods, Id: 1 - Name: Tim Cook])]\n"
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Thank you for the answer – Maihan Nijat Nov 07 '17 at 02:32
  • 1
    Why not just use [`Dictionary.init(grouping:by:)`](https://developer.apple.com/documentation/swift/dictionary/2919592-init)? – Alexander Nov 07 '17 at 02:45
  • 1
    Note you can say `result[initial, default: []].append(element)` instead of `result[initial] = (result[initial] ?? []) + [element]` :) Also see [my above comment](https://stackoverflow.com/questions/47148473/how-to-sort-dictionary-by-keys-where-values-are-array-of-objects-in-swift-4#comment81261919_47148473). – Hamish Nov 07 '17 at 11:12
  • @Hamish I know already https://stackoverflow.com/a/47019843/2303865 I just forgot to change the code that was already implemented in the linked question but thanks anyway – Leo Dabus Nov 07 '17 at 15:22
  • 1
    @LeoDabus No worries :) If you're interested – `subscript(_:default:)` will actually still cause a copy of the underlying array each time `append(_:)` is called on it (you [could optimise by removing and appending](https://stackoverflow.com/a/41154903/2976878)), but [I'm trying to get that changed](https://github.com/apple/swift/pull/12752) such that the underlying array *can* be mutated directly through `subscript(_:default:)`. – Hamish Nov 07 '17 at 15:30
  • @Hamish I am trying to implement a mutating method to append to a dictionary where the value is a collection but I am getting an error when trying to removeValue(forKey). The error says **Cannot invoke 'removeValue' with an argument list of type '(forKey: K)'** Do you know what constrain I am suppose to use and if this is possible to be accomplished? https://www.dropbox.com/s/qth8eerquf87n8o/Dictionary%20append%20to%20key.playground.zip?dl=1 – Leo Dabus Nov 07 '17 at 16:58
  • 1
    @LeoDabus You were introducing a new generic placeholder `K` which shadowed the `Key` placeholder, additionally you need to constrain `Value` to being a `RangeReplaceableCollection` (so you can append), and you cannot use an array literal, as it may not be `ExpressibleByArrayLiteral`. Take a look at http://swift.sandbox.bluemix.net/#/repl/5a01e7ad9ce6876bc6920dc4 – Hamish Nov 07 '17 at 17:05
  • @Hamish Thanks you so much. – Leo Dabus Nov 07 '17 at 17:10
  • 1
    @LeoDabus A more generalised version of efficiently mutating a dictionary value could look like this: http://swift.sandbox.bluemix.net/#/repl/5a01e9449ce6876bc6920dc6. Although this will be redundant once `subscript(_:default:)` (hopefully!) supports direct mutation via an addressor. – Hamish Nov 07 '17 at 17:13
  • @Hamish The last one is really amazing. Again thank you so much for the precise feedback – Leo Dabus Nov 07 '17 at 17:17
  • 1
    @LeoDabus No worries – I enjoy this stuff :) – Hamish Nov 07 '17 at 17:17
2

According to Apple's documentation

A dictionary stores associations between keys of the same type and values of the same type in a collection with no defined ordering. Each value is associated with a unique key, which acts as an identifier for that value within the dictionary. Unlike items in an array, items in a dictionary do not have a specified order. You use a dictionary when you need to look up values based on their identifier, in much the same way that a real-world dictionary is used to look up the definition for a particular word.

Further information is available on Apple's Website

Workaround

One thing that could be done is to create an array of sorted keys and then use that array to access the dictionary values

Malik
  • 3,763
  • 1
  • 22
  • 35