Swiftui Dynamic Cloudkit predicate results not filling array - swiftui

I hope someone can help me - I have a dynamic cloudkit array which works fine up until it gets to the List part of my view. In the quertresultBlock when I print returnedItems the records are all there and filtered inline with my predicate. However, the staff array is empty. Its driving me mad. Could someone help please?
struct MarketingModel: Hashable {
let firstName: String
let lastName: String
let country: String
let department: String
let phoneNumber: String
let job: String
let headshot: URL?
let record: CKRecord
}
struct predicateFilter: View {
#State var text: String = ""
#State var staff: [MarketingModel] = []
#State private var countryselected: Country = .Management
var body: some View {
List {
ForEach(staff, id: \.self) { thepeople in
VStack (alignment: .leading) {
Text(thepeople.firstName)
extension predicateFilter {
init(filter: String) {
UISegmentedControl.appearance().selectedSegmentTintColor = UIColor(red: 116/255, green: 152/255, blue: 192/255, alpha: 1.0)
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
//func fetchItems() {
//let predicate = NSPredicate(value: true)
let predicate = NSPredicate(format: "department == %#", filter)
let query = CKQuery(recordType: "Staff", predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "lastName", ascending: true)]
let queryOperation = CKQueryOperation(query: query)
var returnedItems: [MarketingModel] = []
queryOperation.recordMatchedBlock = { (returnedRecordID, returnedResult) in
switch returnedResult {
case .success(let record):
guard let firstName = record["firstName"] as? String else { return }
guard let lastName = record["lastName"] as? String else { return }
guard let department = record["department"] as? String else { return }
guard let country = record["country"] as? String else { return }
guard let mobile = record["phone"] as? String else { return }
guard let job = record["job"] as? String else { return }
let imageAsset = record["headshot"] as? CKAsset
let imageURL = imageAsset?.fileURL
returnedItems.append(MarketingModel(firstName: firstName, lastName: lastName, country: country, department: department, phoneNumber: mobile, job: job, headshot: imageURL, record: record))
case .failure(let error):
print("Error recordMatchedBlock: \(error)")
}
}
queryOperation.queryResultBlock = { [self] returnedResult in
print("RETURNED RESULT \(returnedResult)")
DispatchQueue.main.async {
staff = returnedItems
print(staff)
}
}
addOperation(opertaion: queryOperation)
}
func addOperation(opertaion: CKDatabaseOperation) {
CKContainer.default().publicCloudDatabase.add(opertaion)
print("working")
}
}

Related

Update and Pass published variable value to view after click of Button

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
}
}

How to parse value from API by its id in SwiftUI

I work with this API and I figured out how to parse all the data from it, except one value – payload weight. The problem is I have to parse it by id – "leo", but I don't understand how to do this.
This is my code:
// MARK: - API
class InfoApi {
func getRockets(completion: #escaping ([RocketInfo]) -> ()) {
guard let url = URL(string: "https://api.spacexdata.com/v4/rockets") else {
return
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
let rocketsInfo = try JSONDecoder().decode([RocketInfo].self, from: data!)
DispatchQueue.main.async {
completion(rocketsInfo)
}
} catch {
print(error.localizedDescription)
}
}
.resume()
}
}
// MARK: - MODEL
struct RocketInfo: Codable, Identifiable {
let id = UUID()
let name: String
let firstFlight: String
let country: String
let costPerLaunch: Int
let firstStage: StageInfo
let payloadWeights: [Payload]
enum CodingKeys: String, CodingKey {
case id
case name
case firstFlight = "first_flight"
case country
case costPerLaunch = "cost_per_launch"
case firstStage = "first_stage"
case payloadWeights = "payload_weights"
}
// MARK: - STAGE
struct StageInfo: Codable {
let engines: Int
let fuelAmountTons: Double
let burnTimeSec: Int?
enum CodingKeys: String, CodingKey {
case engines
case fuelAmountTons = "fuel_amount_tons"
case burnTimeSec = "burn_time_sec"
}
static let firstStage = StageInfo(engines: 1, fuelAmountTons: 44.3, burnTimeSec: 169)
static let secondStage = StageInfo(engines: 1, fuelAmountTons: 3.30, burnTimeSec: 378)
}
// MARK: - PAYLOAD
struct Payload: Codable {
let id: String
let kg: Int
let lb: Int
static let payloadWeights = Payload(id: "leo", kg: 450, lb: 992)
}
// MARK: - EXAMPLE
static let example = RocketInfo(name: "Falcon 1", firstFlight: "2006-03-24", country: "Republic of the Marshall Islands", costPerLaunch: 6700000, firstStage: StageInfo.firstStage, payloadWeights: [Payload.payloadWeights])
}
// MARK: - CONTENT VIEW
struct ParametersView: View {
#State var rockets: [RocketInfo] = []
var body: some View {
List(rockets) { rocket in
VStack(spacing: 20) {
HStack {
Text("First flight of \(rocket.name)")
Spacer()
Text("\(rocket.firstFlight)")
}
HStack {
Text("Payload of \(rocket.name)")
Spacer()
Text("\(rocket.payloadWeights[0].kg)") //<-- Here I try to parse a payload weight value
}
}
}
.onAppear {
InfoApi().getRockets { rockets in
self.rockets = rockets
}
}
}
}
// MARK: - PREVIEW
struct ParametersView_Previews: PreviewProvider {
static var previews: some View {
ParametersView()
}
}
I can access payload weight value by pointing an index of the first element of the Payload array in API, but I want to figure out how I can get this value by special id – "Leo".
In API it looks this way:
You can use first(where:) to search through the array and return the first element matching a condition (in this case, matching a certain id):
if let leo = rocket.payloadWeights.first(where: { $0.id == "leo" }) {
Text("\(leo.kg)") //<-- Here I try to parse a payload weight value
}

Dynamically Changing the Base Currency

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()
}
}

