Make Dictionary conform to value type protocol - swift

This is simplified code that doesn't compile. Is there anyway to make this work in Swift? Thanks.
protocol Person {
var name:String { get }
var age:Int { get }
}
extension Dictionary : Person {
var name: String {
return self["name"] as String
}
var age: Int {
return self["Int"] as Int
}
}
Let me give some context to why I would want to do this.
Lets say I have some person data coming in over the wire as json. As I pass it through JSONSerialization I get a [String:AnyObject] Dictionary back.
So I would like to declare the JSON data interfaces in protocols, make the dictionary objects conform to the protocols and then extract the values from the dictionaries via typed properties, rather then via magic strings and casts. This way the client code would only know about protocol types even though they are implemented as dictionaries behind the curtain.
Not sure it's doable or a good idea, just wanted to try it. But compiler is giving me all sorts of trouble.

I understand you want to encapsulate the logic to link a json to it's model representation.
The suggested solution
First of all I am suggesting another way to achieve what you are looking for instead
Look at this Struct.
struct Person {
let name: String
let age: Int
init?(json: [String:Any]) {
guard let name = json["name"] as? String, age = json["age"] as? Int else { return nil }
self.name = name
self.age = age
}
}
The logic to extract data from the json is encapsulated into its initializer. And if the provided json is not valid the initialization fails. It's safe because it will never crash and it's easy to use.
The direct answer (don't do this at home!)
protocol Person {
var name: String? { get }
var age: Int? { get }
}
extension Dictionary : Person {
private var dictWithStringKeys: [String:Any] {
return reduce([String:Any]()) { (dict, elm) -> [String:Any] in
var dict = dict
if let key = elm.0 as? String {
dict[key] = elm.1
}
return dict
}
}
var name: String? {
return dictWithStringKeys["name"] as? String
}
var age: Int? {
return dictWithStringKeys["age"] as? Int
}
}

Related

Property Wrappers vs Mirroring

Happy holidays!
I have a property wrapper that is used to define the key in a key-value pair.
#propertyWrapper
final public class KeyValuePair<T> {
public var wrappedValue: T?
private(set) var key: String
public init(key: String) {
self.key = key
}
}
EDIT:
And using the following protocol, to which all KV models should conform...
protocol Parametrized {
var toParameters: [String: Any] { get }
}
the property wrapper is used in the following fashion:
struct Person: Parametrized {
#KeyValuePair(key: "first_name") var firstName: String?
#KeyValuePair(key: "last_name") var lasteName: String?
#KeyValuePair(key: "age") var age: Int?
#KeyValuePair(key: "isSingle") var isSingle: Bool?
#KeyValuePair(key: "isOpenForDating") var isOpenForDating: Bool?
}
var missUniverse2022 = Person()
missUniverse2022.firstName = "Miss"
missUniverse2022.lastName = "Universe"
missUniverse2022.age = 23
missUniverse2022.isSingle = true // Rad <3
missUniverse2022.isOpenForDating = false // Sad :D
So, the whole idea is to generate a dictionary of type [String: Any] that holds the key-value pairs from that struct using Mirror.
The problem is that the following code is not working (I know why, but I can't figure out the workaround):
extension Parametrized {
var toParameters: [String: Any] {
var parameters = [String: Any]()
for child in Mirror(reflecting: self).children {
/***********************************************
Generic parameter 'T' could not be inferred in cast to 'KeyValuePair'
Explicitly specify the generic arguments to fix this issue
***********************************************/
guard let kvPair = child.value as? KeyValuePair else { continue }
parameters[kvPair.key] = kvPair.wrappedValue
}
return parameters
}
}
So, I turn to the experts here: how to use the mirror logic to get the k-v pairs and put them in a collection?
I really appreciate any help you can provide.
Thanks to the Swift community, I got my answer.
https://forums.swift.org/t/property-wrappers-with-generics-vs-mirroring/54325/4

Exposing dictionary in Swift property wrappers

I have an internal dictionary that I don't want to expose to the user. Instead, I expose only certain values using properties, like this:
public var artist: String? {
get {
return items["artist"]
}
set {
items["artist"] = newValue
}
}
//...so on for another 20 or so items
As you can imagine, this ends up getting repeated quite a lot. I was thinking that property wrappers would be a nice way to clean this up - however, it's not possible to pass items directly to the wrapper, since property wrappers are created before init (so self would not be accessible).
Is there a way around this, or is this just one of the limitations of propertyWrappers?
You could build a generic solution. I did one, but you can probably improve it:
class PropertyWrapper {
private var items: [String: Any] = ["artist": "some dude"]
enum Key: String {
case artist
}
func getItem<T: Any>(key: Key) -> T {
guard let item = items[key.rawValue] as? T else {
preconditionFailure("wrong type asked for")
}
return item
}
func setItem(value: Any, key: Key) {
items[key.rawValue] = value
}
}
class GetValueClass {
func getValue() {
let wrapper = PropertyWrapper()
let value: String = wrapper.getItem(key: .artist)
}
}
class SetValueClass {
func setValue() {
let wrapper = PropertyWrapper()
wrapper.setItem(value: "some", key: .artist)
}
}

Accessing custom Swift structs directly like a String, without having to use "struct.property" notation

I'm not exactly sure how to describe this one but I'm trying to create a Swift struct that can be accessed like a standard String. I think it will be easier to explain with a code example.
Let's say that we have the following as a struct:
struct MultilangString {
let dictionary:[String:String]
init(_ dict:[String:String]){
self.dictionary = dict
}
func getLocalString() -> String {
let lang:String = MultilangString.getCurrentLocale()
if let stringLocale = self.dictionary[lang]{
return stringLocale
} else {
return "Error while getting localized string."
}
}
static func getCurrentLocale() -> String {
// code to get current device locale
}
}
And we use it like:
let name = MultilangString(["en":"First name","fr":"Prénom"])
print(name.getLocalString())
Is there a way, a special(magic) getter etc. to declare a struct that is accessible like a standard String? For example print(name) should be the same as print(name.getLocalString())
extension MultilangString: CustomStringConvertible {
var description: String {
return "your custom string here after compute"
}
}
print(name) // Prints - your custom string here after compute

Can I use Swift's map() on Protocols?

I have some model code where I have some Thoughts that i want to read and write to plists. I have the following code:
protocol Note {
var body: String { get }
var author: String { get }
var favorite: Bool { get set }
var creationDate: Date { get }
var id: UUID { get }
var plistRepresentation: [String: Any] { get }
init(plist: [String: Any])
}
struct Thought: Note {
let body: String
let author: String
var favorite: Bool
let creationDate: Date
let id: UUID
}
extension Thought {
var plistRepresentation: [String: Any] {
return [
"body": body as Any,
"author": author as Any,
"favorite": favorite as Any,
"creationDate": creationDate as Any,
"id": id.uuidString as Any
]
}
init(plist: [String: Any]) {
body = plist["body"] as! String
author = plist["author"] as! String
favorite = plist["favorite"] as! Bool
creationDate = plist["creationDate"] as! Date
id = UUID(uuidString: plist["id"] as! String)!
}
}
for my data model, then down in my data write controller I have this method:
func fetchNotes() -> [Note] {
guard let notePlists = NSArray(contentsOf: notesFileURL) as? [[String: Any]] else {
return []
}
return notePlists.map(Note.init(plist:))
}
For some reason the line return notePlists.map(Note.init(plist:)) gives the error 'map' produces '[T]', not the expected contextual result type '[Note]'
However, If I replace the line with return notePlists.map(Thought.init(plist:)) I have no issues. Clearly I can't map the initializer of a protocol? Why not and what's an alternate solution?
If you expect to have multiple types conforming to Note and would like to know which type of note it is stored in your dictionary you need to add an enumeration to your protocol with all your note types.
enum NoteType {
case thought
}
add it to your protocol.
protocol Note {
var noteType: NoteType { get }
// ...
}
and add it to your Note objects:
struct Thought: Note {
let noteType: NoteType = .thought
// ...
}
This way you can read this property from your dictionary and map it accordingly.

Access properties via subscripting in Swift

I have a custom class in Swift and I'd like to use subscripting to access its properties, is this possible?
What I want is something like this:
class User {
var name: String
var title: String
subscript(key: String) -> String {
// Something here
return // Return the property that matches the key…
}
init(name: String, title: String) {
self.name = name
self.title = title
}
}
myUser = User(name: "Bob", title: "Superboss")
myUser["name"] // "Bob"
Update: The reason why I'm looking for this is that I'm using GRMustache to render from HTML templates. I'd like to be able to just pass my model object to the GRMustache renderer…
GRMustache fetches values with the keyed subscripting objectForKeyedSubscript: method and the Key-Value Coding valueForKey: method. Any compliant object can provide values to templates.
https://github.com/groue/GRMustache/blob/master/Guides/view_model.md#viewmodel-objects
This is a bit of a hack using reflection. Something along the lines of the following could be used.
protocol PropertyReflectable { }
extension PropertyReflectable {
subscript(key: String) -> Any? {
let m = Mirror(reflecting: self)
for child in m.children {
if child.label == key { return child.value }
}
return nil
}
}
struct Person {
let name: String
let age: Int
}
extension Person : PropertyReflectable {}
Then create a Person and access it's keyed properties.
let p = Person(name: "John Doe", age: 18)
p["name"] // gives "John Doe"
p["age"] // gives 18
You could modify the subscript to always return an interpolated string of the property value.
Adding some syntax sugar to Benzi's answer:
protocol PropertyReflectable { }
extension PropertyReflectable {
subscript(key: String) -> Any? {
let m = Mirror(reflecting: self)
return m.children.first { $0.label == key }?.value
}
}
struct Person: PropertyReflectable {
let name: String
let age: Int
}
Then create a Person and access it's keyed properties.
let p = Person(name: "John Doe", age: 18)
p["name"] // gives "John Doe"
p["age"] // gives 18
Using valueForKey should enable you to access properties using their names. Be sure that you're working with a object that inherit NSObject
class people: NSObject {
var age: NSString = "44"
var height: NSString = "153"
}
let person:people = people()
let stringVariable = "age"
person.valueForKey("age")
// Print "44"
person.valueForKey("\(stringVariable)")
// Print "44"
(GRMustache author here)
Until a swift-oriented Mustache library is out, I suggest having your classes inherit from NSObject (so that they have the valueForKey: method). GRMustache will then fetch values with this method.
In case this would still not work (blank values in the rendering), you may try to disable GRMustache security features (see https://github.com/groue/GRMustache/blob/master/Guides/security.md#disabling-safe-key-access)
Should you experience any other trouble, please open an issue right into the repository: https://github.com/groue/GRMustache/issues
EDIT February 2, 2015: GRMustache.swift is out: http://github.com/groue/GRMustache.swift
Shim's answer above doesn't work anymore in Swift 4. There are two things you should be aware of.
First of all, if you want to use value(forKey:) function, your class must inherit NSObject.
Secondly, since Objective-C doesn't know anything about value type, you have to put the #objc keyword in front of your value type properties and Swift will do the heavy-lifting for you.
Here is the example:
import Foundation
class Person: NSObject {
#objc var name: String = "John Dow"
#objc var age: Int = 25
#objc var height: Int = 180
subscript(key: String) -> Any? {
return self.value(forKey: key)
}
}
let person: Person = Person()
person["name"] // "John Dow"
person["age"] // 25
person["height"] // 180
I suppose you could do:
class User {
let properties = Dictionary<String,String>()
subscript(key: String) -> String? {
return properties[key]
}
init(name: String, title: String) {
properties["name"] = name
properties["title"] = title
}
}
Without knowing your use case I would strongly advise against doing this.
Another approach:
class User {
var name : String
var title : String
subscript(key: String) -> String? {
switch key {
case "name" : return name
case "title" : return title
default : return nil
}
}
init(name: String, title: String) {
self.name = name
self.title = title
}
}
It might be worth noting that Swift doesn't appear to currently support reflection by names. The reflect function returns a Mirror whose subscript is Int based, not String based.