Swift JSON Parsing issue - Possible problem with struct? - ios

Upon login and validation, the user is sent to the main application page. I have the following code set.
import SwiftUI
typealias MyDefendant = [Defendant]
struct ContentView: View {
var email: String
#State var myDefendant: MyDefendant = []
func getUserData(completion:#escaping (MyDefendant)->()) {
var urlRequest = URLRequest(url: URL(string: "https://milanobailbonds.com/getDefendant.php")!)
urlRequest.httpMethod = "post"
let authData = [
"defEmail" : email
] as [String : Any]
do {
let authBody = try JSONSerialization.data(withJSONObject: authData, options: .prettyPrinted)
urlRequest.httpBody = authBody
urlRequest.addValue("application/json", forHTTPHeaderField: "content-type")
} catch let error {
debugPrint(error.localizedDescription)
}
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
return
}
guard let data = data else {
return
}
do {
let responseString = String(data: data, encoding: .utf8)!
print(responseString)
var returnValue: MyDefendant?
let decoder = JSONDecoder()
returnValue = try decoder.decode([Defendant].self, from: data)
completion(returnValue!)
}
catch { fatalError("Couldn't Parse")
}
}.resume()
return
}
var body: some View {
NavigationView {
VStack {
Text(email)
Text("I Need Bail")
.font(.largeTitle)
.fontWeight(.semibold)
Button {
print("Test")
} label: {
Label("I Need Bail", systemImage: "iphone.homebutton.radiowaves.left.and.right")
.labelStyle(IconOnlyLabelStyle())
.font(.system(size: 142.0))
}
} .foregroundColor(.green)
.shadow(color: .black, radius: 2, x: 2, y: 2)
.navigationBarTitle("Home")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading:
Button {
print("Test")
} label: {
Label("I Need Bail", systemImage: "line.3.horizontal")
.labelStyle(IconOnlyLabelStyle())
})
}.onAppear() {
getUserData() { myDefendant in
self.myDefendant = myDefendant
}
}
}
}
In my data models I have created a struct for Defendant as such:
struct Defendant: Codable, Hashable, Identifiable {
var id: Int
var defImage: String
var defName: String
var defAddress: String
var defCity: String
var defState: String
var defZip: String
var defPhone: String
var defEmail: String
var defUserName: String
var defPW: String
var defDOB: String
var defPriorFTA: Int
var defFTAExplained: String
var defAssignedAgency: Int
} // Defendant Model
The PHP is working fine and returning valid JSON with all of the required items for the struct.
"\"[\\n {\\n \\\"Id\\\": 5,\\n \\\"defImage\\\": \\\"\\\",\\n \\\"defName\\\": \\\"Some Dude\\\",\\n \\\"defAddress\\\": \\\"123 Main St\\\",\\n \\\"defCity\\\": \\\"Some City\\\",\\n \\\"defState\\\": \\\"FL\\\",\\n \\\"defZip\\\": \\\"12345\\\",\\n \\\"defPhone\\\": \\\"888-888-8888\\\",\\n \\\"defEmail\\\": \\\"someone#someone.com\\\",\\n \\\"defUserName\\\": \\\"\\\",\\n \\\"defPW\\\": \\\"91492cffa4032765f6b025ec6b2c873e49fe5e58\\\",\\n \\\"defDOB\\\": \\\"01\\\\\\/01\\\\\\/1955\\\",\\n \\\"defPriorFTA\\\": 0,\\n \\\"defFTAExplained\\\": \\\"\\\",\\n \\\"defAssignedAgency\\\": 0\\n }\\n]\""
Unfortunately, I keep getting an error "Unable to Parse".
I'm new to Swift, and coding in general.
Any thoughts or ideas are greatly appreciated.
Thank you

Im not sure but you can try to change id into Id in your struct. Please remember name of your struct property must exactly the same with key in Json response.

Related

Show API Data in a View without ID

