0

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)
        }
    }
}
2
  • Is localisation a concern for you? If not, you can just do Text(String(describing: item)). Commented Jan 11, 2024 at 13:14
  • Thanks, tried that and indeed seems to work nicely for ints and strings! I did end up going with the custom TextSupport protocol as described by @joakim-danielson as that will allow me to be more flexible with the desired output. Commented Jan 11, 2024 at 13:49

1 Answer 1

2

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() }
Sign up to request clarification or add additional context in comments.

2 Comments

Unrelated perhaps but why not declare the selection property as Set<Element>?
That was a mistake indeed, should have been Set<Element>

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.