I was making this app which would show certain information about upcoming animes, am using jikan API for this and it doesn't require any Authentication or any key
here is how the API look -
{
"request_hash": "request:top:3506eaba6445f7ad5cc2f78417bf6ed916b6aaad",
"request_cached": true,
"request_cache_expiry": 43675,
"top": [
{
"mal_id": 40356,
"rank": 1,
"title": "Tate no Yuusha no Nariagari Season 2",
"url": "https://myanimelist.net/anime/40356/Tate_no_Yuusha_no_Nariagari_Season_2",
"image_url": "https://cdn.myanimelist.net/images/anime/1245/111800.jpg?s=7302aaeb3bc4e1433b32d094e9d6f6f0",
"type": "TV",
"episodes": {},
"start_date": "Apr 2022",
"end_date": {},
"members": 300837,
"score": 0
},
{
"mal_id": 48583,
"rank": 2,
"title": "Shingeki no Kyojin: The Final Season Part 2",
"url": "https://myanimelist.net/anime/48583/Shingeki_no_Kyojin__The_Final_Season_Part_2",
"image_url": "https://cdn.myanimelist.net/images/anime/1989/116577.jpg?s=f6312bda2e67f86595936d0264696a91",
"type": "TV",
"episodes": {},
"start_date": "Jan 2022",
"end_date": {},
"members": 253849,
"score": 0
},
this is how I have written my code and for some reason when I run the app am just getting a plain blank white screen and also it prints out the last print statement that I have added saying " Fetch Failed: Unknown error "
please help me with this
import SwiftUI
struct Response: Codable{
var top: [Result]
}
struct Result: Codable {
var mal_id: Int
var rank: Int
var title: String
var type: String
var start_date: String
}
struct ContentView: View {
func loadData() {
guard let url = URL(string: "https://api.jikan.moe/v3/top/anime/1/upcoming") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.top = decodedResponse.top
}
// 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")")
}.resume()
}
#State private var top = [Result]()
var body: some View {
ScrollView {
List(top, id: \.mal_id) { item in
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(String("\(item.rank)"))
.font(.headline)
Text(item.type)
.font(.headline)
Text(item.start_date)
.font(.headline)
}
}
}
.onAppear(perform: loadData)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First, The problem is encountered when decoding your data, it looks like some fields are coming as null, so you should decode them as optional, in this case I found that var start_date: String field is the one causing the problem, so just make sure you make it optional: var start_date: String?
And Second, You don't need to put the List inside the ScrollView because by default a List is a type of ScrollView.
Model: (make some files optional)
struct Result: Codable {
var mal_id: Int
var rank: Int
var title: String
var type: String
var start_date: String?
}
Body Content: (removed the scrollview)
var body: some View {
List(top, id: \.mal_id) { item in
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(String("\(item.rank)"))
.font(.headline)
Text(item.type)
.font(.headline)
Text(item.start_date ?? "")
.font(.headline)
}
}
.onAppear(perform: loadData)
}
Related
I am trying to create a simple Swift UI app to download some json from the web and then display it on the screen.
I have 2 calls - a general lookup call for an array of items which are displayed in a View. I then have a detailed lookup, based upon the id chosen by the user in the first View which is then displayed in a second View.
I am able to access the parsed data via 2 #Published properties in my NetworkController class which I access from my Views, using: .onAppear to trigger the calls when the user arrives at each view.
My first call is working and I am able to display a list of items and then select them and read their id, which I then use for the second call.
The second call is causing me problems, though and I am getting an error decoding.
As there is quite a bit of code I’ve created a single Playground which includes some sample Json and the functions which I use, which is producing the error.
I am doing both calls using the same procedure - e.g. a number of function - which I have applied from another app which does a similar thing. My guess is that the error is because I have misunderstood some part of how these work. Because of this, I’ve commented what I think they are doing, to give an idea of what I am trying to achieve. Because the first call is working I am thinking that my understanding is partially correct, but I must be missing something.
The element that differs between the first data call and the second one is that the first one only requires a single struct to parse all the data whereas the second call uses a nested struct. I am thinking that I may need to adjust something to accomodate this - I am using the outermost struct - but I’m not clear exactly what I need to do.
If anyone has any suggestions I would be very grateful.
######################################################
# PLAYGROUND
######################################################
import Cocoa
var itemDetailed: ItemCodable? = nil
var item: Item
// ################################################################
// EXAMPLE JSON DATA
// ################################################################
let data = """
{
"item": {
"general": {
"id": 11,
"name": "app_install",
"enabled": true,
"trigger": "CHECKIN",
"trigger_checkin": true,
"trigger_enrollment_complete": false,
"trigger_login": false,
"trigger_network_state_changed": false,
"trigger_startup": false,
"trigger_other": "",
"frequency": "Ongoing",
"retry_event": "none",
"retry_attempts": -1,
"notify_on_each_failed_retry": false,
"location_user_only": false,
"target_drive": "/",
"offline": false,
"category": {
"id": 3,
"name": "Apps"
},
"date_time_limitations": {
"activation_date": "",
"activation_date_epoch": 0,
"activation_date_utc": "",
"expiration_date": "",
"expiration_date_epoch": 0,
"expiration_date_utc": "",
"no_execute_on": {},
"no_execute_start": "",
"no_execute_end": ""
},
"network_limitations": {
"minimum_network_connection": "No Minimum",
"any_ip_address": true,
"network_segments": []
},
"override_default_settings": {
"target_drive": "default",
"distribution_point": "",
"force_afp_smb": false,
"sus": "default"
},
"network_requirements": "Any",
"site": {
"id": -1,
"name": "None"
}
},
"scope": {
"all_computers": false,
"computers": [],
"computer_groups": [
{
"id": 1,
"name": "All Managed Clients"
}
],
"buildings": [],
"departments": [],
"limit_to_users": {
"user_groups": []
},
"limitations": {
"users": [],
"user_groups": [],
"network_segments": [],
"ibeacons": []
},
"exclusions": {
"computers": [],
"computer_groups": [
{
"id": 9,
"name": "app_installed_testutil"
}
],
"buildings": [],
"departments": [],
"users": [],
"user_groups": [],
"network_segments": [],
"ibeacons": []
}
},
"self_service": {
"use_for_self_service": false,
"self_service_display_name": "",
"install_button_text": "Install",
"reinstall_button_text": "Reinstall",
"self_service_description": "",
"force_users_to_view_description": false,
"self_service_icon": {},
"feature_on_main_page": false,
"self_service_categories": [],
"notification": "Self Service",
"notification_subject": "app_install",
"notification_message": ""
},
"package_configuration": {
"packages": [
{
"id": 3,
"name": "testutil_2.0.5.psr",
"action": "Install",
"fut": false,
"feu": false
}
]
},
"scripts": [],
"printers": [
""
],
"dock_items": [],
"account_maintenance": {
"accounts": [],
"directory_bindings": [],
"management_account": {
"action": "doNotChange"
},
"open_firmware_efi_password": {
"of_mode": "none",
"of_password_sha256": "xxxxxyyyyyyyyyzzzzzzzzaaaabbbbbbccccccc"
}
},
"reboot": {
"message": "This computer will restart in 5 minutes.",
"startup_disk": "Current Startup Disk",
"specify_startup": "",
"no_user_logged_in": "Restart if a package or update requires it",
"user_logged_in": "Restart if a package or update requires it",
"minutes_until_reboot": 5,
"start_reboot_timer_immediately": false,
"file_vault_2_reboot": false
},
"maintenance": {
"recon": true,
"reset_name": false,
"install_all_cached_packages": false,
"heal": false,
"prebindings": false,
"permissions": false,
"byhost": false,
"system_cache": false,
"user_cache": false,
"verify": false
},
"files_processes": {
"search_by_path": "",
"delete_file": false,
"locate_file": "",
"update_locate_database": false,
"spotlight_search": "",
"search_for_process": "",
"kill_process": false,
"run_command": ""
},
"user_interaction": {
"message_start": "",
"allow_users_to_defer": false,
"allow_deferral_until_utc": "",
"allow_deferral_minutes": 0,
"message_finish": ""
},
"disk_encryption": {
"action": "none"
}
}
}
""".data(using: .utf8)
// ################################################################
// DATA STRUCTS
// ################################################################
struct ItemCodable: Codable{
let item: Item }
struct Item: Codable, Hashable, Identifiable {
var id = UUID()
let general: General?
enum CodingKeys: String, CodingKey {
case general = "general"
}
}
struct General: Codable, Hashable, Identifiable {
var id = UUID()
let name: String?
let enabled: Bool?
let trigger: String?
let triggerCheckin, triggerEnrollmentComplete, triggerLogin, triggerLogout: Bool?
let triggerNetworkStateChanged, triggerStartup: Bool?
let triggerOther, frequency: String?
let locationUserOnly: Bool?
let targetDrive: String?
let offline: Bool?
let networkRequirements: String?
let mac_address: String?
let ip_address: String?
let payloads: String?
enum CodingKeys: String, CodingKey {
case name = "name"
case enabled = "enabled"
case trigger = "trigger"
case triggerCheckin = "trigger_checkin"
case triggerEnrollmentComplete = "trigger_enrollment_complete"
case triggerLogin = "trigger_login"
case triggerLogout = "trigger_logout"
case triggerNetworkStateChanged = "trigger_network_state_changed"
case triggerStartup = "trigger_startup"
case triggerOther = "trigger_other"
case frequency = "frequency"
case locationUserOnly = "location_user_only"
case targetDrive = "target_drive"
case offline = "offline"
case networkRequirements = "network_requirements"
case mac_address = "mac_address"
case ip_address = "ip_address"
case payloads = "payloads"
}
}
// ################################################################
// DECODE DATA
// ################################################################
struct ItemsDetailReply: Codable {
// Struct to parse data
let itemDetailed: ItemCodable
static func decode(_ data: Data) -> Result<ItemCodable,Error> {
let decoder = JSONDecoder()
do {
let response = try decoder.decode(ItemsDetailReply.self, from: data)
print("ItemsDetailReply Decoding succeeded")
separationLine()
print("Response is:\n\(response)")
return .success(response.itemDetailed)
} catch {
separationLine()
print("Decoding error")
return .failure(error)
}
}
}
func separationLine() {
print("------------------------------------------------------------------")
}
// ################################################################
// DECODE AND REDIRECT TO MAIN QUEUE
// ################################################################
func processDetail(data: Data) {
// func that initiates the decoding using the decoding struct - then redirects the
// returned data to the main queue
let decoded = ItemsDetailReply.decode(data)
switch decoded {
case .success(let itemDetailed):
receivedItemDetail(itemDetailed: itemDetailed)
separationLine()
print("itemDetailed name is:\(String(describing: itemDetailed.item.general?.name))")
separationLine()
case .failure(let error):
separationLine()
print("Error encountered")
separationLine()
print(error)
separationLine()
}
}
// ################################################################
// SET PROPERTY VIA MAIN QUEUE
// ################################################################
func receivedItemDetail(itemDetailed: ItemCodable) {
DispatchQueue.main.async {
// self.itemDetailed = itemDetailed
}
}
// CALL FUNCTION
processDetail(data: data!)
The problem lies in the decode method:
struct ItemsDetailReply: Codable {
// Struct to parse data
let itemDetailed: ItemCodable
static func decode(_ data: Data) -> Result<ItemCodable,Error> {
let decoder = JSONDecoder()
do {
let response = try decoder.decode(ItemsDetailReply.self, from: data)
print("ItemsDetailReply Decoding succeeded")
separationLine()
print("Response is:\n\(response)")
return .success(response.itemDetailed)
} catch {
separationLine()
print("Decoding error")
return .failure(error)
}
}
}
You're trying to decode a ItemsDetailReply but the data you pass into this method represents an ItemCodable. The decoder tries to locate a top level key itemDetailed but it fails.
You can probably ditch the ItemsDetailReply struct and just decode ItemCodable like this:
let response = try decoder.decode(ItemCodable.self, from: data)
I am trying to parse some JSON data with SwiftUI/Combine and I am a bit confused on the error I am getting. I am really new to Combine, so I could be completely overlooking something. I'm sure this has nothing to do with the real issue, as this would probably happen if I was parsing the normal way with urlsession/#escaping.
Here is the code:
struct FilmModel: Identifiable, Codable {
let adult: Bool
let backdropPath: String
let budget: Int
let genres: [Genre]
let homepage: String
let id: Int
let imdbID, originalLanguage, originalTitle, overview: String
let popularity: Double
let posterPath: String
let productionCompanies: [ProductionCompany]
let productionCountries: [ProductionCountry]
let releaseDate: String
let revenue, runtime: Int
let spokenLanguages: [SpokenLanguage]
let status, tagline, title: String
let video: Bool
let voteAverage: Double
let voteCount: Int
enum CodingKeys: String, CodingKey {
case adult
case backdropPath = "backdrop_path"
case budget
case genres
case homepage
case id
case imdbID = "imbd_id"
case originalLanguage = "original_language"
case originalTitle = "original_title"
case overview
case popularity
case posterPath = "poster_path"
case productionCompanies = "production_companies"
case productionCountries = "production_countries"
case releaseDate = "release_date"
case revenue
case runtime
case spokenLanguages = "spoken_languages"
case status, tagline, title
case video
case voteAverage = "vote_average"
case voteCount = "vote_count"
}
struct Genre: Identifiable, Codable {
let id: Int
let name: String
}
struct ProductionCompany: Codable {
let id: Int
let logoPath: String?
let name, originCountry: String
}
struct ProductionCountry: Codable {
let iso3166_1, name: String
}
struct SpokenLanguage: Codable {
let englishName, iso639_1, name: String
}
JSON response:
{
"adult": false,
"backdrop_path": "/rr7E0NoGKxvbkb89eR1GwfoYjpA.jpg",
"belongs_to_collection": null,
"budget": 63000000,
"genres": [
{
"id": 18,
"name": "Drama"
}
],
"homepage": "http://www.foxmovies.com/movies/fight-club",
"id": 550,
"imdb_id": "tt0137523",
"original_language": "en",
"original_title": "Fight Club",
"overview": "A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on, with underground \"fight clubs\" forming in every town, until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion.",
"popularity": 46.456,
"poster_path": "/pB8BM7pdSp6B6Ih7QZ4DrQ3PmJK.jpg",
"production_companies": [
{
"id": 508,
"logo_path": "/7PzJdsLGlR7oW4J0J5Xcd0pHGRg.png",
"name": "Regency Enterprises",
"origin_country": "US"
},
{
"id": 711,
"logo_path": "/tEiIH5QesdheJmDAqQwvtN60727.png",
"name": "Fox 2000 Pictures",
"origin_country": "US"
},
{
"id": 20555,
"logo_path": "/hD8yEGUBlHOcfHYbujp71vD8gZp.png",
"name": "Taurus Film",
"origin_country": "DE"
},
{
"id": 54051,
"logo_path": null,
"name": "Atman Entertainment",
"origin_country": ""
},
{
"id": 54052,
"logo_path": null,
"name": "Knickerbocker Films",
"origin_country": "US"
},
{
"id": 25,
"logo_path": "/qZCc1lty5FzX30aOCVRBLzaVmcp.png",
"name": "20th Century Fox",
"origin_country": "US"
},
{
"id": 4700,
"logo_path": "/A32wmjrs9Psf4zw0uaixF0GXfxq.png",
"name": "The Linson Company",
"origin_country": "US"
}
],
"production_countries": [
{
"iso_3166_1": "DE",
"name": "Germany"
},
{
"iso_3166_1": "US",
"name": "United States of America"
}
],
"release_date": "1999-10-15",
"revenue": 100853753,
"runtime": 139,
"spoken_languages": [
{
"english_name": "English",
"iso_639_1": "en",
"name": "English"
}
],
"status": "Released",
"tagline": "Mischief. Mayhem. Soap.",
"title": "Fight Club",
"video": false,
"vote_average": 8.4,
"vote_count": 22054
Data service:
class FilmDataService {
#Published var films: [FilmModel] = []
var filmSubscription: AnyCancellable?
init() {
getFilms()
}
private func getFilms() {
guard let url = URL(string: "https://api.themoviedb.org/3/movie/550?api_key=<key>") else { return }
filmSubscription = URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .default))
.tryMap { (output) -> Data in
guard let response = output.response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
return output.data
}
.decode(type: [FilmModel].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { [weak self] (returnedFilms) in
self?.films = returnedFilms
self?.filmSubscription?.cancel()
}
}
View model:
class FilmViewModel: ObservableObject {
#Published var tabBarImageNames = ["house", "rectangle.stack", "clock.arrow.circlepath", "square.and.arrow.down"]
#Published var films: [FilmModel] = []
private let dataService = FilmDataService()
private var cancellables = Set<AnyCancellable>()
init() {
addSubscribers()
}
func addSubscribers() {
dataService.$films
.sink { [weak self] (returnedFilms) in
self?.films = returnedFilms
}
.store(in: &cancellables)
}
my observations. Your error is probably not to do with Combine.
you are trying to decode "[FilmModel].self", but the response is only for one film, FilmModel.self.
Also I would make most/all var in your FilmModel etc... optional, add "?".
It works well in my test.
EDIT:
This is the code I use to test my answer. Works well for me:
import Foundation
import SwiftUI
import Combine
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var movies = FilmViewModel()
var body: some View {
VStack (spacing: 50) {
Text("movie test")
ForEach(movies.films, id: \.id) { movie in
Text(movie.title ?? "no title").foregroundColor(.red)
}
}
}
}
class FilmDataService {
#Published var films: [FilmModel] = []
var filmSubscription: AnyCancellable?
init() {
getFilms()
}
private func getFilms() {
guard let url = URL(string: "https://api.themoviedb.org/3/movie/550?api_key=1f632307cea6ce33f288f9a232b9803b") else { return }
filmSubscription = URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .default))
.tryMap { (output) -> Data in
guard let response = output.response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
return output.data
}
.decode(type: FilmModel.self, decoder: JSONDecoder()) // <--- here
.receive(on: DispatchQueue.main)
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { [weak self] (returnedFilms) in
self?.films.append(returnedFilms) // <--- here
self?.filmSubscription?.cancel()
}
}
}
class FilmViewModel: ObservableObject {
#Published var tabBarImageNames = ["house", "rectangle.stack", "clock.arrow.circlepath", "square.and.arrow.down"]
#Published var films: [FilmModel] = []
private let dataService = FilmDataService()
private var cancellables = Set<AnyCancellable>()
init() {
addSubscribers()
}
func addSubscribers() {
dataService.$films
.sink { [weak self] (returnedFilms) in
self?.films = returnedFilms
}
.store(in: &cancellables)
}
}
struct FilmModel: Identifiable, Codable {
let adult: Bool?
let backdropPath: String?
let budget: Int?
let genres: [Genre]?
let homepage: String?
let id: Int
let imdbID, originalLanguage, originalTitle, overview: String?
let popularity: Double?
let posterPath: String?
let productionCompanies: [ProductionCompany]?
let productionCountries: [ProductionCountry]?
let releaseDate: String?
let revenue, runtime: Int?
let spokenLanguages: [SpokenLanguage]?
let status, tagline, title: String?
let video: Bool?
let voteAverage: Double?
let voteCount: Int?
enum CodingKeys: String, CodingKey {
case adult
case backdropPath = "backdrop_path"
case budget
case genres
case homepage
case id
case imdbID = "imbd_id"
case originalLanguage = "original_language"
case originalTitle = "original_title"
case overview
case popularity
case posterPath = "poster_path"
case productionCompanies = "production_companies"
case productionCountries = "production_countries"
case releaseDate = "release_date"
case revenue
case runtime
case spokenLanguages = "spoken_languages"
case status, tagline, title
case video
case voteAverage = "vote_average"
case voteCount = "vote_count"
}
}
struct Genre: Identifiable, Codable {
let id: Int
let name: String?
}
struct ProductionCompany: Codable {
let id: Int
let logoPath: String?
let name, originCountry: String?
}
struct ProductionCountry: Codable {
let iso3166_1, name: String?
}
struct SpokenLanguage: Codable {
let englishName, iso639_1, name: String?
}
First of all, never write your api key (or any other keys) online!
Second of all:
It seems the endpoint that you are calling is returning a single FilmModel. So you have to decode it to a single one:
Change this:
.decode(type: [FilmModel].self, decoder: JSONDecoder())
to this:
.decode(type: FilmModel.self, decoder: JSONDecoder())
And then change this:
.sink { [weak self] (returnedFilms) in
self?.films = returnedFilms
}
to:
.sink { [weak self] (returnedFilm) in
self?.films = [returnedFilm]
}
Handle both single and multi object result
Some times you don't know if the server will return a single or multiple objects (and you don't have control on the server to fix that).
You can implement a custom decoder to handle both single and multi object responses:
enum FilmsResult {
case single(FilmModel)
case array([FilmModel])
}
extension FilmsResult: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let singleFilm = try? container.decode(FilmModel.self) {
self = .single(singleFilm)
} else {
try self = .array(container.decode([FilmModel].self))
}
}
}
extension FilmsResult {
var values: [FilmModel] {
switch self {
case .single(let film): return [film]
case .array(let films): return films
}
}
}
Than you can decode the result to:
.decode(type: FilmsResult.self, decoder: JSONDecoder())
and use it like:
.sink { [weak self] filmsResult in
self?.films = filmsResult.values
}
I'm trying to filter data from two JSON fields at the same time depending on the system language: at this point, system language recognition seems to work (because different elements appears according to the system language), but for some reasons I can't find the exact code to do what I want to do...
As you can see from the code, I first tried to filter the field "data" for a specific word ("italiano" or "english"), then the field "autore" to categorize the various informations...but the results are not good: sometimes I see the same information repeated in the list, other times I only see the informations filtered from the "data" field but not from the "autore" field...
So, in conclusion, the result I would like to obtain is the following: if the system language is Italian, all the JSON data containing the keyword "italian" in the "data" field must be filtered first, and then filtered further based on the specific keyword contained in the "autore" field; if, on the other hand, the system language is not Italian (but any other language), all the JSON data containing the keyword "english" in the "data" field must be filtered first, and then - as in the previous case - filtered further based on the specific keyword contained in the "autore" field.
Do you have any advice? Because I think I'm not very far from the exact code...or maybe not :)
Thank you!
Here's the SwiftUI code:
import SwiftUI
import URLImage
struct HistoryView: View {
#ObservedObject var dm: DownloadManager
let sysLanguage = NSLocale.current.languageCode
var body: some View {
if sysLanguage == "it" {
List {
ForEach(dm.JSON.filter {
$0.data == "italiano"
}) { busso in
ForEach(dm.JSON.filter {
$0.autore == "storia"
}) { busso in
NavigationLink(
destination: DetailView(busso: busso)) {
HStack {
URLImage(URL(string: busso.fotoUrl) ?? furl)
.resizable()
.aspectRatio(contentMode: .fit)
Text(busso.titolo)
.font(.headline)
Spacer().layoutPriority(-0.1)
}
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 50)
}
}
}
}
.navigationTitle(Text("Storia di Busso"))
.navigationBarTitleDisplayMode(.large)
} else {
List {
ForEach(dm.JSON.filter {
$0.autore == "storia"
}) { busso in
ForEach(dm.JSON.filter {
$0.data == "english"
}) { busso in
NavigationLink(
destination: DetailView(busso: busso)) {
HStack {
URLImage(URL(string: busso.fotoUrl) ?? furl)
.resizable()
.aspectRatio(contentMode: .fit)
Text(busso.titolo)
.font(.headline)
Spacer().layoutPriority(-0.1)
}
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: 50)
}
}
}
}
.navigationTitle(Text("Storia di Busso"))
.navigationBarTitleDisplayMode(.large)
}
}
}
struct HistoryView_Previews: PreviewProvider {
static var previews: some View {
HistoryView(dm: DownloadManager())
}
}
Here's the JSON file:
[
{
"id": "8",
"titolo": "View",
"autore": "galleria",
"testo": "",
"data": "english",
"extra1": "",
"extra2": "",
"creazione": "2021-01-13 22:55:57",
"foto": "foto\/WP_20161110_001.jpg",
"fotoUrl": "http:\/\/geniuspointfrezza.altervista.org\/foto\/WP_20161110_001.jpg"
},
{
"id": "7",
"titolo": "Storia di Busso",
"autore": "storia",
"testo": "Testo di prova",
"data": "italiano",
"extra1": "",
"extra2": "",
"creazione": "2021-01-10 21:11:03",
"foto": "foto\/1a3e733334ec8948b0328af4e5b7288a.jpg",
"fotoUrl": "http:\/\/geniuspointfrezza.altervista.org\/foto\/1a3e733334ec8948b0328af4e5b7288a.jpg"
},
{
"id": "6",
"titolo": "Test 2",
"autore": "ricette",
"testo": "",
"data": "english",
"extra1": "",
"extra2": "",
"creazione": "2021-01-08 10:49:56",
"foto": "foto\/test_2.jpg",
"fotoUrl": "http:\/\/geniuspointfrezza.altervista.org\/foto\/test_2.jpg"
},
{
"id": "5",
"titolo": "Test",
"autore": "eventi",
"testo": "",
"data": "english",
"extra1": "",
"extra2": "",
"creazione": "2021-01-08 10:47:53",
"foto": "foto\/coastal-wash-web.jpg",
"fotoUrl": "http:\/\/geniuspointfrezza.altervista.org\/foto\/coastal-wash-web.jpg"
},
{
"id": "4",
"titolo": "Immagine di prova",
"autore": "luoghi",
"testo": "",
"data": "italiano",
"extra1": "",
"extra2": "",
"creazione": "2021-01-08 10:24:46",
"foto": "foto\/unnamed.jpg",
"fotoUrl": "http:\/\/geniuspointfrezza.altervista.org\/foto\/unnamed.jpg"
},
{
"id": "3",
"titolo": "Panorama",
"autore": "galleria",
"testo": "",
"data": "italiano",
"extra1": "",
"extra2": "",
"creazione": "2021-01-07 11:21:53",
"foto": "foto\/WP_20161110_001.jpg",
"fotoUrl": "http:\/\/geniuspointfrezza.altervista.org\/foto\/WP_20161110_001.jpg"
},
{
"id": "2",
"titolo": "Comune di Busso",
"autore": "contatti",
"testo": "Indirizzo, telefono, mail, altri dati da inserire",
"data": "italiano",
"extra1": "",
"extra2": "",
"creazione": "2021-01-01 19:33:56",
"foto": "foto\/DSCN0914.JPG",
"fotoUrl": "http:\/\/geniuspointfrezza.altervista.org\/foto\/DSCN0914.JPG"
},
{
"id": "1",
"titolo": "Chiesa",
"autore": "commercio",
"testo": "Testo di prova, abbastanza lungo per verificare l'impaginazione e correggere eventuali errori.",
"data": "english",
"extra1": "",
"extra2": "",
"creazione": "2021-01-01 19:32:02",
"foto": "foto\/CAM_0044.JPG",
"fotoUrl": "http:\/\/geniuspointfrezza.altervista.org\/foto\/CAM_0044.JPG"
}
]
Here's the DownloadManager code:
import SwiftUI
import Combine
class DownloadManager: ObservableObject {
#Published var JSON: [BussoModel] = []
#Published var searchText: String = "" {
didSet {
self.searchResults = self.JSON.filter { $0.titolo.contains(self.searchText) }
}
}
#Published var searchResults: [BussoModel] = []
init() {
let url = URL(string: "https://geniuspointfrezza.altervista.org/index.php?json=1")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let bussoData = data {
let decodedData = try JSONDecoder().decode([BussoModel].self, from: bussoData)
DispatchQueue.main.async {
self.JSON = decodedData
}
} else {
print("No data")
}
} catch {
print(error)
}
}.resume()
}
}
Based on your comments, I believe this is what you're trying to do (unless I misunderstood something). I removed the URL image from the code, so you'll have to add it back.
A couple notes:
Try to do all filtering and data management within the DownloadManager. I added a filter function, which is called when the data gets downloaded and also when the view gets initialized.
Try to avoid hard coding strings into your code. I created a Language enum that will handle the "english" and "italian" filter.
If you ever run into a situation in your code where you're duplicating a whole section (like in your post you rewrote the view for "it" and "else"), then there's definitely a better way to do it.
.
import SwiftUI
//import URLImage
struct HistoryView: View {
#ObservedObject var dm: DownloadManager
let title: String
init(dm: DownloadManager, autore: String) {
self.dm = dm
dm.filter(autore: autore)
self.title = "\(autore)".capitalized + " di Busso"
}
var body: some View {
List {
if !dm.searchResults.isEmpty && !dm.isLoading {
ForEach(dm.searchResults) { busso in
NavigationLink(
destination: Text(busso.titolo)) {
HStack {
Text(busso.fotoUrl)
Text(busso.titolo)
.font(.headline)
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity)
.frame(height: 50)
}
}
} else if dm.isLoading {
ProgressView()
} else {
Text("No results.")
}
}
.navigationTitle(title)
}
}
struct HistoryView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
HistoryView(dm: DownloadManager(), autore: "galleria")
}
}
}
import SwiftUI
import Combine
class DownloadManager: ObservableObject {
#Published private(set) var JSON: [BussoModel] = []
#Published private(set) var isLoading: Bool = true
#Published private(set) var searchAutore: String?
#Published private(set) var searchResults: [BussoModel] = []
let language: Language
enum Language: String {
case italian
case english
}
init() {
language = NSLocale.current.languageCode == "it" ? .italian : .english
getData()
}
private func getData() {
let url = URL(string: "https://geniuspointfrezza.altervista.org/index.php?json=1")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let bussoData = data {
let decodedData = try JSONDecoder().decode([BussoModel].self, from: bussoData)
DispatchQueue.main.async {
self.JSON = decodedData
self.isLoading = false
self.filter(autore: self.searchAutore)
}
} else {
print("No data")
self.isLoading = false
}
} catch {
print(error)
self.isLoading = false
}
}.resume()
}
func filter(autore: String?) {
searchAutore = autore
searchResults = JSON.filter({ (bussoModel) -> Bool in
return bussoModel.data == language.rawValue && (bussoModel.autore == searchAutore)
})
}
}
struct BussoModel: Codable, Identifiable {
let id: String
let titolo: String
let autore: String
let testo: String
let data: String
let extra1: String
let extra2: String
let creazione: String
let foto: String
let fotoUrl: String
}
I have trouble decoding json data using SwiftUI, I have the following json.
{
"data": [
{
"id": "project:xxxxxx",
"project_manager": {
"employee_id": "employee:xxxxxx",
"id": "employee:xxxxxx",
"person_id": "person: xxxxxx",
"name": "Peter Post"
},
"project_status": {
"id": "projectstatus:xxxxxx",
"label": "active"
},
"created": "2019-01-08 15:39:59",
"modified": "2019-01-24 14:39:13",
"created_at": "2019-01-08 15:39:59",
"updated_at": "2019-01-24 14:39:13",
"url": "https://url.com/projects/project/view?id=000",
...
I'm decoding the json with the following code
import Foundation
struct Projects: Decodable {
let data: [Data]
}
struct Data : Decodable, Identifiable {
let id: String
let url: String
let organization: Organization?
let project_status: ProjectStatus?
}
struct Organization : Decodable, Identifiable {
let id: String?
let name: String?
}
struct ProjectStatus: Decodable, Identifiable {
let id: String?
let label: String?
}
import Foundation
import SwiftUI
import Combine
class NetworkingManager: ObservableObject {
#Published var projectList = Projects(data: [])
init() {
var request = URLRequest(url: URL(string: "https://api-url/projects")!,timeoutInterval: Double.infinity)
request.addValue("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", forHTTPHeaderField: "Authentication-Key")
request.addValue("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", forHTTPHeaderField: "Authentication-Secret")
URLSession.shared.dataTask(with: request) { (data, _, _) in
guard let data = data else { return }
let projectList = try! JSONDecoder().decode(Projects.self, from: data)
DispatchQueue.main.async {
self.projectList = projectList
print(self.projectList)
}
}.resume()
}
}
import SwiftUI
struct ContentView : View {
#ObservedObject var networkingManager = NetworkingManager()
var body: some View {
VStack {
List(networkingManager.projectList.data, id: \.id) { project in
HStack {
Text(project.id)
Text(project.url)
}
}
}
}
}
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
This results in a list of id and url string values but my question is: How can I list multiple levels of the json files. Do I have to decode each level of the json I want to use or is there a better way?
It was less complicated than I thought. I got a nil value back when I was calling project_status?.label
this was resolved when I called it like this:
project_status?.label ?? self.defaultString
I just started using combine swift to handle the data request and response.
The Json data returned is a nested data which I will only need one part of it.
such as:
{
"page": 1,
"data": [
{
"id": 1,
"title": "news-1",
"content": "content 1"
},
{
"id": 2,
"title": "news-2",
"content": "content 2"
},
{
"id": 3,
"title": "news-3",
"content": "content 3"
}
],
"time": 202021313,
"description" :"xxxx"
}
I will need to use the data array.
Fetch functions below:
func fetchData() throws -> URLSession.DataTaskPublisher {
let headers = [
"Content-Type": "application/json",
"cache-control": "no-cache",
]
guard let url = URL(string: endpointStr ) else {
throw APIError.invalidEndpoint
}
var request = URLRequest(url: url,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let session = URLSession.shared
return session.dataTaskPublisher(for: request)
}
let publisher = try? fetchData()
let decoder = JSONDecoder()
let cancellable = publisher?
.receive(on: DispatchQueue.main)
.map {
$0.data
}
.decode(type: DataModel.self, decoder: decoder)
.sink(receiveCompletion: { (completion) in
switch completion {
case .failure(let error):
print("Error:")
print(error)
case .finished:
print("DONE - get Publisher")
}
}, receiveValue: { data in
print(data.title)
})
The data it returned is the complete json data, is there any elegant way to get only the array of data and convert into an array of [DataModel] and handle the data in receiveValue.
I have tried to edit map with no luck:
.map {
if let dataString = String(data: $0.data, encoding: .utf8) {
let dataDic = APIService.convertToDictionary(text: dataString)
if let DataArray = dataDic?["data"] {
return listDataDic!
}
return $0.data
}
Please clarify if i've misunderstood the question but what if you use another model to decode your [DataModel] and then map to the decoded [DataModel] array?
Here is a unit test example. Response is the new model that decodes the [DataModel] array into something you can work with.
import XCTest
import Combine
let data = """
{
"page": 1,
"data": [
{
"id": 1,
"title": "news-1",
"content": "content 1"
},
{
"id": 2,
"title": "news-2",
"content": "content 2"
},
{
"id": 3,
"title": "news-3",
"content": "content 3"
}
],
"time": 202021313,
"description": "xxxx"
}
""".data(using: .utf8)!
class Response: Codable {
var data: [DataModel]
}
class DataModel: Codable {
let id: Int
let title: String
let content: String
}
class Test: XCTestCase {
func testDecodeDataModel() {
let e = expectation(description: "finished expectation")
let decoder = JSONDecoder()
let cancellable = Just(data)
.decode(type: Response.self, decoder: decoder)
.map { $0.data }
.sink(receiveCompletion: { (completion) in
// handle completion..
}, receiveValue: { dataArray in
print(dataArray.count) // here you can work with your [DataModel] array
e.fulfill()
})
wait(for: [e], timeout: 1)
}
}