I have a small weather project and I got stuck in the phase where I have to show the results from the API in a view. The API is from WeatherAPI.
I mention that the JSON file doesn't have an id and I receive the results in Console.
What is the best approach for me to solve this problem?
Thank you for your help!
This is the APIService.
import Foundation
class APIService: ObservableObject {
func apiCall(searchedCity: String) {
let apikey = "secret"
guard let url = URL(string: "https://api.weatherapi.com/v1/current.json?key=\(apikey)&q=\(searchedCity)&aqi=no") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-type")
let task = URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data, error == nil else {
return
}
do {
let response = try JSONDecoder().decode(WeatherModel.self, from: data)
print(response)
print("SUCCESS")
}
catch {
print(error)
}
}
task.resume()
}
}
This is the ForecastModel
import Foundation
struct WeatherModel: Codable {
var location: Location
var current: Current
}
struct Location: Codable {
var name: String
}
struct Current: Codable, {
var temp_c: Decimal
var wind_kph: Decimal
}
Here is the view where I want to call.
import SwiftUI
struct WeatherView: View {
#StateObject var callApi = APIService()
#Binding var searchedCity: String
var body: some View {
NavigationView {
ZStack(alignment: .leading) {
Color.backgroundColor.ignoresSafeArea(edges: .all)
VStack(alignment: .leading, spacing: 50) {
comparation
temperature
clothes
Spacer()
}
.foregroundColor(.white)
.font(.largeTitle)
.onAppear(perform: {
self.callApi.apiCall(searchedCity: "\(searchedCity)")
})
}
}
}
}
struct WeatherView_Previews: PreviewProvider {
#State static var searchedCity: String = ""
static var previews: some View {
WeatherView(searchedCity: $searchedCity)
}
}
fileprivate extension WeatherView {
var comparation: some View {
Text("Today is ")
.fontWeight(.medium)
}
var temperature: some View {
Text("It is ?C with ? and ? winds")
.fontWeight(.medium)
}
var clothes: some View {
Text("Wear a ?")
.fontWeight(.medium)
}
}

How do I refresh Widgets in SwiftUI with JSON

I am trying to display data from an api. When I try to do it on the app it works but once I move the code to the widget it doesn't work the onAppear or Button. I need help on how to refresh the json when the decoder loads it. I think my decoder needs works I am not sure what the issue is.
struct panchangWidgetEntryView : View {
#State private var panchangData: PanchangData?
var entry: Provider.Entry
var body: some View {
HStack {
Text(panchangData?.tithi ?? "")
.font(.subheadline)
.padding()
Text(panchangData?.paksha ?? "")
.font(.subheadline)
.padding()
}
}
func loadData(){
guard let url = URL(string: "API URL") else {
return
}
URLSession.shared.dataTask(with: url){ data, response, error in
guard let data = data else { return }
if let decodedData = try? JSONDecoder().decode(PanchangData.self, from: data){
DispatchQueue.main.async {
self.panchangData = decodedData
}
}
}.resume()
}
}
struct PanchangData: Decodable{
var slon: Double
var mlon: Double
var tithinum: Int
var tithi: String
var paksha: String
var tithiTill: String
var nakshatra: String
var nakshatraTill: String
var yoga: String
var yogTill: String
var karana: String
var karanTill: String
var rashi: String
var requestsremaining: Int
var requeststotal: Int
var plan: String
var status: String
var reqdate: String
var reqtime: String
}

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

How do you implement custom delegates in SwiftUI

