Bind uitextfield value to viewModel using mvvm - swift

I'm trying to bind a UITextField to a viewModel, however whatever i do i keep getting Cannot invoke 'bind' with an argument list of type '(to: EmailViewModel). What am i doing wrong?
SignUpViewModel
class SignUpViewModel {
let model: SignUpModel
private let disposeBag = DisposeBag()
let emailFieldViewModel = EmailViewModel()
init(model :SignUpModel) {
self.model = model
}
}
EmailViewModel
struct EmailViewModel : FieldViewModel {
var value: Variable<String> = Variable("")
var errorValue: Variable<String?> = Variable(nil)
let title = "Email"
let errorMessage = "Email is wrong"
func validate() -> Bool {
let emailPattern = "[A-Z0-9a-z._%+-]+#([A-Za-z0-9.-]{2,64})+\\.[A-Za-z]{2,64}"
guard validateString(value.value, pattern:emailPattern) else {
errorValue.value = errorMessage
return false
}
errorValue.value = nil
return true
}
}
viewcontroller
class SignUpViewController: UIViewController {
#IBOutlet var emailField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
configureBinding()
}
private func configureBinding() {
// binding
self.emailField.rx.text.bind(to: viewModel.emailFieldViewModel)
}
}

The bind function expects an object that conforms to the ObserverType protocol. Here, EmailViewModel does not conform to that type, hence the error.
Writing an extension to make EmailViewModel conform to the ObserverType protocol would solve the compilation error.
extension EmailViewModel: ObserverType {
func on(_ event: Event<String?>) {
switch event {
case .next(let newValue): value.value = newValue ?? ""
case .error(_), .completed: fatalError("Completion and error are not handled")
}
}
}

Related

Boolean returns nil and unable to access value from response on view controller

