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.
Related
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 = []
}
}
}
}
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
}
Im trying to do a simple SwiftUI App to fetch recent movies premiers
Trying to follow MVVM and found a couple of tutorials to make the Api calls but they put the Api calls on the model
So i so try to make a Service class And a State class to handle my #Observed objects the problem is when I try to see the movie details the details don't load, but when I try another movie the details are of the last movie
You can see the bug here
This is my Movies Service
public class MoviesService {
private let apiKey = "?api_key=" + "xxx"
private let baseAPIURL = "https://api.themoviedb.org/3/movie/"
private let language = "&language=" + "es-MX"
var nextPageToLoad = 1
var nowPlayingMovies = [Movie]()
var movieDetail : MovieDetail?
init() {
loadNowPlaying()
}
func loadNowPlaying(){
let urlString = "\(baseAPIURL)now_playing\(apiKey)\(language)&page=\(nextPageToLoad)"
print(urlString)
let url = URL(string: urlString)!
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request, completionHandler:parseMovies(data:response:error:))
task.resume()
}
func parseMovies(data: Data?, response: URLResponse?, error: Error?){
var NowPlayingMoviesResult = [Movie]()
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(NowPlaying.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async { [self] in
// update our UI
NowPlayingMoviesResult = decodedResponse.results!
self.nextPageToLoad += 1
for movie in NowPlayingMoviesResult {
nowPlayingMovies.append(movie)
}
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}
func loadDetailMovie(id : Int) {
let urlString = String("\(baseAPIURL)\(id)\(apiKey)\(language)")
let url = URL(string: urlString)!
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request, completionHandler:parseDetailMovie(data:response:error:))
task.resume()
}
func parseDetailMovie(data: Data?, response: URLResponse?, error: Error?){
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(moviesApp.MovieDetail.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
self.movieDetail = decodedResponse
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}
}
This is my State class
class StateController: ObservableObject, RandomAccessCollection {
typealias Element = Movie
#Published var movies = [Movie]()
#Published var movie : MovieDetail?
private let moviesService = MoviesService()
func shouldLoadMoreData(item : Movie? = nil) -> Bool {
if item == movies.last {
return true
}
return false
}
func reloadMovies(item : Movie? = nil){
DispatchQueue.main.async {
if self.shouldLoadMoreData(item: item) {
self.moviesService.loadNowPlaying()
}
self.movies = self.moviesService.nowPlayingMovies
}
}
func loadMovieDetails(id: Int){
DispatchQueue.main.async {
self.moviesService.loadDetailMovie(id: id)
self.movie = self.moviesService.movieDetail
}
}
var startIndex: Int { movies.startIndex }
var endIndex: Int { movies.endIndex }
subscript(position: Int ) -> Movie {
return movies[position]
}
}
And this is My Movie Detail View
import SwiftUI
struct MovieDetailView: View {
#EnvironmentObject private var stateController: StateController
#State var id : Int
var body: some View {
DetailMovieContent(movie: $stateController.movie)
.onAppear{
stateController.loadMovieDetails(id: id)
}
}
}
So this is my questions
Im doing a good Approach?
Whats the right way of make and Api call using MVVM and SwiftUI?
The full App is here
I have a simple user profile model which is returned as a single node from a JSON API.
(Model) UserProfile.swift
struct UserProfile: Codable, Identifiable {
let id: Int
var name: String
var profile: String
var image: String?
var status: String
var timezone: String
}
(Service) UserProfileService.swift
class UserProfileService {
func getProfile(completion: #escaping(UserProfile?) -> ()) {
guard let url = URL(string: "https://myapi.com/profile") else {
completion(nil)
return
}
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
do {
let profile = try JSONDecoder().decode(UserProfile.self, from: data)
DispatchQueue.main.async {
completion(profile)
}
} catch {
print("ERROR: ", error)
}
}.resume()
}
}
(View Model) UserProfileViewModel.swift
class UserProfileRequestViewModel: ObservableObject {
#Published var profile = UserProfile.self
init() {
fetchProfile()
}
func fetchProfile() {
UserProfileService().getProfile { profile in
if let profile = profile {
self.profile = UserProfileViewModel.init(profile: profile)
}
}
}
}
class UserProfileViewModel {
var profile: UserProfile
init(profile: UserProfile) {
self.profile = profile
}
var id: Int {
return self.profile.id
}
}
Could someone please tell what I need to put instead self.profile = UserProfileViewModel.init(profile: profile) above as that results in the error "Cannot assign value of type 'UserProfileViewModel' to type 'UserProfile.Type'"?
If I have a loop of data, then there is no issue looping over this like below but how do I handle a single node?
if let videos = videos {
self.videos = videos.map(VideoViewModel.init)
}
Seems your UserProfileService().getProfile already return UserProfile type so you maybe need to
UserProfileService().getProfile { profile in
if let profile = profile {
self.profile = profile
}
}
and
#Published var profile : UserProfile?
Working version with the correct View Model, so adding this for others that might have the same issues!
(View Model) UserProfileViewModel.swift
class UserProfileViewModel: ObservableObject {
#Published var profile: UserProfile?
init() {
fetchProfile()
}
func fetchProfile() {
UserProfileService().getProfile { profile in
self.profile = profile
}
}
var name: String {
return self.profile?.name ?? "Name"
}
}
When I tried to use SwiftUI & Combine to download image asynchrously, it works fine. Then, I try to implement this into a dynamic list, and I found out there is only one row(the last row) will be show correctly, images in other cells are missing. I have trace the code with breakpoints and I'm sure the image download process is success in others, but only the last row will trigger the #ObjectBinding to update image. Please check my sample code and let me know if there's any wrong. Thanks!
struct UserView: View {
var name: String
#ObjectBinding var loader: ImageLoader
init(name: String, loader: ImageLoader) {
self.name = name
self.loader = loader
}
var body: some View {
HStack {
Image(uiImage: loader.image ?? UIImage())
.onAppear {
self.loader.load()
}
Text("\(name)")
}
}
}
struct User {
let name: String
let imageUrl: String
}
struct ContentView : View {
#State var users: [User] = []
var body: some View {
NavigationView {
List(users.identified(by: \.name)) { user in
UserView(name: user.name, loader: ImageLoader(with: user.imageUrl))
}
.navigationBarTitle(Text("Users"))
.navigationBarItems(trailing:
Button(action: {
self.didTapAddButton()
}, label: {
Text("+").font(.system(size: 36.0))
}))
}
}
func didTapAddButton() {
fetchUser()
}
func fetchUser() {
API.fetchData { (user) in
self.users.append(user)
}
}
}
class ImageLoader: BindableObject {
let didChange = PassthroughSubject<UIImage?, Never>()
var urlString: String
var task: URLSessionDataTask?
var image: UIImage? = UIImage(named: "user") {
didSet {
didChange.send(image)
}
}
init(with urlString: String) {
print("init a new loader")
self.urlString = urlString
}
func load() {
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
if error == nil {
DispatchQueue.main.async {
self.image = UIImage(data: data!)
}
}
}
task.resume()
self.task = task
}
func cancel() {
if let task = task {
task.cancel()
}
}
}
class API {
static func fetchData(completion: #escaping (User) -> Void) {
let request = URLRequest(url: URL(string: "https://randomuser.me/api/")!)
let task = URLSession.shared.dataTask(with: request) { (data, _, error) in
guard error == nil else { return }
do {
let json = try JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any]
guard
let results = json!["results"] as? [[String: Any]],
let nameDict = results.first!["name"] as? [String: String],
let pictureDict = results.first!["picture"] as? [String: String]
else { return }
let name = "\(nameDict["last"]!) \(nameDict["first"]!)"
let imageUrl = pictureDict["thumbnail"]
let user = User(name: name, imageUrl: imageUrl!)
DispatchQueue.main.async {
completion(user)
}
} catch let error {
print(error.localizedDescription)
}
}
task.resume()
}
}
every images should be downloaded successfully no matter how many items in the list.
There seems to be a bug in #ObjectBinding. I am not sure and I cannot confirm yet. I want to create a minimal example code to be sure, and if so, report a bug to Apple. It seems that sometimes SwiftUI does not invalidate a view, even if the #ObjectBinding it is based upon has its didChange.send() called. I posted my own question (#BindableObject async call to didChange.send() does not invalidate its view (and never updates))
In the meantime, I try to use EnvironmentObject whenever I can, as the bug doesn't seem to be there.
Your code then works with very few changes. Instead of using ObjectBinding, use EnvironmentObject:
Code Replacing #ObjectBinding with #EnvironmentObject:
import SwiftUI
import Combine
struct UserView: View {
var name: String
#EnvironmentObject var loader: ImageLoader
init(name: String) {
self.name = name
}
var body: some View {
HStack {
Image(uiImage: loader.image ?? UIImage())
.onAppear {
self.loader.load()
}
Text("\(name)")
}
}
}
struct User {
let name: String
let imageUrl: String
}
struct ContentView : View {
#State var users: [User] = []
var body: some View {
NavigationView {
List(users.identified(by: \.name)) { user in
UserView(name: user.name).environmentObject(ImageLoader(with: user.imageUrl))
}
.navigationBarTitle(Text("Users"))
.navigationBarItems(trailing:
Button(action: {
self.didTapAddButton()
}, label: {
Text("+").font(.system(size: 36.0))
}))
}
}
func didTapAddButton() {
fetchUser()
}
func fetchUser() {
API.fetchData { (user) in
self.users.append(user)
}
}
}
class ImageLoader: BindableObject {
let didChange = PassthroughSubject<UIImage?, Never>()
var urlString: String
var task: URLSessionDataTask?
var image: UIImage? = UIImage(named: "user") {
didSet {
didChange.send(image)
}
}
init(with urlString: String) {
print("init a new loader")
self.urlString = urlString
}
func load() {
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
if error == nil {
DispatchQueue.main.async {
self.image = UIImage(data: data!)
}
}
}
task.resume()
self.task = task
}
func cancel() {
if let task = task {
task.cancel()
}
}
}
class API {
static func fetchData(completion: #escaping (User) -> Void) {
let request = URLRequest(url: URL(string: "https://randomuser.me/api/")!)
let task = URLSession.shared.dataTask(with: request) { (data, _, error) in
guard error == nil else { return }
do {
let json = try JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any]
guard
let results = json!["results"] as? [[String: Any]],
let nameDict = results.first!["name"] as? [String: String],
let pictureDict = results.first!["picture"] as? [String: String]
else { return }
let name = "\(nameDict["last"]!) \(nameDict["first"]!)"
let imageUrl = pictureDict["thumbnail"]
let user = User(name: name, imageUrl: imageUrl!)
DispatchQueue.main.async {
completion(user)
}
} catch let error {
print(error.localizedDescription)
}
}
task.resume()
}
}