Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I make Realm Results class to use protocol as generics?

I want to create two Realm model classes and one protocol, which is adopted by the two model class. For example:

class Dog: Object, Animal {
    dynamic var name = ""
}
class Cat: Object, Animal {
    dynamic var name = ""
}
protocol Animal {
    var name: String { get }
}

In this case, I created two model class and one protocol.

However, when I moved to the implementation, the problem occurred. The code below is written in view controller:

var dogs: Results<Dog>? {
    return try! Realm().objects(Dog)
}
var cats: Results<Cat> {
    return try! Realm().objects(Cat)
}

This code does not have any problems. But the code below:

var animals: Results<Animal>? {
    switch currentSegmented { // this is from UISegmentedControl
    case .Cat:  // this is from enum
        return self.cats
    case .Dog:
        return self.dogs
}

is not compiled with the error: Results requires that Animal inherit from Object.

However, Animal is a protocol and thus cannot be inherited from Object.

Is it still possible to utilize the protocol here?

like image 806
Blaszard Avatar asked Dec 09 '15 06:12

Blaszard


1 Answers

I don't think there's a nice solution. User-defined generics in Swift are invariant, so even if Animal is a class you can't convert Results<Dog> to Results<Animal>.

The unpleasantly verbose solution is to create an explicit wrapper type around your different kinds of Results:

enum AnimalResultsEnum {
    case DogResults(dogs: Results<Dog>)
    case CatResults(cats: Results<Cat>)
}

class AnimalResults {
    var animals = AnimalResultsEnum.DogResults(dogs: try! Realm().objects(Dog))

    var realm: Realm? {
        switch animals {
        case .DogResults(let dogs):
            return dogs.realm
        case .CatResults(let cats):
            return cats.realm
        }
    }

    var count: Int {
        switch animals {
        case .DogResults(let dogs):
            return dogs.count
        case .CatResults(let cats):
            return cats.count
        }
    }

    subscript(index: Int) -> Animal {
        switch animals {
        case .DogResults(let dogs):
            return dogs[index]
        case .CatResults(let cats):
            return cats[index]
        }
    }

    // ... wrap the rest of the methods needed ...
}

You can make this generic by instead creating a semi-type-erased container to wrap Results:

class CovariantResults<T: Object> {
    private var base: _CovariantResultsBase<T>

    init<U: Object>(_ inner: Results<U>) {
        base = _CovariantResultsImpl<T, U>(inner)
    }

    subscript(index: Int) -> T {
        return base[index]
    }

    // ... wrap the rest of the methods needed ...
}

class _CovariantResultsBase<T: Object> {
    subscript(index: Int) -> T { fatalError("abstract") }
    // ... wrap the rest of the methods needed ...
}

class _CovariantResultsImpl<T: Object, U: Object>: _CovariantResultsBase<T> {
    private let impl: Results<U>

    init(_ inner: Results<U>) {
        impl = inner
    }

    override subscript(index: Int) -> T {
        return impl[index] as! T
    }

    // ... wrap the rest of the methods needed ...
}

// Used as:
let animals = CovariantResults<Animal>(try! Realm().objects(Dog))
like image 99
Thomas Goyne Avatar answered Oct 19 '22 22:10

Thomas Goyne