Saving favorites to UserDefaults using struct id

I'm trying to save the users favorite cities in UserDefaults. Found this solution saving the struct ID - builds and runs but does not appear to be saving: On app relaunch, the previously tapped Button is reset.
I'm pretty sure I'm missing something…
Here's my data struct and class:
struct City: Codable {
var id = UUID().uuidString
var name: String
}
class Favorites: ObservableObject {
private var cities: Set<String>
let defaults = UserDefaults.standard
var items: [City] = [
City(name: "London"),
City(name: "Paris"),
City(name: "Berlin")
]
init() {
let decoder = PropertyListDecoder()
if let data = defaults.data(forKey: "Favorites") {
let cityData = try? decoder.decode(Set<String>.self, from: data)
self.cities = cityData ?? []
return
} else {
self.cities = []
}
}
func getTaskIds() -> Set<String> {
return self.cities
}
func contains(_ city: City) -> Bool {
cities.contains(city.id)
}
func add(_ city: City) {
objectWillChange.send()
cities.contains(city.id)
save()
}
func remove(_ city: City) {
objectWillChange.send()
cities.remove(city.id)
save()
}
func save() {
let encoder = PropertyListEncoder()
if let encoded = try? encoder.encode(tasks) {
defaults.setValue(encoded, forKey: "Favorites")
}
}
}
and here's the TestDataView
struct TestData: View {
#StateObject var favorites = Favorites()
var body: some View {
ForEach(self.favorites.items, id: \.id) { item in
VStack {
Text(item.title)
Button(action: {
if self.favorites.contains(item) {
self.favorites.remove(item)
} else {
self.favorites.add(item)
}
}) {
HStack {
Image(systemName: self.favorites.contains(item) ? "heart.fill" : "heart")
.foregroundColor(self.favorites.contains(item) ? .red : .white)
}
}
}
}
}
}
There were a few issues, which I'll address below. Here's the working code:
struct ContentView: View {
#StateObject var favorites = Favorites()
var body: some View {
VStack(spacing: 10) {
ForEach(Array(self.favorites.cities), id: \.id) { item in
VStack {
Text(item.name)
Button(action: {
if self.favorites.contains(item) {
self.favorites.remove(item)
} else {
self.favorites.add(item)
}
}) {
HStack {
Image(systemName: self.favorites.contains(item) ? "heart.fill" : "heart")
.foregroundColor(self.favorites.contains(item) ? .red : .black)
}
}
}
}
}
}
}
struct City: Codable, Hashable {
var id = UUID().uuidString
var name: String
}
class Favorites: ObservableObject {
#Published var cities: Set<City> = []
#Published var favorites: Set<String> = []
let defaults = UserDefaults.standard
var initialItems: [City] = [
City(name: "London"),
City(name: "Paris"),
City(name: "Berlin")
]
init() {
let decoder = PropertyListDecoder()
if let data = defaults.data(forKey: "Cities") {
cities = (try? decoder.decode(Set<City>.self, from: data)) ?? Set(initialItems)
} else {
cities = Set(initialItems)
}
self.favorites = Set(defaults.array(forKey: "Favorites") as? [String] ?? [])
}
func getTaskIds() -> Set<String> {
return self.favorites
}
func contains(_ city: City) -> Bool {
favorites.contains(city.id)
}
func add(_ city: City) {
favorites.insert(city.id)
save()
}
func remove(_ city: City) {
favorites.remove(city.id)
save()
}
func save() {
let encoder = PropertyListEncoder()
if let encoded = try? encoder.encode(self.cities) {
self.defaults.set(encoded, forKey: "Cities")
}
self.defaults.set(Array(self.favorites), forKey: "Favorites")
defaults.synchronize()
}
}
Issues with the original:
The biggest issue was that items was getting recreated on each new launch and City has an id that is assigned a UUID on creation. This guaranteed that every new launch, each batch of cities would have different UUIDs, so a saving situation would never work.
There were some general typos and references to properties that didn't actually exist.
What I did:
Made cities and favorites both #Published properties so that you don't have to call objectWillChange.send by hand
On init, load both the cities and the favorites. That way, the cities, once initially created, will always have the same UUIDs, since they're getting loaded from a saved state
On save, I save both Sets -- the favorites and the cities
In the original ForEach, I iterate through all of the cities and then only mark the ones that are part of favorites
Important note: While testing this, I discovered that at least on Xcode 12.3 / iOS 14.3, syncing to UserDefaults is slow, even when using the now-unrecommended synchronize method. I kept wondering why my changes weren't reflected when I killed and then re-opened the app. Eventually figured out that everything works if I give it about 10-15 seconds to sync to UserDefaults before killing the app and then opening it again.

