Расширение идентифицируемого протокола для данных списка SwiftUI
Я экспериментировал с SwiftUI и столкнулся с проблемой при реализации модели данных для одного из моих списков. Мой план состоял в том, чтобы создать протокол CardProtocol
в качестве протокола данных для элементов моих списков, а затем есть реализация протокола CoreData, а также фиктивная для модульного тестирования и использования Canvas. Если вы используете сбор данных в SwiftUI List
отдельные элементы должны соответствовать Identifiable
протокол.
Код выглядит так:
import SwiftUI
import Combine
final class CardsModel: BindableObject {
var cards: [CardProtocol] = []
let didChange = PassthroughSubject<CardsModel, Never>()
}
protocol CardProtocol: Identifiable {
var id: Int { get set }
var firstName: String? { get set }
var lastName: String? { get set }
var email: String? { get set }
var phone: String? { get set }
}
Это даже не скомпилируется как Identifiable
Протокол имеет 2 связанных типа, которые необходимо указать, если протокол должен использоваться для определения переменной.
/// A type that can be compared for identity equality.
public protocol Identifiable {
/// A type of unique identifier that can be compared for equality.
associatedtype ID : Hashable
/// A unique identifier that can be compared for equality.
var id: Self.ID { get }
/// The type of value identified by `id`.
associatedtype IdentifiedValue = Self
/// The value identified by `id`.
///
/// By default this returns `self`.
var identifiedValue: Self.IdentifiedValue { get }
}
Точная ошибка error: protocol 'CardProtocol' can only be used as a generic constraint because it has Self or associated type requirements
, В настоящее время ID
не проблема и может быть исправлена, но IdentifiedValue
это по своей природе отличается от CoreData и фиктивной реализации.
Единственное разумное решение, которое я нашел, было удалить соответствие Identifiable
из протокола и повторно ввести его позже в представлении, используя cardsModel.cards.identified(by: \.id)
, Есть ли лучший выход из этого, который позволил бы мне поддерживать соответствие идентифицируемой информации на уровне протокола?
2 ответа
Единственное решение, кроме добавления идентифицируемого через .identified(by: \.id)
, использует шаблон стирания типа. Это использует 3 класса для блокирования и скрытия связанного типа и позволяет затем объявлять массив объектов AnyCard. Реализация довольно громоздкая и, вероятно, не стоит моей проблемы. Но здесь это идет:
final class CardsModel<IdentifiedValue:CardProtocol>: BindableObject {
var cards: [AnyCard<IdentifiedValue>] = []
let didChange = PassthroughSubject<CardsModel, Never>()
}
protocol CardProtocol: Identifiable{
var id: Int32 { get set }
var firstName: String? { get set }
var lastName: String? { get set }
var email: String? { get set }
var phone: String? { get set }
}
struct TestCard: CardProtocol {
var id: Int32
var firstName: String?
var lastName: String?
var email: String?
var phone: String?
}
extension CardsModel where IdentifiedValue == TestCard {
convenience init(cards: [TestCard]) {
self.init()
self.cards = cards.map({ (card) -> AnyCard<TestCard> in
return AnyCard(card)
})
}
}
private class _AnyCardBase<IdentifiedValue>: CardProtocol {
init() {
guard type(of: self) != _AnyCardBase.self else {
fatalError("_AnyCardBase<Model> instances can not be created; create a subclass instance instead")
}
}
var id: Int32 {
get { fatalError("Must override") }
set { fatalError("Must override") }
}
var firstName: String? {
get { fatalError("Must override") }
set { fatalError("Must override") }
}
var lastName: String? {
get { fatalError("Must override") }
set { fatalError("Must override") }
}
var email: String? {
get { fatalError("Must override") }
set { fatalError("Must override") }
}
var phone: String? {
get { fatalError("Must override") }
set { fatalError("Must override") }
}
}
private final class _AnyCardBox<Concrete: CardProtocol>: _AnyCardBase<Concrete.IdentifiedValue> {
var concrete: Concrete
init(_ concrete: Concrete) {
self.concrete = concrete
}
override var id: Int32 {
get {
return concrete.id
}
set {
concrete.id = newValue
}
}
override var firstName: String? {
get {
return concrete.firstName
}
set {
concrete.firstName = newValue
}
}
override var lastName: String? {
get {
return concrete.lastName
}
set {
concrete.lastName = newValue
}
}
override var email: String? {
get {
return concrete.email
}
set {
concrete.email = newValue
}
}
override var phone: String? {
get {
return concrete.phone
}
set {
concrete.phone = newValue
}
}
}
final class AnyCard<IdentifiedValue>: CardProtocol {
private let box: _AnyCardBase<IdentifiedValue>
init<Concrete: CardProtocol>(_ concrete: Concrete) where Concrete.IdentifiedValue == IdentifiedValue {
box = _AnyCardBox(concrete)
}
var id: Int32 {
get {
return box.id
}
set {
box.id = newValue
}
}
var firstName: String? {
get {
return box.firstName
}
set {
box.firstName = newValue
}
}
var lastName: String? {
get {
return box.lastName
}
set {
box.lastName = newValue
}
}
var email: String? {
get {
return box.email
}
set {
box.email = newValue
}
}
var phone: String? {
get {
return box.phone
}
set {
box.phone = newValue
}
}
}
//NSManagedObject extention
extension Card:CardProtocol {}
Простой ластик решает эту проблему.
final class CardsModel: BindableObject {
var cards: [AnyCardModel] = [AnyCardModel]()
let didChange = PassthroughSubject<CardsModel, Never>()
}
protocol CardProtocol: Identifiable {
var id: Int { get set }
var firstName: String? { get set }
var lastName: String? { get set }
var email: String? { get set }
var phone: String? { get set }
}
class AnyCardModel: CardProtocol {
var _id: Int
var _firstName: String?
var _lastName: String?
var _email: String?
var _phone: String?
init<T: CardProtocol>(card: T) {
_id = card.id
_firstName = card.firstName
_lastName = card.lastName
_email = card.email
_phone = card.phone
}
var id: Int {
get { return _id }
set { _id = newValue}
}
var firstName: String? {
get { return _firstName }
set { _firstName = newValue}
}
var lastName: String? {
get { return _lastName }
set { _lastName = newValue}
}
var email: String?{
get { return _email }
set { _email = newValue}
}
var phone: String?{
get { return _phone }
set { _phone = newValue}
}
}