I am trying to build an autocomplete text field in swift, but am having a strange problem with my source data, which is a list of sealed classes from a KMM module.
My code is adapted from this answer: SwiftUI example for autocompletion
I have modified it to use a generic type for the list of possible options, and also pass a function to get the string value from the type.
The source list of options is coming from a KMM project and is an array/list of a sealed class.
The source data is correctly being passed to the struct, and when I display a list of the options on screen (in the VStack), it is displaying as expected.
However, in my makePrediction function, the list of predictableValues is empty.
If I pass a list of strings to my AutoCompleteTextField then the list isn't empty and it works as expected.
Printing predictableValues in the init block works, displaying them in the VStack works, so why is my list suddenly empty when I try to use it in makePrediction? (Yet if I just use plain strings as my type, the list isn't empty)
Here's my code for the AutoCompleteTextField
struct AutoCompleteTextField<T: Hashable>: View {
var optionText: (T) -> String
var predictableValues: Array<T>
@State var textFieldInput: String = ""
@State var predictedValue: Array<T> = []
var body: some View {
VStack(alignment: .leading){
PredictingTextField(predictableValues: self.predictableValues, predictedValues: self.$predictedValue, textFieldInput: self.$textFieldInput, optionText: optionText)
.textFieldStyle(RoundedBorderTextFieldStyle())
VStack{
ScrollView{
VStack{
ForEach(self.predictedValue, id: \.self) { suggestion in
ZStack {
Text(optionText(suggestion))
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.onTapGesture {
}
}
}
}
}
}.padding()
}
}
struct PredictingTextField<T: Hashable>: View {
var optionText: (T) -> String
/// All possible predictable values. Can be only one.
var predictableValues: Array<T>
/// This returns the values that are being predicted based on the predictable values
@Binding var predictedValues: Array<T>
/// Current input of the user in the TextField. This is Binded as perhaps there is the urge to alter this during live time. E.g. when a predicted value was selected and the input should be cleared
@Binding var textFieldInput: String
/// The time interval between predictions based on current input. Default is 0.1 second. I would not recommend setting this to low as it can be CPU heavy.
@State var predictionInterval: Double?
/// Placeholder in empty TextField
@State var textFieldTitle: String?
@State private var isBeingEdited: Bool = false
init(predictableValues: Array<T>, predictedValues: Binding<Array<T>>, textFieldInput: Binding<String>, textFieldTitle: String? = "", predictionInterval: Double? = 0.1, optionText: @escaping (T) -> String){
self.predictableValues = predictableValues
self._predictedValues = predictedValues
self._textFieldInput = textFieldInput
self.textFieldTitle = textFieldTitle
self.predictionInterval = predictionInterval
self.optionText = optionText
}
var body: some View {
VStack{
ForEach(self.predictableValues, id: \.self){ value in
Text(optionText(value)) //This works correctly with both string and sealed class
}
}
TextField(self.textFieldTitle ?? "", text: self.$textFieldInput, onEditingChanged: { editing in self.realTimePrediction(status: editing)}, onCommit: { self.makePrediction()})
}
/// Schedules prediction based on interval and only a if input is being made
private func realTimePrediction(status: Bool) {
self.isBeingEdited = status
if status == true {
Timer.scheduledTimer(withTimeInterval: self.predictionInterval ?? 1, repeats: true) { timer in
self.makePrediction()
if self.isBeingEdited == false {
timer.invalidate()
}
}
}
}
/// Makes prediciton based on current input
private func makePrediction() {
print(self.predictableValues) //the list is empty here when using the LocationOption sealed class, but correctly holds the list of strings when using strings
self.predictedValues = []
if !self.textFieldInput.isEmpty{
for value in self.predictableValues {
if optionText(value).lowercased().contains(self.textFieldInput.lowercased()) {
self.predictedValues.append(value)
}
}
}
}
}
Usage:
AutoCompleteTextField(
optionText: {$0.text()},
predictableValues: state.basicInfo.locationOptions //Not working correctly
)
AutoCompleteTextField(
optionText: {$0},
predictableValues: [
"ABC", "DEF" //Works correctly
]
)
And here's the KMM class I'm trying to use:
sealed class LocationOption {
data class TypeA(val value: database.TypeA) : LocationOption()
data class TypeB(val value: database.TypeB) : LocationOption()
fun text() = when (this) {
is TypeA-> value.r_name
is TypeB-> value.name
}
fun location() = when (this) {
is TypeB-> Location(value.lat, value.lng, 0.0)
is TypeA-> Location(value.lat, value.lng, 0.0)
}
}