I have a usermodel that checks the backend if the email exists - then I drill back into a viewcontroller and set a boolean value that should trigger a function run. However the value is unchanged and I am trying to change this value from the usermodel but it is not accessible. I understand why it does not work.. but do not know how to resolve the issue.
static func sendEmailWithResetLink(email: String) {
let params : Parameters = [
PARAM_EMAIL : email
]
request(URL_RESET_PASSWORD as String, method: .post, parameters: params, headers: nil).responseJSON {
(response: DataResponse<Any>) in
hideProgress()
print("this is response \(response)")
switch(response.result)
{
case .success(_):
print("it did not fail")
let passwordResetVC = PasswordResetViewController()
passwordResetVC.hasFailed = false
break
case .failure(_):
print("it failed")
let passwordResetVC = PasswordResetViewController()
//here boolean is set that I am trying to access in viewcontroller
passwordResetVC.hasFailed = true
break
}
}
}
Here's what I would suggest. You probably have some of these in place already:
Create an PasswordResetViewController object has an #IBAction func resetButtonClicked triggered by a button or whatever, which kicks off the password reset process.
Create a UserManager class. This class is responsible for all profile management activies in your app. Among other things, it has the ability to reset user passwords. This UserManager would probably be a singleton, that' sprobably good enough for now.
Create a new UserManagerDelegate protocol. Add to it all capabilities that are required by the UserManager to inform them of whatever happened. For example: var passwordResetHasFailed: Bool { get set }.
Extend your PasswordResetViewController conform to this protocol.
Your VC gets a reference to the singleton UserManager object, stores it in an instance variable, and uses that to access the shared object from then on.
Make your PasswordResetViewController register itself as the delegate to the user manager, with userManager.delegate = self
The #IBAction func resetButtonClicked will just call userManager.resetPassword()
Your UserManager does whatever it needs to do to reset the user's password.
When it's done, it'll call self.delegate?.passwordResetHasFailed = true/false.
Since your PasswordResetViewController registered itself as the delegate of the UserManager, when the operation is done, its passwordResetHasFailed property will be changed, giving it a chance to respond (by updating some UI or whatever).
There are some limitations to this approach, but it's a decent way to get started. Some thing to note:
This lets you unit test your PasswordResetViewController. You can create a MockUserManager, and set tesPasswordResetViewController.userManager = MockUserManager(), allowing you to separate out the user manager, and test PasswordResetViewController in isolation.
You'll run into issues if you need multiple objects to subscribe to receive delegate call backs (since there can only be 1 delegate object). At that point, you can switch to using something like Promises, RxSwift or Combine. But that's a problem for a later time, and the migration would be easy.
Going off of #Alexander - Reinstate Monica and what I assume what the code to look like to approach your problem.
Using MVC:
In Models folder (data/ logic part)
public class User {
private var name: String!
private var userEmail: String!
public var hasFailed: Bool?
init() {
name = ""
userEmail = ""
hasFailed = nil
}
public func setName(name: String) { self.name = name }
public func getName() -> String { return name }
public func setEmail(email: String) { userEmail = email }
public func getEmail() ->String { return userEmail }
public static func sendEmailWithRestLing(email: String) {
// your other code
switch response.result {
case .success(_):
//your code
hasFailed = false
break
case .failuare(_):
// your code
hasFailed = true
break
}
}
}
User Manager class applying singleton design
final class UserManager {
private var user = User()
static let instance = UserManager()
private init(){}
public func userName(name: String) {
if (name.count > 3) {
user.setName(name: name)
}
else { print("user name is too short") }
}
public func userEmail(email: String) {
if (email.count > 3) {
user.setEmail(email: email)
}
else { print("user email is too short") }
}
public func getUserName() -> String {
let name = user.getName()
if (name.isEmpty) { return "user name is Empty" }
return name
}
public func getUserEmail() -> String {
let email = user.getEmail()
if (email.isEmpty) { return "user email is Empty" }
return email
}
public func doKatieTask(link: String) -> Int {
guard let myValue = user.hasFailed else {
return -1
}
if (myValue) { return 1}
return 0
}
}
So, Now in the Controllers folder and since we a one-to-one relation we will use delegate design pattern. If had had one-to-many with the view controller. Use observers.
class ViewController: UIViewController {
#IBOutlet weak var nameTextField: UITextField!
#IBOutlet weak var emailTextField: UITextField!
var _hasFail: Bool!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func doTask() {
UserManager.instance.userName(name: nameTextField.text!)
UserManager.instance.userEmail(email: emailTextField.text!)
switch UserManager.instance.doKatieTask(link: emailTextField.text!) {
case 0:
_hasFail = false
break
case 1:
_hasFail = true
break
default:
print("hasFailed is nil")
break
}
if let vc = storyboard?.instantiateViewController(identifier: "passwordVC") as? PasswordResetViewController {
vc.modalPresentationStyle = .fullScreen
vc.delegate = self
self.present(vc, animated: true, completion: nil)
}
}
}
extension ViewController: KatieDelegate {
var hasFailed: Bool {
get {
return _hasFail
}
set {
_hasFail = newValue
}
}
}
In PasswordReset UIViewController
protocol KatieDelegate {
var hasFailed: Bool { get set }
}
class PasswordResetViewController: UIViewController {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var emailLabel: UILabel!
var delegate: KatieDelegate?
override func viewDidLoad() {
super.viewDidLoad()
nameLabel.text = UserManger.instance.getUserName()
emailLabel.text = UserManger.instance.getUserEmail()
if let delegate = delegate {
print("The value for has failed is: .....\(delegate.hasFailed)!")
}
else { print("error with delegate") }
}
}

Swift MVVM Bind with Boxing

