How to load UI with escaping closures and async calls? - swift

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 :)

Related

delegate method does not get called second time

I am building a simple currency converter app. When ViewController gets opened it calls a function from CoinManager.swift:
class ViewController: UIViewController {
var coinManager = CoinManager()
override func viewDidLoad() {
super.viewDidLoad()
coinManager.delegate = self
coinManager.getCoinPrice(for: "AUD", "AZN", firstCall: true)
}
...
}
CoinManager.swift:
protocol CoinManagerDelegate {
func didUpdatePrice(price1: Double, currency1: String, price2: Double, currency2: String)
func tellTableView(descriptions: [String], symbols: [String])
func didFailWithError(error: Error)
}
struct CoinManager {
var delegate: CoinManagerDelegate?
let baseURL = "https://www.cbr-xml-daily.ru/daily_json.js"
func getCoinPrice (for currency1: String,_ currency2: String, firstCall: Bool) {
if let url = URL(string: baseURL) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
self.delegate?.didFailWithError(error: error!)
return
}
if let safeData = data {
if let coinData = self.parseJSON(safeData) {
if firstCall {
var descriptions = [""]
let listOfCoins = Array(coinData.keys)
for key in listOfCoins {
descriptions.append(coinData[key]!.Name)
}
descriptions.removeFirst()
self.delegate?.tellTableView(descriptions: descriptions, symbols: listOfCoins)
}
if let coinInfo1 = coinData[currency1] {
let value1 = coinInfo1.Value
if let coinInfo2 = coinData[currency2] {
let value2 = coinInfo2.Value
//this line does not do anything the second time I call getCoinPrice:
self.delegate?.didUpdatePrice(price1: value1, currency1: currency1, price2: value2, currency2: currency2)
//And this one does work
print("delegate:\(currency1)")
} else {
print("no name matches currency2")
}
} else {
print("no name matches currency1")
}
}
}
}
task.resume()
}
}
func ParseJSON....
}
The method it calls (ViewController.swift):
extension ViewController: CoinManagerDelegate {
func didUpdatePrice(price1: Double, currency1: String, price2: Double, currency2: String) {
print("didUpdatePrice called")
DispatchQueue.main.async {
let price1AsString = String(price1)
let price2AsString = String(price2)
self.leftTextField.text = price1AsString
self.rightTextField.text = price2AsString
self.leftLabel.text = currency1
self.rightLabel.text = currency2
}
}
...
}
and finally, CurrencyViewController.swift:
var coinManager = CoinManager()
#IBAction func backButtonPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
coinManager.getCoinPrice(for: "USD", "AZN", firstCall: false)
}
So when I launch the app i get following in my debug console:
didUpdatePrice called
delegate:AUD
And when I call getCoinPrice() from CurrencyViewController the delegate method does not get called. I know that my code goes through the delegate function line as I get this in debug console:
delegate:USD
I just can't wrap my head around it. The delegate method does not work when gets called second time. Even though it is called by the same algorithm
It's because you're creating a new object of CoinManager in CurrencyViewController where the delegate is not set. So you've to set the delegate every time you create a new instance of CoinManager.
#IBAction func backButtonPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
coinManager.delegate = self
coinManager.getCoinPrice(for: "USD", "AZN", firstCall: false)
}
Update: So, the above solution would require for you to make the delegate conformance in CurrencyViewController. If you're looking for an alternate solution you should probably pass the instance of coinManager in ViewController to CurrencyViewController. For that here are the things you need to update.
In CurrencyViewController:
class CurrencyViewController: UIViewController {
var coinManager: CoinManager! // you can optional unwrap if you intent to use CurrencyViewController without coinManager
//...
And in ViewController:
currencyViewController.coinManager = coinManager // passing the instance of coinManager
Can you share the full code of CoinManager? I see this part
if firstCall {
...
}
Maybe some block logic here or unhandled cases? And can you share the full code of protocol?
Also try to print something before this code:
if error != nil {
self.delegate?.didFailWithError(error: error!)
return
}

Swift unit testing view model interface

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

Table View Cell not loading

I'm trying to populate custom UITableViewCell inside UITableView but it's not loading the data in the cell. I have created an array of Recipes and populate the array with data from API call. after debugging the code, it seems that the array count is 0 hence not loading the data in the cell although I'm calling getRecipes() method to populate the array.. any idea on what's causing this or how to fix it??
Below is my code:
class MainPageViewController: UIViewController
{
//declare variables
#IBOutlet weak var recipeTableView: UITableView!
var recipes: [Recipe] = []
override func viewDidLoad()
{
recipeTableView.delegate = self
recipeTableView.dataSource = self
self.recipeTableView.reloadData()
recipes = self.getRecipes()
print("array: \(recipes.count)")
super.viewDidLoad()
}
}
//ui table view functions
extension MainPageViewController: UITableViewDataSource, UITableViewDelegate
{
//set the number of items in the table view to the array of objects
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
print("array count: \(recipes.count)")
return recipes.count
}
override func viewDidAppear(_ animated: Bool) {
recipeTableView.reloadData()
super.viewDidAppear(animated)
}
//fetch the data in the array and set it in the table view cells
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
print("code here")
let recipe = self.recipes[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "RecipeCell")
as! UIRecipeCell
print("code here")
cell.setRecipe(recipe: recipe)
return cell;
}
func getRecipes() -> [Recipe]
{
var recipesTemp: [Recipe] = []
// Hit Spoonacular endpoint using Moya as the middleman
let provider = MoyaProvider<SpoonacularAPI>()
provider.request(.getRecipes(limitLicense: true, number: 10, tags: "vegetarian, dessert"))
{
switch $0
{
case .success(let response):
do {
// Only allow successful HTTP codes
_ = try response.filterSuccessfulStatusCodes()
// Parse data as JSON
let json = try JSON(data: response.data)
print("json log: \(json)")
// Parse each recipe's JSON
recipesTemp = json["recipes"].arrayValue.map({ Recipe(json: $0) })
print("array count: \(recipesTemp.count)")
print("array : \(recipesTemp)")
}
catch {
print(error.localizedDescription)
}
case .failure(let error):
print(error.localizedDescription)
}
self.recipeTableView.reloadData()
}
return recipesTemp
recipeTableView.reloadData()
}
}
Your API to get the recipes works asynchronously.
You cannot return anything from a method which contains an asynchronous task.
In viewDidLoad call only the method.
In getRecipes remove the return value, it's pointless.
In the asynchronous closure assign the result to the data source array and reload the table view.
And you might reload the table view on the main thread.
Alternatively use a completion handler.
override func viewDidLoad()
{
recipeTableView.delegate = self
recipeTableView.dataSource = self
getRecipes()
}
func getRecipes()
{
// Hit Spoonacular endpoint using Moya as the middleman
let provider = MoyaProvider<SpoonacularAPI>()
provider.request(.getRecipes(limitLicense: true, number: 10, tags: "vegetarian, dessert"))
{
switch $0
{
case .success(let response):
do {
// Only allow successful HTTP codes
_ = try response.filterSuccessfulStatusCodes()
// Parse data as JSON
let json = try JSON(data: response.data)
print("json log: \(json)")
// Parse each recipe's JSON
self.recipes = json["recipes"].arrayValue.map({ Recipe(json: $0) })
print("array count: \(recipesTemp.count)")
print("array : \(recipesTemp)")
DispatchQueue.main.async {
self.recipeTableView.reloadData()
}
}
catch {
print(error.localizedDescription)
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
And be aware you can not do something with recipes at the end of viewDidLoad.
the issue here is that your request function return a result before the actual request hit the response
You should modify the logic with a completion handler instead
func getRecipes(complition: #escaping ((_ response: [Recipe]) -> ())) {
// Your request logic
// onSuccess return the array like this
// complition(resultArray)
}

RXSwift + Moya + Error Handling + Refresh Button

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)
}
}

My double-closure function is not working

I have two async function which send requests to my server.
DispatchQueue.global(qos: .userInitiated).async {
weak var weakself = self
self.unregisterPushServer(token: token!) { [weak self] success in
print("0")
if success {
print("1")
weakself?.unregisterPushInfoKeychain(token: token!)
print("2")
if let this = self {
print("PLEASE")
weakself?.registerPushServer(token: token!) { [weak this] success in
print("3")
if success {
print("4")
this?.registerPushInfoKeychain()
print("5")
}
}
}
print("success")
}
}
}
And the functions are
private func registerPushServer(token: String, completion: #escaping (Bool) -> ()) {
request() { (data, error) in
if data != nil {
completion(true)
} else {
completion(false)
}
}
private func unregisterPushServer(token: String, completion: #escaping (Bool) -> ()) {
request2() { (data, error) in
if data != nil {
completion(true)
} else {
completion(false)
}
}
But in console,
0
1
2
success
not seemed to executes codes after my PLEASE sign.
Why is my code is not working?
I first thought that the problem was about the queue, but it was not.
You don't need this line:
weak var weakself = self
By including [weak self] in the closure's capture list, self automatically becomes weak.
Try and replace the instances of weakself with just self.
I'm also thinking you may not even need the if let this = self condition.
I hope this helps.
OK, the problem was not in this code.
When I call this function, I did it like this.
func messaging(_ messaging: Messaging, didRefreshRegistrationToken fcmToken: String) {
print("Firebase registration token: \(fcmToken)")
let pushService = PushService()
pushService.updateRegistrationStatus(token: fcmToken)
}
the
pushService.updateRegistrationStatus(token: fcmToken)
was the function which contains the code I asked above.
In this situation, the function updateRegistrationStatus is not guaranteed because pushService itself is released by ARC when messaging(...) function block is end.
class AppDelegate: UIResponder, UIApplicationDelegate {
let pushService = PushService()
...
func messaging(_ messaging: Messaging, didRefreshRegistrationToken fcmToken: String) {
print("Firebase registration token: \(fcmToken)")
self.pushService.updateRegistrationStatus(token: fcmToken)
}
}
Now the pushService object is not released because it is declared as a global variable.