As an example I have a SwitUI ContentView. The one that comes when you first make the project.
import SwiftUI
struct ContentView: View {
var manager = TestManager()
var body: some View {
ZStack{
Color(.green)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Test Text")
Button(action:{}) {
Text("Get number 2")
.font(.title)
.foregroundColor(.white)
.padding()
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 5))
}
}
}
}
}
I have a TestManager that will handle an Api call. I Made a delegate for the class that has two functions.
protocol TestManagerDelegate {
func didCorrectlyComplete(_ testName: TestManager, model: TestModel)
func didFailWithError(_ error: Error)
}
struct TestManager {
var delegate: TestManagerDelegate?
let urlString = "http://numbersapi.com/2/trivia?json"
func Get(){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil{
self.delegate?.didFailWithError(error!)
return
}
if let safeData = data{
if let parsedData = self.parseJson(safeData){
self.delegate?.didCorrectlyComplete(self, model: parsedData)
}
}
}
task.resume()
}
}
func parseJson(_ jsonData: Data) -> TestModel?{
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(TestModel.self, from: jsonData)
let mes = decodedData.message
let model = TestModel(message: mes)
return model
} catch {
delegate?.didFailWithError(error)
return nil
}
}
}
This is the testModel data class. Only grabbing the text of the Json returned.
struct TestModel :Decodable{
let text: String
}
How do I connect the TestManager to the view and have the view handle the delegate like how we could do in in storyboards?
Regarding the TestModel
Decodable protocol (in your context) assumes you to create the model struct with all the properties, that you get via JSON. When requesting http://numbersapi.com/2/trivia?json you'll get something like:
{
"text": "2 is the number of stars in a binary star system (a stellar system consisting of two stars orbiting around their center of mass).",
"number": 2,
"found": true,
"type": "trivia"
}
Which means, your model should look like the following:
struct TestModel: Decodable {
let text: String
let number: Int
let found: Bool
let type: String
}
Regarding Delegates
In SwiftUI this approach is not reachable. Instead, developers need to adapt the Combine framework's features: property wrappers #ObservedObject, #Published, and ObservableObject protocol.
You want to put your logic into some struct. Bad news, that (currently) ObservableObject is AnyObject protocol (i.e. Class-Only Protocol). You'll need to rewrite your TestManager as class as:
class TestManager: ObservableObject {
// ...
}
Only then you could use it in your CurrentView using #ObservedObject property wrapper:
struct ContentView: View {
#ObservedObject var manager = TestManager()
// ...
}
Regarding the TestManager
Your logic now excludes the delegate as such, and you need to use your TestModel to pass the data to your CustomView. You could modify TestManager by adding new property with #Published property wrapper:
class TestManager: ObservableObject {
let urlString = "http://numbersapi.com/2/trivia?json"
// 1
#Published var model: TestModel?
func get(){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { [weak self] (data, response, error) in
// 2
DispatchQueue.main.async {
if let safeData = data {
if let parsedData = self?.parseJson(safeData) {
// 3
self?.model = parsedData
}
}
}
}
task.resume()
}
}
private func parseJson(_ jsonData: Data) -> TestModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(TestModel.self, from: jsonData)
return decodedData
} catch {
return nil
}
}
}
To be able to access your model "from outside", in your case the ContentView.
Use DispatchQueue.main.async{ } for async tasks, because Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
Simply use your parsed model.
Then in ContentView use your TestManager like this:
struct ContentView: View {
#ObservedObject var manager = TestManager()
var body: some View {
ZStack{
Color(.green)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Trivia is: \(self.manager.model?.text ?? "Unknown")")
Button(action:{ self.manager.get() }) {
Text("Get number 2")
.font(.title)
.foregroundColor(.white)
.padding()
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 5))
}
}
}
}
}
Regarding HTTP
You use the link http://numbersapi.com/2/trivia?json which is not allowed by Apple, please, use https instead, or add the App Transport Security Settings key with Allow Arbitrary Loads parameter set to YES into your Info.Plist. But do this very carefully as the http link simply will not work.
Further steps
You could implement the error handling by yourself basing on the description above.
Full code (copy-paste and go):
import SwiftUI
struct ContentView: View {
#ObservedObject var manager = TestManager()
var body: some View {
ZStack{
Color(.green)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Trivia is: \(self.manager.model?.text ?? "Unknown")")
Button(action:{ self.manager.get() }) {
Text("Get number 2")
.font(.title)
.foregroundColor(.white)
.padding()
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 5))
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class TestManager: ObservableObject {
let urlString = "http://numbersapi.com/2/trivia?json"
#Published var model: TestModel?
func get(){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { [weak self] (data, response, error) in
DispatchQueue.main.async {
if let safeData = data {
if let parsedData = self?.parseJson(safeData) {
self?.model = parsedData
}
}
}
}
task.resume()
}
}
private func parseJson(_ jsonData: Data) -> TestModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(TestModel.self, from: jsonData)
return decodedData
} catch {
return nil
}
}
}
struct TestModel: Decodable {
let text: String
let number: Int
let found: Bool
let type: String
}

