Elaborating the Problem in depth with code
I have a data model (API response) using which I am creating a list. The list items should also display their details in detail view. The problem is details of list items are coming from a different API other than the API used for creating the list. The id of one item is passed to API which in response provides the details of the item.
This is the data model(including items only problem specific):
struct TrackSample : Codable, Identifiable {
let id = UUID()
let success : Bool
let message : String
let trackResponse : [TrackResponse]
enum CodingKeys: String, CodingKey{
case success = "IsSuccess"
case message = "Message"
case trackResponse = "ResponseData"
}}
struct TrackResponse : Codable, Identifiable {
let id = UUID()
let patientID : String
let name : String
let testCount : Int
//..some more//
enum CodingKeys: String, CodingKey {
case patientID = "PatientId"
case name = "Name"
case status = "Status"
case testCount = "NoOfTests"
}}
ViewModel to fetch API response ( TrackResource() is a different class which implements the networking call):
class TrackViewModel : ObservableObject
{
#Published var trackReport = [TrackResponse]()
#Published var navigate:Bool = false
//other var
private let trackResource = TrackResource()
func getTrack()
{
//code to get data based on which button is clicked in Options
screen.
There are five options to choose:
if else statement follows//
centerID = "100"
let trackRequest = TrackRequest(CenterId:centerID,
SearchText:searchText, StartDate:startDate, EndDate:endDate)
trackResource.track(trackRequest: trackRequest)
{
response in
if(response?.success==true)
{
DispatchQueue.main.async {
self.navigate = true
self.trackReport = response?.trackResponse ?? []
}
}
else
{
DispatchQueue.main.async {
self.errorMessage = response?.message ?? "No Data"
// self.isPresentingAlert
}
}}}}
The view YdaySample which represents the list :
struct YdaySample: View {
#ObservedObject var tracking : TrackViewModel
var body: some View {
NavigationView{
List{
ForEach(tracking.trackReport)
{ truck in
NavigationLink(destination: TrackDetail(track:truck))
{
YdayRow(truck: truck)
}
}
if(tracking.trackReport.isEmpty){
Text("No Record Found !")
//formatting
}}}}}
struct YdaySample_Previews: PreviewProvider {
static var previews: some View {
YdaySample(tracking: TrackViewModel())
}}
The YdayRow() :
struct YdayRow: View {
var truck : TrackResponse
var body: some View {
VStack{
HStack{
Text(truck.name)
//formatting//
}
HStack{
Text("P.Id:")
//formatting//
Text(truck.patientID)
//formatting//
Spacer()
Text("Total Test:")
//formatting//
Text("\(truck.testCount)")
//formatting//
}}}}
struct YdayRow_Previews: PreviewProvider {
static var previews: some View {
YdayRow(truck: TrackResponse(patientID: "1", name: "test",
status: "null", crmNo: "null",
recordDate: "somedate", uniqueID: "null", testCount: 4))
}
}
TrackDetail() updated:
struct TrackDetail: View {
var track: TrackResponse
#State var patientDetail: DetailResponse
var body: some View {
VStack{
HStack{
Text(track.name)
//formatting
}.frame(maxWidth: .infinity, alignment: .center)
HStack{
Text("P.Id:")
//formatting
Text(track.patientId)
//formatting
}
List{ForEach(patientDetail.detailResponse)
{detail in
HStack
{ Text("Barcode: ")
Text(detail.barcode)
}
}
}.onAppear{
Task{
do{
try await getDetail()
}catch{
Alert(title: Text("Error"),message: Text("Not Found"),
dismissButton: .cancel())
}}}}}
func getDetail() async throws{
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "xx.xxx.xx.xx"
urlComponents.path = "/api/reports/getalltests"
urlComponents.queryItems = [URLQueryItem(name: "centerId", value:
("\(668)")),URLQueryItem(name: "patientId", value: "\
(track.patientId)")]
let url = urlComponents.url
var request = URLRequest(url: url!)
print(request)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("application/json", forHTTPHeaderField: "Content-
Type")
request.setValue("Basic xcvgjhalddjdj",forHTTPHeaderField:
"Authorization")
// Send HTTP Request
let task = URLSession.shared.dataTask(with: request) { (data,
response, error) in
// Check if Error took place
if let error = error {
print("Error took place \(error)")
return
}
// Read HTTP Response Status code
if let response = response as? HTTPURLResponse {
print("Response HTTP Status code: \(response.statusCode)")
}
if let data = data
{
let dataString = String(data:data,encoding: .utf8)
print(dataString!)
do{
let json = try JSONDecoder().decode(DetailResponse.self,
from: data)
print(json)
DispatchQueue.main.async {
self.patientDetail = json
}
}catch{
print("error \(error)")
}
}
};task.resume()
}}
struct TrackDetail_Previews: PreviewProvider {
static var previews: some View {
TrackDetail(track: TrackResponse(patientId: "4193716", name: "Dummy
Report HCV RNA", status: "null", crmNo: "null", recordDate: "2022-
04-15", uniqueId: "null", testCount: 10), patientDetail:
DetailResponse(success: false, message: "mess", detailResponse:
[]))
}}
print(request) is printing right URL as desired also the patientID is correct in url (http://xxx..x.x/api/reports/getalltests/centerId=668&patientId=(tapped id))
But it is throwing error in decoding saying "Authrization has been denied for this request" error keynotfound(codingkeys(stringvalue: "ResponseData", intvalue:nil), Swift.decodingerror.context(codingpath: [],debugdescription: "No value associated with key CodingKeys(stringvalue: "ResponseData", intvalue:nil)("ResponseData", underlyingError:nil)
struct DetailResponse : Codable{
let success : Bool ?
let message : String
let detailResponse : [PatResponse]
enum CodingKeys: String, CodingKey{
case success = "IsSuccess"
case message = "Message"
case patResponse = "ResponseData"
}}
struct PatResponse : Codable, Identifiable {
var barcode: String
var:id String {barcode}
let testname : String?
let packageName : String?
let status: String?
let sampleRecievedTime: String?
let recordDate: String
enum CodingKeys: String, CodingKey {
case packageName = "PackageName"
case testname = "TestName"
case status = "Status"
case sampleRecievedTime = "SampleRecievedTime"
case recordDate = "RecordDate"
case barcode = "Barcode"
}}
///////**************///////////////////
The detail view is showing name and ID as they are coming from TrackResponse
but status and barcode are not as they are from different API.
When the user click/tap an item in list, the patientID and centerId is sent as query param in GET request which in response will provide the details associated with the given patientId (center ID is constant). How to implement this?
How about something like this?
struct TrackDetail: View{
var track: TrackResponse
#State var details: TrackDetails?
var body: some View{
HStack{
//Detail implementation
}.onAppear{
Task{
do{
try await doStuff()
}catch{
//Alert
}
}
}
}
func doStuff() async throws{
// pull Details from Api
}
}
Related
I have a json-result that I get from my own "api". In this I have a list of different devices, that I like to view in a list.
When I add the ForEach, then I get the following error:
Generic struct 'List' requires that 'some AccessibilityRotorContent' conform to 'View'
The JsonResponse:
[{"name":"Tormek-T8","topHorizontal":55,"topVertical":55,"frontHorizontal":44,"frontVertical":44},{"name":"SH-332","topHorizontal":77,"topVertical":77,"frontHorizontal":88,"frontVertical":88}]
Can it be, that there are numbers in this JSON-String and not all is a String?
In my very simple code I had made a struct for the single Device and try to pull it from the url. Why I would get this error? Because the response of the JSON is not nil.
struct Device: Hashable, Codable {
let name: String
let topHorizontal: Double
let topVertical: Double
let frontHorizontal: Double
let frontVertical: Double
}
class ViewModel: ObservableObject {
#Published var devices: [Device] = []
func fetch() {
guard let url = URL(string: "https://cdn.rowoco.de/grindcalculator/devices") else {
return
}
let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
guard let data = data, error == nil else {
return
}
do {
let devices = try JSONDecoder().decode([Device].self, from: data)
DispatchQueue.main.async {
self?.devices = devices
}
} catch {
print(error)
}
}
task.resume()
}
}
struct CdnDevicesListView: View {
#Environment(\.presentationMode) var presentationMode
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.devices, id: \.self) { device in
device.name
}
}
.navigationTitle("cdn devices")
.onAppear {
viewModel.fetch()
}
}
}
}
ForEach's content needs to be a View.
device.name is not a View.
Summary:
I have a list loaded from an API. Each list item have a button. On click of button, a unique ID associated with the list item is sent to server which in response provides a pdf directly there is no other response just a pdf file, the api is like :
http://myhost/api/DownloadPDF/uniqueID=67198287_239878092_8089
I have created the list and also able to download the pdf in documentDirectory by calling download task. However, I am unable to open the pdf automatically in app itself after downloading. I have created DisplayPDF struct which uses PDFKit to display as follows:
struct DisplayPDF: View {
var url:URL
var body:some View
{
PDFKitRepresentedView(url)
}
}
struct PDFKitRepresentedView: UIViewRepresentable{
func updateUIView(_ uiView: UIView, context:
UIViewRepresentableContext<PDFKitRepresentedView>) {
}
let url: URL
init(_ url:URL)
{
self.url = url
}
func makeUIView(context:
UIViewRepresentableContext<PDFKitRepresentedView>) ->
PDFKitRepresentedView.UIViewType {
let pdfView = PDFView()
pdfView.document = PDFDocument(url: self.url)
pdfView.autoScales = true
return pdfView
}}
I need to pass the url into the above struct. The url can be the saved location or API directly. However, the url is not passed when the DisplayPDF view is called.
What I have tried so far
1> Pass the DisplayPDF into navigationlink in ReportList(where list is loaded) struct and than either call getFile func in onAppear in DisplayPDF struct or ReportRow struct.
2> Call getFile() on ReportRow in onAppear and pass the url in DisplayPDF() there.
3> Call getFile() on DisplayPDF() onAppear and pass the url there
4> Also tried, sheet method blank sheet pops up
All failed, no value is sent to DisplayPDF(url) the moment it is called from any of the listed method.
ReportList struct
import SwiftUI
struct ReportList: View {
#ObservedObject var reportLink : ReportViewModel
var body: some View {
List{
ForEach(reportLink.trackReport)
{report in
VStack {
ReportRow(report: report)
}
if(reportLink.trackReport.isEmpty)
{
Text("No Report Found")
.foregroundColor(.accentColor)
.fontWeight(.semibold)
}
}
}
}
}
ReportRow struct:
struct ReportRow: View {
var report : ReportResponse
#StateObject var pdfDownload = PDFDownload()
var body: some View {
VStack{
HStack{
Text(report.name)
.font(.system(size: 16))
.foregroundColor(.black)
.padding(.bottom,1)
.padding(.top,1)
}.frame(maxWidth: .infinity, alignment: .leading)
HStack{
Text("P.Id:")
.foregroundColor(.black)
.font(.system(size: 14))
Text(report.patientID)
.foregroundColor(.purple)
.font(.system(size: 14))
Spacer()
Button(action: {
pdfDownload.uniqueReportId = report.uniqueID
pdfDownload.patientName = report.name
pdfDownload.getFile()
}, label:
{
Text("\(report.status)")
.foregroundColor(.blue)
.font(.system(size: 14))
.padding(.trailing,2)
}).frame(maxWidth: .infinity, alignment: .trailing)
}
}
}}
I have made this PDFDownload model in which openURL is declared a published var which should provide updated url to a view(like DisplayPDF() view):
class PDFDownload : UIViewController, ObservableObject
{
#Published var uniqueReportId:String = String()
#Published var patientName:String = String()
#Published var isNavigate:Bool = false
#Published var openURL:URL = URL(fileURLWithPath: "")
func getFile()
{
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "myHost"
urlComponents.port = 80
urlComponents.path = "/api/Reports/DownloadReport"
urlComponents.queryItems = [URLQueryItem(name: "uniquePackageId",
value: uniqueReportId)]
let url = urlComponents.url
print(url?.absoluteString)
let downloadTask = URLSession.shared.downloadTask(with: url!)
{
urlOrNil, responseOrNil, errorOrNil in
guard let fileURL = urlOrNil else {return}
do{
let documentURL = try FileManager.default.url(for:
.documentDirectory, in: .userDomainMask, appropriateFor:
nil, create: false)
let savedURL = documentURL.appendingPathComponent("\
(self.patientName)_\(UUID().uuidString).pdf")
print(savedURL)
try FileManager.default.moveItem(at: fileURL, to:
savedURL)
DispatchQueue.main.async {
self.openURL = savedURL
}
}
catch{
print("Error while writting")
}
}
downloadTask.resume()
}}
So what is the correct way of solving this problem that the correct URL can be passed to DisplayPDF() view.
Extra: ReportResponse model:
struct DownReport : Codable, Identifiable {
let id = UUID()
let success : Bool
let message : String
let reportResponse : [ReportResponse]
enum CodingKeys: String, CodingKey{
case success = "IsSuccess"
case message = "Message"
case reportResponse = "ResponseData"
}}
struct ReportResponse : Codable, Identifiable {
var id:String {uniqueID}
let patientID : String
let name : String
let status : String
let crmNo : String?
let recordDate : String
let uniqueID : String
let testCount : Int
enum CodingKeys: String, CodingKey {
case patientID = "PatientId"
case name = "Name"
case status = "Status"
case crmNo = "CrmNo"
case recordDate = "RecordDate"
case uniqueID = "UniquePackageId"
case testCount = "NoOfTests"
}
}
The above response is from POST request which is sent to generate list. To get pdf only unique id as Query is sent as I have posted on top.
The above structure successfully downloads the file but fail to open the file automatically. How to do that?
Here is some sample code that shows how to download a pdf document (wikipedia),
copy it to a local file, and display it on the screen by passing the savedURL to the View. You should be able to adapt the sample code for your purpose.
import Foundation
import SwiftUI
import PDFKit
struct ContentView: View {
#StateObject var downloader = PDFDownload()
var body: some View {
VStack (spacing: 30) {
Button("download1", action: {
downloader.patientName = "patient-1"
downloader.uniqueReportId = "astwiki-Homo_heidelbergensis-20200728.pdf/astwiki-Homo_heidelbergensis-20200728.pdf"
downloader.getFile()
}).buttonStyle(.bordered)
Button("download2", action: {
downloader.patientName = "patient-2"
downloader.uniqueReportId = "rowiki-Biban_european-20200728.pdf/rowiki-Biban_european-20200728.pdf"
downloader.getFile()
}).buttonStyle(.bordered)
if downloader.isDownloading { ProgressView("downloading ...") }
}
.fullScreenCover(item: $downloader.openURL) { siteUrl in
DisplayPDF(url: siteUrl.url)
}
}
}
struct DisplayPDF: View {
#Environment(\.dismiss) var dismiss
let url: URL
var body:some View {
VStack {
#if targetEnvironment(macCatalyst)
Button("Done", action: {dismiss()})
#endif
PDFViewer(url: url)
}
}
}
struct PDFViewer: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -> PDFView {
let pdfView = PDFView()
pdfView.document = PDFDocument(url: url)
pdfView.autoScales = true
return pdfView
}
func updateUIView(_ uiView: PDFView, context: Context) { }
}
class PDFDownload : ObservableObject {
#Published var uniqueReportId = ""
#Published var patientName = ""
#Published var isNavigate = false
#Published var openURL: SiteURL?
#Published var isDownloading = false
func getFile() {
isDownloading = true
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "ia803207.us.archive.org"
urlComponents.path = "/0/items/\(uniqueReportId)" // <-- just for testing
// urlComponents.port = 80
// urlComponents.queryItems = [URLQueryItem(name: "uniquePackageId", value: uniqueReportId)]
guard let url = urlComponents.url else {return}
let downloadTask = URLSession.shared.downloadTask(with: url) { urlOrNil, responseOrNil, errorOrNil in
guard let fileURL = urlOrNil else { return }
do {
let documentURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let savedURL = documentURL.appendingPathComponent("\(self.patientName)_\(UUID().uuidString).pdf")
try FileManager.default.moveItem(at: fileURL, to: savedURL)
DispatchQueue.main.async {
self.openURL = SiteURL(url: savedURL)
self.isDownloading = false
}
}
catch {
print("Error \(error)")
}
}
downloadTask.resume()
}
}
struct SiteURL: Identifiable {
let id = UUID()
var url: URL
}
Updated Answer with your update question: the row will update when the file is downloaded, it will then be a navigation link to display pdf
struct DownReport : Codable, Identifiable {
let id = UUID()
let success : Bool
let message : String
let reportResponse : [ReportResponse]
enum CodingKeys: String, CodingKey{
case success = "IsSuccess"
case message = "Message"
case reportResponse = "ResponseData"
}
}
struct ReportResponse : Codable, Identifiable {
var id:String {uniqueID}
let patientID : String
let name : String
let status : String
let crmNo : String?
let recordDate : String
let uniqueID : String
let testCount : Int
// This URL is only set when report has been downloaded and it does not need to be part of the response
var localFileUrl: URL?
enum CodingKeys: String, CodingKey {
case patientID = "PatientId"
case name = "Name"
case status = "Status"
case crmNo = "CrmNo"
case recordDate = "RecordDate"
case uniqueID = "UniquePackageId"
case testCount = "NoOfTests"
}
}
class ReportViewModel: ObservableObject {
// some dummy value
#Published var trackReport: [ReportResponse] = [ReportResponse(patientID: "0001", name: "patient-1", status: "status", crmNo: nil, recordDate: "today", uniqueID: "010001", testCount: 1),ReportResponse(patientID: "0002", name: "patient-2", status: "status", crmNo: nil, recordDate: "today", uniqueID: "010002", testCount: 3)]
// Update the report in the array ussing report unique ID
func updateReport(withId reportId: String, url: URL) {
guard let index = trackReport.firstIndex(where: {$0.uniqueID == reportId}) else {return}
var report = trackReport[index]
report.localFileUrl = url
trackReport[index] = report
}
}
// no need for any observation on pdfDownload object as the completion will do the jobs
class PDFDownload {
var uniqueReportId: String
var patientName: String
init(uniqueReportId:String, patientName:String) {
self.uniqueReportId = uniqueReportId
self.patientName = patientName
}
func getFile(completion: #escaping (URL) -> ())
{
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "myHost"
urlComponents.port = 80
urlComponents.path = "/api/Reports/DownloadReport"
urlComponents.queryItems = [URLQueryItem(name: "uniquePackageId",value: uniqueReportId)]
let url = urlComponents.url
// print(url?.absoluteString)
let downloadTask = URLSession.shared.downloadTask(with: url!)
{
urlOrNil, responseOrNil, errorOrNil in
// Simulation of downloading
sleep(3)
DispatchQueue.main.async {
completion(URL(fileURLWithPath: "report\(self.patientName).pdf"))
}
guard let fileURL = urlOrNil else {return}
do{
let documentURL = try FileManager.default.url(for:
.documentDirectory, in: .userDomainMask, appropriateFor:
nil, create: false)
let savedURL = documentURL.appendingPathComponent("\(self.patientName)_\(UUID().uuidString).pdf")
print(savedURL)
try FileManager.default.moveItem(at: fileURL, to:
savedURL)
// Update the report url
DispatchQueue.main.async {
completion(savedURL)
}
}
catch{
print("Error while writting")
}
}
downloadTask.resume()
}
}
struct ReportList: View {
#ObservedObject var reportLink : ReportViewModel
var body: some View {
NavigationView{
List{
ForEach(reportLink.trackReport) { report in
if let url = report.localFileUrl {
NavigationLink {
DisplayPDF(url: url)
} label: {
Text(report.name)
}
} else {
ReportRow(report: report, updateReport: updateReport)
}
}
// Moved out of ForEach
if(reportLink.trackReport.isEmpty)
{
Text("No Report Found")
.foregroundColor(.accentColor)
.fontWeight(.semibold)
}
}
}
}
func updateReport(withId reportId: String, url: URL) {
reportLink.updateReport(withId: reportId, url: url)
}
}
struct ReportRow: View {
var report: ReportResponse
var updateReport: (String, URL) -> ()
var body: some View {
VStack{
HStack{
Text(report.name)
.font(.system(size: 16))
.foregroundColor(.black)
.padding(.bottom,1)
.padding(.top,1)
}.frame(maxWidth: .infinity, alignment: .leading)
HStack{
Text("P.Id:")
.foregroundColor(.black)
.font(.system(size: 14))
Text(report.patientID)
.foregroundColor(.purple)
.font(.system(size: 14))
Spacer()
Button(action: {
let pdfDownload = PDFDownload(uniqueReportId: report.uniqueID, patientName: report.name)
pdfDownload.getFile(completion: updateReportUrl)
}, label:
{
Text("\(report.status)")
.foregroundColor(.blue)
.font(.system(size: 14))
.padding(.trailing,2)
}).frame(maxWidth: .infinity, alignment: .trailing)
}
}
}
func updateReportUrl(url: URL) {
updateReport(report.uniqueID, url)
}
}
struct DisplayPDF: View {
var url:URL
var body:some View
{
// Stub as I can not download
Text(url.absoluteString)
// PDFKitRepresentedView(url)
}
}
struct PDFKitRepresentedView: UIViewRepresentable{
func updateUIView(_ uiView: UIView, context:
UIViewRepresentableContext<PDFKitRepresentedView>) {
}
let url: URL
init(_ url:URL)
{
self.url = url
}
func makeUIView(context:
UIViewRepresentableContext<PDFKitRepresentedView>) ->
PDFKitRepresentedView.UIViewType {
let pdfView = PDFView()
pdfView.document = PDFDocument(url: self.url)
pdfView.autoScales = true
return pdfView
}
}
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)")
}
}
I have a currency API that returns a JSON object containing a strange arrangement: the base currency is used as a label. Typical currency APIs have labels like "base", "date", "success", and "rates", but this API doesn't have any of those.
{
"usd": {
"aed": 4.420217,
"afn": 93.3213,
"all": 123.104693,
"amd": 628.026474,
"ang": 2.159569,
"aoa": 791.552347,
"ars": 111.887966,
"aud": 1.558363,
"awg": 2.164862,
"azn": 2.045728,
"bam": 1.9541,
"bbd": 2.429065,
"bch": 0.001278
}
}
The "usd" (US dollars) at the top is called the base or home currency. At the moment the storage structure and state parameter are hardcoded to "usd" which prevents using exchange rates with other base currencies. The exchange rate API works great for a base currency of US dollars.
I need help modifying things so that I can download the exchange rates with different base currencies. For example, can I use a string variable in the storage model and state parameter? Any enlightenment will be greatly appreciated.
struct RateResult: Codable {
let usd: [String: Double]
}
#State private var results = RateResult(usd: [:])
struct ContentView: View {
var body: some View {
}
func UpdateRates() {
let baseUrl = "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api#1/latest/currencies/"
let baseCur = baseCurrency.baseCur.baseS // usd
let requestType = ".json"
guard let url = URL(string: baseUrl + baseCur + requestType) 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(RateResult.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse
// save off currency exchange rates
}
// everything is good, so we can exit
return
}
}
// if we're still here it means there was a problem
print("Currency Fetch Failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
import SwiftUI
//You can't use the standard Codable for this. You have to make your own.
class BaseCurrency: Codable {
let id = UUID()
var baseCurrencies: [String : [String: Double]] = [:]
required public init(from decoder: Decoder) throws {
do{
print(#function)
let baseContainer = try decoder.singleValueContainer()
let base = try baseContainer.decode([String : [String: Double]].self)
for key in base.keys{
baseCurrencies[key] = base[key]
}
}catch{
print(error)
throw error
}
}
//#State should never be used outside a struct that is a View
}
struct CurrencyView: View {
#StateObject var vm: CurrencyViewModel = CurrencyViewModel()
var body: some View {
VStack{
List{
if vm.results != nil{
ForEach(vm.results!.baseCurrencies.sorted{$0.key < $1.key}, id: \.key) { key, baseCurrency in
DisclosureGroup(key){
ForEach(baseCurrency.sorted{$0.key < $1.key}, id: \.key) { key, rate in
HStack{
Text(key)
Text(rate.description)
}
}
}
}
}else{
Text("waiting...")
}
}
//To select another rate to go fetch
RatesPickerView().environmentObject(vm)
}.onAppear(){
vm.UpdateRates()
}
}
}
struct RatesPickerView: View {
#EnvironmentObject var vm: CurrencyViewModel
var body: some View {
if vm.results != nil{
//You can probaly populate this picker with the keys in
// baseCurrency.baseCur.baseS
Picker("rates", selection: $vm.selectedBaseCurrency){
ForEach((vm.results!.baseCurrencies.first?.value.sorted{$0.key < $1.key})!, id: \.key) { key, rate in
Text(key).tag(key)
}
}
}else{
Text("waiting...")
}
}
}
class CurrencyViewModel: ObservableObject{
#Published var results: BaseCurrency?
#Published var selectedBaseCurrency: String = "usd"{
didSet{
UpdateRates()
}
}
init() {
//If you can .onAppear you don't need it here
//UpdateRates()
}
func UpdateRates() {
print(#function)
let baseUrl = "https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api#1/latest/currencies/"
let baseCur = selectedBaseCurrency // usd
let requestType = ".json"
guard let url = URL(string: baseUrl + baseCur + requestType) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do{
let decodedResponse = try JSONDecoder().decode(BaseCurrency.self, from: data)
DispatchQueue.main.async {
if self.results == nil{
//Assign a new base currency
self.results = decodedResponse
}else{ //merge the existing with the new result
for base in decodedResponse.baseCurrencies.keys{
self.results?.baseCurrencies[base] = decodedResponse.baseCurrencies[base]
}
}
//update the UI
self.objectWillChange.send()
}
}catch{
//Error thrown by a try
print(error)//much more informative than error?.localizedDescription
}
}
if error != nil{
//data task error
print(error!)
}
}.resume()
}
}
struct CurrencyView_Previews: PreviewProvider {
static var previews: some View {
CurrencyView()
}
}
Scenario:
I'm using an Observable Class to acquire data from the network.
In this case some elementary weather data.
Problem:
I don't know how to display this data in the calling View.
For the time-being, I merely am trying to populate a Textfield (and worry about more-eleborate layout later).
I get the following:
.../StandardWeatherView.swift:22:13: Cannot invoke initializer for
type 'TextField<_>' with an argument list of type '(Text, text:
Sample?)'
Here's is my calling View which is the receiver of #ObservedObject data:
import SwiftUI
struct StandardWeatherView: View {
#EnvironmentObject var settings: Settings
#ObservedObject var standardWeatherReportLoader = StandardWeatherReportLoader()
init() {
self.standardWeatherReportLoader.doStandard()
}
var body: some View {
ZStack {
Color("FernGreen").edgesIgnoringSafeArea(.all)
TextField(Text("Weather Data"), text: standardWeatherReportLoader.weatherReport)
}
}
}
struct StandardWeatherView_Previews: PreviewProvider {
static var previews: some View {
StandardWeatherView()
}
}
Here's the publisher, acquiring data:
import Foundation
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("\n Standard Weather ----------------")
print(#function, "line: ", #line, "Result: ",result)
print("\n")
} catch let error as NSError {
print(error)
}
}
}
task.resume()
}
}
What's the simplest way of passing a string of data to the View via #Published var?
Log:
Standard Weather ---------------- doStandard() line: 38 Result:
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")
But I'm getting nil at the TextField:
(lldb) po standardWeatherReportLoader.weatherReport nil
One option is to set a binding within your body to track whenever the TextField has updated. From within this binding, you can then edit your Published variable as you wish:
#ObservedObject var reportLoader = StandardWeatherReportLoader()
var body: some View {
// Binding to detect when TextField changes
let textBinding = Binding<String>(get: {
self.reportLoader.networkMessage
}, set: {
self.reportLoader.networkMessage = $0
})
// Return view containing the text field
return VStack {
TextField("Enter the Network Message", text: textBinding)
}
}
Edit: Also in your original post, you were passing an object of optional type Sample into the TextField which was expecting a binding String type which could cause some issues.