Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using generic function in base class

What I'm trying to do...

In my app I have a lot of form fields that look alike with a bunch of custom functionality (change color on highlight e.c.t.).

I want to create a sort of wrapper class, that abstracts all of this code, then inherit from that to implement my different input types such as date input and text input.

The inherited classes will just need to setup the correct input control for it's type.

What i've tried

This is more like pseudo-code. I have been trying for hours but I just don't understand how to achieve what I need

I think i start with a base class, this needs to define a reference to the input control, and a few methods that each one will override such as being able to set or get the current value

class BaseInput<T>: UIView {
   let label = UILabel()
   let control: T

   ... A bunch of methods for layout and stuff ...

   func setControlValue(_ value: U) {
      print("I'm not a real input yet, so i can't do that")
   }
}

I then create an inherited class for a date input. This uses a basic label for the control, and internally will use a UIDatePicker to set the value

class DateInput: BaseInput<UILabel> {

    override init() {
        self.control = UILabel()
    }    

    override func setControlValue(_ value: Date) {
        MyGlobalDateFormatter.string(format:value)
    }

}

and another for a text input field

class TextInput: BaseInput<UITextField> {

    override init() {
        self.control = UITextField()
    }    

    override func setControlValue(_ value: String) {
        control.textLabel!.text = value 
    }

}

What i'm ultimately looking for is the ability to initialise a input component, and for the MyInput.control property to be of the correct class for that specific input, and for the setControlValue method to accept the correct kind of Data (i.e. a String, Int or Date depending on the type of control)

I believe this can be solved using generic's but i'm really struggling to understand how. If anyone can point me in the right direction that would be great.

Note: I don't expect or want anyone to write all of the code for me. Pseudo-code would be enough to allow me to work it all out.

Attempt 1:

protocol CustomControl {
    associatedtype Control
    associatedtype Value

    var control : Control { get }
    var label : UILabel { get }

    func setControlValue(_ value: Value)
}

class AbstractInputField: UIView {
   // This class contains all the setup
   // for the label and wrapping UI View
}

class TextInputField: AbstractInputField, CustomControl {
   typealias Control = UITextField
   typealias Value = String

   let control = UITextField()

   func setControlValue(_ value: String) {
        control.text = value
    }
}

class DateInputField: AbstractInputField, CustomControl {
   typealias Control = UILabel
   typealias Value = String

   let control = UILabel()

   private let picker = UIDatePicker()

   func setControlValue(_ value: Date) {
        control.text = GlobalDateFormatter.string(from: value)
   }

   .. Also in this class it's a bunch of date picker methods ..
}

Elsewhere if I do:

override func viewDidLoad() {

  let firstInput = makeControl("text")
  firstInput.label.text = "First name"
  firstInput.setControlValue(myUser.first_name)

  let dobInput = makeControl("date")
  dobInput.label.text = "Date of birth"
  dobInput.setControlValue(myUser.dob)

}

func makeControl(controlType: String) -> CustomControl {
   // Im using strings just for testing, i'd probably make this an enum or something
   if controlType == "text" {
      return TextInputField()
   } else { 
      return DateInputField()
   }
}

I get the error: `Protocol 'CustomControl' can only be used as a generic constraint because it has Self or associated type requirements

What i'm ultimately trying to achieve, is a very simple API to my inputs where i can set the label text, and set the input value. The rest of my app doesn't care if it's a textfield, textview or complete custom input type. My app wants to work with the protocol (or a base class of some kind) that says it has these methods & properties.

Maybe i'm being stupid.

like image 431
TRG Avatar asked Jun 02 '26 20:06

TRG


1 Answers

I would suggest to use protocols.

protocol CustomControl {
    associatedtype Control
    associatedtype Value

    var control: Control { get }
    func setControlValue(_ value: Value)
}

and then create custom classes conform to this protcol

class CustomTextField: CustomControl {
    typealias Control = UITextField
    typealias Value = String

    let control: UITextField

    init() {
        self.control = UITextField()
    }

    func setControlValue(_ value: String) {
        control.text = value
    }
}

Edited:

class CustomLabel: CustomControl {
    typealias Control = UILabel
    typealias Value = String

    let control: UILabel

    init() {
        self.control = UILabel()
    }

    func setControlValue(_ value: String) {
        control.text = value
    }
}

Edit 2: Alternative approach

protocol HasSettableValue: class {
    associatedtype Value
    var customValue: Value { get set }
}

protocol IsInitializable {
    init()
}

extension UITextField: IsInitializable {}
extension UITextField: HasSettableValue {
    typealias Value = String?
    var customValue: String? {
        get {
            return text
        }
        set {
            text = newValue
        }
    }
}

class BaseClass<T> where T: HasSettableValue, T: IsInitializable {
    let control: T

    init() {
        self.control = T()
    }

    func setControlValue(_ value: T.Value) {
        control.customValue = value
    }
}

class CustomTextField: BaseClass<UITextField> {

}

let customTextField = CustomTextField()
customTextField.setControlValue("foo")
print(customTextField.control) // prints <UITextField: 0x...; frame = (0 0; 0 0); text = 'foo'; opaque = NO; layer = <CALayer: 0x...>>

Final update:

The problem with protocols having associated types is you can't use them for declaration of variables. You always need to specify the concrete implementation.

I guess I found a solution fitting your needs:

enum ControlType {
    case textField, datePicker
}

enum ControlValueType {
    case text(text: String)
    case date(date: Date)

    var string: String {
        switch self {
        case .text(text: let text):
            return text
        case .date(date: let date):
            // apply custom format
            return "\(date)"
        }
    }

    var date: Date {
        switch self {
        case .date(date: let date):
            return date
        default:
            preconditionFailure("`date` can be only used with `.date` value type")
        }
    }
}

protocol Control: class {
    var controlValue: ControlValueType { get set }
}

class TextInputField: Control {
    private let textField = UITextField()

    var controlValue: ControlValueType {
        get {
            return .text(text: textField.text ?? "")
        }
        set {
            textField.text = newValue.string
        }
    }
}

class DateInputField: Control {
    private let picker = UIDatePicker()

    var controlValue: ControlValueType {
        get {
            return .date(date: picker.date)
        }
        set {
            picker.date = newValue.date
        }
    }
}

func createControl(ofType type: ControlType) -> Control {
    switch type {
    case .textField:
        return TextInputField()
    case .datePicker:
        return DateInputField()
    }
}

let customDatePicker = createControl(ofType: .datePicker)
customDatePicker.controlValue = .date(date: Date())
print(customDatePicker.controlValue.string) // prints 2017-09-06 10:47:22 +0000

let customTextFiled = createControl(ofType: .textField)
customTextFiled.controlValue = .text(text: "Awesome text")
print(customTextFiled.controlValue.string) // prints Awesome text

I hope this helps.

PS: What you are trying to achieve is not very common pattern in iOS so far I know.

like image 180
Andrey Gershengoren Avatar answered Jun 05 '26 12:06

Andrey Gershengoren



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!