Generic Function to Output to UITextField or UILabel - ios

I am trying to write a function in Swift 2 for iOS that processes some text and writes it to either a UITextField or a UILabel. Currently, I have the following that works:
#IBOutlet weak var textField: UITextField!
#IBOutlet weak var textLabal: UILabel!
​func writeSomeText(string: String, toOutput: UITextField) {
// do some text processing
toOutput.text = // processed string
}
​func writeSomeText(string: String, toOutput: UILabel) {
// do some text processing
toOutput.text = // processed string
}
As you see, right now I am overloading the function, essentially duplicating it for both UITextField and UILabel, and since I have a bunch of text processing that is exactly the same, I am duplicating code.
Is there any way to write a function using generics to achieve this with one function definition?

try look at this approach ...
import UIKit
protocol P {
func foo(str: String)->String
}
extension P {
func foo(str: String)->String {
// do some processing
let res = str
return res
}
}
extension UILabel:P {
func bar(str: String) {
self.text = foo(str)
}
}
extension UITextField:P {
func bar(str: String) {
self.text = foo(str)
}
}
let l = UILabel()
l.text = "alfa"
l.bar("ALFA")
let t = UITextField()
t.text = "beta"
t.bar("BETA")
print(l.text, t.text) // Optional("ALFA") Optional("BETA")
by the way, you don't need protocol P at all :-), it is there just as a 'namespace' (not to have a global func foo)
another approach is to define some common protocol and extend UILabel and UITextField
import UIKit
protocol P: class {
var text: String? {get set}
}
extension UILabel: P {}
extension UITextField: P {}
func writeSomeText<T:P>(string: String, toOutput: T ){
// do some text processing
toOutput.text = string
}
let l = UILabel()
writeSomeText("label", toOutput: l)
l.text // "label"
let tv = UITextView()
tv.text = "text"
// but
writeSomeText("text view", toOutput: tv) // error: cannot invoke 'writeSomeText' with an argument list of type '(String, toOutput: UITextView)'
later you can extend other classes with text property, if you want ... without changing implementation of func writeSomeText

A Swifty way (without Generics) would be
#objc
protocol UITextOutputProtocol: class {
func setOutputText(text: String)
}
extension UITextField : UITextOutputProtocol {
func setOutputText(text: String) {
self.text = text
}
}
extension UILabel : UITextOutputProtocol {
func setOutputText(text: String) {
self.text = text
}
}
class ViewController: UIViewController {
//An outlet Collection containing Labels and TextField
#IBOutlet var texts: [UITextOutputProtocol]!
//An example function that will be replicating a text in all controls
func setTexts() {
for text in texts {
text.setOutputText("example Text")
}
}
UPDATE:
As UILabel and UITextField are now implementing protocol UITextOutputProtocol the following code will work.
#IBOutlet weak var textField: UITextField!
#IBOutlet weak var textLabel: UILabel!
​func writeSomeText(string: String, toOutput: UITextOutputProtocol) {
// do some text processing
toOutput.setOutputText("example Text") // change the text here for your processed string
}
func doSomeProcessingOnText(text:String) {
writeSomeText(text, textField)
writeSomeText(text, textLabel)
}
}

This is a little bit of a hack since UITextField and UILabel don't conform any shared protocols for text editing, but you could use KVO:
func writeSomeText(string: String, output: AnyObject) {
// do some text processing
if output.respondsToSelector("text") {
output.setValue(string, forKey: "text")
}
}
respondsToSelector checks to make sure the object you're passing in has a property text before continuing setting a value to that key. This check is used to prevent a crash a runtime if you pass in an object that is not KVO compliant for the key text`.

Related

I got error "Type '(UITextRange) -> String?' cannot conform to 'BinaryInteger'; only struct/enum/class types can conform to protocol"

