How to bind data to ViewModel for showing it on UI in MVVM? - swift

In my app I am using MVVM pattern.
Below is my Model.
struct NewsModel: Codable {
let status: String
let totalResults: Int
let articles: [Article]
}
struct Article: Codable {
let source: Source
let author: String?
let title: String
let articleDescription: String?
let url: String
let urlToImage: String?
let publishedAt: Date
let content: String?
enum CodingKeys: String, CodingKey {
case source, author, title
case articleDescription = "description"
case url, urlToImage, publishedAt, content
}
}
struct Source: Codable {
let id: String?
let name: String
}
Below is my ViewModel. Which is used for show the data from API.
struct NewsArticleViewModel {
let article: Article
var title:String {
return self.article.title
}
var publication:String {
return self.article.articleDescription!
}
var imageURL:String {
return self.article.urlToImage!
}
}
Below is my API request class.
class Webservice {
func getTopNews(completion: #escaping (([NewsModel]?) -> Void)) {
guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb") else {
fatalError("URL is not correct!!!")
}
URLSession.shared.dataTask(with: url) {
data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
let news = try? JSONDecoder().decode([NewsModel].self, from: data)
DispatchQueue.main.async {
completion(news)
}
}.resume()
}
}
After receiving response from my API I want to show it on screen. For this I added below ViewModel.
class NewsListViewModel: ObservableObject {
#Published var news: [NewsArticleViewModel] = [NewsArticleViewModel]()
func load() {
fetchNews()
}
private func fetchNews() {
Webservice().getTopNews {
news in
if let news = news {
//How to bind this data to NewsArticleViewModel and show it on UI?
}
}
}
}
Please let me know. What I have to write there for showing it on UI.

According to the documentation of newsapi.org your request will return one NewsModel object not an array. So change your Webservice class to:
class Webservice {
//Change the completion handler to return an array of Article
func getTopNews(completion: #escaping (([Article]?) -> Void)) {
guard let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb") else {
fatalError("URL is not correct!!!")
}
URLSession.shared.dataTask(with: url) {
data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
// decode to a single NewsModel object instead of an array
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let news = try? decoder.decode(NewsModel.self, from: data)
DispatchQueue.main.async {
// completion with an optional array of Article
completion(news?.articles)
}
}.resume()
}
}
You would need to map those received values to NewsArticleViewModel types. For example:
Webservice().getTopNews { articles in
if let articles = articles {
self.news = articles.map{NewsArticleViewModel(article: $0)}
}
}
And remove let news: NewsModel from the NewsArticleViewModel struct as it is not needed.
Edit:
It seems:
let publishedAt: Date
is throwing an error. Jsondecoder fails to interpret the string to a date. Change your Webservice. IĀ“ve updated it in my answer.

You could remove the legacy MVVM pattern and do it in proper SwiftUI like this:
struct ContentView: View {
#State private var articles = [Article]()
var body: some View {
NavigationView {
List(articles) { article in
Text(article.title)
}
.navigationTitle("Articles")
}
.task {
do {
let url = URL(string: "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=2bfee85c94e04fc998f65db51ec540bb")!
let (data, _) = try await URLSession.shared.data(from: url)
articles = try JSONDecoder().decode([Article].self, from: data)
} catch {
articles = []
}
}
}
}

Related

What is wrong with this API call for SwiftUI

