4

Dealing with some objC API, I receive an NSDictionary<NSString *, id> *> which translates to [String : Any] in Swift and which I was using for NSAttributedString.addAttributes:range:.

However, this method signature has now changed with Xcode 9 and now requires an [NSAttributedStringKey : Any].

let attr: [String : Any]? = OldPodModule.getMyAttributes()
// Cannot assign value of type '[String : Any]?' to type '[NSAttributedStringKey : Any]?'
let newAttr: [NSAttributedStringKey : Any]? = attr
if let newAttr = newAttr {
    myAttributedString.addAttributes(newAttr, range: range)
}

How to convert a [String : Any] to a [NSAttributedStringKey : Any]?

Hamish
  • 78,605
  • 19
  • 187
  • 280
Cœur
  • 37,241
  • 25
  • 195
  • 267

3 Answers3

16

NSAttributedStringKey has an initialiser that takes a String, and you can use Dictionary's init(uniqueKeysWithValues:) initialiser in order to build a dictionary from a sequence of key-value tuples where each key is unique (such as is the case here).

We just have to apply a transform to attr that converts each String key into an NSAttributedStringKey prior to calling Dictionary's initialiser.

For example:

let attributes: [String : Any]? = // ...

let attributedString = NSMutableAttributedString(string: "hello world")
let range = NSRange(location: 0, length: attributedString.string.utf16.count)

if let attributes = attributes {
    let convertedAttributes = Dictionary(uniqueKeysWithValues:
        attributes.lazy.map { (NSAttributedStringKey($0.key), $0.value) }
    )
    attributedString.addAttributes(convertedAttributes, range: range)
}

We're using lazy here to avoid the creation of an unnecessary intermediate array.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • 4
    Thanks! To be exhaustive, I had to do the reverse operation as well (`[NSAttributedStringKey : Any]` to `[String : Any]`) and in that case it was `Dictionary(uniqueKeysWithValues: attr.lazy.map { ($0.key.rawValue, $0.value) })`. – Cœur Jun 07 '17 at 10:10
  • @Hamish Why `lazy`? Wouldn't `attr.map { /* ... */ }` do the same in this case? – Matusalem Marques Jun 07 '17 at 11:13
  • 1
    @MatusalemMarques It would achieve the same result, but using `lazy` avoids the creation of an unnecessary intermediate array (the key-value pairs are instead just iterated over once and inserted into the new dictionary, with the given transformation applied). – Hamish Jun 07 '17 at 11:16
0

You can use the

`NSAttributedStringKey(rawValue: String)`

initializer. But, with this, it will create an object even if the attributed string won't be affected. For example,

`NSAttributedStringKey(rawValue: fakeAttribute)` 

will still create a key for the dictionary. Also, this is only available in iOS 11 so use with caution for backward compatibility.

Cody Weaver
  • 4,756
  • 11
  • 33
  • 51
0

While Hamish provided a perfect Swift answer, please note that in the end I solved the problem at the Objective-C API level directly. It can be done also with a small Objective-C wrapper if you have no control on the source code.

We simply replace NSDictionary<NSString *, id> * with NSDictionary<NSAttributedStringKey, id> * and we add a typedef for compatibility with earlier versions of Xcode:

#ifndef NS_EXTENSIBLE_STRING_ENUM
// Compatibility with Xcode 7
#define NS_EXTENSIBLE_STRING_ENUM
#endif

// Testing Xcode version (https://stackoverflow.com/a/46927445/1033581)
#if __clang_major__ < 9
// Compatibility with Xcode 8-
typedef NSString * NSAttributedStringKey NS_EXTENSIBLE_STRING_ENUM;
#endif
Cœur
  • 37,241
  • 25
  • 195
  • 267