list view is not updating in swift UI

I have started project in swiftUI. I am receiving data from web services adding into array list when I am changing data in backend but list view is not updating.
import Foundation
import SwiftUI
import Combine
var orders_List: [OrdersModel] = []
struct OrdersModel: Identifiable,Hashable{
var id: String = UUID().uuidString
let order_no: String
let seat_name: String
let seat_no: String
let time: String
init(id: String, order_no: String,seat_name:String,seat_no:String,time:String){
self.id = id
self.order_no = order_no
self.seat_name = seat_name
self.seat_no = seat_no
self.time = time
}
}
class Order_Manager: ObservableObject {
var objectWillChange = ObservableObjectPublisher()
var fetchedSongsResults = [orders_List] {
willSet {
objectWillChange.send()
}
}
init() {
fetch_orders()
}
func fetch_orders() {
let defaults = UserDefaults.standard
let parameters = ["tokken": "a56af0a01e137f61a44a93398195f5db","order_status":"Submitted"]
guard let url = URL(string: "https://www.appvelo.com/golfcourseios/api/OrderApis/fetch_status_based_orders") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
guard let httpBody = try? JSONSerialization.data(withJSONObject: parameters, options: []) else { return }
request.httpBody = httpBody
let session = URLSession.shared
session.dataTask(with: request) { (data, response, error) in
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String:Any]
let posts = json["data"] as? [[String: Any]] ?? []
orders_List.removeAll()
for result in posts {
//print(json)
let id = result["id"] as! String
let order_no = result["order_no"] as! String
let seat_no = result["order_no"] as! String
let seat_name = result["seat_type_image"] as! String
let time = result["time"] as! String
let order_Model = OrdersModel(id: id,order_no: order_no, seat_name: seat_name, seat_no: seat_no,time:time)
orders_List.append(order_Model)
// print(order_Model)
}
} catch {
print(error)
}
}
}.resume()
}
}
struct ContentView: View {
var body: some View {
VStack{
List {
ForEach(orders_List){ person in
Text("\(person.order_no)")
}
}
}
}
}
As mentioned in comments, you need to fix your Combine syntax:
class Order_Manager: ObservableObject {
#Published var fetchedSongsResults = [orders_List]
}
You need to remove var objectWillChange = ObservableObjectPublisher()