I want to call "getTestCounts" before displaying "Text(testCounts.counts[index].number)".
However, if I use onAppear, I end up with an infinite loop.
I think that since we are creating unique IDs with UUID and spinning them around in ForEach, we end up with an infinite loop.
Infinite loop when using onAppear in SwiftUI
I tried to solve this problem using init() with reference to
"Cannot use instance member 'calendar' within property initializer; property initializers run before 'self' is available"
The following error occurs.
How can I solve this problem? Thanks for suggestions.
Here is the UI I want to create
I want to display the API response value under the date.
Here is the source code where the infinite loop occurs.
struct CalendarView: View {
#Binding var year: String
#Binding var month: String
#Binding var day: String
#EnvironmentObject var testCounts: TestCounts
var body: some View {
let calendar = Calendar(identifier: .gregorian)
let selectedDate = calendar.date(from: DateComponents(year: Int(year), month: Int(month), day: Int(day)))
let calendarDates = generateDates(selectedDate!)
LazyVGrid(columns: Array(repeating: GridItem(.fixed(60.0), spacing: 0), count: 7), spacing: 0) {
ForEach(calendarDates) { date in
Button(action: {}, label: {
VStack(spacing: 0) {
if let date = date.date, let day = Calendar.current.day(for: date) {
Text("\(day)").fontWeight(.semibold).frame(width: 45, alignment: .leading).foregroundColor(Color("dayTextBrown"))
ForEach(0 ..< testCounts.counts.count, id: \.self) { index in
if testCounts.counts[index].date == DateTime.dateToStr(date) {
Text(testCounts.counts[index].number)
Text(testCounts.counts[index].hcount)
}
}
} else {
Text("").frame(width: 45, alignment: .leading)
}
}.frame(width: 60, height: 60).onAppear {
getTestCounts(date.date ?? Date(), "all")
}
}).background(.white).border(Color("drabBrown"), width: 1)
}
}
}
func getTestCounts(_ date: Date, _ timeType: String) {
let since = Calendar.current.startOfMonth(for: date)
let stringSince = DateTime.dateToStr(since!)
let until = Calendar.current.endOfMonth(for: date)
let stringUntil = DateTime.dateToStr(until!)
TestCountsApi(LoginInformation.shared.token, shopId: LoginInformation.shared.defaultShopId, since: stringSince, until: stringUntil, timeType: timeType).request { json, error, result in
switch result {
case .success, .successWithMessage:
TestCounts.shared.setTestCounts(json!)
case .apiError:
errorMessage = json!.message!
case .communicationError:
errorMessage = error!.localizedDescription
case .otherError:
errorMessage = "otherError"
}
}
}
}
struct CalendarDates: Identifiable {
var id = UUID()
var date: Date?
}
func generateDates(_ date: Date) -> [CalendarDates] {
var days = [CalendarDates]()
let startOfMonth = Calendar.current.startOfMonth(for: date)
let daysInMonth = Calendar.current.daysInMonth(for: date)
guard let daysInMonth = daysInMonth, let startOfMonth = startOfMonth else {
return []
}
for day in 0 ..< daysInMonth {
days.append(CalendarDates(date: Calendar.current.date(byAdding: .day, value: day, to: startOfMonth)))
}
}
guard let firstDay = days.first, let lastDay = days.last,
let firstDate = firstDay.date, let lastDate = lastDay.date,
let firstDateWeekday = Calendar.current.weekday(for: firstDate),
let lastDateWeekday = Calendar.current.weekday(for: lastDate)
else { return [] }
let firstWeekEmptyDays = firstDateWeekday - 1
let lastWeekEmptyDays = 7 - lastDateWeekday
for _ in 0 ..< firstWeekEmptyDays {
days.insert(CalendarDates(date: nil), at: 0)
}
for _ in 0 ..< lastWeekEmptyDays {
days.append(CalendarDates(date: nil))
}
return days
}
class TestCounts: ObservableObject {
struct TestCount {
var date: String
var number: Int
var hcount: Int
}
static let shared = TestCounts()
#Published var counts: [TestCount] = []
func setTestCounts(_ json: TestCountsJson) {
counts = []
if let countsJsons = json.counts {
for countJson in countsJsons {
counts.append(TestCount(
date: countJson.date ?? "",
number: countJson.number ?? 0,
hcount: countJson.hcount ?? 0
))
}
}
}
}
Here is the source code that tries to use init().
struct CalendarView: View {
#Binding var year: String
#Binding var month: String
#Binding var day: String
#EnvironmentObject var testCounts: TestCounts
let calendar = Calendar(identifier: .gregorian)
let selectedDate = calendar.date(from: DateComponents(year: Int(year), month: Int(month), day: Int(day)))
let calendarDates = generateDates(selectedDate!)
var body: some View {
LazyVGrid(columns: Array(repeating: GridItem(.fixed(60.0), spacing: 0), count: 7), spacing: 0) {
ForEach(calendarDates) { date in
Button(action: {}, label: {
VStack(spacing: 0) {
if let date = date.date, let day = Calendar.current.day(for: date) {
Text("\(day)").fontWeight(.semibold).frame(width: 45, alignment: .leading).foregroundColor(Color("dayTextBrown"))
ForEach(0 ..< testCounts.counts.count, id: \.self) { index in
if testCounts.counts[index].date == DateTime.dateToStr(date) {
Text(testCounts.counts[index].number)
Text(testCounts.counts[index].hcount)
}
}
} else {
Text("").frame(width: 45, alignment: .leading)
}
}.frame(width: 60, height: 60)
}).background(.white).border(Color("drabBrown"), width: 1)
}
}
}
}
class TestViewModel {
var date: Date
var timeType: String
var errorMessage = ""
init (date: Date, timeType: String) {
self.date = date
self.timeType = timeType
getTestCounts(date, timeType)
}
func getTestCounts(_ date: Date, _ timeType: String) {
let since = Calendar.current.startOfMonth(for: date)
let stringSince = DateTime.dateToStr(since!)
let until = Calendar.current.endOfMonth(for: date)
let stringUntil = DateTime.dateToStr(until!)
TestCountsApi(LoginInformation.shared.token, shopId: LoginInformation.shared.defaultShopId, since: stringSince, until: stringUntil, timeType: timeType).request { json, error, result in
switch result {
case .success, .successWithMessage:
print("success")
TestCounts.shared.setTestCounts(json!)
case .apiError:
self.errorMessage = json!.message!
print("errorMessage")
case .communicationError:
self.errorMessage = error!.localizedDescription
print("errorMessage")
case .otherError:
self.errorMessage = "otherError"
print("errorMessage")
}
}
}
}
Related
I am trying to update a List using an availableModels array inside an EnvironmentObject. For some reason it doesn't refresh my view but I have no idea why. Very confused about this so help would be much appreciated, I'm new to SwiftUI and transferring data between views. I'm pretty sure that the array is updating because when I print it out the values are correct, but the list doesn't update in the ForEach loop or in the Text fields.
This is my view with the List:
import SwiftUI
var selectedModel: String = "rdps"
struct ModelSelectView: View {
#EnvironmentObject var viewModel: WeatherData
var body: some View {
NavigationView {
List{
Group {
WeatherModelHeader()
}
if !viewModel.weatherArray.isEmpty {
ForEach(viewModel.weatherArray[0].availableModels, id: \.apiName) { model in
WeatherModelCell(weatherModel: model)
}
} else {
Text("No Weather 4 u")
}
Button(action: {
fetchModelInventory(for: viewModel)
}, label: {
Text("Fetch Inventory")
})
Text(String(viewModel.weatherArray[0].availableModels[0].apiName))
Text(String(viewModel.weatherArray[0].availableModels[1].apiName))
Text(String(viewModel.weatherArray[0].availableModels[2].apiName))
Text(String(viewModel.weatherArray[0].availableModels[3].apiName))
}
.navigationTitle("Models")
.onAppear(perform: {
fetchModelInventory(for: viewModel)
print("viewModel.weatherArray.availableModels \(viewModel.weatherArray[0].availableModels)")
})
}
}
}
//Layout for the header of the list
struct WeatherModelHeader: View {
var body: some View {
HStack {
Text("Model \nName")
.bold()
.frame(width: 60, alignment: .leading)
Text("Range")
.bold()
.frame(width: 80, alignment: .leading)
.padding(.leading, 30)
Text("Resolution")
.bold()
.frame(width: 85, alignment: .leading)
.padding(.leading, 30)
}
}
}
//create the layout for the weather model list
struct WeatherModelCell: View {
let weatherModel: WeatherModel
#EnvironmentObject var viewModel: WeatherData
var body: some View {
HStack {
//need to make navlink go to correct graph model. This will be passed in the GraphViews(). Clicking this nav link will trigger the API Call for the coordinates and model.
NavigationLink(
destination: InteractiveChart()
.onAppear{
selectedModel = weatherModel.apiName
fetchData(for: viewModel)
},
label: {
Text(weatherModel.name)
.frame(width: 60, alignment: .leading)
Text("\(String(weatherModel.range)) days")
.frame(width: 80, alignment: .leading)
.padding(.leading, 30)
Text("\(String(weatherModel.resolution)) km")
.frame(width: 80, alignment: .leading)
.padding(.leading, 30)
//this triggers the api call when the model is tapped. it passes the selected model name to the variable selectedModel for the call.
})
}
}
}
And these are my models where I set up my Observable Object:
class WeatherForecastClass: ObservableObject {
//var id = UUID()
var chartMODEL: String = "Model"
var chartModelHeight: Int = 0
var chartLAT: Double = selectedCords.latitude
var chartLON: Double = selectedCords.longitude
var chartDATETIME: Date = formatDate(date: "2023-02-10 18:00")
var chartTMP: Int = 1
var chartRH: Int = 0
var chartDP: Float = 0.0
var chartTTLSnow: Float = 0.0
var chartTTLRain: Float = 0.0
var chartWindSpeed: Int = 0
var chartWindDirDegs: Int = 0
var chartWindDirString: String = ""
var availableModels: [WeatherModel] = weatherModels
}
class WeatherData: ObservableObject {
/// weather data. This is the master class.
#Published var weatherArray: [WeatherForecastClass] = [WeatherForecastClass()]
//#Published var weatherProperties: WeatherForecastClass = WeatherForecastClass()
}
And this is my function for updating the availableModels array:
import Foundation
import SwiftUI
var modelInventory: [String] = []
func fetchModelInventory(for weatherData: WeatherData) {
// #EnvironmentObject var viewModel: WeatherData
let lat = Float(selectedCords.latitude)
let lon = Float(selectedCords.longitude)
let key = "my apiKey"
//this is the url with the API call. it has the data for the call request and my API key.
let urlString = "https://spotwx.io/api.php?key=\(key)&lat=\(lat)&lon=\(lon)&model=inventory"
guard let url = URL(string: urlString) else {
return
}
print(url)
//Gets the data from the api call. tasks are async
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
print("Error")
return
}
//clear the model inventory
modelInventory = []
weatherData.weatherArray[0].availableModels = []
//decode the modelinventory csv and fill the array
if let csvString = String(data: data, encoding: .utf8) {
let lines = csvString.split(separator: "\n")
for line in lines {
let columns = line.split(separator: ",")
for column in columns {
let value = String(column)
modelInventory.append(value)
}
}
}
print("Model Inventory -----")
print(modelInventory)
if !modelInventory.isEmpty {
DispatchQueue.main.async {
for model in weatherModels {
if modelInventory.contains(model.apiName) {
weatherData.weatherArray[0].availableModels.append(model)
}
}
print(weatherData.weatherArray[0].availableModels)
}
} else {
return
}
}
task.resume()
}
I feel like I've tried everything but I no matter what the list wont update.
I have made a qr code generator with date and I want to add another picker for hours, and I have tried to conver Int to String, there isn't any problem, when I tried to convert multiple Int to string and it doesn't work and also i want to change the Int format to something like this 12 02:11, first is integer space and time. how could I do that?
My Code:
struct GenerateQRCode: View {
#Binding var time: Date
#Binding var hours: Int
let hour = ["3","6","9","12"]
let filter = CIFilter.qrCodeGenerator()
let cont = CIContext()
var dateFormatter: DateFormatter {
let df = DateFormatter()
df.dateFormat = "HH:mm"
return df
}
var body: some View {
NavigationView{
Image(uiImage: imageGenerate(times:time, hours: hours))
.interpolation(.none)
.resizable()
.frame(width: 150, height: 150, alignment: .center)
}.navigationBarBackButtonHidden(true)
}
func imageGenerate(hours: Int, times: Date)-> UIImage { //<--here how to add integer parameter?
let str = dateFormatter.string(from: start)
let ts = String(hours)
let com = ts + str
let data = com.data(using: .utf8)
filter.setValue(data, forKey: "inputMessage") //
if let qr = filter.outputImage {
if let qrImage = cont.createCGImage(qr, from: qr.extent){
return UIImage(cgImage: qrImage)
}
}
return UIImage(systemName: "xmark") ?? UIImage()
}
}
Preview:
import Foundation
import SwiftUI
import CoreImage.CIFilterBuiltins
struct DatePicker: View {
#State var Time = Date()
#State var sHours = Int()
#State var navigated = false
let hour = ["3", "6", "9", "12"]
var body: some View {
NavigationView{
VStack{
Section{
Text("Please Select Time")
DatePicker("", selection: $startTime, displayedComponents: [.hourAndMinute])
.datePickerStyle(.wheel)
}
Section{
Text("Please Select Minutes")
Picker(selection: $sMinutes, label: Text("Please Select Minutes"))
{
ForEach(0 ..< minutes.count) {
index in Text(self.minutes[index]).tag(index)
}
}
}
Section
{
NavigationLink(destination: GenerateQRCode(start: $Time, minutes: $sHours), isActive: self.$navigated)
{
Text("Complete")
}
}.padding(100)
}.navigationBarTitle("Visitor")
}
}
}
struct DatePicker_Previews: PreviewProvider {
static var previews: some View {
DatePicker()
}
}
Output: "012:30"
what i expected output, should be like this: "3 12:30" first should be hours and then time. i don't know why it only shows 0 if my picker turns to 3. How can i solve it out?
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
}
}
Let's say I have a simple DatePicker View:
struct ContentView: View {
#State private var date = Date()
var body: some View {
DatePicker("Date", selection: $date)
.datePickerStyle(GraphicalDatePickerStyle())
}
}
How can I make it so the user is not allowed to tap on either Sundays or Holidays?
Okay, so this was no easy task. After a lot of investigation, I ended up using WenchaoD's FSCalendar.
It took a lot of work, and I added other stuff to the calendar other that what I originally wanted, but this is the end result:
struct MyCalendar: UIViewRepresentable {
typealias UIViewType = FSCalendar
#Binding var selectedDate: Date
#Binding var listOfAvailableTimes: [String]
#Binding var appointmentType: String
#Binding var currentPage: Date
#ObservedObject var eventsManager = EventsManager()
var calendar = FSCalendar()
var today: Date {
return Date()
}
func makeCoordinator() -> MyCalendar.Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> FSCalendar {
eventsManager.fetchHolidays(year: 2021)
calendar.delegate = context.coordinator
calendar.dataSource = context.coordinator
calendar.scrollDirection = .horizontal
calendar.scope = .month
calendar.locale = Locale(identifier: "es")
calendar.appearance.titleFont = UIFont.systemFont(ofSize: 17)
calendar.appearance.headerTitleFont = UIFont.boldSystemFont(ofSize: 18)
calendar.appearance.weekdayFont = UIFont.boldSystemFont(ofSize: 16)
calendar.appearance.todayColor = .none
calendar.appearance.titleTodayColor = .black
calendar.appearance.weekdayTextColor = .systemGray
calendar.appearance.headerTitleColor = .black
calendar.placeholderType = .none
calendar.appearance.headerMinimumDissolvedAlpha = 0
return calendar
}
func updateUIView(_ uiView: FSCalendar, context: Context) {
uiView.setCurrentPage(currentPage, animated: true)
calendar.reloadData()
}
class Coordinator: NSObject, FSCalendarDelegateAppearance, FSCalendarDataSource, FSCalendarDelegate {
fileprivate let gregorian: Calendar = Calendar(identifier: .gregorian)
var parent: MyCalendar
init(_ calendar: MyCalendar) {
self.parent = calendar
}
var formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"
return formatter
}()
//STARTING HERE IS WHERE YOU CAN SEE THE CODE TO INDICATE
//WHICH DAYS SHOULD NOT BE SELECTED. I GOT THE LIST OF HOLIDAYS
//FROM AN API. LET ME KNOW IF YOU NEED HELP WITH THIS OTHER PART.
func calendar(_ calendar: FSCalendar, appearance: FSCalendarAppearance, titleDefaultColorFor date: Date) -> UIColor? {
// IF DATE IS A HOLIDAY, IT SHOULD APPEAR RED
let listOfHolidays = self.parent.eventsManager.holidays
for holiday in listOfHolidays {
guard let excludedDate = formatter.date(from: holiday) else { return nil }
if date.compare(excludedDate) == .orderedSame {
return .red
}
}
// IF DATE IS A SUNDAY, IT SHOULD APPEAR RED
if [1].contains((self.gregorian.component(.weekday, from: date))) {
return .red
}
return nil
}
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
var stringSelectedDate: String {
let year = Calendar.current.component(.year, from: date)
let month = Calendar.current.component(.month, from: date)
let day = Calendar.current.component(.day, from: date)
let stringDate = String(format: "%04d%02d%02d", year, month, day)
return stringDate
}
self.parent.eventsManager.searchAppointment(day: stringSelectedDate, appointmentType: self.parent.appointmentType) { (appointment) in
self.parent.listOfAvailableTimes = appointment?.times ?? [""]
}
self.parent.selectedDate = date
print("Date Selected = \(formatter.string(from: date))")
}
func minimumDate(for calendar: FSCalendar) -> Date {
return Date()
}
func maximumDate(for calendar: FSCalendar) -> Date {
return Date().addingTimeInterval((60 * 60 * 24) * 365)
}
func calendar(_ calendar: FSCalendar, shouldSelect date: Date, at monthPosition: FSCalendarMonthPosition) -> Bool {
let listOfHolidays = self.parent.eventsManager.holidays
for holiday in listOfHolidays {
// IF DATE IS A HOLIDAY, IT SHOULD NOT BE CLICKABLE
guard let excludedDate = formatter.date(from: holiday) else { return true }
if date.compare(excludedDate) == .orderedSame {
return false
}
}
// IF DATE IS A SUNDAY, IT SHOULD NOT BE CLICKABLE
if [1].contains((self.gregorian.component(.weekday, from: date))) {
return false
}
return true
}
}
}
So in ContentView, instead of using DatePicker, I used MyCalendar
Today I have a problem on SwiftUI in a ForEach.
I have a several list of products from different stores, the thing is I need to modify Stock when I press one of the buttons to increase or decrease it. but when I really try to make this I have the following error:
Left side of mutating operator isn't mutable: 'product' is a 'let' constant SwiftUI
I don't know how to make it works and I'm here to hear some suggestions
#State var shoppingCartList : [Product]
ForEach(productsByStore(storeId: store.id), id: \.self){product in
VStack(alignment: .leading, spacing: 0){
HStack{
Group{
HStack{
HStack{
Button(action: {
if (product.quantity > 1){ // error here: product.quantity -= 1
}
}){
Text("—")
.multilineTextAlignment(.center)
}
.padding([.leading], 5)
Text(String(product.quantity))
Button(action: {
if product.quantity < product.stock!{
// error here: product.quantity += 1
}
}){
Text("+")
.multilineTextAlignment(.center)
}
}
Spacer()
Text("available: " + String(describing: product.stock!))
}
}
}
}
Divider()
}
Spacer()
}
func productsByStore(storeId: Int) ->[Product] {
return shoppingCartList.filter{$0.storeId == storeId}
}
PS. productsByStore() returns a list of products according to the store
PS2. product.stock is the number of available items on stock and product.quantity is the number of items that user can select.
ex: Palm One -> Quantity = 4. Stock = 10
I could add or remove the number of palm ones using buttons (+ and -)
PS3. Product Struct
struct Product: Hashable, Codable{
var name: String
var quantity: Int
var stock: Int?
var storeId: Int
init(from decoder: Decoder) throws{
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
quantity = try container.decodeIfPresent(Int.self, forKey: .quantity) ?? 0
stock = try container.decodeIfPresent(Int?.self, forKey: .stock) ?? 0
storeId = try container.decodeIfPresent(Int.self, forKey: .storeId) ?? 0
}
private enum CodingKeys: String, CodingKey{
case name = "Nombre"
case quantity = "Cantidad"
case stock = "Stock"
case storeId = "IdStore"
}
init(name: String, quantity: Int, stock: Int?, storeId: Int ){
self.name = name
self.quantity = quantity
self.stock = stock
self.storeId = storeId
}
}
I solved like this:
#State var selectedStock: Int = 0
Button(action: {
self.selectedStock = product.quantity
if self.selectedStock > 1 {
selectedStock -= 1
if let listIndex = shoppingCartList.firstIndex(where: {
$0 == currentProduct
}) {
var newProduct = product
newProduct.quantity = selectedStock
shoppingCartList[listIndex] = newProduct
}
}
}) {
Text("—")
}
Button(action: {
self.selectedStock = product.quantity
if self.selectedStock < product.stock!{
selectedStock += 1
if let listIndex = shoppingCartList.firstIndex(where: {
$0 == currentProduct
}) {
var newProduct = product
newProduct.quantity = selectedStock
shoppingCartList[listIndex] = newProduct
}
}
}) {
Text("+")
}