Unable to extract data from Observable Object

I'm trying to display data from an observed object.
Initially I tried to keep it simple, to merely display one member of a struct: 'base' (value: "stations").
struct Sample: Codable {
let coord: Coord
let weather: [Weather]
let base: String
let main: Main
let visibility: Int
let wind: Wind
let clouds: Clouds
let dt: Double
// let sys: Sys
let id: Int
let name: String
// let cod: Int
}
I did some defensive coding with '?' but I got the following compiler error:
So I replaced the '?' with '!', knowing that I should have some data.
I find this frustrating, coming from an imperative-paradigm background.
What am I doing wrong?
Here's the data dump:
Sample(coord: DataTaskPubTab.Coord(lon: -0.13, lat: 51.51), weather:
[DataTaskPubTab.Weather(id: 300, main: "Drizzle", description: "light
intensity drizzle")], base: "stations", main:
DataTaskPubTab.Main(temp: 280.32, pressure: 1012, humidity: 81,
tempMin: 279.15, tempMax: 281.15), visibility: 10000, wind:
DataTaskPubTab.Wind(speed: 4.1, deg: 80), clouds:
DataTaskPubTab.Clouds(all: 90), dt: 1485789600.0, id: 2643743, name:
"London")
Here's the Observable Object:
class StandardWeatherReportLoader: ObservableObject {
#Published var networkMessage: String?
#Published var hasAlert = false
#Published var weatherReport: Sample?
#Published var hasReport = false
func doStandard() {
let url = EndPoint.weather.path()
var request = URLRequest(url: EndPoint.weather.path()!)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: url!) { (data: Data?, _: URLResponse?, error: Error?) -> Void in
DispatchQueue.main.async {
guard error == nil else {
self.networkMessage = error?.localizedDescription
self.hasAlert = true
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Sample.self, from: data!)
self.weatherReport = result
self.hasReport = true
print(result)
} catch let error as NSError {
print(error)
}
}
}
task.resume()
}
}
Apparently this works:
struct StandardWeatherView: View {
#EnvironmentObject var settings: Settings
#ObservedObject var standardWeatherReportLoader = StandardWeatherReportLoader()
init() {
self.standardWeatherReportLoader.doStandard()
}
var body: some View {
NavigationView {
ZStack {
Color("FernGreen").edgesIgnoringSafeArea(.all)
VStack {
if standardWeatherReportLoader.weatherReport?.base == nil {
Text("Sorry, NO data.")
} else {
Text(standardWeatherReportLoader.weatherReport!.base)
}
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
}
.navigationBarTitle(Text("Weather Report"), displayMode: .inline)
}.alert(isPresented: $standardWeatherReportLoader.hasAlert, content: { () -> Alert in
Alert(title: Text(verbatim: standardWeatherReportLoader.networkMessage!))
})
}
}
}
Very probably the web services data is not yet available when the view renders for the first time. So you shouldn't force unwrap the value with !. If you want to create a Text() element in both cases, you can simply use the ?? operation (see Nil-Coalescing Operator in the Swift language documentation):
Text(standardWeatherReportLoader.weatherReport?.base ?? "Sorry, NO data.")

Resources