I am unable to decode my JSON data into my model. This is my first time attempting to use MVVM pattern and for some reason I cannot figure out why I cannot get a reference to my model variables such as title or id. Besides the error shown below I also get a message saying 'This property is defined on Movie and may not be available in this context'
class MovieViewModel {
let movie: APIResponse
init(movie: APIResponse) {
self.movie = movie
}
var title: String {
return movie.data.title // Value of type [Movie] has no member 'title'
}
}
struct APIResponse: Codable {
var data: [Movie]
}
struct Movie: Codable {
var id: String
var title: String
}
class MovieListViewModel {
private var movieViewModels = [MovieViewModel]()
func addMovieViewModels(_ vm: MovieViewModel) {
movieViewModels.append(vm)
}
func numberOfRows(_ section: Int) -> Int {
return movieViewModels.count
}
func modelAtIndex(_ Index: Int) -> GiphyViewModel {
return movieViewModels[Index]
}
}
I think you should seperate API calls from the view model.
You can take example on this repo to know how to proceed.
Related
I'm trying to instantiate my viewmodel for testing, in this case I don't need its parameters, but as it asks me to add them, when I try I get an error "Constant 'data' used before being initialized"
This is my code:
struct Account: Codable {
let details: [Details]?
}
struct Details: Codable {
let id: String?
let currency: String?
let interest: Float64?
let date: String?
}
class DetailViewModel {
private let data: Details?
init(data: Details) {
self.data = data
}
}
Tests:
class DetailViewModelTest: QuickSpec {
override func spec() {
var viewModel : DetailViewModel!
let data: Details!
viewModel = DetailViewModel(data: data) // I have error in this line
}
}
Is there a way to instantiate my viewmodel without parameters? Or a better way to handle this?
To use Details in a test with hardcoded values you either need to create it from some json or add another init to initialise all values, here I am using the latter. I am adding it in an extension and created a static method that uses the init to create an object with hard coded values.
extension Details {
private init(id: String, currency: String, interest: Float64, date: String) {
self.id = id
self.currency = currency
self.interest = interest
self.date = date
}
static func createStub() -> Details {
Details(id: "1", currency: "EUR", interest: 1.23, date: "2022-02-12")
}
}
This is one way of doing it, the init could be designed in many ways but this is to show you how to move forward.
This can then be used in the test class
class DetailViewModelTest: QuickSpec {
override func spec() {
let viewModel = DetailViewModel(data: Details.createStub())
//...
}
}
you should:
let data: Details = Details() // create your data
I have the following code. When I try to build the app I get the following error:
Cannot convert return expression of type '[Post]' to return type '[T]'
struct Post: Codable {
let id: Int
let title: String
let body: String
}
class Loader {
func load<T: Codable>(name: String) -> [T] {
return [Post]()
}
}
Any ideas why?
UPDATE:
THIS WORKS:
class PreviewData {
func load<T: Codable>(name: String) -> [T]? {
return [Post]() as? [T]
}
}
You basically says my load method gonna return a generic array named T but you are trying to return Array of Post model , just change :
func load<T: Codable>(name: String) -> [T] {
return [T]()
}
One API key can only make 100 requests per day. So one API key can't handle a lot of requests per day. There are other ways to solve this problem, but I would like to solve this problem by entering various API keys. For example, if the first API key makes 100 requests and the request value returns as an error, I want to add a function that automatically moves to the second API key.
Can you tell me how to make it with Rxswift?
I would appreciate any help you can provide.
The code is as below.
private func loadTopNews() {
let resource = Resource<ArticleResponse>(url: URL(string: "https://newsapi.org/v2/top-headlines?country=\(selectedLanguagesCode[0])&sortBy=%20popularity&apiKey=\(apiKey[0])")!)
URLRequest.load(resource: resource)
.subscribe(onNext: { articleResponse in
let topArticle = articleResponse.articles.first
self.articleVM = ArticleViewModel(topArticle!)
}).disposed(by: disposeBag)
}
struct Resource<T: Decodable> {
let url: URL
}
extension URLRequest {
static func load<T>(resource: Resource<T>) -> Observable<T> {
return Observable.just(resource.url)
.flatMap { url -> Observable<Data> in
let request = URLRequest(url: url)
return URLSession.shared.rx.data(request: request)
}.map { data -> T in
return try JSONDecoder().decode(T.self, from: data)
}
}
}
struct ArticleResponse: Decodable {
let articles: [Article]
}
struct Article: Decodable {
let title: String
let publishedAt: String
let urlToImage: String?
let url: String
}
struct ArticleListViewModel {
let articlesVM: [ArticleViewModel]
}
extension ArticleListViewModel {
init(_ articles: [Article]) {
self.articlesVM = articles.compactMap(ArticleViewModel.init)
}
}
extension ArticleListViewModel {
func articleAt(_ index: Int) -> ArticleViewModel {
return self.articlesVM[index]
}
}
struct ArticleViewModel {
let article: Article
init(_ article: Article) {
self.article = article
}
}
extension ArticleViewModel {
var title: Observable<String> {
return Observable<String>.just(article.title)
}
var publishedAt: Observable<String> {
return Observable<String>.just(article.publishedAt)
}
var urlToImage: Observable<String> {
return Observable<String>.just(article.urlToImage ?? "NoImage")
}
var url: Observable<String> {
return Observable<String>.just(article.url)
}
}
I wrote an article covering this very thing (albeit in a different context): RxSwift and Handling Invalid Tokens
The above article will help if you are making multiple requests at the same time and need to restart all of them with the new token. It might be overkill in this specific case.
To solve this problem, you need:
A function that will build a resource with a given api key
An Observable that emits a different API key whenever it's subscribed to.
Once you have those two pieces, you can just retry your subscription until one of the keys works.
Solution
I suggest you use the above as clues and try to solve the problem yourself. Then you can check your answer against the solution below...
For item 1, I see that you need two arguments to create a resource. So I suggest making a function factory that will produce a function that takes an apiKey. Like this:
func makeResource(selectedLanguagesCode: String) -> (String) -> Resource<ArticleResponse> {
{ apiKey in
Resource<ArticleResponse>(url: URL(string: "https://newsapi.org/v2/top-headlines?country=\(selectedLanguagesCode)&sortBy=%20popularity&apiKey=\(apiKey)")!)
}
}
Note that this function is not part of the class. It doesn't need self.
For item 2, we need a function that takes the array of apiKeys and produces an Observable that will emit a different key each time it's subscribed to:
Something like this should work:
func produceApiKey(apiKeys: [String]) -> Observable<String> {
var index = 0
return Observable.create { observer in
observer.onNext(apiKeys[index % apiKeys.count])
observer.onCompleted()
index += 1
return Disposables.create()
}
}
Again, this function doesn't need self so it's not part of the class.
Now that you have these two elements, you can use them in your loadTopNews() method. Like this:
private func loadTopNews() {
produceApiKey(apiKeys: apiKey)
.map(makeResource(selectedLanguagesCode: selectedLanguagesCode[0]))
.flatMap(URLRequest.load(resource:))
.retry(apiKey.count - 1)
.subscribe(onNext: { articleResponse in
let topArticle = articleResponse.articles.first
self.articleVM = ArticleViewModel(topArticle!)
})
.disposed(by: disposeBag)
}
I am trying to display some json data inside my tableView cell, and parsed the json data.
But
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.contents.count
}
returning 0
and as a result, I cannot display the data inside the UITableViewCell eventhough I have all my JSON data have been parsed and ready to be displayed.. .
how to fix it?
my response Model class and the rest of implementation are as follows:
Model classes for json response
// MARK: - TradingPairElement
class TradingPair: Entity {
var id: Int?
//var name: String?
var quoteAsset: QuoteAsset?
}
enum QuoteAsset: String, Codable {
case btc = "BTC"
case krw = "KRW"
}
// MARK: - TickerByPair
class TickerByPair: Entity {
var ask: Int?
//var price: Double?
//var volume: Double?
var askVolume: Double?
var bid: Int?
var bidVolume: Double?
var time: String?
}
And a wrapper class for the above two class 's contents:
class Entity: Codable {
var name: String?
var price: Double?
var volume: Double?
}
and here is how i am getting the data from the api and assigning to my self variables:
func APIcall() {
ServerCommunicator.getPairs().done{ response -> Void in
for keyPathParam in response {
self.keyPathParam = keyPathParam.name
}
ServerCommunicator.getPair(with: self.keyPathParam).done{ ticker -> Void in
for data in self.contents {
data.name = ticker.name
data.price = ticker.price
data.volume = ticker.volume
self.contents.append(data)
}
}.catch{(err) in
print(err)
}
}.catch{(error) in
print(error)
}
}
First of all if the API sends always all keys declare the properties non-optional and as constants and most likely you don't need a class and conformance to Encodable
struct Entity: Decodable {
let name: String
let price: Double
let volume: Double
}
After getting the data from the server you have to create new instances of Entity and assign them to the data source array. Further you need DispatchGroup to handle the loop and reload the table view after the last entity has been created.
If you want to overwrite self.contents with the received data uncomment the removeAll line
func APIcall() {
ServerCommunicator.getPairs().done{ response in
// self.contents.removeAll()
let group = DispatchGroup()
for keyPathParam in response {
group.enter()
ServerCommunicator.getPair(with: keyPathParam.name).done{ ticker in
let entity = Entity(name: ticker.name, price: ticker.price, volume: ticker.volume)
self.contents.append(entity)
group.leave()
}.catch{(err) in
print(err)
group.leave()
}
}
group.notify(queue: .main) {
self.tableView.reloadData()
}
}.catch{(error) in
print(error)
}
}
Consider the following class:
class CalculatorButton: CalculatorButtonProtocol, CustomStringConvertible {
var type: CalculatorButtonType
var description: String {
return label
}
let label: String
init(_ label: String, type: CalculatorButtonType) {
self.label = label
self.type = type
}
func action(n1: String, n2: String?) -> String {
fatalError("Not implemented")
}
func format(_ n: Float) -> String {
if n == floor(n) {
return String(Int(n))
}
return String(n)
}
}
While debugging using breakpoints, it's very useful to see a string representation of a class in the debugger window. For the above, I'd like to see the label. However, Xcode shows the reference of the button instance instead. People are saying that adopting CustomStringConvertible protocol gives a class human-readable representation, but that didn't help.
GitHub link: https://github.com/asarkar/ios-bootcamp/tree/master/Calculator-SwiftUI
Make your class implement CustomDebugStringConvertible, this will print your custom representation instead of the instance address.
class CalculatorButton: CustomDebugStringConvertible {
// ...
var debugDescription: String {
label
}
}