How to test the UI Binding between RxSwift Variable and RxCocoa Observable? - ios

I have a simple ViewModel with one property:
class ViewModel {
var name = Variable<String>("")
}
And I'm binding it to its UITextField in my ViewController:
class ViewController : UIViewController {
var viewModel : ViewModel!
#IBOutlet weak var nameField : UITextField!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Binding
nameField.rx.text
.orEmpty
.bind(to: viewModel.name)
.disposed(by: disposeBag)
}
}
Now, I want to Unit Test it.
I'm able to fill the nameField.text property though Rx using .onNext event - nameField.rx.text.onNext("My Name Here").
But the viewModel.name isn't being filled. Why? How can I make this Rx behavior work in my XCTestCase?
Please see below the result of my last test run:

I believe the issue you are having is that the binding is not explicitly scheduled on the main thread. I recommend using a Driver in this case:
class ViewModel {
var name = Variable<String>("")
}
class ViewController: UIViewController {
let textField = UITextField()
let disposeBag = DisposeBag()
let vm = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
textField.rx.text
.orEmpty
.asDriver()
.drive(vm.name)
.disposed(by: disposeBag)
}
// ..
With a Driver, the binding will always happen on the main thread and it will not error out.
Also, in your test I would call skip on the binding:
sut.nameTextField.rx.text.skip(1).onNext(name)
because the Driver will replay the first element when it subscribes.
Finally, I suggest using RxTest and a TestScheduler instead of GCD.
Let me know if this helps.

rx.text relies on the following UIControlEvents: .allEditingEvents and .valueChanged. Thus, explicitly send a onNext events will not send action for these event. You should send action manually.
sut.nameField.rx.text.onNext(name)
sut.nameField.sendActions(for: .valueChanged)

Related

Using Arrays in Singleton Class in iOS

PetInfo.class
class PetInfo {
static let shared: PetInfo = PetInfo()
lazy var petArray: [PetInfo] = []
var PetID:Int
var PetName:String
...
init(){ .. }
}
ViewController.swift
class ViewController: UIViewController {
var PetArray = PetInfo.shared.petArray
override func viewDidLoad() {
super.viewDidLoad()
let pet = PetInfo()
pet.PetName = "Jack"
PetArray.append(pet). **Create Object and gave a name**
print(PetArray[0].PetName) //works!
}
}
secondViewController.swift
class secondViewController: UIViewController {
var PetArray = PetInfo.shared.petArray
override func viewDidLoad() {
super.viewDidLoad()
let label: UILabel = {
let label = UILabel()
...
label.text = PetArray[0].PetName **tried to print**
return label
}()
view.addSubview(label)
}
}
I want to share PetArray array in all of the view controllers.(It's more than two.)
It put data in the first VC, but doesn't work in the Second VC.
How can I share this array using a Singleton pattern?
Except for the array, It works perfect.(like String.. PetID, PetName.. )
Array in swift is implemented as Struct, which means Array is a value type and not a reference type. Value types in Swift uses copy on write (COW) mechanism to handle the changes to their values.
So in your ViewController when you assigned
var PetArray = PetInfo.shared.petArray
your PetArray was still pointing to the same array in your PetInfo.shared instance (I mean same copy of array in memory) . But as soon as you modified the value of PetArray by using
PetArray.append(pet)
COW kicks in and it creates a new copy of petArray in memory and now your PetArray variable in your ViewController and PetInfo.shared.petArray are no longer pointing to same instances instead they are pointing to two different copies of array in memory.
So all the changes you did by using PetArray.append(pet) is obviously not reflected when you access PetInfo.shared.petArray in secondViewController
What can I do?
remove PetArray.append(pet) and instead use PetInfo.shared.petArray.append(pet)
What are the other issues in my code?
Issue 1:
Never use Pascal casing for variable name var PetArray = PetInfo.shared.petArray instead use camel casing var petArray = PetInfo.shared.petArray
Issue 2:
class PetInfo {
static let shared: PetInfo = PetInfo()
lazy var petArray: [PetInfo] = []
var PetID:Int
var PetName:String
...
init(){ .. }
}
This implementation will not ensure that there exists only one instance of PetInfo exists in memory (I mean it cant ensure pure singleton pattern), though you provide access to instance of PetInfo using a static variable named shared there is nothing which stops user from creating multiple instances of PetInfo simply by calling PetInfo() as you did in let pet = PetInfo()
rather use private init(){ .. } to prevent others from further creating instances of PetInfo
Issue 3:
You are holding an array of PetInfo inside an instance of PetInfo which is kind of messed up pattern, am not really sure as to what are you trying to accomplish here, if this is really what you wanna do, then probably you can ignore point two (creating private init) for now :)
I think the best solution to use Combine and Resolver frameworks. Works perfectly in my case with shared arrays.
In your case it could be
import Combine
import Resolver // need to add pod 'Resolver' to Podfile and install it first
// Data Model
struct PetInfo: Codable {
var PetID:Int
var PetName:String
...
}
// Repository to read manage data (read/write/search)
class PetRepository: ObservableObject {
#Published var petArray = Array<PetInfo>()
override init() {
super.init()
load()
}
private func load() {
// load pets info from server
}
}
Need to add AppDelegate+Injection.swift that will contain repository registration:
import Foundation
import Resolver
extension Resolver: ResolverRegistering {
public static func registerAllServices() {
// Services
register { PetRepository() }.scope(application)
}
}
Then use it in any controllers
import UIKit
import Combine
import Resolver
class ViewController: UIViewController {
#LazyInjected var petRepository: PetRepository
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
petRepository.$petArray
.receive(on: DispatchQueue.main)
.debounce(for: 0.8, scheduler: RunLoop.main)
.sink { [weak self] petInfos in
// set UI here
}
.store(in: &cancellables)
}
}
If you want PetInfo to be a singleton, make its initializer private:
class PetInfo {
static let shared: PetInfo = PetInfo()
lazy var petArray: [PetInfo] = []
var PetID:Int
var PetName:String
...
private init(){ .. } // !!
}
This way, any attempt to create new instances (like you do in your first ViewController) will fail, and will remind you to always use PetInfo.shared to access the singleton.