I have been trying to make an API call with swiftui, but I keep running into threading errors when I run the code. This is the current program:
import SwiftUI
struct Post: Codable, Identifiable {
var id = UUID()
var title: String
var body: String
}
class Api {
func getPosts(){
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return}
URLSession.shared.dataTask(with: url) { data, _, _ in
let posts = try! JSONDecoder().decode([Post].self, from: data!)
print(posts)
}
.resume()
}
}
// Content view file
import SwiftUI
struct PostList: View {
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
.onAppear{
Api().getPosts()
}
}
}
struct PostList_Previews: PreviewProvider {
static var previews: some View {
PostList()
}
}
I got this code verbatim from a swift tutorial, but I am getting errors from it. Any help would be greatly appreciated!
the problem happen because in this line:
let posts = try! JSONDecoder().decode([Post].self, from: data!)
you are making force unwrap try! and swift can't decode your data into [Post] because your model is wrong, change for this:
struct Post: Codable {
var userId: Int
var id: Int
var title: String
var body: String
}
your app will compile, and please avoid to use force unwrap.
To "fix" your error, use var id: Int in your Post model.
Also use the following code, that is more robust than the tutorial code you have been using: see this SO post: Preview Crashed when I get data from api
struct ContentView: View {
#State var posts: [Post] = []
var body: some View {
VStack {
List(posts) { post in
Text(post.title)
}
.onAppear{
Api.shared.getPosts { posts in // <-- here
self.posts = posts
}
}
}
}
}
struct Post: Codable, Identifiable {
var id: Int // <-- here
var title: String
var body: String
}
class Api {
static let shared = Api() // <-- here
func getPosts(completion: #escaping([Post]) -> ()) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return
print("bad url")
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { // <-- here
print("no data") // todo deal with no data
completion([])
return
}
do {
let posts = try JSONDecoder().decode([Post].self, from: data)
DispatchQueue.main.async {
completion(posts)
}
print(posts)
} catch {
print("\(error)")
}
}
.resume()
}
}

Unable to decode a JSON file

I am learning Swift/SwiftUI and I faced a problem. The Xcode's writing this:
"Expected to decode Dictionary<String, CurrencyData> but found an array instead."
Here's a code:
import Foundation
import SwiftUI
struct CurrencyData: Codable {
let r030: Int
let txt: String
let rate: Double
let cc: String
let exchangedate: String
}
typealias Currency = [String: CurrencyData]
import SwiftUI
class API: ObservableObject {
#Published var currencyCode: [String] = []
#Published var priceRate: [Double] = []
#Published var exchangeDate: [String] = []
init() {
fetchdata { (currency) in
switch currency {
case .success(let currency):
currency.forEach { (c) in
DispatchQueue.main.async {
self.currencyCode.append(c.value.cc)
self.priceRate.append(c.value.rate)
self.exchangeDate.append(c.value.exchangedate)
}
}
case(.failure(let error)):
print("Unable to featch the currencies data", error)
}
}
}
func fetchdata(completion: #escaping (Result<Currency,Error>) -> ()) {
guard let url = URL(string: "https://bank.gov.ua/NBUStatService/v1/statdirectory/exchange?json") else { return }
URLSession.shared.dataTask(with: url) { data, responce, error in
if let error = error {
completion(.failure(error))
return
}
guard let safeData = data else { return }
do {
let currency = try JSONDecoder().decode(Currency.self, from: safeData)
completion(.success(currency))
}
catch {
completion(.failure(error))
}
}
.resume()
}
}
Change the type to correctly decode what you receive ( an array obviously )
This should work, but we can't answer for sure since we have no idea of the data. An attached json sample could help.
typealias Currency = [CurrencyData]
Change the exchangeDate in your CurrencyData model to exchangedate which need to match exactly the same as api response. It was case sensitive.

how can I init struct: Codable without default value

I want to get Json from API, and use Codable protocol, but when I init published var, I get an error.
struct Search: Codable {
let result: [String]
}
class SearchViewModel: ObservableObject {
#Published var data = Search()
func loadData(search: String) {
var urlComps = URLComponents(string: getUrl)
let queryItems = [URLQueryItem(name: "result", value: search)]
urlComps!.queryItems = queryItems
let url = urlComps!.url!.absoluteString
guard let Url = URL(string: url) else { return }
URLSession.shared.dataTask(with: Url) { (data, res, err) in
do {
if let data = data {
let decoder = JSONDecoder()
let result = try decoder.decode(Search.self, from: data)
DispatchQueue.main.async {
self.data = result
}
} else {
print("there's no DatašŸ˜­")
}
} catch (let error) {
print("Error!")
print(error.localizedDescription)
}
}.resume()
}
}
Change
#Published var data:Search?

Cannot find 'posts' in scope error in SwiftUI