I am simply trying to create a weather application with WeatherViewController displaying the tableView with cells, and when the cell is tapped leads to WeatherDetailsViewController.
I am using the boxing way for binding and I am confused if I set Dynamic type in both the model and viewModel in the example below. You will know what I mean.
This is the Boxing Class
class Dynamic<T>: Decodable where T: Decodable {
typealias Listener = (T) -> ()
var listener: Listener?
var value: T {
didSet {
listener?(value)
}
}
func bind(listener: #escaping Listener) {
self.listener = listener
self.listener?(self.value)
}
init(_ value: T) {
self.value = value
}
private enum CodingKeys: CodingKey {
case value
}
}
This is the Weather Model Struct
struct Weather: Decodable {
let date: Dynamic<Int>
let description: Dynamic<String>
let maxTemperature: Dynamic<Double>
private enum CodingKeys: String, CodingKey {
case date = "time"
case description = "summary"
case maxTemperature = "temperatureMax"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
date = try Dynamic(container.decode(Int.self, forKey: .date))
description = try Dynamic(container.decode(String.self, forKey: .description))
maxTemperature = try Dynamic(container.decode(Double.self, forKey: .maxTemperature))
}
}
Here is my WeatherListViewModel & WeatherViewModel
Inside my WeatherViewModel I have assigned the type to be Dynamic but also in the model in order to bind in my WeatherDetailsViewController, is that right?
class WeatherListViewModel {
var weatherViewModels: [WeatherViewModel]
private var sessionProvider: URLSessionProvider
init(sessionProvider: URLSessionProvider) {
self.sessionProvider = sessionProvider
self.weatherViewModels = [WeatherViewModel]()
}
func numberOfRows(inSection section: Int) -> Int {
return weatherViewModels.count
}
func modelAt(_ index: Int) -> WeatherViewModel {
return weatherViewModels[index]
}
func didSelect(at indexPath: Int) -> WeatherViewModel {
return weatherViewModels[indexPath]
}
}
This is WeatherListViewModel Extension for network fetching where I initialize the WeatherViewModel
func fetchWeatherLocation(withLatitude latitude: CLLocationDegrees, longitude: CLLocationDegrees, completion: #escaping handler) {
sessionProvider.request(type: WeatherWrapper.self, service: WeatherService.specificLocation, latitude: latitude, longitude: longitude) { [weak self] result in
switch result {
case let .success(weatherWrapper):
let weathers = weatherWrapper.daily.weathers
self?.weatherViewModels = weathers.map {
return WeatherViewModel(weather: $0)
}
completion()
case let .failure(error):
print("Error: \(error)")
}
}
}
This is WeatherViewModel
struct WeatherViewModel {
private(set) var weather: Weather
var temperature: Dynamic<Double>
var date: Dynamic<Int>
var description: Dynamic<String>
init(weather: Weather) {
self.weather = weather
self.temperature = Dynamic(weather.maxTemperature)
self.date = Dynamic(weather.date)
self.description = Dynamic(weather.description)
}
}
Here is my WeatherDetailsViewController
Here I assign the binding to the labels respectively to get the changes
class WeatherDetailsViewController: UIViewController {
#IBOutlet private var imageView: UIImageView!
#IBOutlet private var cityLabel: UILabel!
#IBOutlet private var dateLabel: UILabel!
#IBOutlet private var descriptionLabel: UILabel!
#IBOutlet private var temperatureLabel: UILabel!
var viewModel: WeatherViewModel?
override func viewDidLoad() {
super.viewDidLoad()
setupVMBinding()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationItem.largeTitleDisplayMode = .never
}
private func setupVMBinding() {
if let viewModel = viewModel {
viewModel.date.bind {
self.dateLabel.text = $0.toString()
}
viewModel.temperature.bind {
self.temperatureLabel.text = "\($0)"
}
viewModel.description.bind {
self.descriptionLabel.text = $0.description
}
}
}
}
Question is, did I just repeat writing the type Dynamic in both model and viewModel? Is there a better way of doing this or am I on the right track. Sorry for the long code example.
I think you repeat writing Dynamic inside your Weather Model.
It does not need to be Dynamic type.
You can create a GenericDataSource
class GenericDataSource<T>: NSObject {
var data: Dynamic<T>?
}
Inside your View Model. This will Reference to your Weather Model without the need for creating dynamic type.
class WeatherViewModel {
var dataSource: GenericDataSource<Weather>?
....
}
Inside your View Controller
class WeatherDetailsViewController {
var viewModel: WeatherViewModel?
override func viewDidLoad() {
viewModel = ViewModel()
var dataSource = GenericDataSource<Weather>()
dataSource.data = Dynamic(Weather)
viewModel.dataSource = dataSource
setupVMBinding()
}
private func setupVMBinding() {
viewModel?.dataSource?.data?.bind {
self.dateLabel.text = $0.date
self.temperatureLabel.text = "\($0.maxTemperature)"
self.descriptionLabel.text = $0.description
}
}
}