Unit testing Rx binding with UITextView

I have a ControlProperty binding like this in my UIViewController:
textView.rx.text.orEmpty.asObservable()
.bind(to: viewModel.descriptions)
.disposed(by: disposeBag)
, where viewModel.descriptions is of type BehaviorRelay<String>.
In a TDD approach, I would like to write an (initially failing) test (as the initial step of the red-green-refactor cycle) to see that this binding exists. I tried with a test code like this:
sut.textView.insertText("DESC")
sut.textView.delegate?.textViewDidChange?(sut.textView)
expect(mockViewModel.lastDescriptions).to(equal("DESC"))
, where sut is my ViewController and mockViewModel contains code to subscribe to the descriptions BehaviorRelay:
class MockViewModel: MyViewModelProtocol {
var descriptions = BehaviorRelay<String>(value: "")
var lastDescriptions: String?
private var disposeBag = DisposeBag()
init() {
descriptions.asObservable()
.skip(1)
.subscribe(onNext: { [weak self] (title: String) in
self?.lastDescriptions = title
})
.disposed(by: disposeBag)
}
}
However, I cannot push the new value to the textView so that the descriptions BehaviorRelay gets it.
How can I achieve descriptions.asObservable() to fire when entering a value in the textView? If it were a UITextField then I would do:
textField.text = "DESC"
textField.sendActions((for: .editingChanged))
Thanks in advance for any help.

RxSwift: Is it safe to always use [unowned self] when a class has a disposeBag property?

I recently found an article that says using [unowned self] is always safe as long as you are adding the subscription to a DisposeBag and it is inside the view controller.
Assuming I have a ViewController where deinit is not being called due to a strong reference:
class ViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var tableView: UITableView!
private let disposeBag = DisposeBag()
private var results = Variable<[Item]>([])
private var searchText = Variable("")
var selectedCompletion: ((Item) -> Void)!
override func viewDidLoad() {
super.viewDidLoad()
results.asObservable()
.bind(to: tableView.rx.items(cellIdentifier: "CustomCell", cellType: CustomCell.self)) { row, item, cell in
cell.configure(with: item)
}
.disposed(by: disposeBag)
tableView.rx.itemSelected
.subscribe(onNext: { ip in
self.selectedCompletion(self.results.value[ip.row])
self.navigationController?.popViewController(animated: true)
})
.disposed(by:disposeBag)
searchBar.rx.text
.debounce(0.6, scheduler: MainScheduler.instance)
.subscribe(onNext: { searchText in
if searchText == nil || searchText!.isEmpty { return }
self.search(query: searchText!)
})
.disposed(by: disposeBag)
}
private func search(query: String) {
// Search asynchronously
search(for: query) { response in
// Some logic here...
self.results.value = searchResult.results
}
}
}
I should simply be able to declare [unowned self] in my subscription closures and not have to worry about my app crashing from self being nil.
Where I'm confused is, because search is asynchronous, doesn't that mean self can be nil if the ViewController has been popped off the navigation stack before the query completes?
Or would the disposeBag be deallocated first and the closure wouldn't complete?
Any clarification about how to know whether or not a class owns a closure would be great too.
In my experience it's a safe approach to use unowned with a dispose bag, except one block - onDisposed. There have been the cases when an app crashed because of unowed keyword -> weak is useful here.
as #kzaher says on github
you should never use unowned.
sources:
https://github.com/RxSwiftCommunity/RxDataSources/issues/169
https://github.com/ReactiveX/RxSwift/issues/1593

