I have an application where I want to make an API call once the screen is awaken in the ViewController. Basically, I am using Universal Link to activate the ViewCOntroller and when it displays the UIViewController, I want to make an API call based on the Data got. I am currently using the MVVM Architecture and I have added my code below
My ViewModel
class EmailVerificationViewModel: ViewModel, ViewModelType {
struct Input {
let editEmailTrigger: Driver<Void>
}
struct Output {
}
let routeManager: BehaviorRelay<RouteMatchResult?>
let currentEmail: BehaviorRelay<String?>
init(routeManager: RouteMatchResult?, provider: Api, currentEmail: String?) {
self.routeManager = BehaviorRelay(value: routeManager)
self.currentEmail = BehaviorRelay(value: currentEmail)
super.init(provider: provider)
}
func transform(input: Input) -> Output {
// THE CALL I WANT TO MAKE
routeManager.errorOnNil().asObservable()
.flatMapLatest { (code) -> Observable<RxSwift.Event<User>> in
log("=========++++++++++++==========")
// guard let code = code else {return}
let params = code.values
let challengeId = Int(params["xxx"] as? String ?? "0")
let login = LoginResponseModel(identifier: params["xxxx"] as? String, key: params["xxxxxx"] as? String, oth: params["xxxxx"] as? String, id: 0, challengeId: challengeId)
return self.provider.postVerifyApp(challengeId: login.challengeId!, oth: login.oth!, identifier: login.identifier!)
.trackActivity(self.loading)
.trackError(self.error)
.materialize()
}.subscribe(onNext: { [weak self] (event) in
switch event {
case .next(let token):
log(token)
AuthManager.setToken(token: token)
// self?.tokenSaved.onNext(())
case .error(let error):
log(error.localizedDescription)
default: break
}
}).disposed(by: rx.disposeBag)
return Output()
}
}
My Viewcontroller
override func bindViewModel() {
super.bindViewModel()
guard let viewModel = viewModel as? EmailVerificationViewModel else { return }
let input = EmailVerificationViewModel.Input(editEmailTrigger: editEmailBtn.rx.tap.asDriver())
let output = viewModel.transform(input: input)
viewModel.loading.asObservable().bind(to: isLoading).disposed(by: rx.disposeBag)
viewModel.parsedError.asObservable().bind(to: error).disposed(by: rx.disposeBag)
isLoading.asDriver().drive(onNext: { [weak self] (isLoading) in
isLoading ? self?.startAnimating() : self?.stopAnimating()
}).disposed(by: rx.disposeBag)
error.subscribe(onNext: { [weak self] (error) in
var title = ""
var description = ""
let image = R.image.icon_toast_warning()
switch error {
case .serverError(let response):
title = response.message ?? ""
}
self?.view.makeToast(description, title: title, image: image)
}).disposed(by: rx.disposeBag)
}
so how can I make the call on the commented like THE CALL I WANT TO MAKE once the application catches the universal link and loads up. Basically making an API call on viewDidLoad
The code in your sample was way more than is needed to answer the question. Here is how you make a network call on viewDidLoad:
class ViewController: UIViewController {
var viewModel: ViewModel!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let input = ViewModel.Input()
let output = viewModel.transform(input: input)
output.viewData
.bind(onNext: { viewData in
// setup the view with viewData
})
.disposed(by: disposeBag)
}
}
class ViewModel {
struct Input { }
struct Output {
let viewData: Observable<ViewData>
}
init(api: API) {
self.api = api
}
func transform(input: Input) -> Output {
let viewData = api.networkCall()
.map { ViewData(from: $0) }
return Output(viewData: viewData)
}
let api: API
}
Related
Hi i'm just getting started with RxSwift and decided to make simple Currency Exchange application. My app has two view's (allCurrenciesList and userFavouritesView). Basically all logic works, but only if i run networking func every single time one of view didAppear/didLoad. My point is two fetch it only once, and received many times, when necessary. Application fetch dictionary of currencies and in ViewModel pass it to BehaviorSubject, and when view being load/appear it just subscribe it, and use it in UITableView. Thanks
class ListViewModel {
let service: CurrencyService!
var curriencies = BehaviorRelay<[Currency]>(value: [])
var currienciesObservable: Observable<[Currency]> {
return curriencies.asObservable().share()
}
let disposeBag = DisposeBag()
init(service: CurrencyService) {
self.service = service
}
func fetchAllCurrencies() {
self.service.fetchAllSymbols { result in
switch result{
case .success(let currencies):
self.dictionaryIntoArray(currencies: currencies["symbols"] as! [String : Any])
case .failure:
print("error")
}
}
}
private func dictionaryIntoArray(currencies: [String: Any]) {
var currencyArray = [Currency]()
for (symbol, name) in currencies {
currencyArray.append(Currency(symbol: symbol, fullName: name as! String))
}
let sortedArray = currencyArray.sorted { $0.fullName < $1.fullName }
self.curriencies.accept(sortedArray)
}
allCurrenciesList
override func viewDidLoad() {
super.viewDidLoad()
setupView()
configureTableViewDataSource()
tableView.delegate = self
fetchData()
}
private func fetchData() {
viewModel.fetchAllCurrencies() // this func is necceserry everysingle time
viewModel.currienciesObservable.subscribe(onNext: { curriencies in
self.applySnapshot(curriencies: curriencies)
}).disposed(by: disposeBag)
}
userFavouritesView
override func viewDidLoad() {
super.viewDidLoad()
viewModel.fetchAllCurrencies() // this func is necceserry everysingle time
viewModel.currienciesObservable.subscribe(onNext: { allCurencies in
let selectedItems = UserDefaults.standard.array(forKey: "SelectedCells") as? [Int] ?? [Int]()
var currenciesArray: [Currency] = []
selectedItems.forEach { int in
self.pickerValues.append(allCurencies[int])
currenciesArray.append(allCurencies[int])
}
self.applySnapshot(curriencies: currenciesArray)
}).disposed(by: disposeBag)
}
The key here is to not use a Subject. They aren't recommended for regular use. Just define the currienciesObservable directly.
Something like this:
class ListViewModel {
let currienciesObservable: Observable<[Currency]>
init(service: CurrencyService) {
self.currienciesObservable = service.rx_fetchAllSymbols()
.map { currencies in
currencies["symbols"]?.map { Currency(symbol: $0.key, fullName: $0.value as! String) }
.sorted(by: { $0.fullName < $1.fullName }) ?? []
}
}
}
extension CurrencyService {
func rx_fetchAllSymbols() -> Observable<[String: [String: Any]]> {
Observable.create { observer in
self.fetchAllSymbols { result in
switch result {
case let .success(currencies):
observer.onNext(currencies)
observer.onCompleted()
case let .failure(error):
observer.onError(error)
}
}
return Disposables.create()
}
}
}
With the above, every time you subscribe to the currenciesObservable the fetch will be called.
As I understand, it's because your fetchAllSymbols function was not stored in the DisposeBag.
func fetchAllCurrencies() {
self.service.fetchAllSymbols { result in
switch result{
case .success(let currencies):
self.dictionaryIntoArray(currencies: currencies["symbols"] as! [String : Any])
case .failure:
print("error")
}
}.dispose(by: disposeBag)
}
I want to learn MVVM RxSwift with input and output method, I want to get a username from textfield.
I have a scenario when user not enter a username it will present an error and when user enter a username it will present in viewController.
This is when I confuse. I got the error message and successfully present error but, how can I catch the query in my viewModel and passed the data to viewController.
This is how I setup my searchViewModel
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
class SearchViewModel: ViewModelType {
// MARK: Binding
struct Input {
let searchText: Observable<String>
let validate: Observable<Void>
}
struct Output {
let username: Driver<String>
}
func transform(input: Input) -> Output {
let username = input.validate
.withLatestFrom(input.searchText)
.map { query in
if query.isEmpty {
return "Please enter a username. We need to know who to look for"
} else {
return query
}
}.asDriver(onErrorJustReturn: "")
return Output(username: username)
}
}
and this is my viewDidLoad in SearchViewController
let searchTextField = GFTextField()
let calloutBtn = GFButton(backgroundColor: .systemGreen, title: "Get followers")
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupImageView()
setupTextfield()
setupCalloutBtn()
let input = SearchViewModel.Input(
searchText: searchTextField.rx.text.orEmpty.asObservable(),
validate: calloutBtn.rx.tap.asObservable())
let output = viewModel.transform(input: input)
output.username.drive { [weak self] username in
guard let self = self else { return }
self.presentGFAlertOnMainThread(title: "Empty Username", message: username, buttonTitle: "Dismiss")
}.disposed(by: disposeBag)
}
It depends on what you want to do with the text of course, but below I assume you want to make a network request. But of course that requires understanding what your API layer looks like. I have to make some assumptions there as well, but the key is that you need to inject your API layer into your view model through its constructor/init method.
Like this:
class SearchViewModel: ViewModelType {
struct Input {
let username: Observable<String>
let getFollowers: Observable<Void>
}
struct Output {
let errorMessage: Driver<String>
let followers: Driver<[Follower]>
}
let api: API
init(api: API) {
self.api = api
}
func transform(input: Input) -> Output {
let followersResponse = input.getFollowers
.withLatestFrom(input.username)
.filter { !$0.isEmpty }
.map { makeEndpoint(using: $0) }
.flatMap { [api] in
api.load($0)
}
.share()
let missingName = input.getFollowers
.withLatestFrom(input.username)
.compactMap { $0.isEmpty ? "Please enter a username. We need to know who to look for" : nil }
let errorMessage = Observable.merge(
api.error.map { $0.localizedDescription },
missingName
)
.asDriver(onErrorJustReturn: "")
let followers = followersResponse
.asDriver(onErrorJustReturn: [])
return Output(errorMessage: errorMessage, followers: followers)
}
}
-- EDIT --
If all you want to do is push the non-empty text field back to the view controller, then it would look like this:
class SearchViewModel: ViewModelType {
struct Input {
let username: Observable<String>
let getFollowers: Observable<Void>
}
struct Output {
let errorMessage: Driver<String>
let username: Driver<String>
}
func transform(input: Input) -> Output {
let errorMessage = input.getFollowers
.withLatestFrom(input.username)
.compactMap { $0.isEmpty ? "Please enter a username. We need to know who to look for" : nil }
.asDriver(onErrorJustReturn: "")
let username = input.getFollowers
.withLatestFrom(input.username)
.filter { !$0.isEmpty }
.asDriver(onErrorJustReturn: "")
return Output(errorMessage: errorMessage, username: username)
}
}
The key here is that you need a Driver for each output that the view controller will want to subscribe to.
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 want to get data from server and update my DB after that I'll show received data to the user. For this goal I have a method(getData()) in my view model that returns a Single I call and subscribe this method in the view controller(myVC.getData.subscribe({single in ...})) in this method at first I call and subscribe(#1)(getUnread()->Single) the method run but I can not get the single event, I can not understand why I can't get the event(#3) in call back(#4)
after that I want to save data with calling(#2)(save([Moddel])->single)
//I removed some part of this code it was to big
//This method is View Model
func getData() -> Single<[Model]> {
return Single<[Model]>.create {[weak self] single in
//#1
self!.restRepo.getUnread().subscribe({ [weak self] event in
//#4
switch event {
case .success(let response):
let models = response
//#2
self!.dbRepo.save(issues!).subscribe({ event in
switch event {
case .success(let response):
let models = response
single(.success(models))
case .error(let error):
single(.error(error))
}
}).disposed(by: self!.disposeBag)
case .error(let error):
single(.error(error))
}
}).disposed(by: self!.disposeBag)
return Disposables.create()
}
}
.
.
//I removed some part of this code it was to big
//This method is in RestRepo class
func getUnread() -> Single<[Model]> {
return Single<[Model]>.create { single in
let urlComponent = ApiHelper.instance.dolphinURLComponents(for: ApiHelper.ISSUES_PATH)
var urlRequest = URLRequest(url: urlComponent.url!)
ApiHelper.instance.alamofire.request(urlRequest).intercept().responseJSON { response in
debugPrint(response)
let statusCode = response.response?.statusCode
switch statusCode {
case 200:
do {
let models = try JSONDecoder().decode([Model].self, from: response.data!)
//#3
single(.success(models))
}catch{
print(error)
}
case 304:
debugPrint(response)
default:
single(.error(IssueResponseStatusCodeError(code: statusCode ?? 0)))
}
}
return Disposables.create()
}
First you need to change your thinking. You don't do anything in the app. At best, you lay out the Observable chains (which don't do anything anymore than water pipes "do" something.) Then you start the app and let the "water" flow.
So with that in mind, let's examine your question:
I want to get data from server...
It's not that "you" want to get the data. The request is made as a result of some action (probably a button tap) by the user or by some other side effect. What action is that? That needs to be expressed in the code. For the following I will assume it's a button tap. That means you should have:
class Example: UIViewController {
var button: UIButton!
var restRepo: RestRepo!
override func viewDidLoad() {
super.viewDidLoad()
let serverResponse = button.rx.tap
.flatMapLatest { [restRepo] in
restRepo!.getUnread()
.map { Result<[Model], Error>.success($0) }
.catchError { .just(Result<[Model], Error>.failure($0)) }
}
.share(replay: 1)
}
}
protocol RestRepo {
func getUnread() -> Observable<[Model]>
}
struct ProductionRestRepo: RestRepo {
func getUnread() -> Observable<[Model]> {
let urlComponent = ApiHelper.instance.dolphinURLComponents(for: ApiHelper.ISSUES_PATH)
let urlRequest = URLRequest(url: urlComponent.url!)
return URLSession.shared.rx.data(request: urlRequest)
.map { try JSONDecoder().decode([Model].self, from: $0) }
}
}
class ApiHelper {
static let ISSUES_PATH = ""
static let instance = ApiHelper()
func dolphinURLComponents(for: String) -> URLComponents { fatalError() }
}
struct Model: Decodable { }
The thing to notice here is that getUnread() is an effect that is caused by button.rx.tap. The above establishes a cause-effect chain.
Your question goes on to say "you" want to:
... update my DB...
Here, the cause is the network request and the effect is the DB save so we simply need to add this to the viewDidLoad (note that the code below uses RxEnumKit.):
let dbResponse = serverResponse
.capture(case: Result.success)
.flatMapLatest { [dbRepo] models in
dbRepo!.save(models)
.map { Result<Void, Error>.success(()) }
.catchError { .just(Result<Void, Error>.failure($0)) }
}
Your question also says that "you" want to:
... show received data to the user.
Note here that showing the received data to the user has nothing to do with the DB save. They are two independent operations that can be done in parallel.
Showing the received data to the user has the serverResponse as the cause, and the showing as the effect.
serverResponse
.capture(case: Result.success)
.subscribe(onNext: { models in
print("display the data to the user.", models)
})
.disposed(by: disposeBag)
Lastly, you don't mention it, but you also have to handle the errors:
So add this to the viewDidLoad as well:
Observable.merge(serverResponse.capture(case: Result.failure), dbResponse.capture(case: Result.failure))
.subscribe(onNext: { error in
print("an error occured:", error)
})
.disposed(by: disposeBag)
The code below is all of the above as a single block. This compiles fine...
import UIKit
import RxSwift
import RxCocoa
import EnumKit
import RxEnumKit
extension Result: CaseAccessible { }
class Example: UIViewController {
var button: UIButton!
var restRepo: RestRepo!
var dbRepo: DBRepo!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let serverResponse = button.rx.tap
.flatMapLatest { [restRepo] in
restRepo!.getUnread()
.map { Result<[Model], Error>.success($0) }
.catchError { .just(Result<[Model], Error>.failure($0)) }
}
.share(replay: 1)
let dbResponse = serverResponse
.capture(case: Result.success)
.flatMapLatest { [dbRepo] models in
dbRepo!.save(models)
.map { Result<Void, Error>.success(()) }
.catchError { .just(Result<Void, Error>.failure($0)) }
}
serverResponse
.capture(case: Result.success)
.subscribe(onNext: { models in
print("display the data to the user.", models)
})
.disposed(by: disposeBag)
Observable.merge(serverResponse.capture(case: Result.failure), dbResponse.capture(case: Result.failure))
.subscribe(onNext: { error in
print("an error occured:", error)
})
.disposed(by: disposeBag)
}
}
protocol RestRepo {
func getUnread() -> Observable<[Model]>
}
protocol DBRepo {
func save(_ models: [Model]) -> Observable<Void>
}
struct ProductionRestRepo: RestRepo {
func getUnread() -> Observable<[Model]> {
let urlComponent = ApiHelper.instance.dolphinURLComponents(for: ApiHelper.ISSUES_PATH)
let urlRequest = URLRequest(url: urlComponent.url!)
return URLSession.shared.rx.data(request: urlRequest)
.map { try JSONDecoder().decode([Model].self, from: $0) }
}
}
class ApiHelper {
static let ISSUES_PATH = ""
static let instance = ApiHelper()
func dolphinURLComponents(for: String) -> URLComponents { fatalError() }
}
struct Model: Decodable { }
I hope all this helps you, or at least generates more questions.
New to RxSwift here. I have a (MVVM) view model that represents a Newsfeed-like page, what's the correct way to subscribe to change in data model's properties? In the following example, startUpdate() constantly updates post. The computed properties messageToDisplay and shouldShowHeart drives some UI event.
struct Post {
var iLiked: Bool
var likes: Int
...
}
class PostViewModel: NSObject {
private var post: Post
var messageToDisplay: String {
if post.iLiked { return ... }
else { return .... }
}
var shouldShowHeart: Bool {
return iLiked && likes > 10
}
func startUpdate() {
// network request and update post
}
...
}
It seems to me in order to make this whole thing reactive, I have to turn each properties of Post and all computed properties into Variable? It doesn't look quite right to me.
// Class NetworkRequest or any name
static func request(endpoint: String, query: [String: Any] = [:]) -> Observable<[String: Any]> {
do {
guard let url = URL(string: API)?.appendingPathComponent(endpoint) else {
throw EOError.invalidURL(endpoint)
}
return manager.rx.responseJSON(.get, url)
.map({ (response, json) -> [String: Any] in
guard let result = json as? [String: Any] else {
throw EOError.invalidJSON(url.absoluteString)
}
return result
})
} catch {
return Observable.empty()
}
}
static var posts: Observable<[Post]> = {
return NetworkRequest.request(endpoint: postEndpoint)
.map { data in
let posts = data["post"] as? [[String: Any]] ?? []
return posts
.flatMap(Post.init)
.sorted { $0.name < $1.name }
}
.shareReplay(1)
}()
class PostViewModel: NSObject {
let posts = Variable<[Post]>([])
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
posts
.asObservable()
.subscribe(onNext: { [weak self] _ in
DispatchQueue.main.async {
//self?.tableView?.reloadData() if you want to reload all tableview
self.tableView.insertRows(at: [IndexPath], with: UITableViewRowAnimation.none) // OR if you want to insert one or multiple rows.
//OR update UI
}
})
.disposed(by: disposeBag)
posts.asObservable()
.bind(to: tableView.rx.items) { [unowned self] (tableView: UITableView, index: Int, element: Posts) in
let cell = tableView.dequeueReusableCell(withIdentifier: "postCell") as! PostCell
//for example you need to update the view model and cell with textfield.. if you want to update the ui with a cell then use cell.button.tap{}. hope it works for you.
cell.textField.rx.text
.orEmpty.asObservable()
.bind(to: self.posts.value[index].name!)
.disposed(by: cell.disposeBag)
return cell
}
startDownload()
}
}
func startDownload {
posts.value = NetworkRequest.posts
}
If you want to change anything then use subscribe, bind, concat .. There are many methods and properties which you can use.