How to loop through API call data and display in view - swiftui

I know I can use for each for this, but every time I try to implement according to documentation it throws some kind of error regarding syntax.
Here is my view:
import SwiftUI
import Combine
struct HomeTab: View {
#StateObject var callDevices = CallDevices()
var body: some View {
NavigationView {
devices
.onAppear {
callDevices.getDevices()
}
}
}
private var devices: some View {
VStack(alignment: .leading, spacing: nil) {
ForEach(content: callDevices.getDevices(), id: \.self) { device in
// i want to loop through and display here //
HStack{
Text(device.Name)
Text(device.Status)
}
}
Spacer()
}
}
}
struct HomeTab_Previews: PreviewProvider {
static var previews: some View {
HomeTab()
}
}
Here is my Call Devices which works without issue in other views:
class CallDevices: ObservableObject {
private var project_id: String = "r32fddsf"
#Published var devices = [Device]()
func getDevices() {
guard let url = URL(string: "www.example.com") else {return}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Authorization")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard error == nil else {print(error!.localizedDescription); return }
// guard let data = data else {print("empty data"); return }
let theData = try! JSONDecoder().decode(Welcome.self, from: data!)
DispatchQueue.main.async {
self.devices = theData.devices
}
}
.resume()
}
}
is the issue in the way I am calling my function?

try this:
(you may need to make Device Hashable)
private var devices: some View {
VStack(alignment: .leading, spacing: nil) {
ForEach(callDevices.devices, id: \.self) { device in // <-- here
// i want to loop through and display here //
HStack{
Text(device.Name)
Text(device.Status)
}
}
Spacer()
}
}
If Device is Identifiable, you can remove the id: \.self.
struct Device: Identifiable, Hashable, Codable {
let id = UUID()
var Name = ""
var Status = ""
// ... any other stuff you have
}

Related

Swiftui Mapping Nested JSON to Flat Model

I need to access the data in the nested dictionary of the Memodel struct. From both the music and image dictionary. Please any help is needed to map out correctly, i have tried using AzampSharp's example https://www.youtube.com/watch?v=b5wVIQNrI6k but i believe i am doing something wrong. Thanks.
import SwiftUI
struct MemodelAPIResult: Codable {
let data: [Memodel]
enum CodingKeys: String, CodingKey {
case data = "results"
}
}
struct Memodel: Identifiable, Codable {
var id: String
var followers: String
var following: String
let music: [MemodelMusic]
let images: [MemodelImages]
}
struct MemodelMusic: Identifiable, Codable, Hashable {
var id: String
var musicfile: URL
var musicname: String
var musicartistname: String
var musicgenre: String
}
struct MemodelImages: Identifiable, Codable, Hashable {
var id: String
var albumimages: URL
var abumlikes: String
var albumviews: String
}
Below is my ObservableObject in my View Model
import Foundation
import SwiftUI
import Combine
import CryptoKit
class MeViewmodel: ObservableObject {
#Published var me: [Memodel]? = nil
init() {
self.fetchme()
}
func fetchme() {
let url = ""
let session = URLSession(configuration: .default)
session.dataTask(with: URL(string: url)!) { (data, _, err) in
if let error = err{
print(error.localizedDescription)
return
}
guard let APIData = data else {
print("No Data found")
return
}
do {
let new = try JSONDecoder().decode(MemodelAPIResult.self, from: APIData)
DispatchQueue.main.async {
self.me = new.data
}
}
catch{
print(error)
}
}
.resume()
}
}
And then the item view
struct MeMusicItemView: View {
//E-MARK: - Properties
var me: Memodel
//E-MARK: - Body
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(me.music[0].musicname)
.font(.callout)
.fontWeight(.medium)
.foregroundColor(.white)
Text(me.music[0].musicartistname)
.font(.caption2)
.fontWeight(.light)
.foregroundColor(.white)
Text(me.music[0].musicgenre)
.font(.system(size: 8))
.fontWeight(.light)
.foregroundColor(.gray)
}
}
}
}
And also the ForEach in the parent View....
if let meMusicData = meMusicData.mememe {
ForEach(meMusicData) { music in
MeMusicItemView(memusic: music)
}
} else {
ProgressView()
.padding(.top, 20)
}
There is not enough info for me to really understand what you are doing, but
here is some code you can have a look at and recycle for your purpose:
struct ContentView: View {
#StateObject var viewModel = MeViewmodel() // <-- here your model
var body: some View {
List {
ForEach(viewModel.me) { memod in // <-- loop over the Memodel array
ForEach(memod.music) { music in // <-- loop over the MemodelMusic array
MeMusicItemView(memusic: music) // <-- display 1 MemodelMusic
}
}
}
}
}
struct MeMusicItemView: View {
//E-MARK: - Properties
#State var memusic: MemodelMusic // <-- here
//E-MARK: - Body
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(memusic.musicname)
.font(.callout)
.fontWeight(.medium)
.foregroundColor(.pink)
Text(memusic.musicartistname)
.font(.caption2)
.fontWeight(.light)
.foregroundColor(.green)
Text(memusic.musicgenre)
.font(.system(size: 8))
.fontWeight(.light)
.foregroundColor(.blue)
}
}
}
}
class MeViewmodel: ObservableObject {
#Published var me: [Memodel] = [] // <--- here no optional, it is easier to deal with
init() {
self.fetchme()
}
func fetchme() {
// ......
}
}
struct Memodel: Identifiable, Codable {
var id: String
var followers: String
var following: String
let music: [MemodelMusic]
let images: [MemodelImages]
}
struct MemodelMusic: Identifiable, Codable, Hashable {
var id: String
var musicfile: URL
var musicname: String
var musicartistname: String
var musicgenre: String
}
struct MemodelImages: Identifiable, Codable, Hashable {
var id: String
var albumimages: URL
var abumlikes: String
var albumviews: String
}