RxSwift: Two way binding

I used official two-way-binding solution
func <-> <T>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable{
let bindToUIDisposable = variable.asObservable()
.bindTo(property)
let bindToVariable = property
.subscribe(onNext: { n in
variable.value = n
}, onCompleted: {
bindToUIDisposable.dispose()
})
return Disposables.create(bindToUIDisposable, bindToVariable)
}
Usage: (textField.rx.text <-> object.property).addDisposableTo(disposeBag)
Property definition: var property = Variable<String?>(nil)
In onNext method all ok and variable changed its value, but my object.property doesn't changed.
Is there any way to set variable current value into ControlProperty inside of <-> method, bcs I need set initial value, before subscribe starts?
Working in Swift 4
Example of two way binding between String & Textfield with MVVM architecture:
In ViewController:
#IBOutlet weak var emailTextfield: UITextField!
var viewModel : CCRegisterViewModel?
In ViewModel:
var email = Variable<String>("")
Use this code in ViewController
viewModel?.email.asObservable()
.bind(to: emailTextfield.rx.text)
.disposed(by: disposeBag)
emailTextfield.rx.text.orEmpty.bind(to:viewModel!.email)
.disposed(by: disposeBag)
My fault. I replaced object with another instance after binding
This code works well and control property receive initial value from variable

Updating a UILabel via protocol results in crash (found nil)

I want to implement the MVP pattern for a new app. So the View shouldn't have any logic besides one that exclusively concerns UI elements. Therefore I want to request initial data from an "Interpreter" (interpreting user input in later code), which in turn requests data from my model and gives it to the "Presenter". The presenter holds a protocol with functions of the view.
The problem is: Calling updateUIData() from the presenter results in a
fatal error: unexpectedly found nil while unwrapping an Optional value
while calling the function from within the View at the same position is working just fine.
I suspect the error comes from the initialization of the specific MainViewController in the init of the presenter, but I don't know how to resolve this, if my guess is right.
Here's my (relevant) code:
MainViewController:
class MainViewController: UIViewController {
lazy var interpreter = Interpreter() // lazy needed b/c Interpreter holds Presenter which holds MainViewController
#IBOutlet var dateLabel: UILabel!
#IBOutlet var totalTimeLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// updateUIData()
requestData()
}
func requestData() {
interpreter.requestData()
}
}
extension MainViewController: MainViewSetters {
func updateUIData() {
dateLabel.text = "Data"
totalTimeLabel.text = "loaded"
}
}
MainViewSetters (Protocol):
protocol MainViewSetters {
func updateUIData()
}
Interpreter:
class Interpreter {
let presenter = Presenter()
func requestData() {
// normally: get data from model and pass it to presenter
presenter.presentData()
}
}
Presenter:
class Presenter {
let mainView: MainViewSetters
init(withMainViewController mainVC: MainViewSetters = MainViewController()) {
mainView = mainVC
}
func presentData() {
mainView.updateUIData()
}
}
Your problem here is that you are not passing the reference to MainViewController to your instance of Presenter.
This code :
lazy var interpreter = Interpreter()
Should be more like this : (Type is needed here because with lazy the compiler can't infer properly)
lazy var interpreter: Interpreter = Interpreter(for: self)
You then have to create a special initializer in Interpreter which will pass the viewController instance to its presenter property :
class Interpreter {
let presenter: Presenter
init(for viewController: MainViewSetters) {
presenter = Presenter(withMainViewController: viewController)
}
func requestData() {
// normally: get data from model and pass it to presenter
presenter.presentData()
}
}
I also highly suggest you to remove the default value to Presenter's init method, it's very unlikely you'll want to assign a random instance of MainViewController as mainView of any Presenter object.
Finally, please note that this code is creating a retain cycle and neither your MainViewController instance nor your Presenter instance will be deallocated. This is due to the fact the Presenter class holds a strong reference to the MainViewController instance with its property mainView. To fix this you have to mark the mainView as weak as well as making it optional.
Please see the fixed implementation below :
class Presenter {
weak var mainView: MainViewSetters?
init(withMainViewController mainVC: MainViewSetters) {
mainView = mainVC
}
func presentData() {
mainView?.updateUIData()
}
}
For weak to be acceptable on a property of type MainViewSetters (which is not a real type but only a protocol) you have to specify that its a protocol that will only be applied to classes :
protocol MainViewSetters: class {
func updateUIData()
}
You are initializing interpreter passing a default MainViewController().
Change that code from:
lazy var interpreter = Interpreter()
to
lazy var interpreter = Interpreter(withMainViewController: self)

Resources