How to express a relationship between generic parameters in a SwiftUI view - swift

I have defined the following protocols:
protocol ListableArrayElement: Identifiable, Equatable {
associatedtype T = Hashable
var id:T { get }
}
protocol Listable: ObservableObject, RandomAccessCollection where Element == ArrayElement {
associatedtype ArrayElement: ListableArrayElement
var _elements: [ArrayElement] { get set }
var count: Int { get }
var isEmpty: Bool { get }
var endIndex: Int { get }
var startIndex: Int { get }
subscript(position: Int) -> ArrayElement { get set }
func index(after i: Int) -> Int
func append(_ element: ArrayElement)
func insert(_ newElement: ArrayElement, at i: Int)
func remove(at index: Int)
func removeAll()
func index(of element: ArrayElement) -> Int?
}
protocol FavouritesArray: Listable, Codable where Element: Codable {
// MARK: - PROPERTIES
var _elements: [Element] { get set }
var key: String { get set }
init()
init(key: String)
}
There is an associated extension to FavouritesArray that provides conforming types with the ability to add/remove elements, and persist/load the array to/from UserDefaults via the key. All well and good. (Also note the listable protocol helps me avoid writing some boiler plate code for ViewModels that have an array of 'something' at their heart.)
Now I also want to write a generic SwiftUI view that can build a menu, using the FavouriteArray functions. I am struggling to understand how to express the type signature:
I'm passing instances of types conforming to FavouritesArray via EnvironmentObject, and thus want to write something like:
struct FavouritesArrayView<Favourites: FavouritesArray, Favourite>: View
where Favourite == FavouritesArray.Element {
#EnvironmentObject var favourites: Favourites
#ObservedObject var favourite: Favourite
// Other properties here
var body: some View {
// Layout code here
}
}
This gives the compiler error: Associated type 'Element' can only be used with a concrete type or generic parameter base
Any tips on how to achieve this?

First of all you need to declare that Favourite conforms to ObservableObject and then the where condition should use your associated type and not the protocol it conforms to, where Favourites.Element == Favourite
struct FavouritesArrayView<Favourites: FavouritesArray, Favourite: ObservableObject>: View where Favourites.Element == Favourite

Related

Using swift protocols as Equatable argument

For example we have two simple protocols:
protocol Container {
var items: [Item] {get}
}
protocol Item {
var name: String {get}
}
And we want to use a function that requires Item to be Equatable, like:
extension Container {
func indexOfItem(_ item: Item) -> Int? {
items.firstIndex(of: item)
}
}
Yet we can't write it like this, as firstIndex requires argument conforming to Equatable. But Item being protocol, can't conform to Equatable.
I've tried to implement it using type erasure, but it became too complicated.
So I am curious is there some workaround to implement such functions (provided that actual items are equatable)?
Since you're using func firstIndex of an array in this func indexOfItem(_ item: Item) -> Int? therefore the Item has to be a concrete object (behind the scene of firstIndex func is comparing each element of an array and print out the index of the element).
There are 2 ways to do this
First is using associatedtype to keep your protocol generic
protocol Item: Equatable {
var name: String { get }
}
protocol Container {
associatedtype Item
var items: [Item] { get }
}
struct MyItem: Item {
var name: String
}
extension Container where Item == MyItem {
func indexOfItem(_ item: Item) -> Int? {
return items.firstIndex(of: item)
}
}
Second is using an equatable object MyItem instead a protocol Item inside the Container protocol
protocol Item {
var name: String { get }
}
protocol Container {
var items: [MyItem] { get }
}
struct MyItem: Item, Equatable {
var name: String
}
extension Container {
func findIndex(of item: MyItem) -> Int? {
return items.firstIndex(of: item)
}
}
Finally find simple enough solution:
То make protocol generic with associated type and constraint
this type to Equatable.
public protocol Container {
associatedtype EquatableItem: Item, Equatable
var items: [EquatableItem] {get}
}
public protocol Item {
var name: String {get}
}
public extension Container {
func indexOfItem(_ item: EquatableItem) -> Int? {
items.firstIndex(of: item)
}
}
This compiles and now if I have some types
struct SomeContainer {
var items: [SomeItem]
}
struct SomeItem: Item, Equatable {
var name: String
}
I only need to resolve associatedtype to provide protocol conformance for SomeContainer type:
extension SomeContainer: Container {
typealias EquatableItem = SomeItem
}

How to generate a binding for each array element

