So now in the app i'm currently developing I decided to refactor it by moving to the MVVM design pattern. And here it is where I got to know the famous "Observables".
I managed to understand how they work and the importance of their existence when using MVVM, I've read a couple of explanations on the different techniques for the implementation. By techniques I mean:
Observables (the one I'm currently using)
Event Bus / Notification Center
FRP Techinque (ReactiveCocoa / RxSwift)
I've declared my Bindable class like this:
import UIKit
class Bindable<T> {
var value: T? {
didSet {
observer?(value)
}
}
var observer: ((T?) -> ())?
func bind(observer: #escaping (T?) -> ()) {
self.observer = observer
}
}
What I wanted to do is to bind 2 UITextField's (that are inside one of my ViewController's) with the respective ViewModel. Inside my ViewController there are 2 textfields (emailInput - passwordInput) and a 'Log In' button, that I want it to be disabled unless both textfields aren't empty.
For that I've added both textfield's this target:
emailInput.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
passwordInput.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
Then:
/// Enable / Disable --> Log In button
#objc func textFieldDidChange(_ textField: UITextField) {
if (emailInput.text == "") || (passwordInput.text == "") {
logInButton.enableButton(false)
} else {
logInButton.enableButton(true)
}
}
But my question is... How could I implement this same thing inside my ViewModel??
And is it possible to do a two-way binding using my Bindable class?
(If more code is needed to solve this, just ask me to and I'll edit the question)
Observable is used to communicate changes from the view model to the view. There is no need for your view model to use the Observable pattern in order to respond to the updates in your text fields. You can provide a simple function setCredentials(email: String, password: String). In this function you can check if those values are empty and set var loginEnabled: Bindable<Bool>. Your view observes the loginEnabled and sets the login button state accordingly.
struct ViewModel {
var loginEnabled = Bindable<Bool>()
var email = ""
var password = ""
init() {
self.loginEnabled.value = false
}
func setCredentials(email: String, password: String) {
self.email = email
self.password = password
self.loginEnabled.value = !email.isEmpty && !password.isEmpty
}
}
Then in your view controller you have something like
var viewModel: ViewModel
override func viewDidLoad {
super.viewDidLoad()
self.viewModel.loginEnabled.bind { value in
self.logInButton.isEnabled = value ?? false
}
}
#objc func textFieldDidChange(_ textField: UITextField) {
self.viewModel.setCredentials(email: self.emailInput.text ?? "", password: self.passwordInput.text ?? "")
}
Related
I am new in RxSwift and want to implement a feature in my project.
I have to validate 2 fields, OTP and Confirm OTP using input/output MVVM using RxSwift on submit click.
Case1: If any textfield is empty submit button should be disabled, so if user starts typing the first textfield the submit button will be enabled (Same for confirm OTP textfield also)
Case2: On submit click i need to validate if either textfield is empty or not and show error on screen, Also if both the textfields value doesnt matches, it will show the error on submit button click.
let otpChangedText = BehaviorSubject<String>(value:"")
let confirmOtpChangedText = BehaviorSubject<String>(value:"")
let submitButtonTapped = PublishSubject<Void>()
let otp1Validation = otpChangedText.skipWhile { $0.isEmpty}.map {Validator.isEmpty(string: $0)}
let isValidOtp = otp1Validation.asDriver(onErrorJustReturn:false)
outputs = Outputs (isValidOTP: isValidOtp)
I have achieved the submit button disable state somehow but not getting any idea how should i show the error on screen if any of the field is empty and if the value of both the textfields dont match.
Please guide me. Thanks
As an aside, from the book "Intro To Rx":
Subjects provide a convenient way to poke around Rx, however they are not recommended for day to day use.
You shouldn't be throwing all those Subjects into the mix...
First let's create your business rules:
// Case 1
func eitherFilled(first: String?, second: String?) -> Bool {
first != nil && !first!.isEmpty || second != nil && !second!.isEmpty
}
// Case 2
func hasError(first: String?, second: String?) -> Bool {
first == nil || second == nil || first != second
}
You can test these independently from Rx or anything else to assure yourself that they are correct.
Now create your view models:
// Case 1
func buttonEnabled(first: Observable<String?>, second: Observable<String?>) -> Observable<Bool> {
Observable.combineLatest(first, second, resultSelector: eitherFilled)
}
// Case 2
func displayError(first: Observable<String?>, second: Observable<String?>) -> Observable<Void> {
Observable.combineLatest(first, second, resultSelector: hasError)
.filter { $0 }
.map { _ in }
}
Again, these are very easy to test using RxTest.
Now you can install your view models into your view controller and you know they work because you have tested them. (Note: I have not tested them. I leave it to you to make sure the logic is correct.)
final class MyViewController: UIViewController {
#IBOutlet weak var otpField: UITextField!
#IBOutlet weak var confirmOtpField: UITextField!
#IBOutlet weak var button: UIButton!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Case 1
buttonEnabled(
first: otpField.rx.text.asObservable(),
second: confirmOtpField.rx.text.asObservable()
)
.bind(to: button.rx.isEnabled)
.disposed(by: disposeBag)
// Case 2
displayError(
first: otpField.rx.text.asObservable(),
second: confirmOtpField.rx.text.asObservable()
)
.bind {
finalPresentScene(animated: true) {
UIAlertController(title: "Error", message: "OTP fileds do not match.", preferredStyle: .alert).scene { $0.connectOK() }
}
}
.disposed(by: disposeBag)
}
}
So with all the above, you have two models representing your business rules, two view models converting those business rules into view updates, and your view controller serves to connect the view models to the views.
(If you want to learn more about the code I'm using to present the alert, see this GitHub repository: https://github.com/danielt1263/CLE-Architecture-Tools)
I'm programmatically adding a UITapGestureRecognizer to one of my views:
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(modelObj:myModelObj)))
self.imageView.addGestureRecognizer(gesture)
func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
The first problem I encountered was "Argument of '#selector' does not refer to an '#Objc' method, property, or initializer.
Cool, so I added #objc to the handleTap signature:
#objc func handleTap(modelObj: Model) {
// Doing stuff with model object here
}
Now I'm getting the error "Method cannot be marked #objc because the type of the parameter cannot be represented in Objective-C.
It's just an image of the map of a building, with some pin images indicating the location of points of interest. When the user taps one of these pins I'd like to know which point of interest they tapped, and I have a model object which describes these points of interest. I use this model object to give the pin image it's coordinates on the map so I thought it would have been easy for me to just send the object to the gesture handler.
It looks like you're misunderstanding a couple of things.
When using target/action, the function signature has to have a certain form…
func doSomething()
or
func doSomething(sender: Any)
or
func doSomething(sender: Any, forEvent event: UIEvent)
where…
The sender parameter is the control object sending the action message.
In your case, the sender is the UITapGestureRecognizer
Also, #selector() should contain the func signature, and does NOT include passed parameters. So for…
func handleTap(sender: UIGestureRecognizer) {
}
you should have…
let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
Assuming the func and the gesture are within a view controller, of which modelObj is a property / ivar, there's no need to pass it with the gesture recogniser, you can just refer to it in handleTap
Step 1: create the custom object of the sender.
step 2: add properties you want to change in that a custom object of the sender
step 3: typecast the sender in receiving function to a custom object and access those properties
For eg:
on click of the button if you want to send the string or any custom object then
step 1: create
class CustomButton : UIButton {
var name : String = ""
var customObject : Any? = nil
var customObject2 : Any? = nil
convenience init(name: String, object: Any) {
self.init()
self.name = name
self.customObject = object
}
}
step 2-a: set the custom class in the storyboard as well
step 2-b: Create IBOutlet of that button with a custom class as follows
#IBOutlet weak var btnFullRemote: CustomButton!
step 3: add properties you want to change in that a custom object of the sender
btnFullRemote.name = "Nik"
btnFullRemote.customObject = customObject
btnFullRemote.customObject2 = customObject2
btnFullRemote.addTarget(self, action: #selector(self.btnFullRemote(_:)), for: .touchUpInside)
step 4: typecast the sender in receiving function to a custom object and access those properties
#objc public func btnFullRemote(_ sender: Any) {
var name : String = (sender as! CustomButton).name as? String
var customObject : customObject = (sender as! CustomButton).customObject as? customObject
var customObject2 : customObject2 = (sender as! CustomButton).customObject2 as? customObject2
}
Swift 5.0 iOS 13
I concur a great answer by Ninad. Here is my 2 cents, the same and yet different technique; a minimal version.
Create a custom class, throw a enum to keep/make the code as maintainable as possible.
enum Vs: String {
case pulse = "pulse"
case precision = "precision"
}
class customTap: UITapGestureRecognizer {
var cutomTag: String?
}
Use it, making sure you set the custom variable into the bargin. Using a simple label here, note the last line, important labels are not normally interactive.
let precisionTap = customTap(target: self, action: #selector(VC.actionB(sender:)))
precisionTap.customTag = Vs.precision.rawValue
precisionLabel.addGestureRecognizer(precisionTap)
precisionLabel.isUserInteractionEnabled = true
And setup the action using it, note I wanted to use the pure enum, but it isn't supported by Objective C, so we go with a basic type, String in this case.
#objc func actionB(sender: Any) {
// important to cast your sender to your cuatom class so you can extract your special setting.
let tag = customTag as? customTap
switch tag?.sender {
case Vs.pulse.rawValue:
// code
case Vs.precision.rawValue:
// code
default:
break
}
}
And there you have it.
cell.btn.tag = indexPath.row //setting tag
cell.btn.addTarget(self, action: #selector(showAlert(_ :)), for: .touchUpInside)
#objc func showAlert(_ sender: UIButton){
print("sender.tag is : \(sender.tag)")// getting tag's value
}
Just create a custom class of UITapGestureRecognizer =>
import UIKit
class OtherUserProfileTapGestureRecognizer: UITapGestureRecognizer {
let userModel: OtherUserModel
init(target: AnyObject, action: Selector, userModel: OtherUserModel) {
self.userModel = userModel
super.init(target: target, action: action)
}
}
And then create UIImageView extension =>
import UIKit
extension UIImageView {
func gotoOtherUserProfile(otherUserModel: OtherUserModel) {
isUserInteractionEnabled = true
let gestureRecognizer = OtherUserProfileTapGestureRecognizer(target: self, action: #selector(self.didTapOtherUserImage(_:)), otherUserModel: otherUserModel)
addGestureRecognizer(gestureRecognizer)
}
#objc internal func didTapOtherUserImage(_ recognizer: OtherUserProfileTapGestureRecognizer) {
Router.shared.gotoOtherUserProfile(otherUserModel: recognizer.otherUserModel)
}
}
Now use it like =>
self.userImageView.gotoOtherUserProfile(otherUserModel: OtherUserModel)
You can use an UIAction instead:
self.imageView.addAction(UIAction(identifier: UIAction.Identifier("imageClick")) { [weak self] action in
self?.handleTap(modelObj)
}, for: .touchUpInside)
that may be a terrible practice but I simply add whatever I want to restore to
button.restorationIdentifier = urlString
and
#objc func openRelatedFact(_ sender: Any) {
if let button = sender as? UIButton, let stringURL = factButton.restorationIdentifier, let url = URL(string: stringURL) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
}
}
}
I made a calculator app (swift), and I set up some UIKeyCommands so that if the user has a bluetooth keyboard, they can type numbers/symbols into the calculator.
They work like so:
UIKeyCommand(input: "4", modifierFlags: [], action: "TypeFourInCalculator:")
func TypeFourInCalculator(sender: UIKeyCommand) {
btn4Press(UIButton)
}
That all worked well, adding a four into the calculator when the user pressed the four key (even though the calculator itself has no text field). However: I also have a couple of standard text fields, and I want the UIKeyCommands to stop when the user goes into one of those text fields (so they can type regularly with the BT keyboard again). Without disabling the UIKeyCommands, typing results in calculator functions and no input into the text field.
So I tried this in an attempt to disable UIKeyCommands when the text field becomes the first responder:
let globalKeyCommands = [UIKeyCommand(input: "4", modifierFlags: [], action: "TypeFourInCalculator:"), UIKeyCommand(input: "5", modifierFlags: [], action: "TypeFiveInCalculator:")]
override var keyCommands: [UIKeyCommand]? {
if powertextfield.isFirstResponder() == false { // if the user isn't typing into that text field
return globalKeyCommands // use key commands
} else { // if the user is typing
return nil // disable those UIKeyCommands
}
This works occasionally but yet often doesn't work. If the user has not typed anything with the BT keyboard yet (i.e. not activating the key commands, I guess) then they can type with the BT keyboard as normal into a text field. But if they have already been typing numbers into the calculator via UIKeyCommand, they can not type into the text field (well, sometimes it works with normal behavior, sometimes it fails like it did before I added that preventative code). Typed text just doesn't appear in that text field and, instead, it just calls the calculator command.
So what can I do to disable these UIKeyCommands when the user starts typing
in a normal text field?
Instead of making keyCommands a computed property, you can use addKeyCommand(_:) and removeKeyCommand(_:) methods for UIViewControllers. Subclass your text field like this:
class PowerTextField: UITextField {
var enableKeyCommands: (Bool->())?
override func becomeFirstResponder() -> Bool {
super.becomeFirstResponder()
enableKeyCommands?(false)
return true
}
override func resignFirstResponder() -> Bool {
super.resignFirstResponder()
enableKeyCommands?(true)
return true
}
}
Then in your UIViewController:
override func viewDidLoad() {
super.viewDidLoad()
// globalKeyCommands is a view controller property
// Add key commands to be on by default
for command in globalKeyCommands {
self.addKeyCommand(command)
}
// Configure text field to callback when
// it should enable key commands
powerTextField.enableKeyCommands = { [weak self] enabled in
guard let commands = self?.globalKeyCommands else {
return
}
if enabled {
for command in globalKeyCommands {
self?.addKeyCommand(command)
}
} else {
for command in globalKeyCommands {
self?.removeKeyCommand(command)
}
}
}
}
Instead of an optional stored procedure that gets configured by the UIViewController, you could setup a delegate protocol for the UITextField that the UIViewController will adopt.
Also, if you need to stop these key commands when a UIAlertController pops up, you can make a subclass of UIAlertController that implements the viewWillAppear(_:) and viewWillDisappear(_:) event methods similar to how you implemented becomeFirstResponder() and resignFirstResponder(), by calling an enableKeyCommands(_:) optional stored procedure that's configured by your main view controller during its viewDidLoad().
As for the explanation of why this is happening, perhaps the most likely explanation is that it's a bug. I'm not sure why this is irregular in your testing though. I think you could try to isolate under what conditions it works or doesn't. It's not obvious why this would only happen for bluetooth keyboards, but there are plenty of edge cases that can creep in when you start introducing wireless technologies.
So I was facing the same problem and found a really good and simple solution.
I made a way to figure out, if the current firstResponder is a text field, or similar.
extension UIResponder {
private weak static var _currentFirstResponder: UIResponder? = nil
public static var isFirstResponderTextField: Bool {
var isTextField = false
if let firstResponder = UIResponder.currentFirstResponder {
isTextField = firstResponder.isKind(of: UITextField.self) || firstResponder.isKind(of: UITextView.self) || firstResponder.isKind(of: UISearchBar.self)
}
return isTextField
}
public static var currentFirstResponder: UIResponder? {
UIResponder._currentFirstResponder = nil
UIApplication.shared.sendAction(#selector(findFirstResponder(sender:)), to: nil, from: nil, for: nil)
return UIResponder._currentFirstResponder
}
#objc internal func findFirstResponder(sender: AnyObject) {
UIResponder._currentFirstResponder = self
}
}
And then simply used this boolean value to determine which UIKeyCommands to return when overriding the keyCommands var:
override var keyCommands: [UIKeyCommand]? {
var commands = [UIKeyCommand]()
if !UIResponder.isFirstResponderTextField {
commands.append(UIKeyCommand(title: "Some title", image: nil, action: #selector(someAction), input: "1", modifierFlags: [], propertyList: nil, alternates: [], discoverabilityTitle: "Some title", attributes: [], state: .on))
}
commands.append(UIKeyCommand(title: "Some other title", image: nil, action: #selector(someOtherAction), input: "2", modifierFlags: [.command], propertyList: nil, alternates: [], discoverabilityTitle: "Some title", attributes: [], state: .on))
return commands
}
I have the following:
Two interesting classes: a ViewController and a ViewModel
A button nsButtonMorePlease:NSButton in the view of ViewController
A text box nsTextView:NSTextView in the view as well
I want the following behavior:
When you launch the program, the "count" starts at 0 and is displayed in the text box nsTextView
When you press the button nsButtonMorePlease, the count is incremented by 1 and the updated count is reflected in the nsTextView
I would like to ensure:
I use ReactiveCocoa 4 (that's the point kind of)
The model class contains numberOfBeans: MutableProperty<Int> starting at 0
The design is purely functional or close to it - that is (if I understand the term), every link in the chain mapping the event of mouse click to the MutableProperty of numberOfBeans to responding to it in the text view, is all without side effects.
Here's what I have. Fair warning: this doesn't come close to working or compiling I believe. But I do feel like maybe I want to use one of combineLatest, collect, reduce, etc. Just lost on what to do specifically. I do feel like this makes something easy quite hard.
class CandyViewModel {
private let racPropertyBeansCount: MutableProperty<Int> = MutableProperty<Int>(0)
lazy var racActionIncrementBeansCount: Action<AnyObject?, Int, NoError> = {
return Action { _ in SignalProducer<Int, NoError>(value: 1)
}
}()
var racCocoaIncrementBeansAction: CocoaAction
init() {
racCocoaIncrementBeansAction = CocoaAction.init(racActionIncrementBeansCount, input: "")
// ???
var sig = racPropertyBeansCount.producer.combineLatestWith(racActionIncrementBeansCount.)
}
}
class CandyView: NSViewController {
#IBOutlet private var guiButtonMoreCandy: NSButton!
#IBOutlet private var guiTextViewCandyCt: NSTextView!
}
class CandyViewModel {
let racPropertyBeansCount = MutableProperty<Int>(0)
let racActionIncrementBeansCount = Action<(), Int, NoError>{ _ in SignalProducer(value: 1) }
init() {
// reduce the value from the action to the mutableproperty
racPropertyBeansCount <~ racActionIncrementBeansCount.values.reduce(racPropertyBeansCount.value) { $0 + $1 }
}
}
class CandyView: NSViewController {
// define outlets
let viewModel = CandyViewModel()
func bindObservers() {
// bind the Action to the button
guiButtonMoreCandy.addTarget(viewModel.racActionIncrementBeansCount.unsafeCocoaAction, action: CocoaAction.selector, forControlEvents: .TouchUpInside)
// observe the producer of the mutableproperty
viewModel.racPropertyBeansCount.producer.startWithNext {
self.guiTextViewCandyCt.text = "\($0)"
}
}
}
I'm having trouble using ReactiveCocoa in version 3. I want to build some view model for my login view controller. In my view controller I have outlet for password text field:
#IBOutlet weak var passwordTextField: UITextField!
In view model I have property for the text that is the password
public let emailText = MutableProperty<String>("")
and the question is how to bind it together? I'm able to get SignalProducer from text field:
emailTextField.rac_textSignal().toSignalProducer()
but how to bind it to emailText property? I've read in documentation that SignalProducer is not a Signal, but it can create now. There is method start() but it takes Sink as parameter and I'm a bit confused with design at this moment. Shouldn't emailText be a Sink?
Note: this is not properly an answer to your question, but I think it might help you.
If you are just want to bind your view to your view model, I suggest you to read this post which provides a one-class solution to the problem.
From there, you can very simply implement a 2-way binding so your viewmodel get updated every time the view changes and vice-versa. Here is my extension:
class TwoWayDynamic<T> {
typealias Listener = T -> Void
private var viewListener: Listener?
private var controllerListener: Listener?
private(set) var value: T
func setValueFromController(value: T) {
self.value = value
viewListener?(value)
}
func setValueFromView(value: T) {
self.value = value
controllerListener?(value)
}
func setValue(value: T) {
self.value = value
controllerListener?(value)
viewListener?(value)
}
init(_ v: T) {
value = v
}
func bindView(listener: Listener?) {
self.viewListener = listener
}
func bindController(listener: Listener?) {
self.controllerListener = listener
}
func bindViewAndFire(listener: Listener?) {
self.viewListener = listener
listener?(value)
}
func bindControllerAndFire(listener: Listener?) {
self.controllerListener = listener
listener?(value)
}
}
Hope it helps!