I'm getting an error saying cannot find 'posts' in scope. I've been trying to check for a solution online however didn't come across which would solve this.
ContentView.swift
struct ContentView: View {
var body: some View {
NavigationView{
List(posts) { //error on this line
post in
Text(post.title)
}
.navigationBarTitle("Hacker News")
}
}
}
struct Results: Decodable {
let hits: [Post]
}
struct Post: Decodable, Identifiable {
var id: String {
return objectId
}
let objectId: String
let title: String
let url: String
let points: Int
}
class NetworkManager {
func fetchData() {
if let url = URL(string: "https://hn.algolia.com/api/v1/search?tags=front_page"){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil{
let decoder = JSONDecoder()
if let safeData = data{
do {
let results=try decoder.decode(Results.self, from: safeData)
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
Cannot find 'posts' in scope
This means that you didn't create any variable called posts.
Also note that your code produces a warning in NetworkManager as well:
Initialization of immutable value 'results' was never used; consider
replacing with assignment to '_' or removing it
This is because the data fetched by NetworkManager isn't used anywhere.
In short, the problem is that NetworkManager isn't connected with / used by ContentView in any way.
A possible solution is to make NetworkManager an ObservableObject and create a #Published property of type [Post]:
class NetworkManager: ObservableObject { // make ObservableObject
#Published var posts = [Post]() // create `posts` here
func fetchData() {
if let url = URL(string: "https://hn.algolia.com/api/v1/search?tags=front_page") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.hits // assign fetched data to `posts`
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
Now you need to use NetworkManager in ContentView:
struct ContentView: View {
#StateObject private var networkManager = NetworkManager() // or `#ObservedObject` for iOS 13
var body: some View {
NavigationView {
List(networkManager.posts) { post in // use `posts` from `networkManager`
Text(post.title)
}
.navigationBarTitle("Hacker News")
}
.onAppear {
networkManager.fetchData() // fetch data when the view appears
}
}
}
Also you have a typo in objectId -> it should be objectID:
struct Post: Decodable, Identifiable {
var id: String {
return objectID
}
let objectID: String
let title: String
let url: String
let points: Int
}
If you don't want to change the name, you can use CodingKeys instead:
struct Post: Decodable, Identifiable {
enum CodingKeys: String, CodingKey {
case objectId = "objectID", title, url, points
}
var id: String {
return objectId
}
let objectId: String
let title: String
let url: String
let points: Int
}

How to fetch data by id Swift

So I'm saving data in my database by id to be retrieved by id using http://localhost:8000/albums/whateverId so that is what I'm trying to do in my Swift app, retrieve data by id, by first retrieving all the ids then trying to implement those ids in my url and fetch my data by id.
Fetch all the data (retrieving ids)
class Webservice {
func getAllPosts(completion: #escaping ([Post]) -> ()) {
guard let url = URL(string: "http://localhost:8000/albums")
else {
fatalError("URL is not correct!")
}
URLSession.shared.dataTask(with: url) { data, _, _ in
let posts = try!
JSONDecoder().decode([Post].self, from: data!); DispatchQueue.main.async {
completion(posts)
}
}.resume()
}
}
Variables
struct Post: Codable, Hashable, Identifiable {
let id: String
let title: String
let path: String
let description: String
}
Set the variables to the data from completion(posts) in class Webservice
final class PostListViewModel: ObservableObject {
init() {
fetchPosts()
}
#Published var posts = [Post]()
private func fetchPosts() {
Webservice().getAllPosts {
self.posts = $0
}
}
}
And Here's How I'm Trying To Get The Data By Id By Using The id From The Data I Just Fetched
Here's how I would fetch the data using the url with a certain id
class SecondWebService: Identifiable {
var id:String = ""
init(id: String?) {
self.id = id!
}
func getAllPostsById(completion: #escaping ([PostById]) -> ()) {
guard let url = URL(string: "http://localhost:8000/albums/\(id)")
else {
fatalError("URL is not correct!")
}
URLSession.shared.dataTask(with: url) { data, _, _ in
let posts = try!
JSONDecoder().decode([PostById].self, from: data!); DispatchQueue.main.async {
completion(posts)
}
}.resume()
}
}
Variables
struct PostById: Codable, Hashable, Identifiable {
let id: String
let name: String
let path: String
}
Iterate through my data from PostListViewModel() from the first section and implement id into my class SecondWebService using the url to get data by id
final class PostListViewByIdModel: ObservableObject {
#ObservedObject var model = PostListViewModel()
init() {
fetchPostsById()
}
#Published var postsById = [PostById]()
private func fetchPostsById() {
for post in model.posts {
SecondWebService(id: post.id).getAllPostsById {
self.postsById = $0
print("ALL THAT \($0)")
}
}
}
}
I feel like this code should work but it's not printing anything.