Hello everybody and Happy Holiday!
I am a SwiftUI novice just starting out and playing with APIs. I managed to get used to decoding API but I encounter an issue when I try to access the first item in the array. I am using SwiftyJSON to decode. I tried accessing the index 0 but the build fails with Error: Index Out of Range
Any tip would be extremely helpful especially for a novice like me.
Thank you in advance!
Model
struct dataType: Identifiable {
var id: String
var title: String
var desc: String
var url: String
var image: String
Decoding the JSON
class getData: ObservableObject {
#Published var datas = [dataType]()
init() {
let source = "https://newsapi.org/v2/top-headlines?country=ro&apiKey=c602e42864e148dea1144f1f55255888"
let url = URL(string: source)!
let session = URLSession(configuration: .default)
session.dataTask(with: url) { (data, _, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
let json = try! JSON(data: data!)
for i in json["articles"]{
let title = i.1["title"].stringValue
let description = i.1["description"].stringValue
let url = i.1["url"].stringValue
let image = i.1["urlToImage"].stringValue
let id = i.1["publishedAt"].stringValue
DispatchQueue.main.async {
self.datas.append(dataType(id: id, title: title, desc: description, url: url, image: image))
}
}
}.resume()
}
And finally, the view
struct ContentView: View {
#ObservedObject var list = getData()
var body: some View {
VStack {
WebImage(url: URL(string: list.datas[0].title), options: .highPriority, context: nil)
.resizable()
.frame(width:400, height:250)
NavigationView {
List(list.datas){i in
NavigationLink(destination: webView(url: i.url).navigationBarTitle("", displayMode: .inline))
{
HStack {
VStack(alignment: .leading, spacing: 10) {
Text(i.title).fontWeight(.heavy)
Text(i.desc)
.lineLimit(3)
}
if i.image != "" {
WebImage(url: URL(string:i.image), options: .highPriority, context: nil)
.resizable()
.frame(width: 110, height: 135)
.cornerRadius(20)
}
}.padding(.vertical, 15)
}
}.navigationBarTitle("Noutati")
}
}
}
URLSession executes dataTask asynchronously, so at the start there is no data yet, ie. datas is empty, so trying to get first (below datas[0]) element of empty array result in exception (which you see)
WebImage(url: URL(string: list.datas[0].title), options: .highPriority, context: nil)
.resizable()
.frame(width:400, height:250)
instead you need to show some stub view till data got loaded, like below
if list.data.isEmpty {
Text("Loading...")
} else {
WebImage(url: URL(string: list.datas[0].title), options: .highPriority, context: nil)
.resizable()
.frame(width:400, height:250)
}
Related
I am trying to query the marvel API. I believe my decodable is wrong. I have the following code:
struct ReturnedData : Decodable {
var id : Int?
var name : String?
var description : String?
}
var savedData : [ReturnedData] = []
let urlString = "https://gateway.marvel.com/v1/public/characters?ts=1&apikey=\(myAPIKey)"
let url = URL(string: urlString)
let session = URLSession.shared
let dataTask = session.dataTask(with: url!) { (data, response, error) in
guard let data = data else {return}
do {
let recievedData = try JSONDecoder().decode([ReturnedData].self, from: data)
self.savedData = recievedData
} catch {
print(error)
}
}
dataTask.resume()
}
I am getting the following error message:
typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array but found a dictionary instead.", underlyingError: nil))
according to the documentation, I should get all of the below:
Character
id (int, optional): The unique ID of the character resource.,
name (string, optional): The name of the character.
description (string, optional): A short bio or description of the character.
modified (Date, optional): The date the resource was most recently modified.
resourceURI (string, optional): The canonical URL identifier for this resource.
urls (Array[Url], optional): A set of public web site URLs for the resource.
thumbnail (Image, optional): The representative image for this character.
comics (ComicList, optional): A resource list containing comics which feature this character.
stories (StoryList, optional): A resource list of stories in which this character appears
events (EventList, optional): A resource list of events in which this character appears.
series (SeriesList, optional): A resource list of series in which this character appears.
Also, any tips on how to get the thumbnail image is appreciated.
The error you get is because the model you have does not match the json data. So, try this approach ...to query the marvel API....
In future, copy and paste your json data into https://app.quicktype.io/
this will generate the models for you. Adjust the resulting code to your needs. Alternatively read the docs at: https://developer.marvel.com/docs#!/public/getCreatorCollection_get_0 and create the models by hand from the info given.
class MarvelModel: ObservableObject {
#Published var results = [MarvelResult]()
let myAPIKey = "xxxx"
let myhash = "xxxx"
func getMarvels() async {
guard let url = URL(string: "https://gateway.marvel.com/v1/public/characters?ts=1&apikey=\(myAPIKey)&hash=\(myhash)") else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
let response = try JSONDecoder().decode(MarvelResponse.self, from: data)
Task{#MainActor in
results = response.data.results
}
} catch {
print("---> error: \(error)")
}
}
}
struct ContentView: View {
#StateObject var vm = MarvelModel()
var body: some View {
VStack {
if vm.results.isEmpty { ProgressView() }
List(vm.results) { item in
Text(item.name)
}
}
.task {
await vm.getMarvels()
}
}
}
struct MarvelResponse: Decodable {
let code: Int
let status, copyright, attributionText, attributionHTML: String
let etag: String
let data: MarvelData
}
struct MarvelData: Decodable {
let offset, limit, total, count: Int
let results: [MarvelResult]
}
struct MarvelResult: Identifiable, Decodable {
let id: Int
let name, resultDescription: String
let modified: String
let thumbnail: Thumbnail
let resourceURI: String
let comics, series: Comics
let stories: Stories
let events: Comics
let urls: [URLElement]
enum CodingKeys: String, CodingKey {
case id, name
case resultDescription = "description"
case modified, thumbnail, resourceURI, comics, series, stories, events, urls
}
}
struct Comics: Decodable {
let available: Int
let collectionURI: String
let items: [ComicsItem]
let returned: Int
}
struct ComicsItem: Identifiable, Decodable {
let id = UUID()
let resourceURI: String
let name: String
}
struct Stories: Decodable {
let available: Int
let collectionURI: String
let items: [StoriesItem]
let returned: Int
}
struct StoriesItem: Identifiable, Decodable {
let id = UUID()
let resourceURI: String
let name: String
let type: String
}
struct Thumbnail: Decodable {
let path: String
let thumbnailExtension: String
enum CodingKeys: String, CodingKey {
case path
case thumbnailExtension = "extension"
}
}
struct URLElement: Identifiable, Decodable {
let id = UUID()
let type: String
let url: String
}
EDIT-1: if you want something very basic, then try this:
struct ContentView: View {
#State var results = [MarvelResult]()
var body: some View {
List(results) { item in
Text(item.name)
}
.onAppear {
getMarvels()
}
}
func getMarvels() {
let myAPIKey = "xxxx"
let myhash = "xxxx"
guard let url = URL(string: "https://gateway.marvel.com/v1/public/characters?ts=1&apikey=\(myAPIKey)&hash=\(myhash)") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data {
do {
let response = try JSONDecoder().decode(MarvelResponse.self, from: data)
DispatchQueue.main.async {
self.results = response.data.results
}
}
catch {
print(error)
}
}
}.resume()
}
}
Some time ago, I asked a question involving decoding JSON. I am using this JSON to create an entry in a list and decide whether or not to have a checkbox ticked.
My previous question asked how to decode the JSON, and I have this code which looks useful (below), but to be honest, I have no idea what to do with.
Where would I put this within a SwiftUI file where I could loop through each piece of JSON in this array and add an entry into a SwiftUI?
I have experience with Python, and I understand how it should be done, but am quite the beginner in swift...
My code:
struct Mod: Decodable {
let id: String
let display: String
let description: String
let url: String?
let config: Bool?
let enabled: Bool?
let hidden: Bool?
let icon: String?
let categories: [String]?
// let actions: Array<OptionAction>?
// let warning: ActionWarning?
}
let modsURL = URL(string: "https://raw.githubusercontent.com/nacrt/SkyblockClient-REPO/main/files/mods.json")!
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
if let error = error { print(error); return }
do {
let result = try JSONDecoder().decode([Mod].self, from: data!)
print(result)
} catch {print(error)}
}
task.resume()
let mods_test: () = importJSON(url: modsURL)
let enableds = mods_test.map {$0.enabled}
If there is anything wrong with this question, please tell me! Thanks in advance for the help.
Here, how you can do that,
you can use State to declare variable
struct ContentView: View {
//Declare variable
#State var jsonDataList = [jsonData]()
var body: some View {
VStack {
List(jsonDataList, id: \.id) { jsonDataList in
VStack(alignment: .leading) {
HStack {
VStack(alignment: .leading) {
Text(jsonDataList.id)
.font(.title3)
.fontWeight(.bold)
Text(jsonDataList.display)
.font(.subheadline)
.fontWeight(.bold)
}
Spacer()
Image(systemName: jsonDataList.enabled ?? false ? "checkmark.square": "square")
}
}
}
.onAppear(perform: loadData)
}
}
//MARK: - Web Service
func loadData() {
guard let modsURL = URL(string: "https://raw.githubusercontent.com/nacrt/SkyblockClient-REPO/main/files/mods.json") else {
print("Invalid URL")
return
}
let task = URLSession.shared.dataTask(with: modsURL) { (data, _, error) in
if let error = error { print(error); return }
do {
let result = try JSONDecoder().decode([jsonData].self, from: data!)
jsonDataList = result
print("Response:",jsonDataList)
} catch {
print(error)
}
}
task.resume()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// MARK: - Data Model
struct jsonData: Codable, Identifiable {
let id: String
let display: String
let description: String
let url: String?
let config: Bool?
let enabled: Bool?
let hidden: Bool?
let icon: String?
let categories: [String]?
}
Response Screenshot :
I have tried my first SwiftUI Project.
I only want to show some data stored in Firestore (Google Firebase).
Here is my code:
import SwiftUI
import FirebaseFirestore
import FirebaseFirestoreSwift
struct MonsterObj: Identifiable, Equatable, Hashable {
var id = UUID()
var name: String
var element: String
var immune: String
var size: String
var twoStarWeakness: String
var threeStarWeakness: String
#if DEBUG
static let exampleMonster = MonsterObj(id: UUID(), name: "Test Monster", element: "Test Element", immun: "Test immun", groesse: "Test groesse", twoStarWeakness: "Test 2 Weakness", threeStarWeakness: "Test3 Weakness")
#endif
}
class MonsterC: ObservableObject {
#Published var monsters = [MonsterObj]()
init() {
let db = Firestore.firestore()
var monsterNames: [String] = []
db.collection("Monster").getDocuments() { (querySnapshot, err) in
if let err = err {
print(err)
} else {
for document in querySnapshot!.documents {
monsterNames.append("\(document.documentID)")
print("document: \(document.documentID)")
}
}
}
for monsterName in monsterNames {
print(monsterName)
db.collection("Monster").document(monsterName).getDocument { (document, error) in
if let document = document, document.exists {
let elementGetter = document.get("element") as! String
let immuneGetter = document.get("immune") as! String
let sizeGetter = document.get("size") as! String
let twoStarWeaknessGetter = document.get("2 star weakness") as! String
let threeStarWeaknessGetter = document.get("3 star weakness")as! String
self.monsters.append(MonsterObj(name: monsterName, element: elementGetter, immune: immuneGetter, size: sizeGetter, twoStarWeakness: twoStarWeaknessGetter, threeStarWeakness: threeStarWeaknessGetter))
}
}
}
}
}
This is my View:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var monsterT: MonsterC
var body: some View {
List(monsterT.monsters, id: \.self) { monster in
Text(monster.name)
}
}
}
And I did following to SceneDelegate.swift:
var window: UIWindow?
var monsterT = MonsterC()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView().environmentObject(monsterT)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
So my Problem is the list is empty.
I figured out in init of class MonsterC the line monsterNames.append("\document.documentID)") does not append anything to monsterNames.
But print("document: \(document.documentID)") is printing all monsterNames.
My google Firestore structure looks like this:
Collection -> Document -> Fields
-------------------------------------------
Monster -> Anjanath -> immune: fire,
element: fire
etc.
There's only one collection ("Monster").
Can anyone explain to a beginner why .append is not working here but print is doing everything right?
You need to understand calling asynchronous functions. My advice is to restructure your code, it is not a good idea to do these async calls in your init().
Your function "db.collection("Monster").getDocuments() { (querySnapshot, err) in ..."
is asynchronous. You must either wait till it is finished to use the results, or do what you need inside the function. Note you also have another async function "db.collection("Monster").document(monsterName).getDocument {"
So .append is not working because the results of your function "db.collection("Monster").getDocuments() { (querySnapshot, err) in ..." are not available when you do the .append.
So if you must use this dodgy code, try this to fix your array problem:
class MonsterC: ObservableObject {
#Published var monsters = [MonsterObj]()
init() {
let db = Firestore.firestore()
db.collection("Monster").getDocuments() { (querySnapshot, err) in
if let err = err {
print(err)
} else {
var monsterNames: [String] = []
for document in querySnapshot!.documents {
monsterNames.append("\(document.documentID)")
print("document: \(document.documentID)")
}
for monsterName in monsterNames {
print(monsterName)
db.collection("Monster").document(monsterName).getDocument { (document, error) in
if let document = document, document.exists {
let elementGetter = document.get("element") as! String
let immuneGetter = document.get("immune") as! String
let sizeGetter = document.get("size") as! String
let twoStarWeaknessGetter = document.get("2 star weakness") as! String
let threeStarWeaknessGetter = document.get("3 star weakness")as! String
self.monsters.append(MonsterObj(name: monsterName, element: elementGetter, immune: immuneGetter, size: sizeGetter, twoStarWeakness: twoStarWeaknessGetter, threeStarWeakness: threeStarWeaknessGetter))
}
}
}
}
}
}
}
I have a WordPress post and this contains a post picture that is updated regularly. I would like to dynamically query this post picture in Swift but have not yet found a solution for it.
Could someone help me?
Here is my code for which I am requesting a regular image at the moment:
var coursesPicture = [CoursesPicture]()
// MARK: - GUID
struct GUIDURL: Codable {
let rendered: String
}
// MARK: - Courses
struct Course: Codable {
let guid, title: GUID
let content: Content
let links: Links
enum CodingKeys: String, CodingKey {
case guid, title, content
case links = "_links"
}
}
// MARK: - Content
struct Content: Codable {
let rendered: String
let protected: Bool
}
// MARK: - GUID
struct GUID: Codable {
let rendered: String
}
// MARK: - Links
struct Links: Codable {
}
// MARK: - CoursePicture
struct CoursesPicture: Codable {
let featuredMedia: Int
enum CodingKeys: String, CodingKey {
case featuredMedia = "featured_media"
}
}
public func fetchJSONPicture() {
let urlString = "https://ismaning.de/wp-json/wp/v2/media/4291"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, _, err) in
DispatchQueue.main.sync {
if let err = err {
print("Failed to get data from url:", err)
return
}
guard let data = data else { return }
do {
self.coursesPicture = [try JSONDecoder().decode(CoursesPicture.self, from: data)]
} catch let jsonErr {
print("Failed to decode:", jsonErr)
}
}
}
if let url = URL(string: "https://ismaning.de/wp-content/uploads/ismaning-willkommen-banner_app-ios.jpg") {
URLSession.shared.dataTask(with: url) { (data, urlResponse, error) in
if let data = data {
DispatchQueue.main.sync {
self.imageView.image = UIImage(data: data)
}
}
}.resume()
}
}
I simply query the image here via a URL. Nevertheless, it should be queried dynamically via a WordPress post. I also tried this JSON query but unfortunately I haven't found a solution yet
Thank you in advance for your help, as I have been looking for a solution for a while now
After discussion in the chat we found a working solution for this problem. If anyone else has this you can simply do this:
Create two structs, example Struct Response: Codable{}
Create two arrays which are of the struct types like: var arr:[Response] = []
In the ViewDidLoad you will call the getJsonData method with completion block.
When the data is loaded in the first array, you call the getJsonImage method with completion block.
Once you have the imageURL you can simply retrieve it by creating a URLSession to retrieve the image.
This is the solution code:
//Arrays
var firstArray:[FirstResponse] = []
var imgData:[ImgData] = []
override func viewDidLoad() {
super.viewDidLoad()
//Fetching the JSON data
fetchJSON {
//Fetching the JSON image
self.fetchJSONImage {
let imageUrl = self.imgData[0].link
self.imgView.downloaded(from: imageUrl)
}
}
}
//Retrieving the JSON data
func fetchJSON(completed: #escaping () -> ()){
var urlString = "HIDDEN"
urlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
guard let url = URL(string: urlString) else {return}
//Create the URLSession
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else{
return
}
do{
//Sets the dataArray to JSON data
self.firstArray = [try JSONDecoder().decode(FirstResponse.self, from: data)]
//Complete task in background
DispatchQueue.main.async {
completed()
}
}
catch let jsonErr{
print(jsonErr)
}
}.resume()
}
//Retrieving the image link from JSON
func fetchJSONImage(completed: #escaping () -> ()){
let imgPath = firstArray[0].featured_media
var urlString = "HIDDEN-URL\(imgPath)"
urlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
guard let url = URL(string: urlString) else {return}
//Create the URLSession
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else{
return
}
do{
//Sets the dataArray to JSON data
self.imgData = [try JSONDecoder().decode(ImgData.self, from: data)]
//Complete task in background
DispatchQueue.main.async {
completed()
}
}
catch let jsonErr{
print(jsonErr)
}
}.resume()
}
}
//Struct for getting image ID
struct FirstResponse: Codable{
let featured_media: Int
}
//Struct for getting the image link
struct ImgData: Codable{
let link: String
}
//Extension
extension UIImageView {
//Download the image from the API
func downloaded(from url: URL, contentMode mode: UIView.ContentMode = .scaleToFill) {
contentMode = mode
URLSession.shared.dataTask(with: url) { data, response, error in
guard let image = UIImage(data: data)
else { return }
//Continue in background
DispatchQueue.main.async() {
self.image = image
}
}.resume()
}
func downloaded(from link: String, contentMode mode: UIView.ContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloaded(from: url, contentMode: mode)
}
}
I am trying to load data in JSON format from my server into IOS application.
Here is my JSON:
[
{
"BankName": "bank1",
"CurrencyName": "cur1",
"SellRate": "0.65",
"BuyRate": "0.55",
"OfficialRate": "0.6"
},
{
"BankName": "bank1",
"CurrencyName": "cur2",
"SellRate": "1.65",
"BuyRate": "1.55",
"OfficialRate": "1.6"
}
]
There are 2 files in my project:
1:
import Foundation
class Shot {
var bankName: String!
var currencyName: String!
var sellRate: String!
var buyRate: String!
var offRate: String!
init (data: NSDictionary) {
self.bankName = getStringFromJSON(data, key:"BankName")
self.currencyName = getStringFromJSON(data, key:"CurrencyName")
self.sellRate = getStringFromJSON(data, key:"SellRate")
self.buyRate = getStringFromJSON(data, key:"BuyRate")
self.offRate = getStringFromJSON(data, key: "OfficialRate")
}
func getStringFromJSON(data: NSDictionary, key: String) -> String {
if let info = data[key] as? String{
return info
}
return ""
}
}
2:
import Foundation
class JsonTest {
func loadJson(completion: ((AnyObject) -> Void)!) {
var urlString = "http://a.com/g.php"
let session = NSURLSession.sharedSession()
let sourceUrl = NSURL(string: urlString)
var task = session.dataTaskWithURL(sourceUrl!){
(data, response, error) -> Void in
if error != nil {
println(error.localizedDescription)
} else {
var error: NSError?
var jsonData = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: &error) as NSArray
var shots = [Shot]()
println(jsonData)
for shot in jsonData{
let shot = Shot(data: shot as NSDictionary)
shots.append(shot)
}
println(shots) //[jsontest.Shot, jsontest.Shot]
}
}
task.resume()
}
}
I am trying to populate array automatically when my app starts. To do it I have a code in my mainViewController class.
override func viewDidLoad() {
super.viewDidLoad()
let api = JsonTest()
api.loadJson(nil)
}
The problem occurs when I try to print shots variable in the second file.
it returns [jsontest.Shot, jsontest.Shot] when I was expecting the array of dictionaries.
println(jsonData) works fine and shows JSON data from URL.
Can anybody advise what is wrong in my program?
"shots" is an array of instances of Shot, not a dictionary:
class Shot {
var bankName: String!
var currencyName: String!
var sellRate: String!
var buyRate: String!
var offRate: String!
init (data: NSDictionary) {
self.bankName = getStringFromJSON(data, key:"BankName")
self.currencyName = getStringFromJSON(data, key:"CurrencyName")
self.sellRate = getStringFromJSON(data, key:"SellRate")
self.buyRate = getStringFromJSON(data, key:"BuyRate")
self.offRate = getStringFromJSON(data, key: "OfficialRate")
}
func getStringFromJSON(data: NSDictionary, key: String) -> String {
if let info = data[key] as? String{
return info
}
return ""
}
}
var shots = [Shot]()
let urlString = "http://almazini.lt/getrates.php"
let sourceUrl = NSURL(string: urlString)
// Using NSData instead of NSURLSession for experimenting in Playground
let data = NSData(contentsOfURL: sourceUrl!)
var error: NSError?
// As I'm using Swift 1.2 I had to change "as" with "as!"
let jsonData = NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers, error: &error) as! NSArray
for shot in jsonData{
let shot = Shot(data: shot as! NSDictionary)
shots.append(shot)
}
println(shots[0].bankName)
Update for Swift 2
var shots = [Shot]()
let urlString = "http://almazini.lt/getrates.php"
// Using NSData instead of NSURLSession for experimenting in Playground
if let sourceUrl = NSURL(string: urlString) {
NSURLSession.sharedSession().dataTaskWithURL(sourceUrl, completionHandler: { (data, response, error) in
if error == nil {
if let data = data, jsonData = try? NSJSONSerialization.JSONObjectWithData(data, options: []), jsonArray = jsonData as? [NSDictionary] {
for item in jsonArray {
let shot = Shot(data: item)
shots.append(shot)
}
print(shots[0].bankName)
} else {
print("no JSON data")
}
} else {
print(error!.localizedDescription)
}
}).resume()
}
Seems like there are two problems:
You're trying to use println to debug instead of setting a breakpoint and checking your objects values.
You have not created a description or debugDescription property for your object, so println on your object is just using some default implementation.
shots is an array of your custom object, so when you call println, it's using the description for Array, which prints out the objects in the array, comma separated, and within square brackets.
The default description property for classes in Swift just prints the class name.
Ideally, you should just use a break point to check the values of your object to be certain it initialized correctly, but if it's actually important to get them to print right, it's only a matter of implementing the description property:
override var description: String {
get {
// build and return some string that represents your Shot object
}
}