I'm new to Swift Programming and I made a simple project which will give output to a label, but I'm getting the error:
Type '(UITextRange) -> String?' cannot conform to 'BinaryInteger'; only struct/enum/class types can conform to protocol.
Can anyone explain what's happening?
#IBOutlet weak var lblOutput: UILabel!
#IBOutlet weak var InputAge: UITextField!
#IBOutlet weak var outputAge: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func calcOutput(_ sender: Any) {
var age = Int(InputAge.text) * 8;
lblOutput.text = age;
}
Image containing the error:
There are three mistakes:
You have to unwrap text of inputAge, this can be forced because the text property of UITextField is never nil
For example "ABC" cannot be converted to Int, therefore Int(...) returns an optional and you have to check if it's nil
The type of the text property of UILabel is String. You have to convert the result of the multiplication back to String
And please name functions and variables with starting lowercase letter and declare immutable variables as constants (let)
#IBOutlet weak var inputAge : UITextField!
#IBAction func calcOutput(_ sender: Any) {
let stringAge = inputAge.text!
if let age = Int(stringAge) {
lblOutput.text = String(age * 8)
} else {
lblOutput.text = "The input string is not convertible to Int"
}
}
Unwrap optional of text before use:
#IBAction func calcOutput(_ sender: Any) {
if let text = validatedField.text,
let age = Int(text) {
lblOutput.text = "\(age * 8)"
}
}
Don't use semicolons at the lines end.
Use let if you don't update its values later.
Set text to text fields (not Int).

Enable/Disable button with validating phone number entered in a textfield

I'm very new to ReactiveSwift and MVVM as a whole. I'm trying to validate phone numbers entered into a textfield and enable/disable a button depending on the validation result.
In the app, there is a textfield and a UIButton button called Submit. For phone number validating, I'm using an open source library called [PhoneNumberKit][1]. It also provides a UITextField subclass which formats the user input.
I mashed together a solution that looks like this.
class ViewController: UIViewController {
#IBOutlet weak var textField: PhoneNumberTextField!
#IBOutlet weak var submitButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
textField.becomeFirstResponder()
submitButton.isEnabled = false
let textValuesSignal = textField.reactive.continuousTextValues
SignalProducer(textValuesSignal).start { result in
switch result {
case .value(let value):
print(value)
self.submitButton.isEnabled = self.isPhoneNumberValid(value)
case .failed(let error):
print(error)
self.submitButton.isEnabled = false
case .interrupted:
print("inturrupted")
self.submitButton.isEnabled = false
case .completed:
print("completed")
}
}
}
func isPhoneNumberValid(_ phoneNumberString: String) -> Bool {
do {
let phoneNumber = try PhoneNumberKit().parse(phoneNumberString)
let formattedPhoneNumber = PhoneNumberKit().format(phoneNumber, toType: .e164)
print("Phone number is valid: \(formattedPhoneNumber)")
return true
} catch let error {
print("Invalid phone number: \(error)")
return false
}
}
}
This does the job but not very elegantly. Also there is a significant lag between user input and the UI changing.
Another thing is my above solution doesn't conform to MVVM. I gave it another go.
class ViewController: UIViewController {
#IBOutlet weak var textField: PhoneNumberTextField!
#IBOutlet weak var submitButton: UIButton!
private let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
textField.becomeFirstResponder()
submitButton.reactive.isEnabled <~ viewModel.isPhoneNumberValid
viewModel.phoneNumber <~ textField.reactive.continuousTextValues
}
}
class ViewModel {
let phoneNumber = MutableProperty("")
let isPhoneNumberValid = MutableProperty(false)
init() {
isPhoneNumberValid = phoneNumber.producer.map { self.validatePhoneNumber($0) } // Cannot assign value of type 'SignalProducer<Bool, NoError>' to type 'MutableProperty<Bool>'
}
private func validatePhoneNumber(_ phoneNumberString: String) -> Bool {
do {
let phoneNumber = try PhoneNumberKit().parse(phoneNumberString)
let formattedPhoneNumber = PhoneNumberKit().format(phoneNumber, toType: .e164)
print("Phone number is valid: \(formattedPhoneNumber)")
return true
} catch let error {
print("Invalid phone number: \(error)")
return false
}
}
}
I'm getting the below error in the initializer when I'm assigning the result from the validatePhoneNumber function's to the isPhoneNumberValid property.
Cannot assign value of type 'SignalProducer' to type 'MutableProperty'
I can't figure out how to hook up the phone number validation part with the submit button's isEnabled property and the tap action properly.
Demo project
Try setting the property in init and mapping the property itself rather than a producer:
let isPhoneNumberValid: Property<Bool>
init() {
isPhoneNumberValid = phoneNumber.map { ViewModel.validatePhoneNumber($0) }
}
You’ll have to make validatePhoneNumber a static method because self won’t be available yet.
This is a more typical reactive way of doing this because you define one property completely in terms of another one during the view model’s initialization.

Generic `getText()` function for UI elements in Swift

