I'm trying to create a reusable component for presenting a Swift collections OrderedSet into a SwiftUI List with delete, move and add handlers.
I would like to be able to pass in an OrderedSet of any type that can be used directly as interpolated string, for example Text("hello \(myGenericTypeLikeAStringIntOrFloat) world")
.
I've been trying to find a protocol that can help me define these generic types and thought perhaps the ExpressibleByStringInterpolation
could help here but I think thats only used to create your own types that can be used in SwiftUI Text views.
The error I'm getting on the Text("\(item)")
line is No exact matches in call to instance method 'appendInterpolation'
.
Is there a way to adjust my OrderedSet to only accept items that can be used in Text views?
Here's a stripped down example that hopefully explains it better.
import SwiftUI
import OrderedCollections
struct ReusableListView<Element: Hashable>: View {
@Binding var set: OrderedSet<Element>
@State private var multiSelection: Set<Element> = []
init(_ set: Binding<OrderedSet<Element>>) {
self._set = set
}
var body: some View {
Section {
List(selection: $multiSelection) {
ForEach(set, id: \.self) { item in
Text("\(item)")
}
.onDelete { indexSet in
set.elements.remove(atOffsets: indexSet)
}
}
}
}
}
@main
struct MyApp: App {
@State private var setWithStrings: OrderedSet<String> = ["hello", "world"]
@State private var setWithInts: OrderedSet<Int> = [1, 2, 3]
var body: some Scene {
WindowGroup {
ReusableListView($setWithStrings)
ReusableListView($setWithInts)
}
}
}
*** Added full working code snippet after excellent answer from @Joakim-Danielson ***
import SwiftUI
import OrderedCollections
protocol TextSupport {
var text: String { get }
}
extension String: TextSupport {
var text: String { self }
}
extension Int: TextSupport {
var text: String { self.formatted() }
}
struct ReusableListView<Element: Hashable & TextSupport>: View {
@Binding var set: OrderedSet<Element>
@State private var multiSelection: Set<Element> = []
init(_ set: Binding<OrderedSet<Element>>) {
self._set = set
}
var body: some View {
Section {
List(selection: $multiSelection) {
ForEach(set, id: \.self) { item in
Text(item.text)
}
.onDelete { indexSet in
set.elements.remove(atOffsets: indexSet)
}
}
}
}
}
@main
struct MyApp: App {
@State private var setWithStrings: OrderedSet<String> = ["hello", "world"]
@State private var setWithInts: OrderedSet<Int> = [1, 2, 3]
var body: some Scene {
WindowGroup {
ReusableListView($setWithStrings)
ReusableListView($setWithInts)
}
}
}
One option is to introduce a protocol that the generic type must conform to and that Text
can take advantage of
protocol TextSupport {
var text: String { get }
}
Then we change the declaration of the view to
struct ReusableListView<Element: Hashable & TextSupport>: View
and use the property in the view
ForEach(set, id: \.self) { item in
Text("\(item.text)")
}
Then you need to conform to the protocol for any type you use
extension String: TextSupport {
var text: String { self }
}
extension Int: TextSupport {
var text: String { self.formatted() }
}
Another option is to add a closure property to the view so that each implementation of the view passes its own way of converting the Element
type to a String
var convert: (Element) -> String
the property is set in the init
init(_ set: Binding<OrderedSet<Element>>, convert: @escaping (Element) -> String) {
self._set = set
self.convert = convert
}
and use it in the ForEach
ForEach(set, id: \.self) { item in
Text("\(convert(item))")
}
And then you call the view with a closure
ReusableListView($setWithStrings) { $0 }
ReusableListView($setWithInts) { $0.formatted() }