How do I store a value of type ClassClassImplementingProtocol in a Dictionary of type [String:ClassProtocol] in Swift?
A Thing<Vanilla>
is not a Thing<Flavor>
. Thing
is not covariant. There is no way in Swift to express that Thing
is covariant. There are good reasons for this. If what you were asking for were allowed without careful rules around it, I would be allowed to write the following code:
func addElement(array: inout [Any], object: Any) {
array.append(object)
}
var intArray: [Int] = [1]
addElement(array: &intArray, object: "Stuff")
Int
is a subtype of Any
, so if [Int]
were a subtype of [Any]
, I could use this function to append strings to an int array. That breaks the type system. Don't do that.
Depending on your exact situation, there are two solutions. If it is a value type, then repackage it:
let thing = Thing<Vanilla>(value: Vanilla())
dict["foo"] = Thing(value: thing.value)
If it is a reference type, box it with a type eraser. For example:
// struct unless you have to make this a class to fit into the system,
// but then it may be a bit more complicated
struct AnyThing {
let _value: () -> Flavor
var value: Flavor { return _value() }
init<T: Flavor>(thing: Thing<T>) {
_value = { return thing.value }
}
}
var dict = [String:AnyThing]()
dict["foo"] = AnyThing(thing: Thing<Vanilla>(value: Vanilla()))
The specifics of the type eraser may be different depending on your underlying type.
BTW: The diagnostics around this have gotten pretty good. If you try to call my addElement
above in Xcode 9, you get this:
Cannot pass immutable value as inout argument: implicit conversion from '[Int]' to '[Any]' requires a temporary
What this is telling you is that Swift is willing to pass [Int]
where you ask for [Any]
as a special-case for Arrays (though this special treatment isn't extended to other generic types). But it will only allow it by making a temporary (immutable) copy of the array. (This is another example where it can be hard to reason about Swift performance. In situations that look like "casting" in other languages, Swift might make a copy. Or it might not. It's hard to be certain.)
Turn dictionary into custom type
Based on the comment by tbogosia I looked at what Swift 4 offers and it looks like the new Codable
is the way to go.
Apple doc right here:
https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
It would seem that for lower version of Swift there is no other option but to use custom init
functions for every custom type.
Swift: Reference to a generic class
I finally succeed doing what i wanted!
Not perfect at all, feel free to comment for architecture improvements (especially on the asBaseProtocol() part...)
Here's my complete code (Swift 3.0)
DataFilter
protocol DataFilterDelegate : class {
func didFilter()
func didUpdateItems()
}
class DataFilter<T> {
public weak var delegate : DataFilterDelegate?
private var items : [SelectableItem<T>]?
private var filteredItems : [SelectableItem<T>]?
var source: [SelectableItem<T>]? {
get {
if filteredItems != nil {
return filteredItems
}
return items
}
}
var filter : (T,String) -> Bool
var populateCell : (T) -> UITableViewCell
init(filter : @escaping (T,String) -> Bool, populateCell: @escaping (T) -> UITableViewCell) {
self.filter = filter
self.populateCell = populateCell
}
func updateItems(_ items: [T]) {
self.items = [SelectableItem<T>]()
for item in items {
self.items?.append(SelectableItem(item))
}
delegate?.didUpdateItems()
}
func filterItems(text : String) {
filteredItems = (text == "") ? nil : items?.filter { item in
filter(item.item, text)
}
delegate?.didFilter()
}
func selectedItems() -> [T]? {
guard let items = items else {
return nil
}
var selectedItems = [T]()
for item in items {
if item.isSelected {
selectedItems.append(item.item)
}
}
return selectedItems
}
}
extension DataFilter where T : FIRDataObject {
func asBaseProtocol() -> DataFilter<FIRDataObject> {
return DataFilter<FIRDataObject>(filter: filterAsBaseProtocol(), populateCell: populateCellAsBaseProtocol())
}
private func filterAsBaseProtocol() -> ((FIRDataObject,String) -> Bool) {
return { (object, text) -> Bool in
self.filter(object as! T, text)
}
}
private func populateCellAsBaseProtocol() -> ((FIRDataObject) -> UITableViewCell) {
return { (object) -> UITableViewCell in
self.populateCell(object as! T)
}
}
}
ParentViewController Class
class ParentViewController : UIViewController {
public var dataFilter : DataFilter<FIRDataObject>? {
didSet {
dataFilter!.delegate = self
}
}
// Some Functions using dataFilter
}
ChildViewController Class
class ChildViewController : Parent {
// Stored as a variable to not have to cast objects to the good type everytime I access dataFilter
private var patientDataFilter = DataFilter<Patient>(filter: { patient, text in
patient.firstname.contains(text) ||
patient.lastname.contains(text)
}
, populateCell: { patient in
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Patient")
cell.textLabel?.text = patient.lastname + " " + patient.firstname
cell.detailTextLabel?.text = patient.postalCode + " " + patient.city
return cell
})
override func viewDidLoad() {
super.viewDidLoad()
dataFilter = patientDataFilter.asBaseProtocol()
}
func someFunc() {
let patient1 = patientDataFilter.source[0].item
// OR
let patient2 = dataFilter.source[0].item as! Patient
}
}
Make a Swift dictionary where the key is Type?
Unfortunately, it's currently not possible for metatype types to conform to protocols (see this related question on the matter) – so CellThing.Type
does not, and cannot, currently conform to Hashable
. This therefore means that it cannot be used directly as the Key
of a Dictionary
.
However, you can create a wrapper for a metatype, using ObjectIdentifier
in order to provide the Hashable
implementation. For example:
/// Hashable wrapper for a metatype value.
struct HashableType<T> : Hashable {
static func == (lhs: HashableType, rhs: HashableType) -> Bool {
return lhs.base == rhs.base
}
let base: T.Type
init(_ base: T.Type) {
self.base = base
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(base))
}
// Pre Swift 4.2:
// var hashValue: Int { return ObjectIdentifier(base).hashValue }
}
You can then also provide a convenience subscript on Dictionary
that takes a metatype and wraps it in a HashableType
for you:
extension Dictionary {
subscript<T>(key: T.Type) -> Value? where Key == HashableType<T> {
get { return self[HashableType(key)] }
set { self[HashableType(key)] = newValue }
}
}
which could then use like so:
class CellThing {}
class A : CellThing {}
class B : CellThing {}
var recycle: [HashableType<CellThing>: [CellThing]] = [:]
recycle[A.self] = [A(), A(), A()]
recycle[B.self] = [B(), B()]
print(recycle[A.self]!) // [A, A, A]
print(recycle[B.self]!) // [B, B]
This should also work fine for generics, you would simply subscript your dictionary with T.self
instead.
Unfortunately one disadvantage of using a subscript with a get
and set
here is that you'll incur a performance hit when working with dictionary values that are copy-on-write types such as Array
(such as in your example). I talk about this issue more in this Q&A.
A simple operation like:
recycle[A.self]?.append(A())
will trigger an O(N) copy of the array stored within the dictionary.
This is a problem that is aimed to be solved with generalised accessors, which have been implemented as an unofficial language feature in Swift 5. If you are comfortable using an unofficial language feature that could break in a future version (not really recommended for production code), then you could implement the subscript as:
extension Dictionary {
subscript<T>(key: T.Type) -> Value? where Key == HashableType<T> {
get { return self[HashableType(key)] }
_modify {
yield &self[HashableType(key)]
}
}
}
which solves the performance problem, allowing an array value to be mutated in-place within the dictionary.
Otherwise, a simple alternative is to not define a custom subscript, and instead just add a convenience computed property on your type to let you use it as a key:
class CellThing {
// Convenience static computed property to get the wrapped metatype value.
static var hashable: HashableType<CellThing> { return HashableType(self) }
}
class A : CellThing {}
class B : CellThing {}
var recycle: [HashableType<CellThing>: [CellThing]] = [:]
recycle[A.hashable] = [A(), A(), A()]
recycle[B.hashable] = [B(), B()]
print(recycle[A.hashable]!) // [A, A, A]
print(recycle[B.hashable]!) // [B, B]
Create a dictionary of generic variables in swift
variable1
and variable2
are not of type Entity
but are in fact Entity<Float>
and Entity<String>
which are two unrelated types. Use a protocol
to unite them:
protocol EntityProtocol { }
class Entity<T> : EntityProtocol {
var _value: T
var value: T { get { return _value } set {_value = newValue}}
init (defaultValue: T) {
_value = defaultValue
}
}
class FloatEntity: Entity<Float> {
}
class StringEntity: Entity<String> {
}
func run () {
let variable1: Entity = FloatEntity (defaultValue: 1)
let variable2: Entity = StringEntity (defaultValue: "")
var dictionary: Dictionary<String, EntityProtocol> = [
"One": FloatEntity (defaultValue: 1),
"Two": StringEntity (defaultValue: ""),
]
print (variable1)
print (variable2)
print (dictionary)
}
Determining Swift Types That Can Be Stored in UserDefaults
The Property List type set of UserDefaults
is very limited. The supported types are
NSString
→ SwiftString
NSNumber
→ SwiftInt
,Double
orBool
NSDate
→ SwiftDate
NSData
→ SwiftData
- Arrays and dictionaries containing the 4 value types.
Any
is not supported unless it represents one of the 4 value or 2 collection types.
Property List compliant collection types can be written to UserDefaults
with PropertyListSerialization
(even in Swift).
There are two protocols to serialize custom types to Data
Codable
can serialize structs and classes.NSCoding
can serialize subclasses ofNSObject
.
All types in the structs/classes must be encodable and decodable (means conform to the protocol themselves).
The APIs of PropertyListSerialization
/ PropertyListEncoder/-Decoder
and NSKeyed(Un)Archiver
provide robust error handling to avoid crashes.
Related Topics
Protocol Doesn't Conform to Itself
How to Use Swift 4 Codable in Core Data
Uncaught Error/Exception Handling in Swift
Why Is Swift Compile Time So Slow
Input from the Keyboard in Command Line Application
How to Create a Segue That Can Be Called from a Button That Is Created Programmatically
What Is _: in Swift Telling Me
First Item in a List Is Always Selected
Swift: How to Add a Protocol Extension to a Protocol
Using a Dispatch_Once Singleton Model in Swift
How to Provide a Localized Description With an Error Type in Swift
Difference Between a Weak Reference and an Unowned Reference
Xcode 8 Beta 3 Use Legacy Swift Issue
Non-'@Objc' Method Does Not Satisfy Optional Requirement of '@Objc' Protocol
Accessing Code in Swift 3 Error
Swift Random Float Between 0 and 1
Getting "File Not Found" in Bridging Header When Importing Objective-C Frameworks into Swift Project