If I had a #State or an #ObservedObject variable with an array property, and I wanted to use List and pass a binding of each element of the array into some child View (e.g. Toggle or TextField), is there a standard way to do that?
Simplified example:
struct Person: Identifiable {
var id: UUID = .init()
var name: String
var isFavorite: Bool = false
}
struct ContentView: View {
#State var people = [Person(name: "Joey"), Person(name: "Chandler")]
var body: some View {
List(people) { person in
HStack() {
Text(person.name)
Spacer
Toggle("", isOn: $person.isFavorite) // <- this obviously doesn't work
}
}
}
}
This seems like a fairly common scenario, but I can't figure out an obvious solution aside from manually building a separate array of bindings.
The only elegant solution I came up with (I'll add it as an answer, if there isn't something better) was to create an extension of Binding of a RandomAccessCollection to itself conform to a RandomAccessCollection, which has bindings as elements, like so:
extension Binding: RandomAccessCollection
where Value: RandomAccessCollection & MutableCollection {
// more code here
}
// more required extensions to Collection and Sequence here
UPDATE
In iOS13 release notes (deprecation section), SwiftUI dropped the conformance of Binding to Collection, and instead offered a workaround, so I'm updating this answer with their suggestion.
The idea is to extend RandomAccessCollection to add a .index() method, which works similarly to .enumerated() by creating a collection of tuples of index and element, but unlike .enumerated() conforms to a RandomAccessCollection, which List and ForEach require.
The usage is:
List(people.indexed(), id: \.1.id) { (i, person) in
HStack() {
Toggle(person.name, isOn: $people[i].isFavorite)
}
And the implementation of .indexed() is:
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.startIndex }
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
ORIGINAL
Here's what I wanted to achieve:
List($people) { personBinding in
HStack() {
Text(personBinding.wrappedValue.name)
Spacer()
Toggle("", isOn: personBinding.isFavorite)
}
}
In other words, pass the binding of an array, and get a binding of an element in List's closure.
To achieve that, I created an extension of Binding that makes a Binding of any RandomAccessCollection into a RandomAccessCollection of bindings:
// For all Bindings whose Value is a collection
extension Binding: RandomAccessCollection
where Value: RandomAccessCollection & MutableCollection {
// The Element of this collection is Binding of underlying Value.Element
public typealias Element = Binding<Value.Element>
public typealias Index = Value.Index
public typealias SubSequence = Self
public typealias Indices = Value.Indices
// return a binding to the underlying collection element
public subscript(position: Index) -> Element {
get {
.init(get: { self.wrappedValue[position] },
set: { self.wrappedValue[position] = $0 })
}
}
// other protocol conformance requirements routed to underlying collection ...
public func index(before i: Index) -> Index {
self.wrappedValue.index(before: i)
}
public func index(after i: Index) -> Index {
self.wrappedValue.index(after: i)
}
public var startIndex: Index {
self.wrappedValue.startIndex
}
public var endIndex: Index {
self.wrappedValue.endIndex
}
}
This also requires explicit conformance to inherited protocols:
extension Binding: Sequence
where Value: RandomAccessCollection & MutableCollection {
public func makeIterator() -> IndexingIterator<Self> {
IndexingIterator(_elements: self)
}
}
extension Binding: Collection
where Value: RandomAccessCollection & MutableCollection {
public var indices: Value.Indices {
self.wrappedValue.indices
}
}
extension Binding: BidirectionalCollection
where Value: RandomAccessCollection & MutableCollection {
}
And, if the underlying value is an Identifiable, then it makes the Binding conform to Identifiable too, which removes the need to use id::
extension Binding: Identifiable where Value: Identifiable {
public var id: Value.ID {
self.wrappedValue.id
}
}

Conditional Protocol Conformance to Protocol

I'm learning Swift coming from other languages with powerful type systems and I am wondering if their is any way to make a protocol conditionally conform to another protocol.
Let's take an example: I define a ShowableAsInt protocol, that allows to get an Int representation for any type that conforms to it.
protocol ShowableAsInt {
func intRepr() -> Int
}
extension Int : ShowableAsInt {
func intRepr() -> Int {
return self
}
}
extension String : ShowableAsInt {
func intRepr() -> Int {
return self.count
}
}
func show(_ s: ShowableAsInt) {
print(s.intRepr())
}
show("abc") // "3"
show(42) // "42"
Now I define a Container protocol that simply wraps an element.
protocol Container {
associatedtype T
var element: T {
get
}
}
struct StringContainer : Container {
typealias T = String
let element: String
}
struct IntContainer : Container {
typealias T = Int
let element: Int
}
Because Container is a simple wrapper, whenever the wrapped type can be shown as an Int, the container can also be shown as an Int. I tried to express this, but failed:
// Doesn't compile: can't extend a protocol to conform to another one?
extension Container : ShowableAsInt where T : ShowableAsInt {
func intRepr() -> Int {
return element.intRepr()
}
}
// I would like these to compile
show(IntContainer(42)) // "42"
show(StringContainer("abc")) // "3"
I know that this conditional conformance can be expressed on class and struct. Is there any way to do the same for protocols? If not, is there any reason for this restriction?
The reason why this is not allowed is stated here:
This protocol extension would make any Collection of Equatable elements Equatable, which is a powerful feature that could be put to good use. Introducing conditional conformances for protocol extensions would exacerbate the problem of overlapping conformances, because it would be unreasonable to say that the existence of the above protocol extension means that no type that conforms to Collection could declare its own conformance to Equatable, conditional or otherwise.
See also this question.
If you just don't want to write the duplicate implementation of intRepr everytime, you can do this:
struct StringContainer : ShowableAsIntContainer {
typealias T = String
let element: String
}
struct IntContainer : ShowableAsIntContainer {
typealias T = Int
let element: Int
}
extension Container where T : ShowableAsInt {
func intRepr() -> Int {
return element.intRepr()
}
}
typealias ShowableAsIntContainer = ShowableAsInt & Container

