Where to make an API call to populate a view with data - ios

I am trying to create a view that displays results from an API call, however I keep on running into multiple errors.
My question is basically where is the best place to make such an API call.
Right now I am "trying" to load the data in the "init" method of the view like below.
struct LandingView: View {
#StateObject var viewRouter: ViewRouter
#State var user1: User
#State var products: [Product] = []
init(_ viewRouter : ViewRouter, user: User) {
self.user1 = user
_viewRouter = StateObject(wrappedValue: viewRouter)
ProductAPI().getAllProducts { productArr in
self.products = productArr
}
}
var body: some View {
tabViewUnique(prodArrParam: products)
}
}
I keep on getting an "escaping closure mutating self" error, and while I could reconfigure the code to stop the error,I am sure that there is a better way of doing what I want.
Thanks

struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
}
// this one onAppear you can use it
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
}
In .onAppear you can make api calls

Related

Issue passing data from API call in SwiftUI MVVM pattern

been going back and forth for 2 days trying to figure this out before posting and still hitting a wall.
Created an API specific class, a ViewModel, and a View and trying to shuttle data back and forth and while I see the API call is successful and I decode it without issue on logs, it never reflects on the UI or View.
As far as I see I appear to be trying to access the data before it's actually available. All help greatly appreciated!
API Class:
import Combine
import Foundation
class CrunchbaseApi:ObservableObject
{
#Published var companies:[Company] = [Company]()
#Published var singleCompany:Company?
func retrieve(company:String) async
{
let SingleEntityURL:URL = URL(string:"https://api.crunchbase.com/api/v4/entities/organizations/\(company)?card_ids=fields&user_key=**********REMOVED FOR SECURITY*****************")!
let task = URLSession.shared.dataTask(with:SingleEntityURL){ data, response, error in
let decoder = JSONDecoder()
if let data = data{
do {
self.singleCompany = try decoder.decode(Company.self, from: data)
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
func retrieveCompanyList()
{
//declare
}
}
ViewModel:
import Combine
import Foundation
class CompanyViewModel: ObservableObject
{
var crunchbase:CrunchbaseApi = CrunchbaseApi()
#Published var singleCompany:Company?
func retrieveCompany(company:String) async
{
await self.crunchbase.retrieve(company: company)
self.singleCompany = crunchbase.singleCompany
}
}
View:
import SwiftUI
struct CompanyView: View
{
#State var companyViewModel:CompanyViewModel = CompanyViewModel()
var body: some View
{
NavigationView
{
VStack
{
Text("Company ID: \(companyViewModel.singleCompany?.id ?? "NOTHING")")
// Text("Company Name: \(companyViewModel.companyName)")
// Text("Company Summary: \(companyViewModel.companyDescription)")
// Text("Logo URL: \(companyViewModel.companyLogoURL)")
}.navigationTitle("Company")
}
}
}
Your assumption about accessing the data to early is correct. But there are more things going on here.
just declaring a function async like your retrieve func doesn´t make it async.
using a nested Observable class with #Published will not update the view
Observable classes should have either an #StateObject or an #ObservableObject property wrapper. Depending on if the class is injected or created in the view
Possible solution:
Move the function into the viewmodel:
class CompanyViewModel: ObservableObject
{
#Published var singleCompany:Company?
func retrieve(company:String)
{
let SingleEntityURL:URL = URL(string:"https://api.crunchbase.com/api/v4/entities/organizations/\(company)?card_ids=fields&user_key=**********REMOVED FOR SECURITY*****************")!
let task = URLSession.shared.dataTask(with:SingleEntityURL){ data, response, error in
let decoder = JSONDecoder()
if let data = data{
do {
self.singleCompany = try decoder.decode(Company.self, from: data)
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
}
Change the View to hold the viewmodel as #StateObject, also add an .onApear modifier to load the data:
struct CompanyView: View
{
#StateObject var companyViewModel:CompanyViewModel = CompanyViewModel()
var body: some View
{
NavigationView
{
VStack
{
Text("Company ID: \(companyViewModel.singleCompany?.id ?? "NOTHING")")
// Text("Company Name: \(companyViewModel.companyName)")
// Text("Company Summary: \(companyViewModel.companyDescription)")
// Text("Logo URL: \(companyViewModel.companyLogoURL)")
}.navigationTitle("Company")
.onAppear {
companyViewModel.retrieve(company: "whatever")
}
}
}
}

How to pass data to a sub view from ContentView in SwiftUI

Forgive me if this doesn't make sense, I am a total beginner at Swift. I am creating a recipe app that pulls data from an API and lists it out in navigation links. When the user clicks on the recipe I want it to move to sub view and display information from the API such as recipe name, image, ingredients, and have a button with a link to the webpage.
I was able to get the data pulled into the list with navigation links. However, now I do not know how to go about setting up the recipe details sub view with all of the information I listed above.
This is where I call the API:
class RecipeService {
func getRecipes(_ completion: #escaping (Result<[Recipe], Error>) -> ()) {
let url = URL(string: "http://www.recipepuppy.com/api")!
URLSession.shared.dataTask(with: url) { (data, _, error) in
if let error = error {
return completion(.failure(error))
}
guard let data = data else {
return completion(.failure(NSError(domain: "", code: -1, userInfo: nil)))
}
do {
let response = try JSONDecoder().decode(RecipesResponses.self, from: data)
completion(.success(response.results))
} catch {
completion(.failure(error))
}
}.resume()
}
}
This is where I take in the recipe responses:
struct RecipesResponses: Codable {
let title: String
let version: Double
let href: String
let results: [Recipe]
}
struct Recipe: Codable {
let title, href, ingredients, thumbnail: String
var detail: URL {
URL(string: href)!
}
var thumb: URL {
URL(string: thumbnail)!
}
}
This is my recipe ViewModel:
class RecipeViewModel: ObservableObject {
#Published var recipes = [Recipe]()
#Published var isLoading = false
private let service = RecipeService()
init() {
loadData()
}
private func loadData() {
isLoading = true
service.getRecipes{ [weak self] result in
DispatchQueue.main.async {
self?.isLoading = false
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let recipes):
self?.recipes = recipes
}
}
}
}
}
This is my view where I list out the API responses:
struct ListView: View {
#ObservedObject var viewModel = RecipeViewModel()
var body: some View {
NavigationView {
List(viewModel.recipes, id: \.href) { recipe in
NavigationLink (destination: RecipeDetailView()) {
HStack{
CachedImageView(recipe.thumb)
.mask(Circle())
.frame(width: 80)
VStack(alignment: .leading) {
Text(recipe.title)
.font(.largeTitle)
.foregroundColor(.black)
.padding()
}
}
.buttonStyle(PlainButtonStyle())
}
}
.navigationBarTitle(Text("All Recipes"))
}
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
This is the view where I would like to list out the recipe details and link to the webpage. This is where I am struggling to be able to pull the API data into:
struct RecipeDetailView: View {
#ObservedObject var viewModel = RecipeViewModel()
var body: some View {
Text("Detail View")
}
}
struct RecipeDetailView_Previews: PreviewProvider {
static var previews: some View {
RecipeDetailView()
}
}
Images of app
You can change RecipeDetailView to accept a Recipe as a parameter:
struct RecipeDetailView: View {
var recipe : Recipe
var body: some View {
Text(recipe.title)
Link("Webpage", destination: recipe.detail)
//etc
}
}
Then, in your NavigationLink, pass the Recipe to it:
NavigationLink(destination: RecipeDetailView(recipe: recipe)) {
One thing I'd warn you about is force unwrapping the URLs in Recipe using ! -- you should know that if you ever get an invalid/malformed URL, this style of unwrapping will crash the app.
Update, to show you what the preview might look like:
struct RecipeDetailView_Previews: PreviewProvider {
static var previews: some View {
RecipeDetailView(recipe: Recipe(title: "Recipe name", href: "https://google.com", ingredients: "Stuff", thumbnail: "https://linktoimage.com"))
}
}

WebImage not updating the View - SwiftUI (Xcode 12)

My problem is that when I change my observed objects string property to another value, it updates the JSON Image values printing out the updated values(Using the Unsplash API), but the WebImage (from SDWebImageSwiftUI) doesn't change.
The struct that Result applies to:
struct Results : Codable {
var total : Int
var results : [Result]
}
struct Result : Codable {
var id : String
var description : String?
var urls : URLs
}
struct URLs : Codable {
var small : String
}
Here is the view which includes the webImage thats supposed to update:
struct MoodboardView: View {
#ObservedObject var searchObjectController = SearchObjectController.shared
var body: some View {
List {
VStack {
Text("Mood Board: \(searchObjectController.searchText)")
.fontWeight(.bold)
.padding(6)
ForEach(searchObjectController.results, id: \.id, content: { result in
Text(result.description ?? "Empty")
WebImage(url: URL(string: result.urls.small) )
.resizable()
.frame(height:300)
})
}.onAppear() {
searchObjectController.search()
}
}
}
}
Here is the class which does the API Request:
class SearchObjectController : ObservableObject {
static let shared = SearchObjectController()
private init() {}
var token = "gQR-YsX0OpwkYpbjhPVi3b4kSR-DtWrR5phwDm2kPMM"
#Published var results = [Result]()
#Published var searchText : String = "forest"
func search () {
let url = URL(string: "https://api.unsplash.com/search/photos?query=\(searchText)")
var request = URLRequest(url: url!)
request.httpMethod = "GET"
request.setValue("Client-ID \(token)", forHTTPHeaderField: "Authorization")
print("request: \(request)")
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {return}
print(String(data: data, encoding: .utf8)!)
do {
let res = try JSONDecoder().decode(Results.self, from: data)
DispatchQueue.main.async {
self.results.append(contentsOf: res.results)
}
//print(self.results)
} catch {
print("catch: \(error)")
}
}
task.resume()
}
}
Here is how I change the value of the searchText in a Button, if you would like to see:
struct GenerateView: View {
#ObservedObject var searchObjectController = SearchObjectController.shared
#State private var celsius: Double = 0
var body: some View {
ZStack {
Color.purple
.ignoresSafeArea()
VStack{
Text("Generate a Random Idea")
.padding()
.foregroundColor(.white)
.font(.largeTitle)
.frame(maxWidth: .infinity, alignment: .center)
Image("placeholder")
Slider(value: $celsius, in: -100...100)
.padding()
Button("Generate") {
print("topic changed to\(searchObjectController.searchText)")
searchObjectController.searchText.self = "tables"
}
Spacer()
}
}
}
}
It turns out you update the search text but don't search again. See this does the trick:
Button("Generate") {
print("topic changed to\(searchObjectController.searchText)")
searchObjectController.searchText.self = "tables" // you changed the search text but didnt search again
self.searchObjectController.search() // adding this does the trick
}
Also. I updated your code to use an EnvironmentObject. If you use one instance of an object throughout your app. Consider making it an EnvironmentObject to not have to pass it around all the time.
Adding it is easy. Add it to your #main Scene
import SwiftUI
#main
struct StackoverflowApp: App {
#ObservedObject var searchObjectController = SearchObjectController()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(self.searchObjectController)
}
}
}
And using it even simpler:
struct MoodboardView: View {
// Env Obj. so we reference only one object
#EnvironmentObject var searchObjectController: SearchObjectController
var body: some View {
Text("")
}
}
Here is your code with the changes and working as expected:
I added comments to the changes I made
struct ContentView: View {
var body: some View {
MoodboardView()
}
}
struct GenerateView: View {
#EnvironmentObject var searchObjectController: SearchObjectController
#State private var celsius: Double = 0
var body: some View {
ZStack {
Color.purple
.ignoresSafeArea()
VStack{
Text("Generate a Random Idea")
.padding()
.foregroundColor(.white)
.font(.largeTitle)
.frame(maxWidth: .infinity, alignment: .center)
Image("placeholder")
Slider(value: $celsius, in: -100...100)
.padding()
Button("Generate") {
print("topic changed to\(searchObjectController.searchText)")
searchObjectController.searchText.self = "tables" // you changed the search text but didnt search again
self.searchObjectController.search() // adding this does the trick
}
Spacer()
}
}
}
}
class SearchObjectController : ObservableObject {
//static let shared = SearchObjectController() // Delete this. We want one Object of this class in the entire app.
//private init() {} // Delete this. Empty Init is not needed
var token = "gQR-YsX0OpwkYpbjhPVi3b4kSR-DtWrR5phwDm2kPMM"
#Published var results = [Result]()
#Published var searchText : String = "forest"
func search () {
let url = URL(string: "https://api.unsplash.com/search/photos?query=\(searchText)")
var request = URLRequest(url: url!)
request.httpMethod = "GET"
request.setValue("Client-ID \(token)", forHTTPHeaderField: "Authorization")
print("request: \(request)")
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else {return}
print(String(data: data, encoding: .utf8)!)
do {
let res = try JSONDecoder().decode(Results.self, from: data)
DispatchQueue.main.async {
self.results.append(contentsOf: res.results)
}
//print(self.results)
} catch {
print("catch: \(error)")
}
}
task.resume()
}
}
struct MoodboardView: View {
// Env Obj. so we reference only one object
#EnvironmentObject var searchObjectController: SearchObjectController
var body: some View {
List {
VStack {
Text("Mood Board: \(searchObjectController.searchText)")
.fontWeight(.bold)
.padding(6)
ForEach(searchObjectController.results, id: \.id, content: { result in
Text(result.description ?? "Empty")
WebImage(url: URL(string: result.urls.small) )
.resizable()
.frame(height:300)
})
}.onAppear() {
searchObjectController.search()
}
// I added your update button here so I can use it.
GenerateView()
}
}
}
struct Results : Codable {
var total : Int
var results : [Result]
}
struct Result : Codable {
var id : String
var description : String?
var urls : URLs
}
struct URLs : Codable {
var small : String
}
Ps. please not that it appears when you search you just append the results to the array and don't delete the old ones. Thats the reason why you still see the first images after updating. The new ones just get appended at the bottom. Scroll down to see them. If you don't want that just empty the array with results upon search

Why is my #AppStorage not working on SwiftUI?

I'm trying to set up the #AppStorage wrapper in my project.
I'm pulling Texts from a JSON API (see DataModel), and am hoping to store the results in UserDefautls. I want the data to be fetched .OnAppear and stored into the #AppStorage. When the user taps "Get Next Text", I want a new poem to be fetched, and to update #AppStorage with the newest Text data, (which would delete the past Poem stored).
Currently, the code below builds but does not display anything in the Text(currentPoemTitle).
Data Model
import Foundation
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
// 1.
#Published var poems = [Poem]()
init() {
getPoem()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
TestView
import SwiftUI
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage("currentPoemtTitle") var currentPoemTitle = ""
#AppStorage("currentPoemAuthor") var currentPoemAuthor = ""
var body: some View {
VStack{
Text(currentPoemTitle)
Button("Fetch next text") {
fetch.getPoem()
}
}.onAppear{
if let poem = fetch.poems.first {
currentPoemTitle = "\(poem.title)"
currentPoemAuthor = "\(poem.author)"
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
What am I missing? Thanks.
Here are a few code edits to get you going.
I added AppStorageKeys to manage the #AppStorage keys, to avoid errors retyping key strings (ie. "currentPoemtTitle")
Your question asked how to update the #AppStorage with the data, and the simple solution is to add the #AppStorage variables within the FetchPoem class and set them within the FetchPoem class after the data is downloaded. This also avoids the need for the .onAppear function.
The purpose of using #ObservedObject is to be able to keep your View in sync with the data. By adding the extra layer of #AppStorage, you make the #ObservedObject sort of pointless. Within the View, I added a Text() to display the title using the #ObservedObject values directly, instead of relying on #AppStorage. I'm not sure if you want this, but it would remove the need for the #AppStorage variables entirely.
I also added a getPoems2() function using Combine, which is a new framework from Apple to download async data. It makes the code a little easier/more efficient... getPoems() and getPoems2() both work and do the same thing :)
Code:
import Foundation
import SwiftUI
import Combine
struct AppStorageKeys {
static let poemTitle = "current_poem_title"
static let poemAuthor = "current_poem_author"
}
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
#Published var poems = [Poem]()
#AppStorage(AppStorageKeys.poemTitle) var poemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var poemAuthor = ""
init() {
getPoem2()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
guard let poemData = data else {
print("No data")
return
}
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
self.updateFirstPoem()
}
} catch {
print("Error")
}
}
.resume()
}
func getPoem2() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTaskPublisher(for: url)
// fetch on background thread
.subscribe(on: DispatchQueue.global(qos: .background))
// recieve response on main thread
.receive(on: DispatchQueue.main)
// ensure there is data
.tryMap { (data, response) in
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
// decode JSON data to [Poem]
.decode(type: [Poem].self, decoder: JSONDecoder())
// Handle results
.sink { (result) in
// will return success or failure
print("poetry fetch completion: \(result)")
} receiveValue: { (value) in
// if success, will return [Poem]
// here you can update your view
self.poems = value
self.updateFirstPoem()
}
// After recieving response, the URLSession is no longer needed & we can cancel the publisher
.cancel()
}
func updateFirstPoem() {
if let firstPoem = self.poems.first {
self.poemTitle = firstPoem.title
self.poemAuthor = firstPoem.author
}
}
}
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage(AppStorageKeys.poemTitle) var currentPoemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var currentPoemAuthor = ""
var body: some View {
VStack(spacing: 10){
Text("App Storage:")
Text(currentPoemTitle)
Text(currentPoemAuthor)
Divider()
Text("Observed Object:")
Text(fetch.poems.first?.title ?? "")
Text(fetch.poems.first?.author ?? "")
Button("Fetch next text") {
fetch.getPoem()
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}

How to generate fixed number of views from json API in SwiftUI?

So I am learning how to use SwiftUI with a json api. I am currently generating views through a list and a ForEach Loop. I am wondering how I can make it so that it only generates the first, say 10 elements in the posts array, instead of generating the entire list from the api. Basically I want to use RandomElement() to display 10 random posts from the entire array. I am just beginning here and learning so any help woul dbe appreicated.
Below is the code for my main view that is displaying the list
import SwiftUI
struct postList: View {
//state variable of the posts
#State var posts: [Post] = []
var array = [Post]()
var body: some View {
List {
ForEach(posts) { post in
VStack(alignment: .leading) {
Text(post.title)
.font(.headline)
Text(post.body)
.font(.callout)
}
.padding()
}
}
.onAppear() {
Api().getPosts { (posts) in
self.posts = posts
}
}
}
}
struct postList_Previews: PreviewProvider {
static var previews: some View {
postList()
}
}
Down here is the Data file I use to retrieve the json data
import SwiftUI
struct Post: Codable, Identifiable {
let id = UUID()
var title: String
var body: String
}
class Api {
func getPosts(completion: #escaping ([Post]) -> ()) {
guard let url = URL(string: "http://jsonplaceholder.typicode.com/posts") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let posts = try! JSONDecoder().decode([Post].self, from: data!)
DispatchQueue.main.async {
completion(posts)
}
}
.resume()
}
}

Resources