Search code examples
iosswiftmacosswiftuistring-interpolation

How to accept any variable type that can be used in SwiftUI Text view


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)
        }
    }
}

Solution

  • 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() }