devios1
devios1

Reputation: 38025

Is it possible to have a dictionary with a mutable array as the value in Swift

I am trying to do this:

var dictArray = [String:[String]]()
dictArray["test"] = [String]()
dictArray["test"]! += "hello"

But I am getting the weird error NSString is not a subtype of 'DictionaryIndex<String, [(String)]>'.

I just want to be able to add objects to an array inside a dictionary.

Update: Looks like Apple considers this a "known issue" in Swift, implying it will work as expected eventually. From the Xcode 6 Beta 4 release notes:

...Similarly, you cannot modify the underlying value of a mutable optional value, either conditionally or within a force-unwrap:

tableView.sortDescriptors! += NSSortDescriptor(key: "creditName", ascending: true)

Workaround: Test the optional value explicitly and then assign the result back:

if let window = NSApplication.sharedApplication.mainWindow {
    window.title = "Currently experiencing problems"
}
tableView.sortDescriptors = tableView.sortDescriptors!

Upvotes: 10

Views: 6562

Answers (5)

DarkDust
DarkDust

Reputation: 92384

Since Swift 4.1 you can provide a default value to the subscript which allows you to solve this quite naturally now:

dictArray["test", default: []].append("hello")

Upvotes: 0

Raphael
Raphael

Reputation: 10579

The problem is that we want class semantics here but have to use structs. If you put class objects into the dictionary, you get what you want!

So, if you have¹ to have mutable values, you can wrap them in a class and perform updates with a closure:

class MutableWrapper<T> {
    var rawValue: T

    init(_ val: T) {
        self.rawValue = val
    }

    func update(_ with: (inout T) -> Void) {
        with(&self.rawValue)
    }
}

Example:

func foo() {
    var dict = [String: MutableWrapper<[String]>]()

    dict["bar"] = MutableWrapper(["rum"])
    dict["bar"]?.update({$0.append("gin")})

    print(dict["bar"]!.rawValue)
    // > ["rum", "gin"]
}

For what it's worth, I do not see a way to keep caller and wrapper in sync. Even if we declare init(_ val: inout T) we will end up with a copy in rawValue.


  1. Performance is not necessarily an issue since the compiler optimizes structs heavily. I'd benchmark any mutable solution against what looks like lots of copy-updates in the code.

Upvotes: 0

matt.writes.code
matt.writes.code

Reputation: 674

This is still an issue in Swift 3. At least I was able to create method that can handle it for you.

func appendOrCreate(newValue: Any, toArrayAt key: String, in existingDictionary: inout [AnyHashable:Any]) {

    var mutableArray = [Any]()

    if let array = existingDictionary[key] as? [Any]{
        //include existing values in mutableArray before adding new value
        for existingValue in array {
            mutableArray.append(existingValue)
        }
    }

    //append new value
    mutableArray.append(newValue)

    //save updated array in original dictionary
    existingDictionary[key] = mutableArray
}

Upvotes: 0

Pavel Kondratyev
Pavel Kondratyev

Reputation: 51

You may use NSMutableArray instead of [String] as a value type for your dictionary:

var dictArray: [String: NSMutableArray] = [:]
dictArray["test"] = NSMutableArray()
dictArray["test"]!.addObject("hello")

Upvotes: 3

Bryan Chen
Bryan Chen

Reputation: 46598

You can only do this

var dictArray = [String:[String]]()
dictArray["test"] = [String]()
var arr = dictArray["test"]!;
arr += "hello"
dictArray["test"] = arr

because dictArray["test"] give you Optional<[String]> which is immutable

  6> var test : [String]? = [String]()
test: [String]? = 0 values
  7> test += "hello"
<REPL>:7:1: error: '[String]?' is not identical to 'UInt8'

append also won't work due to the same reason, Optional is immutable

  3> dictArray["test"]!.append("hello")
<REPL>:3:18: error: '(String, [(String)])' does not have a member named 'append'
dictArray["test"]!.append("hello")
                 ^ ~~~~~~

BTW the error message is horrible...

Upvotes: 13

Related Questions