How to use one variable/property instead assigning same property three times?

Let's assume that myVar has same functionality in every implementation of view. I am trying to figure out how to declare/expose some kind of set-only property instead of assigning them n-times (with every new view created), but nothing comes to my head. How could I refactor into one line & one time assignment?
var myVar: (()-> Void)?
private func callBack() {
someClass.view1.myVar = self.myVar
someClass.view2.myVar = self.myVar
someClass.view3.myVar = self.myVar
}
// MARK: - someClass pseudocode
someClass: class {
let view1: CustomView: = CustomView
let view2: CustomView: = CustomView
let view3: CustomView: = CustomView
}
// MARK: - customView pseudocode
class CustomView: UIView {
var myVar: (()-> Void)?
}
something like this, but having all CustomViews in an array is good idea and could be implemented here as well
var a: (() -> Void)?
class CustomView: UIView {
var myVar: (() -> Void)?
}
class SomeClass {
let view1 = CustomView()
let view2 = CustomView()
let view3 = CustomView()
var myVar: (() -> Void)? {
set {
self.view2.myVar = newValue
self.view1.myVar = newValue
self.view3.myVar = newValue
}
get {
return self.myVar
}
}
}
let b = SomeClass()
b.myVar = ({print(3)})
b.view1.myVar!()
Is this what you are trying to do?
[someClass.view1, someClass.view2, someClass.view3].forEach { $0.myVar = self.myVar }
This is how I tend to deal with these issues:
class OtherClass {
var text: String?
init(text: String?) {
self.text = text;
}
}
class TestClass {
var thing1: OtherClass?
var thing2: OtherClass?
var thing3: OtherClass?
var allTheThings: [OtherClass?] { return [thing1, thing2, thing3]}
var ownText: String? {
didSet {
allTheThings.forEach { $0?.text = ownText }
}
}
}
Depending on how much you expect things to change you could make the array property a constant you set in your init rather than a computed property.
If you want to get fancy you could also do something like this for setting:
private var allTheThings: [OtherClass?] {
get {
return [thing1, thing2, thing3]
}
set {
guard newValue.count == 3 else {
//probably should put an assertion in here
return
}
thing1 = newValue[0]
thing2 = newValue[1]
thing3 = newValue[2]
}
}
init() {
self.allTheThings = Array(repeating: OtherClass(text: "Test"), count: 3)
}

How would I write a test to make sure the UIbutton "Show all Providers" turns up when there's more than 12 or more items in the table view?

