List of KMM Sealed Classes in Swift showing as empty when it isn't

39 views Asked by At

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

There are 0 answers