Currently learning RxSwift (Rx in general). I want to update a UILabel regularly with the latest ticker price.
How can .interval and the updateTicker function every period and then update the UILabel accordingly.
ViewController.swift
class ViewController: UIViewController {
private let disposeBag = DisposeBag()
// Dependencies
private var viewModel = ViewModel()
// Outlets
#IBOutlet var tickerLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setupViewModel()
}
func setupViewModel() {
self.viewModel.ticker.asObservable()
.bind(to: self.tickerLabel.rx.text)
.addDisposableTo(self.disposeBag)
}
}
ViewModel.swift
struct ViewModel {
private let disposeBag = DisposeBag()
let provider = RxMoyaProvider<StockAPI>()
var ticker = Variable<String>("")
init() {
startTimer()
// ???
}
func startTimer() -> Observable<Int> {
return Observable<Int>.interval(5, scheduler: MainScheduler.instance)
}
func updateTicker() {
_ = self.provider.request(.ticker(symbol: "AAPL")).subscribe { (event) in
switch event {
case .next(let response):
print(response)
// do something with the data
case .error(let error):
// handle the error
print(error)
break
default:
break
}
}
}
Maybe something like this
var ticker = Observable.just("")
func startTimer() {
self.ticker = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
.map({ _ in self.updateTicker() })
}
The func updateTicker will need return a String.
Related
I am trying to make a GET from a REST API in swift. When I use the print statement (print(clubs)) I see the expected response in the proper format. But in the VC is gives me an empty array.
Here is the code to talk to the API
extension ClubAPI {
public enum ClubError: Error {
case unknown(message: String)
}
func getClubs(completion: #escaping ((Result<[Club], ClubError>) -> Void)) {
let baseURL = self.configuration.baseURL
let endPoint = baseURL.appendingPathComponent("/club")
print(endPoint)
API.shared.httpClient.get(endPoint) { (result) in
switch result {
case .success(let response):
let clubs = (try? JSONDecoder().decode([Club].self, from: response.data)) ?? []
print(clubs)
completion(.success(clubs))
case .failure(let error):
completion(.failure(.unknown(message: error.localizedDescription)))
}
}
}
}
and here is the code in the VC
private class ClubViewModel {
#Published private(set) var clubs = [Club]()
#Published private(set) var error: String?
func refresh() {
ClubAPI.shared.getClubs { (result) in
switch result {
case .success(let club):
print("We have \(club.count)")
self.clubs = club
print("we have \(club.count)")
case .failure(let error):
self.error = error.localizedDescription
}
}
}
}
and here is the view controller code (Before the extension)
class ClubViewController: UIViewController {
private var clubs = [Club]()
private var subscriptions = Set<AnyCancellable>()
private lazy var dataSource = makeDataSource()
enum Section {
case main
}
private var errorMessage: String? {
didSet {
}
}
private let viewModel = ClubViewModel()
#IBOutlet private weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.subscriptions = [
self.viewModel.$clubs.assign(to: \.clubs, on: self),
self.viewModel.$error.assign(to: \.errorMessage, on: self)
]
applySnapshot(animatingDifferences: false)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.viewModel.refresh()
}
}
extension ClubViewController {
typealias DataSource = UITableViewDiffableDataSource<Section, Club>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Club>
func applySnapshot(animatingDifferences: Bool = true) {
// Create a snapshot object.
var snapshot = Snapshot()
// Add the section
snapshot.appendSections([.main])
// Add the player array
snapshot.appendItems(clubs)
print(clubs.count)
// Tell the dataSource about the latest snapshot so it can update and animate.
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
func makeDataSource() -> DataSource {
let dataSource = DataSource(tableView: tableView) { (tableView, indexPath, club) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "ClubCell", for: indexPath)
let club = self.clubs[indexPath.row]
print("The name is \(club.name)")
cell.textLabel?.text = club.name
return cell
}
return dataSource
}
}
You need to apply a new snapshot to your table view once you have fetched the clubs. Your current subscriber simply assigns a value to clubs and nothing more.
You can use a sink subscriber to assign the new clubs value and then call applySnapshot. You need to ensure that this happens on the main queue, so you can use receive(on:).
self.subscriptions = [
self.viewModel.$clubs.receive(on: RunLoop.main).sink { clubs in
self.clubs = clubs
self.applySnapshot()
},
self.viewModel.$error.assign(to: \.errorMessage, on: self)
]
I'm implementing the mvvm using RxSwif.
Here is what happens:
Validate Fields (write 7 characters in both textFields).
Tap the login button.
API is called which is fine.
Tap the button again.
API is not called.
I noticed that the "validObservable" changes every time that I write something in textFields which is fine.
ViewController:
var viewModel: LoginViewModelType!
let disposeBag = DisposeBag()
#IBOutlet weak var dniTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
#IBOutlet weak var logInButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
logInButton.rx
.tap
.bind(to: viewModel.inputs.logInButtonDidTap)
.disposed(by: disposeBag)
dniTextField.rx
.text
.bind(to: viewModel.inputs.dniChanged)
.disposed(by: disposeBag)
passwordTextField.rx
.text
.bind(to: viewModel.inputs.passwordChanged)
.disposed(by: disposeBag)
/*viewModel.outputs.isLoginButtonEnabled
.drive(onNext: { [weak self] isEnabled in
guard let `self` = self else { return }
self.logInButton.isEnabled = isEnabled
})
.disposed(by: disposeBag)
*/
viewModel.outputs.logIn
.drive(onNext: { [weak self] user in
guard let `self` = self else { return }
self.performSegue(withIdentifier: "showMainController", sender: user)
})
.disposed(by: disposeBag)
}
override func awakeFromNib() {
super.awakeFromNib()
viewModel = LoginViewModel()
}
ViewModel:
var inputs: LoginViewModelInputs { return self }
var outputs: LoginViewModelOutputs { return self }
// ---------------------
// MARK: - Inputs
// ---------------------
var dniChanged: BehaviorRelay<String?>
var passwordChanged: BehaviorRelay<String?>
var logInButtonDidTap: PublishSubject<Void> = PublishSubject<Void>()
// ---------------------
// MARK: - Outpuds
// ---------------------
var logIn: Driver<User>
var isLoginButtonEnabled: Driver<Bool>
private let disposeBag = DisposeBag()
public init () {
dniChanged = BehaviorRelay<String?>(value: "")
passwordChanged = BehaviorRelay<String?>(value: "")
let dniObservable = dniChanged.asDriver().filterNil().asObservable()
let passwordObservable = passwordChanged.asDriver().filterNil().asObservable()
let dniValidation = dniObservable.map { $0.count > 4 }
let passwordValidation = passwordObservable.map { $0.count > 3 }
let validObservable = Observable.combineLatest(dniValidation, passwordValidation) { return $0 && $1 }.filter { $0 }
isLoginButtonEnabled = validObservable.asDriver(onErrorDriveWith: .empty())
let loginSuccessObservable = Observable.combineLatest(dniObservable, passwordObservable, validObservable) { (dni, password, valid) -> LogInRequest in
return LogInRequest(dni: dni, password: password)
}
logIn = logInButtonDidTap.withLatestFrom(loginSuccessObservable).flatMapLatest({ request -> Observable<User> in
return API.shared.post(endpoint: EndPoints.Authorize, type: User.self, body: request)
}).asDriver(onErrorDriveWith: .empty())
}
I'd say that .empty() is causing the observable to complete, and the subscription to be disposed of as a consequence.
I'd put some .debug() instructions to make sure what gets disposed and when.
I got this problem because the button size is zero
I am learning RxSwift and I have tried a basic login UI using it. My implementation is as follows.
LoginViewController:
fileprivate let loginViewModel = LoginViewModel()
fileprivate var textFieldArray: [UITextField]!
override func viewDidLoad() {
super.viewDidLoad()
textFieldArray = [textFieldUserName, textFieldPassword, textFieldConfirmPassword]
textFieldUserName.delegate = self
textFieldPassword.delegate = self
textFieldConfirmPassword.delegate = self
loginViewModel.areValidFields.subscribe(
onNext: { [weak self] validArray in
for i in 0..<validArray.count {
if validArray[i] {
self?.showValidUI(index: i)
} else {
self?.showInValidUI(index: i)
}
}
},
onCompleted: {
print("### COMPLETED ###")
},
onDisposed: {
print("### DISPOSED ###")
}).disposed(by: loginViewModel.bag)
}
func showValidUI(index: Int) {
textFieldArray[index].layer.borderColor = UIColor.clear.cgColor
}
func showInValidUI(index: Int) {
textFieldArray[index].layer.borderColor = UIColor.red.cgColor
textFieldArray[index].layer.borderWidth = 2.0
}
extension LoginViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let inputText = (textField.text! as NSString).replacingCharacters(in: range, with: string)
switch textField {
case textFieldUserName:
loginViewModel.updateUserName(text: inputText)
case textFieldPassword:
loginViewModel.updatePassword(text: inputText)
case textFieldConfirmPassword:
loginViewModel.updateConfirmedPassword(text: inputText)
default:
return false
}
return true
}
}
LoginViewModel:
class LoginViewModel {
private var username: String!
private var password: String!
private var confirmedPassword: String!
fileprivate let combinedSubject = PublishSubject<[Bool]>()
let bag = DisposeBag()
var areValidFields: Observable<[Bool]> {
return combinedSubject.asObservable()
}
init() {
self.username = ""
self.password = ""
self.confirmedPassword = ""
}
/*deinit {
combinedSubject.onCompleted()
}*/
func updateUserName(text: String) {
username = text
if username.count > 6 {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([false, true, true])
}
}
func updatePassword(text: String) {
password = text
if password.count > 6 {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([true, false, true])
}
}
func updateConfirmedPassword(text: String) {
confirmedPassword = text
if confirmedPassword == password {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([true, true, false])
}
}
}
With this code, the disposed message gets printed when i move back the navigation stack.
However, if I move forward, the disposed message is not printed. What is the proper way to dispose the observable?
When you move forward, the view controller is not removed from the stack. It remains so that when the user taps the back button, it is ready and still in the same state as the last time the user saw it. That is why nothing is disposed.
Also, since you said you are still learning Rx, what you have is not anywhere near best practices. I would expect to see something more like this:
class LoginViewModel {
let areValidFields: Observable<[Bool]>
init(username: Observable<String>, password: Observable<String>, confirm: Observable<String>) {
let usernameValid = username.map { $0.count > 6 }
let passValid = password.map { $0.count > 6 }
let confirmValid = Observable.combineLatest(password, confirm)
.map { $0 == $1 }
areValidFields = Observable.combineLatest([usernameValid, passValid, confirmValid])
}
}
In your view model, prefer to accept inputs in the init function. If you can't do that, for e.g. if some of the inputs don't exist yet, then use a Subject property and bind to it. But in either case, your view model should basically consist only of an init function and some properties for output. The DisposeBag does not go in the view model.
Your view controller only needs to create a view model and connect to it:
class LoginViewController: UIViewController {
#IBOutlet weak var textFieldUserName: UITextField!
#IBOutlet weak var textFieldPassword: UITextField!
#IBOutlet weak var textFieldConfirmPassword: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = LoginViewModel(
username: textFieldUserName.rx.text.orEmpty.asObservable(),
password: textFieldPassword.rx.text.orEmpty.asObservable(),
confirm: textFieldConfirmPassword.rx.text.orEmpty.asObservable()
)
let textFieldArray = [textFieldUserName!, textFieldPassword!, textFieldConfirmPassword!]
viewModel.areValidFields.subscribe(
onNext: { validArray in
for (field, valid) in zip(textFieldArray, validArray) {
if valid {
field.layer.borderColor = UIColor.clear.cgColor
}
else {
field.layer.borderColor = UIColor.red.cgColor
field.layer.borderWidth = 2.0
}
}
})
.disposed(by: bag)
}
private let bag = DisposeBag()
}
Notice that all of the code ends up in the viewDidLoad function. That's the ideal so you don't have to deal with [weak self]. In this particular case, I would likely put the onNext closure in a curried global function, in which case it would look like this:
class LoginViewController: UIViewController {
#IBOutlet weak var textFieldUserName: UITextField!
#IBOutlet weak var textFieldPassword: UITextField!
#IBOutlet weak var textFieldConfirmPassword: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = LoginViewModel(
username: textFieldUserName.rx.text.orEmpty.asObservable(),
password: textFieldPassword.rx.text.orEmpty.asObservable(),
confirm: textFieldConfirmPassword.rx.text.orEmpty.asObservable()
)
let textFieldArray = [textFieldUserName!, textFieldPassword!, textFieldConfirmPassword!]
viewModel.areValidFields.subscribe(
onNext:update(fields: textFieldArray))
.disposed(by: bag)
}
private let bag = DisposeBag()
}
func update(fields: [UITextField]) -> ([Bool]) -> Void {
return { validArray in
for (field, valid) in zip(fields, validArray) {
if valid {
field.layer.borderColor = UIColor.clear.cgColor
}
else {
field.layer.borderColor = UIColor.red.cgColor
field.layer.borderWidth = 2.0
}
}
}
}
Notice here that the update(fields:) function is not in the class. That way we aren't capturing self and so don't have to worry about weak self. Also, this update function may very well be useful for other form inputs in the app.
You have added disposable in to the dispose bag of LoginViewModel object, which gets released when LoginViewController object gets released.
This means the disposable returned by LoginViewModel observable won't be disposed until LoginViewController gets released or you receive completed or error on areValidFields Observable.
This is in sync with the accepted behaviour in most of the observable cases.
But, in case if you want to dispose the observable when LoginViewController moves out of screen, you can manually dispose:
var areValidFieldsDisposbale:Disposable?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
areValidFieldsDisposbale = loginViewModel.areValidFields.subscribe(
onNext: { [weak self] validArray in
for i in 0..<validArray.count {
if validArray[i] {
self?.showValidUI(index: i)
} else {
self?.showInValidUI(index: i)
}
}
},
onCompleted: {
print("### COMPLETED ###")
},
onDisposed: {
print("### DISPOSED ###")
})
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
areValidFieldsDisposbale?.dispose()
}
My expectation is to add observables on-the-fly (eg: images upload), let them start, and, when I finished dynamically enqueueing everything, wait for all observable to be finished.
Here is my class :
open class InstantObservables<T> {
lazy var disposeBag = DisposeBag()
public init() { }
lazy var observables: [Observable<T>] = []
lazy var disposables: [Disposable] = []
open func enqueue(observable: Observable<T>) {
observables.append(observable)
let disposable = observable
.subscribe()
disposables.append(disposable)
disposable
.addDisposableTo(disposeBag)
}
open func removeAndStop(atIndex index: Int) {
guard observables.indices.contains(index)
&& disposables.indices.contains(index) else {
return
}
let disposable = disposables.remove(at: index)
disposable.dispose()
_ = observables.remove(at: index)
}
open func waitForAllObservablesToBeFinished() -> Observable<[T]> {
let multipleObservable = Observable.zip(observables)
observables.removeAll()
disposables.removeAll()
return multipleObservable
}
open func cancelObservables() {
disposeBag = DisposeBag()
}
}
But when I subscribe to the observable sent by waitForAllObservablesToBeFinished() , all of them are re-executed (which is logic, regarding how Rx works).
How could I warranty that each are executed once, whatever the number of subscription is ?
While writing the question, I got the answer !
By altering the observable through shareReplay(1), and enqueuing and subscribing to this altered observable.. It works !
Here is the updated code :
open class InstantObservables<T> {
lazy var disposeBag = DisposeBag()
public init() { }
lazy var observables: [Observable<T>] = []
lazy var disposables: [Disposable] = []
open func enqueue(observable: Observable<T>) {
let shared = observable.shareReplay(1)
observables.append(shared)
let disposable = shared
.subscribe()
disposables.append(disposable)
disposable
.addDisposableTo(disposeBag)
}
open func removeAndStop(atIndex index: Int) {
guard observables.indices.contains(index)
&& disposables.indices.contains(index) else {
return
}
let disposable = disposables.remove(at: index)
disposable.dispose()
_ = observables.remove(at: index)
}
open func waitForAllObservablesToBeFinished() -> Observable<[T]> {
let multipleObservable = Observable.zip(observables)
observables.removeAll()
disposables.removeAll()
return multipleObservable
}
open func cancelObservables() {
disposeBag = DisposeBag()
}
}
I'm newer to RxSwift. I want to refresh the tableview to show new data.The first request that I can get the data. but when I pull down the tableview, the request didn't finished. I have no ideas about this? My code is belowing:
1: My viewController's code:
class RecommendViewController: UIViewController {
lazy var tableView = DefaultManager.createTableView(HomeImageCell.self,
HomeImageCell.idenfitier)
let disposeBag = DisposeBag()
lazy var viewModel = HomeViewModel()
lazy var dataSource: [HomeListDetailModel] = []
override func viewDidLoad() {
super.viewDidLoad()
viewModel.fetchRecommendList("answer_feed",0)
setupTableView()
configureRefresh()
bindDataToTableView()
}
func setupTableView() {
view.addSubview(tableView)
tableView.snp.makeConstraints { (make) in
make.edges.equalTo(0)
}
tableView.estimatedHeight(200)
}
func bindDataToTableView() {
viewModel.recommend
.observeOn(MainScheduler.instance)
.do(onNext: { [unowned self] model in
print("endAllRefresh")
self.endAllRefresh()
}, onError: { (error) in
self.endAllRefresh()
print("error = \(error)")
})
.map { [unowned self] model in
return self.handleData(model)
}.bind(to: tableView.rx.items(cellIdentifier: HomeImageCell.idenfitier , cellType: HomeImageCell.self )) { index, model, cell in
cell.updateCell(data: model)
}.disposed(by: disposeBag)
}
func configureRefresh() {
tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: { [unowned self] in
let model = self.dataSource[0]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
tableView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: { [unowned self] in
let model = self.dataSource[self.dataSource.count - 1]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
}
func endAllRefresh() {
self.tableView.mj_header.endRefreshing()
self.tableView.mj_footer.endRefreshing()
}
func handleData(_ model: HomeListModel) -> [HomeListDetailModel] {
guard let data = model.detailData else {
return dataSource
}
self.dataSource = data
return data
}
}
2: My ViewModel
protocol HomeProtocol {
func fetchRecommendList(_ category: String, _ behot_time: Int)
}
class HomeViewModel: HomeProtocol {
lazy var provider = HTTPServiceProvider.shared
var recommend: Observable<HomeListModel>!
init() {}
init(_ provider: RxMoyaProvider<MultiTarget>) {
self.provider = provider
}
func fetchRecommendList(_ category: String, _ behot_time: Int) {
recommend = provider.request(MultiTarget(HomeAPI.homeList(category: category,behot_time: behot_time)))
.debug()
.mapObject(HomeListModel.self)
}
}
When I made a breakpoint at request method, it didn't do a request? Does anyone know it ? Thanks first
SomeOne told me the reason,So I write it here. In my ViewModel recommend should be backed by PublishSubject or BehaviourSubject or ReplaySubject and then I should share this for View as Observable. In fetchRecommentList method I should bind request to created Subject.
Now I have created observable, but request will run after subsribe or bind