SWIFTUI Displaying JSON Data in ContentView

I have been having trouble displaying my JSON into my content view. I can decode the data and save it into a dictionary as I have printed and seen. However when its time to display it in ContentView with a ForEach. I'm getting this error Cannot convert value of type '[String : String]' to expected argument type 'Binding' Below is my code for my ContentView, Struct and ApiCall. I have read other solutions on stackoverflow and tried them all but they do not work.
struct ContentView: View {
#StateObject var api = APICALL()
var body: some View {
let country = api.storedData.countries
VStack(alignment: .leading) {
ForEach(country.id, id: \.self) { country in
HStack(alignment: .top) {
Text("\(country.countries)")
}
}
.onAppear {
api.loadData()
}
}
}
}
My ApiCall class which loads the data, as well as the struct.
// MARK: - Country
struct Country: Codable, Identifiable {
let id = UUID()
var countries: [String: String]
enum CodingKeys: String, CodingKey {
case countries = "countries"
}
}
class APICALL: ObservableObject {
#Published var storedData = Country(countries: [:])
func loadData() {
let apikey = ""
guard let url = URL(string:"https://countries-cities.p.rapidapi.com/location/country/list?rapidapi-key=\(apikey)") 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(Country.self, from: data) {
DispatchQueue.main.async {
self.storedData.countries = response.countries
print(self.storedData.countries)
}
return
}
}
}
.resume()
}
}
Any Point in the right direction would be absolutely helpful.
you could try this approach to display your countries data:
struct ContentView: View {
#StateObject var api = APICALL()
var body: some View {
VStack(alignment: .leading) {
// -- here --
ForEach(Array(api.storedData.countries.enumerated()), id: \.0) { index, country in
HStack(alignment: .top) {
Text("\(country.key) \(country.value)")
}
}
.onAppear {
api.loadData()
}
}
}
}
you can also use this, if you prefer:
ForEach(api.storedData.countries.sorted(by: >), id: \.key) { key, value in
HStack(alignment: .top) {
Text("\(key) \(value)")
}
}

Cannot get view to update with downloaded JSON