For a lot of actions, I need to check if the content of label.text, textView.text, textField.text etc. is nil.
So I created some extensions:
extension UILabel {
func getText() -> String {
return self.text ?? ""
}
}
extension UITextField {
func getText() -> String {
return self.text ?? ""
}
}
extension UITextView {
func getText() -> String {
return self.text ?? ""
}
}
The extensions are very redundant. A similar case is when I need to cast an Int, Double, Float etc. to another number format. What I want is a simple toInt() with a return of -1 or 0 when something went wrong.
So how can I create a generic function for the toString() or toInt()? I read about extensions and generics in the Apple documentation, but I didn't see a solution for my problem.
Finally, I tried to expand UIView with the same extension, because it's a superclass of UILabel etc., but I can't call getText().
So what is a good method to create generic extensions?
There are a couple of dubious things here: I wouldn't want any toInt() function to just return -1 or 0 when things went wrong. Swift has a good optionality system for a reason, and there are several obvious pitfalls introduced by returning -1 or 0. I also don't know what you plan to do for implementing getText() on a UIView. Lots of views don't have text. I don't know what this implementation would mean or do.
So I'll ignore those two details and focus on the primary question, which seems to be the redundancy of your extensions. There is a cleaner way, via protocol extensions, to cut down on duplication.
protocol TextProviding {
var text: String? { get }
}
extension TextProviding {
// Following your example here. I would prefer calling
// this a `var textOrEmpty: String` but same idea.
func getText() -> String {
return text ?? ""
}
}
// To conform additional types, you just need 1 line
extension UILabel: TextProviding { }
extension UITextField: TextProviding { }
EDIT: as some have pointed out, using the above code with extension UITextView: TextProviding { } will not work, because UITextView's text is a String!, not a String?. Unfortunately, this means if you want this to work for UITextView as well, you should rename the var text requirement to something else (and this means you will need a couple extra lines to manually conform UILabel and UITextField).
protocol TextProviding {
var string: String? { get }
}
extension TextProviding {
var stringOrEmpty: String {
return string ?? ""
}
}
extension UILabel: TextProviding {
var string: String? { return text }
}
extension UITextField: TextProviding {
var string: String? { return text }
}
extension UITextView: TextProviding {
var string: String? { return text }
}
For the first problem, you should only extend UIView and check whether its a label, a text field or a text view with if let.
extension UIView {
func getText() -> String {
if let label = self as? UILabel {
return label.text ?? ""
} else if let textField = self as? UITextField {
return textField.text ?? ""
} else if let textView = self as? UITextView {
return textView.text ?? ""
}
return ""
}
}
The advantage of this method over creating a protocol is that you can easily extend it, even if the text/title is retrievable with another method, for example, with a UIButton.
[...]
else if let button = self as? UIButton {
return button.title(for: .normal) ?? ""
}
This one-liner reduces redundant code (what you wanted) than all other answers posted here. No need for crazy condition-checks or multiple extensions.
extension UIView {
func getText() -> String? {
return self.responds(to: #selector(getter: UILabel.text)) ?
self.perform(#selector(getter: UILabel.text))?.takeUnretainedValue() as? String : nil
}
}
Create a protocol with an extension and then extend every class to adopt the protocol :)
protocol GetTextProtocol {
var text: String? {get}
func getText() -> String
}
extension GetTextProtocol {
func getText() -> String {
return self.text ?? ""
}
}
extension UILabel: GetTextProtocol {
var text: String? {
return self.text
}
}
extension UITextView: GetTextProtocol {
var text: String? {
return self.text
}
}

can't append text to string array due to Initializer for conditional binding error

My code below works perfectly if i switch the strings for the ints. However with the String where it is it is not working.
import UIKit
class ViewController: UIViewController {
#IBOutlet var txt: UITextField!
var arrayOfInt = [String]()
#IBAction func submit(_ sender: Any) {
if let text = txt.text {
if let name = String(text){
arrayOfInt.append(name)
}}}
}
You are (unnecessarily) initialising a String from a String using String(text); Unlike Int(_ string:String) this is not a failable initializer.
It is possible that a String cannot be parsed to an Int, so you need to check that Int(text) didn't return nil, but a String can always be initialised from a String.
When you say
if let name = String(text) {
The compiler give you an error because the if let... conditional binding construct requires an optional, but String(text) does not return an optional.
Your code can be simplified to:
class ViewController: UIViewController {
#IBOutlet var txt: UITextField!
var arrayOfString = [String]()
#IBAction func submit(_ sender: Any) {
if let text = sender.text {
arrayOfString.append(text)
}
}
}

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.
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.

Resources