Swift Protocol Extension - Can't Access Func

If I have a series of protocols like so:
protocol Customer {
var name:String { get set }
var age: Int { get set }
var startDate:Date { get set }
var meals:Array<String> { get set }
var market:Int { get set }
}
protocol Vegan:Customer {
}
protocol Vegetarian:Customer {
}
protocol Paleo:Customer {
}
and extension like so:
extension Customer where Self:Vegan, Self:Vegetarian {
func getMeals() -> Array<String> {
return ["VeganMeal1", "VeganMeal2", "VeganMeal3", "VeganMeal4"]
}
}
extension Customer where Self:Vegetarian {
func getMeals() -> Array<String> {
return ["VegetarianMeal1", "VegetarianMeal2", "VegetarianMeal3", "VegetarianMeal4"]
}
}
extension Customer where Self:Paleo {
func getMeals() -> Array<String> {
return ["PaleoMeal1", "PaleoMeal2", "PaleoMeal3", "PaleoMeal4"]
}
}
and this struct
struct aCustomer:Customer, Vegan {
var name:String
var age: Int
var startDate:Date
var meals:Array<String>
var market:Int
}
when I create a new object based on that struct
var newCustomer = aCustomer(name:"William", age:40, startDate:Date(), meals:[], market:1)
how come I can't access the getMeals function in the extensions? I get an error stating getMeals is an ambiguous reference.
extension Customer where Self:Vegan, Self:Vegetarian {
extends Customer in the case where the adopter or Customer also both adopts Vegan and Vegetarian. Your aCustomer (a struct type starting with a small letter?? the horror, the horror) does not do that, so the extension doesn't apply to it.
If the goal is to inject the same code either when the adopter adopts Vegan or when the adopter adopts Vegetarian, use two extensions, or, if you don't like the repetition, have them both adopt some "higher" protocol that is extended with the desired code to be injected.

Conformance to a protocol (as a protocol)

Say I have a protocol Item, and a struct ConcreteItem that conforms to it.
protocol Item {
var name: String { get }
}
struct ConcreteItem: Item {
let name: String
}
At some point I want to have two sets of ConcreteItem.
let set1 = Set([ConcreteItem(name: "item1")])
let set2 = Set([ConcreteItem(name: "item2"), ConcreteItem(name: "item1")])
Which I'd expect to return the item with name "item1".
I can make ConcreteItem conform to Hashable and the Set code will work. However, lets say I also had the following:
struct AnotherConcreteItem: Item {
let name: String
}
I'd like AnotherConcreteItem to also conform to Hashable simply for having conformed to Item.
However, when I try to implement that idea:
extension Item: Hashable {
var hashValue: Int {
return name.characters.count
}
}
I get the following error: Extension of protocol 'Item' cannot have an inheritance clause.
Extension of protocol 'Item' cannot have an inheritance clause
Here item is the protocol so conforming Hashable protocol will not work. For more details Refer here
What you are trying to do is possible with some protocols, but not all. If the protocol you are trying to conform to does not have any associated types or Self, this is possible.
Example:
protocol A {
func foo()
}
protocol Item: A {
var name: String { get }
}
struct ConcreteItem: Item {
let name: String
}
extension Item {
func foo() {
}
}
Everything that conforms to Item will also conform to A.
However, Hashable has Self constraints. To conform to Hashable, you must also conform to Equatable. To conform to Equatable, the implementation of == must be in a concrete class, not another protocol because Equatable can't be used as a parameter type. The most you can do is something like this:
protocol Item: Hashable {
var name: String { get }
}
struct ConcreteItem: Item {
let name: String
// you need to implement == in every concrete class
static func ==(lhs: ConcreteItem, rhs: ConcreteItem) -> Bool {
return ...
}
}
extension Item {
var hashValue: Int {
return name.characters.count
}
}