I am stuck at the simplest place right now.. I'm making a network request and just want the view to be updated once the JSON is received..
And I verified that:
JSON is valid
Valid response received (verified in print statement)
I've done this about 50 times and don't know why I'm stuck at this point.
struct ContentView: View {
#StateObject var nm = NetworkManager()
var body: some View {
VStack {
ScrollView {
HStack {
ForEach(nm.articles, id: \.hashValue) { article in
Text("Hello")
}
}.task {
do {
try await NetworkManager().getAllArticles(for: "mario")
} catch { print(error) }
}
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
final class NetworkManager: ObservableObject {
#Published var newsItem: News?
#Published var articles: [Article] = []
private let apiKey = ""//removed
private var baseUrlString: String {
"https://newsapi.org/v2/"
}
func getAllArticles(for searchItem: String) async throws {
let url = URL(string: baseUrlString + "everything?q=\(searchItem)&apiKey=\(apiKey)")!
let (data, _) = try await URLSession.shared.data(from: url)
let news = try JSONDecoder().decode(News.self, from: data)
DispatchQueue.main.async {
self.newsItem = news
self.articles = self.newsItem!.articles
}
}
}
struct News: Codable {
var totalResults: Int?
let articles: [Article]
}
struct Article: Codable, Hashable {
let author: String?
let title: String
let description: String
let url: String
let urlToImage: String?
let publishedAt: String
let content: String
}
Edit: removed apiKey
getAllArticles() is view-related, which means you should probably implement this function inside the View instead of ObservableObject.
struct ContentView: View {
#StateObject var nm = NetworkManager()
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(nm.articles, id: \.hashValue) { article in
Text("Hello")
}
}.task {
do {
try await getAllArticles(for: "mario")
} catch { print(error) }
}
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
extension ContentView {
func getAllArticles(for searchItem: String) async throws {
let url = URL(string: nm.baseUrlString + "everything?q=\(searchItem)&apiKey=\(NetworkManager.apiKey)")!
let (data, _) = try await URLSession.shared.data(from: url)
let news = try JSONDecoder().decode(News.self, from: data)
// Not necessary
// DispatchQueue.main.async {
nm.newsItem = news
nm.articles = nm.newsItem!.articles
// }
}
}
final class NetworkManager: ObservableObject {
#Published var newsItem: News?
#Published var articles: [Article] = []
static let apiKey = "YOUR_API_KEY"
var baseUrlString: String {
"https://newsapi.org/v2/"
}
}

Swiftui ObservableObject with array not update view

I have a problem with Array using ObservableObject in my view. I have an empty array. I call a function at page onAppear. When the data is returned, the view does not update with the new data in array:
class NewsState: ObservableObject {
private let base: String = "api"
let objectWillChange = ObservableObjectPublisher()
#Published var wagsList: Array<UserSlider> = [] {
willSet {
objectWillChange.send()
}
}
func getList() {
let url = NSURL(string: "\(base)/UserApi/getList")
var mutableURLRequest = URLRequest(url: url! as URL)
mutableURLRequest.httpMethod = "GET"
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
AF.request(mutableURLRequest).responseData { response in
guard let data = response.data else { return }
let resp = try! JSONDecoder().decode(Array<UserSlider>.self, from: data)
for i in resp {
let userSlider = UserSlider(id: i.id, uid: i.uid, image: i.image)
self.wagsList.append(userSlider)
}
}
}
}
In my view I have this:
HStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 20) {
if(self.newsState.wagsList.count != 0) {
ForEach(self.newsState.wagsList, id: \.self) { wags in
VStack {
HStack {
URLImage(URL(string: "\(wags.image)")!, expireAfter: Date(timeIntervalSinceNow: 10)) { proxy in
proxy.image
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.clipShape(Circle())
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 2)
)
.contentShape(Circle())
}.frame(width: 62, height: 62)
}
HStack {
Text("10K")
.foregroundColor(Color.white)
.font(Font.custom("Metropolis-Bold", size: 15))
}
HStack {
Text("followers")
.foregroundColor(Color.white)
.font(Font.custom("Metropolis-Normal", size: 15))
}
}
}
} else {
//loader
}
}.onAppear(perform: initPage)
}
}
What am I doing wrong? I see that the problem is caused by ScrollView.
Try this one
class NewsState: ObservableObject {
private let base: String = "api"
#Published var wagsList: Array<UserSlider> = []
func getList() {
let url = NSURL(string: "\(base)/UserApi/getList")
var mutableURLRequest = URLRequest(url: url! as URL)
mutableURLRequest.httpMethod = "GET"
mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
AF.request(mutableURLRequest).responseData { response in
guard let data = response.data else { return }
let resp = try! JSONDecoder().decode(Array<UserSlider>.self, from: data)
let results = resp.map { UserSlider(id: $0.id, uid: $0.uid, image: $0.image) }
DispatchQueue.main.async {
self.wagsList = results
}
}
}
}
As it is not clear to me where the error might lay. It could be either in getList or in your View.
This is an easy example of how it works with a Published and ObserverdObject:
Note: your getList function is not in this solution as the error could be with your API, JSON ect.
import SwiftUI
struct ContentView: View {
#ObservedObject var state = NewsState()
var body: some View {
Group { //needed for the IF Statement below
if state.stringList.count > 0 {
ForEach(self.state.stringList, id: \.self){ s in
Text(String(s))
}
}
}.onTapGesture {
self.state.getNewList()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class NewsState: ObservableObject {
#Published var stringList: Array<String> = []
init() {
self.getList()
}
func getList() {
self.stringList.append("New")
}
func getNewList() {
self.stringList = []
self.stringList.append("New new")
}
}

SwiftUI http request

I need to post a http request to login view (SwiftUI), my code follow
I have in HttpAuth.swift:
import Foundation
import Combine
struct ServerMessage: Decodable {
let res, message: String
}
class HttpAuth: ObservableObject {
let objectWillChange = PassthroughSubject<HttpAuth, Never>()
var authenticated = false {
didSet {
objectWillChange.send(self)
}
}
func postAuth(username: String, password: String) {
guard let url = URL(string: "http://mysite/loginswift.php") else { return }
let body: [String: String] = ["username": username, "password": password]
let finalBody = try! JSONSerialization.data(withJSONObject: body)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = finalBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
let resData = try! JSONDecoder().decode(ServerMessage.self, from: data)
print(resData.res)
if resData.res == "correct" {
DispatchQueue.main.async {
self.authenticated = true
}
}
}.resume()
}
}
And in ContentView.swift:
import SwiftUI
struct ContentView: View {
#State private var username: String = ""
#State private var password: String = ""
#State var manager = HttpAuth()
var body: some View {
VStack(alignment: .leading) {
if self.manager.authenticated {
Text("Correct!").font(.headline)
}
Text("Username")
TextField("placeholder", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.green)
.autocapitalization(.none)
Text("Password")
SecureField("placeholder", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.green)
Button(action: {
self.manager.postAuth(username: self.username, password: self.password)
}) {
HStack{
Spacer()
Text("Login")
Spacer()
}
.accentColor(Color.white)
.padding(.vertical, 10)
.background(Color.red)
.cornerRadius(5)
.padding(.horizontal, 40)
}
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It works, i receive the answer from server but it doesn't update the ContentView, the 'self.manager.authenticated' in ContentView doesn't refresh from HttpAuth class.
This part of code doesn't work:
if self.manager.authenticated {
Text("Correct!").font(.headline)
}
The 'authenticated' still on false.
How can i fix it, thank you.
Although the accepted answer meets the objective I don't think it's the best way to do it because it implements #EnvironmentObject and it should be used for global application issues and not for the specific request of a view.
You can implement a "ViewModel" that is specific for the view, to do the work of the request and make the update of the variable.
Documentation: https://developer.apple.com/documentation/combine/observableobject
The implementation for your code would be like this, based on the changes suggested by #Chris:
This works in Xcode Version 11.2 (11B52)
HttpAuth.swif:
import Foundation
import SwiftUI
import Combine
class HttpAuth: ObservableObject {
#Published var authenticated = false
func postAuth(username: String, password: String) {
guard let url = URL(string: "http://mysite/loginswift.php") else { return }
let body: [String: String] = ["username": username, "password": password]
let finalBody = try! JSONSerialization.data(withJSONObject: body)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = finalBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
let resData = try! JSONDecoder().decode(ServerMessage.self, from: data)
print(resData.res)
if resData.res == "correct" {
DispatchQueue.main.async {
self.authenticated = true
}
}
}.resume()
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State private var username: String = ""
#State private var password: String = ""
#ObservedObject var manager = HttpAuth()
var body: some View {
VStack(alignment: .leading) {
if self.manager.authenticated {
Text("Correct!").font(.headline)
}
Spacer()
Text("Username")
TextField("placeholder", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.green)
.autocapitalization(.none)
Text("Password")
SecureField("placeholder", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.green)
Button(action: {
self.manager.postAuth(username: self.username, password: self.password)
}) {
HStack{
Spacer()
Text("Login")
Spacer()
}
.accentColor(Color.white)
.padding(.vertical, 10)
.background(Color.red)
.cornerRadius(5)
.padding(.horizontal, 40)
}
Spacer()
}.padding()
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
you can try this:
instead of
let objectWillChange = PassthroughSubject<HttpAuth, Never>()
var authenticated = false {
didSet {
objectWillChange.send(self)
}
}
use
#published var authenticated = false
and instead of
#State var manager = HttpAuth()
use
#EnvironmentObject private var manager: HttpAuth
and of course do this when calling ContentView:
ContentView().environmentObject(manager)
and somewhere outside class as global variable do
var manager = HttpAuth()
then it should work.