I'm trying to add RxSwift to a peace of code using MVVM pattern. My app need to get a list of FoodType (desert, meal, etc.) an Food from an API and save them to Realm database. Then, I have a view with an UITextField and a UIButton.
The user write a food type (ex: desert):
Background: the app should get FoodType and FoodList from Api if not in Realm DB
On button click: show list of Food that have the FoodType chosen by the user from Realm
ViewModel
struct FoodTypeViewModel {
// Get datas from API
private func getFoods() {
foodService.getAll(completionHandler: { result in
switch result {
case .Success(let foods):
for food in foods {
food.save()
}
break
case .Failure(let error):
debugPrint(error)
break
}
})
}
// Get datas from API
private func getFoodTypes() {
foodService.getAll(completionHandler: { result in
switch result {
case .Success(let foodTypes):
for type in types {
type.save()
}
break
case .Failure(let error):
debugPrint(error)
break
}
})
}
}
ViewController
class SetupViewController: UIViewController {
#IBOutlet weak var foodTypeTextField: UITextField!
#IBOutlet weak var foodTypeButton: UIButton!
}
Model
class FoodType: Object {
dynamic var identifier: String = ""
dynamic var fullName: String?
let foods = List<Food>()
}
I would like to add RxSwift to that code but how can I handle the asynchronous API. On first start the app have no datas (I don't want to populate at start) but when the user click the button. So on button click, UI should wait the response from the service (using waiting animation) and ViewModel should update UI when service respond. Any idea ?
First, create a generic return object to wrap communication errors.
enum APIResult<T> {
case success(T)
case error(Error)
}
Then, convert your completion handler to return an Observable:
func getFoods() -> Observable<APIResult<[FoodType]>> {
return Observable<APIResult<[FoodType]>>.create { observer -> Disposable in
self.foodService.getAll(completionHandler: { result in
switch result {
case .Success(let foods):
observer.onNext(.success(foods))
break
case .Failure(let error):
observer.onNext(.error(error))
break
}
observer.onCompleted()
return Disposables.create()
})
}
}
Now simply process the observable as any other in RxSwift.
getFoods().subscribe(onNext: { result in
switch result {
case .success(let foods):
print("Received foods: \(foods)")
break
case .error(let error):
print("Received error: \(error)")
break
}
}.addDisposableTo(disposeBag)
Using these utility classes will help you mapping success results and split error and success signals to different observables. For example:
let foodsRequest = getFoods().splitSuccess
foodsRequest.error.subscribe(onNext: { error in
print("Received error: \(error)")
})
foodsRequest.success.subscribe(onNext: { foods in
print("Received foods: \(foods)")
}
You can also convert Realm objects to RxSwift observables:
let realm = try! Realm()
realm.objects(Lap).asObservable()
.subscribeNext {[weak self] laps in
self?.tableView.reloadData()
}
Take a look at Using Realm Seamlessly in an RxSwift App for more information and examples.
Related
i am using Alamofire, SwiftyJSON to get data, but i can not understand how do i use error alert in ViewConroller, so my code here..
class Networking {
static func FetchData() {
AF.request("https://ApiApiApiApi", method: .get).validate().responseJSON { responseJSON -> Void in
switch responseJSON.result {
case .success:
print("Validation Successful")
case .failure(let error):
// Here i need to show alert in viewController
print("\(error)")
}
}
}
}
}
If i got error i need to show alert in the ViewContoller
class ViewContoller: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Networking.FetchData()
}
}
Firstly you can create an enum for error messages.
enum YourErrorMessage: String, Error {
case failed = "There is an error"
}
And then add a completion handler to your method.
class Networking {
static func FetchData(completion: #escaping(YourErrorMessage) -> Void) {
AF.request("https://ApiApiApiApi", method: .get).validate().responseJSON { responseJSON -> Void in
switch responseJSON.result {
case .success:
print("Validation Successful")
case .failure(let error):
completion(.failed)
print("\(error)")
}
}
}
}
Now you can reach the method state(failure, success).
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Networking.FetchData { errorMessage in
print(errorMessage.rawValue)
}
}
}
I'm new in SwiftUI. I'm worked with UIKit and Combine framework building an architecture with ViewModel, UseCases and Repositories. All my architecture is based on the loading states. My loading states are build in this way:
/// Equivalent to #Published with `LoadingState<T, E>` property wrapper
#propertyWrapper public class Loading<T, E: Swift.Error> {
public typealias State = LoadingState<T, E>
public var wrappedValue: State {
willSet {
subject.send(newValue)
}
}
public init(wrappedValue: State) {
self.wrappedValue = wrappedValue
}
private lazy var subject = CurrentValueSubject<State, Never>(wrappedValue)
public var projectedValue: AnyPublisher<State, Never> {
return subject.eraseToAnyPublisher()
}
}
and my ViewModel works in this way:
#Loading<MyData, MyError> var myDataLoadingState = .idle
public func getMyData(ID: String) {
myDataLoadingState = .loading
myDataUseCase.execute(ID: ID)
.receive(on: DispatchQueue.main)
.sink { completion in
guard case .failure(let error) = completion else { return }
myDataLoadingState = .failure(error)
} receiveValue: { myData in
self. myDataLoadingState = .success(myData)
}
.store(in: &self.cancellables)
}
The controller works in this way:
viewModel.$myDataLoadingState
.sink { state in
switch state {
case .idle:
break
case .loading:
self.showLoader()
case .success(let myData):
print(myData)
case .failure(let error):
self.print(error)
self.hideLoader()
}
}
.store(in: &cancellables)
Can I use the loading state and my ViewModel in SwiftUI? I tried in this way but seems not works:
struct ContentView: View {
#StateObject var viewModel: MyViewModel
var body: some View {
switch viewModel.loadingState {
case .idle:
Text("Idle")
case .loading:
Text("Loading")
case .success(let myData):
Text(myData.name)
case .failure(let error):
Text(error.localizedDescription)
}
}
}
The ViewModel now is an ObservableObject
public class MyViewModel: ObservableObject {
}
Thanks in advance
Just recognised that you already has property wrapper for myDataLoadingState, so not sure if you are allowed to make it #Published instead. Anyway to inform #StateObject wrapper (who is a listener) you should fire event about changes in view model. A possible way is to use objectWillChange directly, like
myDataUseCase.execute(ID: ID)
.receive(on: DispatchQueue.main)
.sink { completion in
guard case .failure(let error) = completion else { return }
self.objectWillChange.send() // << here !!
myDataLoadingState = .failure(error)
} receiveValue: { myData in
self.objectWillChange.send() // << here !!
self.myDataLoadingState = .success(myData)
}
.store(in: &self.cancellables)
As I understand, it is best to only test public methods of a class.
Let's have a look at this example. I have a view model for the view controller.
protocol MyViewModelProtocol {
var items: [SomeItem] { get }
var onInsertItemsAtIndexPaths: (([IndexPath]) -> Void)? { get set }
func viewLoaded()
}
class MyViewModel: MyViewModelProtocol {
func viewLoaded() {
let items = createDetailsCellModels()
updateCellModels(with: items)
requestDetails()
}
}
I want to test class viewLoaded(). This class calls two other methods - updateItems() and requestDetails()
One of the methods sets up the items and the other one call API to retrieve data and update those items. Items array us updated two times and onInsertItemsAtIndexPaths are called two times - when setting up those items and when updating with new data.
I can test whether after calling viewLoaded() expected items are set up and that onInsertItemsAtIndexPaths is called.
However, the test method will become rather complex.
What is your view, should I test those two methods separately or just write this one huge test?
By testing only viewLoaded(), my idea is that the implementation can change and I only care that results are what I expect.
I think the same thing, only public functions should be tested, since public ones use private ones, and your view on MVVM is correct. You can improve it by adding a DataSource and a Mapper that allows you to improve testing.
However, yes, the test seems huge to me, the tests should test simple units and ensure that small parts of the code work well, with the example you show is difficult, you need to divide by layers (clean code).
In the example you load the data into the viewModel and make it difficult to mockup the data. But if you have a Domain layer you can pass the UseCase mock to the viewModel and control the result. If you run a test on your example, the result will also depend on what the endpoint returns. (404, 200, empty array, data with error ...). So it is important, for testing purposes, to have a good separation by layers. (Presentation, Domain and Data) to be able to test each one separately.
I give you an example of how I would test a view mode, sure there are better and cooler examples, but it's an approach.
Here you can see a viewModel
protocol BeersListViewModel: BeersListViewModelInput, BeersListViewModelOutput {}
protocol BeersListViewModelInput {
func viewDidLoad()
func updateView()
func image(url: String?, index: Int) -> Cancellable?
}
protocol BeersListViewModelOutput {
var items: Box<BeersListModel?> { get }
var loadingStatus: Box<LoadingStatus?> { get }
var error: Box<Error?> { get }
}
final class DefaultBeersListViewModel {
private let beersListUseCase: BeersListUseCase
private var beersLoadTask: Cancellable? { willSet { beersLoadTask?.cancel() }}
var items: Box<BeersListModel?> = Box(nil)
var loadingStatus: Box<LoadingStatus?> = Box(.stop)
var error: Box<Error?> = Box(nil)
#discardableResult
init(beersListUseCase: BeersListUseCase) {
self.beersListUseCase = beersListUseCase
}
func viewDidLoad() {
updateView()
}
}
// MARK: Update View
extension DefaultBeersListViewModel: BeersListViewModel {
func updateView() {
self.loadingStatus.value = .start
beersLoadTask = beersListUseCase.execute(completion: { (result) in
switch result {
case .success(let beers):
let beers = beers.map { DefaultBeerModel(beer: $0) }
self.items.value = DefaultBeersListModel(beers: beers)
case .failure(let error):
self.error.value = error
}
self.loadingStatus.value = .stop
})
}
}
// MARK: - Images
extension DefaultBeersListViewModel {
func image(url: String?, index: Int) -> Cancellable? {
guard let url = url else { return nil }
return beersListUseCase.image(with: url, completion: { (result) in
switch result {
case .success(let imageData):
self.items.value?.items?[index].image.value = imageData
case .failure(let error ):
print("image error: \(error)")
}
})
}
}
Here you can see the viewModel test using mocks for the data and view.
class BeerListViewModelTest: XCTestCase {
private enum ErrorMock: Error {
case error
}
class BeersListUseCaseMock: BeersListUseCase {
var error: Error?
var expt: XCTestExpectation?
func execute(completion: #escaping (Result<[BeerEntity], Error>) -> Void) -> Cancellable? {
let beersMock = BeersMock.makeBeerListEntityMock()
if let error = error {
completion(.failure(error))
} else {
completion(.success(beersMock))
}
expt?.fulfill()
return nil
}
func image(with imageUrl: String, completion: #escaping (Result<Data, Error>) -> Void) -> Cancellable? {
return nil
}
}
func testWhenAPIReturnAllData() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "All OK")
beersListUseCaseMock.error = nil
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.items.bind { (_) in}
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNotNil(viewModel.items.value)
XCTAssertNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
func testWhenDataReturnsError() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "Error")
beersListUseCaseMock.error = ErrorMock.error
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNil(viewModel.items.value)
XCTAssertNotNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
}
in this way you can test the view, the business logic and the data separately, in addition to being a code that is very reusable.
Hope this helps you, I have it posted on github in case you need it.
https://github.com/cardona/MVVM
I am trying to set up a tableview that refreshes user data after a button is pressed. RXSwift is used for the entire chain of events. Moya is used for routing.
I am trying to use the standard error handling given by Moya, which is:
provider.rx.request(.userProfile("ashfurrow")).subscribe { event in
switch event {
case let .success(response):
image = UIImage(data: response.data)
case let .error(error):
print(error)
}
}
The only way I have been able to get this to work, is to use an inner subscribe method. Please see code below. Can anyone think of a way that does not require an inner subscribe? It seems a bit clumsy as is.
class ViewController: UIViewController {
#IBOutlet weak var refreshBtn: UIButton!
#IBOutlet weak var tableView: UITableView!
let provider = MoyaProvider<MyAPI>()
let disposeBag = DisposeBag()
var latestUsers = Variable<[User]>([])
override func viewDidLoad() {
super.viewDidLoad()
setupObservableBtnRefreshWithDataFetch()
bindDataToTableView()
}
func setupObservableBtnRefreshWithDataFetch() {
let refreshStream = refreshBtn.rx.tap.startWith(())
let responseStream = refreshStream.flatMapLatest { _ -> SharedSequence<DriverSharingStrategy, [User]> in
let request = self.provider.rx.request(.showUsers)
// Inner Subscribe here, to be able to use the standard Moya subscribe methods for error handling
request.subscribe { event in
switch event {
case .success(let user):
print("Success")
case .error(let error):
print("Error occurred: \(error.localizedDescription)")
}
}
return request
.filterSuccessfulStatusCodes()
.map([User].self)
.asDriver(onErrorJustReturn: [])
}
let nilOnRefreshTapStream: Observable<[User]> = refreshBtn.rx.tap.map { _ in return [] }
let tableDisplayStream = Observable.of(responseStream, nilOnRefreshTapStream)
.merge()
.startWith([])
tableDisplayStream
.subscribe { event in
switch event {
case .next(let users):
print("Users are:")
print(users)
self.latestUsers.value = users
break
case .completed:
break
case .error(let error):
print("Error occurred: \(error.localizedDescription)")
break
}
}
.disposed(by: self.disposeBag)
}
func bindDataToTableView() {
latestUsers.asObservable()
.bind(to: tableView.rx.items(cellIdentifier: "cell", cellType: UITableViewCell.self)) { (_, model: User, cell: UITableViewCell) in
cell.textLabel?.text = model.login
}
.disposed(by: disposeBag)
}
}
class User: Decodable {
var name: String?
var mobile: Int?
var userRequestedTime: String?
var login: String?
init(name: String, mobile: Int, login: String = "") {
self.name = name
self.mobile = mobile
self.login = login
}
}
I have investigated Moya and learned it is a wrapper for network operations.
It's not entirely clear to what purpose the inner subscribe serves - based on my understanding, it triggers an identical but separate network request, which should not affect the other request subscription.
It also seems like the refreshButton tap emits two elements in tableDisplayStream (from responseStream (from refreshStream) and nilOnRefreshTapStream).
Note that Variable is deprecated. Personally, I also prefer .debug().subscribe() to manually printing events in the subscription closure.
Based on this, I would write the code as follows instead. I have not tested it. Hope it helps!
class ViewController: UIViewController {
// ...
private let provider = MoyaProvider<MyAPI>()
private let disposeBag = DisposeBag()
/// Variable<T> is deprecated; use BehaviorRelay instead
private let users = BehaviorRelay<[User]>(value: [])
private func setupObservableBtnRefreshWithDataFetch() {
refreshBtn.rx.tap
.startWith(()) // trigger initial load
.flatMapLatest { _ in
self.provider.rx.request(.showUsers)
.debug("moya request")
.filterSuccessfulStatusCodes()
.map([User].self)
.asDriver(onErrorJustReturn: []) // don't let the error escape
}
.drive(users)
.disposed(by: disposeBag)
}
private func bindDataToTableView() {
users
.asDriver()
.debug("driving table view ")
.drive(tableView.rx.items /* ... */)
.disposed(by: disposeBag)
}
}
I've written a function called 'configureLabels()' that is supposed to make a 'GET' request and retrieve a value which is then supposed to be set as the text for a label. The request is async so I thought I would be able to use an escaping closure to update the UI when the request is finished being made. I'm relatively new to coding, so I am not sure what I've done wrong. I'd really appreciate anyone's help in figuring this out.
This is the code containing the 'configureLabels()' method:
import UIKit
import SwiftyJSON
class ItemDetailViewController: UIViewController {
#IBOutlet weak var numberOfFiberGrams: UILabel!
var ndbnoResults = [JSON]()
var ndbno = ""
let requestManager = RequestManager()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
configureLabels()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func configureLabels() {
requestManager.ndbnoRequest(ndbno: ndbno) { (results) in
let json = JSON(results)
let fiber = json["food"]["nutrients"][7].dictionaryValue
for (key, value) in fiber {
if key == "value" {
self.numberOfFiberGrams.text = "\(value.stringValue)"
} else {
self.numberOfFiberGrams.text = "Fail"
}
}
}
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
And here is the code containing the function that 'configureLabels()' calls:
func ndbnoRequest(ndbno: String, apiKey: String, completionHandler: #escaping (_ results: JSON?) -> Void) {
Alamofire.request("https://api.nal.usda.gov/ndb/V2/reports?ndbno=\(ndbno)&type=f&format=json&api_key=\(apiKey)", method: .get).validate().responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
completionHandler(json)
print("successful ndbno request")
case .failure(let error):
completionHandler(nil)
print(error)
}
}
}
Your code looks ok only issue I have find with your code is you are not calling the completionHandler in failure part, You need to always call completion block so it will gave you idea have you got response or not as of your completionHandler argument is type of [JSON] as of your not having response in failure part you are not calling completionHandler in it. What you can do is make it optional and call completionHandler with nil argument in case of failure.
func ndbnoRequest(ndbno: String, completionHandler: #escaping (_ results: [JSON]?) -> Void) {
let parameters = ["api_key": "tIgopGnvNSP7YJOQ17lGVwazeYI1TVhXNBA2Et9W", "format": "json", "ndbno": "\(ndbno)"]
Alamofire.request("https://api.nal.usda.gov/ndb/reports/V2", method: .get, parameters: parameters).responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
let ndbnoResults = json["foods"].arrayValue
completionHandler(ndbnoResults)
print("successful ndbno request")
case .failure(let error):
completionHandler(nil)
print("error with ndbno request")
}
}
}
Now call it this way and wrapped the optional in completion block so you can confirm you get response.
requestManager.ndbnoRequest(ndbno: ndbno) { (results) in
if let result = results {
let json = JSON(result)
let fiber = json["food"]["nutrients"][7].dictionaryValue
for (key, value) in fiber {
if key == "value" {
self.numberOfFiberGrams.text = "\(value.stringValue)"
} else {
self.numberOfFiberGrams.text = "Fail"
}
}
}
else {
print("Problem to get response")
}
}
Everything related to UI must be ALWAYS done on the main thread.
So try this:
DispatchQueue.main.async {
let json = JSON(results)
let fiber = json["food"]["nutrients"][7].dictionaryValue
for (key, value) in fiber {
if key == "value" {
self.numberOfFiberGrams.text = "\(value.stringValue)"
} else {
self.numberOfFiberGrams.text = "Fail"
}
}
}
P.S. I agree with Nirav about failure callback - you should handle it too. And I strongly recommend you to give functions and vars more readable and meaningful names, not "ndbnoRequest" and "ndbno". You won't remember what does it mean in few weeks :)