So I'm completely new to testing and I just needed some help figuring out for example how I would write a test for each of the three cases in the enum of the View Model (none, dontSeeProvider, showAllProviders).
enum ProvidersButtonType {
case none, dontSeeProvider, showAllProviders
}
I haven't been able to figure out how to write a test for cases "showAllProviders" and "dontSeeProviders".
This is the View Model:
import RxSwift
import RxCocoa
struct TopProvidersPickerItem {
let provider: MVPD
let logoImage: Observable<UIImage>
init(provider: MVPD, imageLoader: DecodableProviding) {
self.init(provider: provider, logoImage: imageLoader.image(fromURL: provider.logoUrl))
}
init(provider: MVPD, logoImage: Observable<UIImage>) {
self.provider = provider
self.logoImage = logoImage.catchErrorJustReturn(UIImage())
}
}
enum ProvidersButtonType {
case none, dontSeeProvider, showAllProviders
}
struct TopProvidersPickerViewModel {
var caption: String {
return "Get access to more full episodes by signing in with your TV Provider"
}
let buttonType = Variable<ProvidersButtonType>(.none)
let items: Observable<[TopProvidersPickerItem]>
let selectedItem: PublishSubject<TopProvidersPickerItem> = PublishSubject()
let showAllProvidersTrigger: PublishSubject<Void> = PublishSubject()
let mvpdPicked: Observable<MVPD>
init(topProviders: Observable<[MVPD]>, imageLoader: DecodableProviding) {
let items = topProviders.map({ mvpds in
return mvpds.map { mvpd in
TopProvidersPickerItem(provider: mvpd, imageLoader: imageLoader)
}
})
self.init(items: items)
}
init(items: Observable<[TopProvidersPickerItem]>) {
self.items = items
mvpdPicked = selectedItem.map { $0.provider }
let buttonType = items.map { (array) -> ProvidersButtonType in
if array.count > 12 {
return .showAllProviders
} else {
return .dontSeeProvider
}
}
buttonType.bind(to: self.buttonType)
}
}
This is the View Controller:
import UIKit
import RxCocoa
import RxSwift
public class ProviderCollectionViewCell: UICollectionViewCell {
#IBOutlet public private(set) weak var imageView: UIImageView!
}
public class TopProvidersPickerViewController: UIViewController,
ViewModelHolder {
var viewModel: TopProvidersPickerViewModel! = nil
private let bag = DisposeBag()
#IBOutlet public private(set) weak var collectionView: UICollectionView!
#IBOutlet public private(set) weak var captionLabel: UILabel!
#IBOutlet weak var viewAllProvidersButton: UIButton!
override public func viewDidLoad() {
super.viewDidLoad()
captionLabel.text = viewModel.caption
setupRx()
}
private func setupRx() {
viewModel.buttonType.asObservable().subscribe(onNext: { [button = self.viewAllProvidersButton] type in
button?.isHidden = false
switch type {
case .none:
button?.isHidden = true
case .dontSeeProvider:
button?.setTitle("Don't see provider", for: .normal)
case .showAllProviders:
button?.setTitle("Show all providers", for: .normal)
}
})
.disposed(by: bag)
viewModel.items
.bind(to: collectionView
.rx
.items(cellIdentifier: "ProviderCell", cellType: ProviderCollectionViewCell.self)) { [ unowned self ] _, item, cell in
item.logoImage.bind(to: cell.imageView.rx.image).addDisposableTo(self.bag)
}
.addDisposableTo(bag)
collectionView
.rx
.modelSelected(TopProvidersPickerItem.self)
.bind(to: self.viewModel.selectedItem)
.addDisposableTo(bag)
viewAllProvidersButton
.rx
.tap
.bind(to: self.viewModel.showAllProvidersTrigger)
.addDisposableTo(bag)
}
}
I wrote a test for the "none" case, but haven't been able to figure out the other two cases:
import FBSnapshotTestCase
import OHHTTPStubs
import RxSwift
#testable import AuthSuite
class TopProvidersPickerViewControllerTests: FBSnapshotTestCase,
ProvidersViewControllerTests {
override func setUp() {
super.setUp()
recordMode = true
}
func testDoesNotShowButtonWhenLoadingProviders() {
let viewModel = TopProvidersPickerViewModel(items: .never())
let controller = TopProvidersPickerViewController.instantiateViewController(with: viewModel)
presentViewController(controller)
FBSnapshotVerifyView(controller.view)
}
I've never used FB Snapshot Tester. I'm going to have to look into that.
Here's how I would do it:
I wouldn't expose the enum to the ViewController. setupRx() would contain this instead:
private func setupRx() {
viewModel.buttonTitle
.bind(to: viewAllProvidersButton.rx.title(for: .normal))
.disposed(by: bag)
viewModel.buttonHidden
.bind(to: viewAllProvidersButton.rx.isHidden)
.disposed(by: bag)
// everything else
}
Then to test the title of the button, for example, I would use these tests:
import XCTest
import RxSwift
#testable import RxPlayground
class TopProvidersPickerViewModelTests: XCTestCase {
func testButtonTitleEmptyItems() {
let topProviders = Observable<[MVPD]>.just([])
let decodableProviding = MockDecodableProviding()
let viewModel = TopProvidersPickerViewModel(topProviders: topProviders, imageLoader: decodableProviding)
var title: String = ""
_ = viewModel.buttonTitle.subscribe(onNext: { title = $0 })
XCTAssertEqual(title, "Don't see provider")
}
func testButtonTitle12Items() {
let topProviders = Observable<[MVPD]>.just(Array(repeating: MVPD(), count: 12))
let decodableProviding = MockDecodableProviding()
let viewModel = TopProvidersPickerViewModel(topProviders: topProviders, imageLoader: decodableProviding)
var title: String = ""
_ = viewModel.buttonTitle.subscribe(onNext: { title = $0 })
XCTAssertEqual(title, "Don't see provider")
}
func testButtonTitle13Items() {
let topProviders = Observable<[MVPD]>.just(Array(repeating: MVPD(), count: 13))
let decodableProviding = MockDecodableProviding()
let viewModel = TopProvidersPickerViewModel(topProviders: topProviders, imageLoader: decodableProviding)
var title: String = ""
_ = viewModel.buttonTitle.subscribe(onNext: { title = $0 })
XCTAssertEqual(title, "Show all providers")
}
}
class MockDecodableProviding: DecodableProviding {
// nothing needed for these tests.
}

What does 'Type not allowed here' mean?

I'm using Swift 2.3 and trying to create a callback function that will cause my UIViewController to update itself with the updated view model but I'm getting a compile error in my view model class - 'Type not allowed here'. I'm getting other errors as well but they all seem to be cause by a fundamental problem with my CalculatorViewModel class. It's worth noting that I'm following an example of MVVM used in this great post about iOS architecture patterns and trying to adapt it to my app.
Here's my view controller:
class CalculatorViewController: UIViewController, UITextFieldDelegate, DismissalDelegate {
var viewModel: CalculatorViewModelProtocol! {
didSet {
self.viewModel.oneRepMaxDidChange = { [unowned self] viewModel in
self.oneRepMaxField.text = String(viewModel.oneRepMax!)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = CalculatorViewModel() // use of unresolved identifier 'CalculatarViewModel'
self.viewModel = viewModel
liftNameButton.setTitle(viewModel.liftName, forState: .Normal)
weightLiftedField.text = String(viewModel.weightLifted)
repetitionsField.text = String(viewModel.repetitions)
units.text = viewModel.units
oneRepMaxField.text = String(viewModel.oneRepMax!)
// a bunch of formatting code and then I add a target to a button the user will press:
calculateButton.addTarget(self, action: #selector(onCalculateTapped), forControlEvents: UIControlEvents.TouchUpInside)
func onCalculateButtonTapped() {
if let weightLifted = weightLiftedField.text, let repetitions = repetitionsField.text {
// error: Argument passed to call that takes no arguments (except that it does)
viewModel!.calculateOneRepMax(weightLifted, repetitions: repetitions)
//weightPercentages = getPercentages(pureOneRepMax!)
} else {
return
}
and here's my view model and a view model protocol where the 'Type not allowed error' appears:
protocol CalculatorViewModelProtocol: class {
var liftName: String? { get }
var weightLifted: Double? { get }
var repetitions: Int? { get }
var oneRepMax: String? { get set }
var oneRepMaxDidChange: ((CalculatorViewModelProtocol) -> ())? { get set }
var units: String? { get }
var date: String? { get }
func calculateOneRepMax()
**// the 'Type not allowed here' error is here**
class CalculatorViewViewModel: CalculatorViewModelProtocol, LiftEventDataManagerDelegate {
let calculator = CalculatorBrain()
private let dataManager = LiftEventDataManager()
var liftName: String?
var weightLifted: String!
var repetitions: String!
var oneRepMax: String? {
didSet {
self.oneRepMaxDidChange?(self)
}
}
var units: String?
var date: String?
var oneRepMaxDidChange: ((CalculatorViewModelProtocol) -> ())?
#objc func calculateOneRepMax(weightLifted: String, repetitions: String) {
let result = calculator.calculateOneRepMax(Double(weightLifted)!, repetitions: UInt8(repetitions)!)
}
init() {
dataManager.requestData(withViewModel: self)
}
}
I've done a lot of searching but haven't found any answers that help.
You can not implement a class inside a protocol. Move your CalculatorViewModel to a separate file or at least outside the scope of the protocol