I am transitioning from UIKit to SwiftUI in my app and am updating network calls to async await as well. I created a Large size widget that displays the weather for 7 airports. The airports identifiers are stored in an app group shared userdefatults container. I update the timeline every minute (just for testing, normally it would be every 20 minutes). Initially when the widget is selected and appears, all data is there and correct. After a timeline update the data updates, but not all the airports are returned and after two updates or so (not consistent), the screen goes blank. The userdefaults airports are updated from the main app and saved in the shared user defaults container and it calls WidgetCenter.shared.reloadAllTimelines. This is all working fine as I have another process that uses the same container for a small widget, but with only one airport returning data without the need for an appended array. If I remove the calls to empty the array, the data remains and doesn't go blank, but of course the array keeps appending. I've tried the removeAll() and [] to empty the array at different places in the code, but same result. I am trying to understand the flow in the async/await calls, but seem to be missing something here? Any help would be greatly appreciated. I've been googling and searching stack overflow for a month and don't really know how to solve this issue. Thanks in advance!
actor MetarService: NSObject, XMLParserDelegate, URLSessionDelegate, ObservableObject {
enum MetarFetcherError: Error {
case invalidServerResponse
case missingData
}
#Published var metarArray = [String]()
#Published var metarDataModel: [MetarDataModel] = []
var tempDataModel: [MetarDataModel] = []
func fetchMetars(metarAPTs: String) async throws -> [MetarDataModel] {
let wxUrl = URL(string: "https://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&hoursBeforeNow=3&mostRecent=true&stationString=" + metarAPTs)!
let (data, response) = try await URLSession.shared.data(from: wxUrl)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw MetarFetcherError.invalidServerResponse
}
guard let xml = SWXMLHash.parse(data) as XMLIndexer? else {
throw MetarFetcherError.missingData
}
noDataResponse = (xml["response"]["data"].element?.attribute(by: "num_results")?.text) ?? "0"
if noDataResponse == "1" && (xml["response"]["data"]["METAR"]["observation_time"].element?.text) != nil {
if (xml["response"]["data"]["METAR"]["station_id"].element?.text) != nil {
myairport = xml["response"]["data"]["METAR"]["station_id"].element!.text
} else {
myairport = "MSNG"
}
if (xml["response"]["data"]["METAR"]["flight_category"].element?.text) != nil {
myfltcat = xml["response"]["data"]["METAR"]["flight_category"].element!.text
} else {
myfltcat = "MISNG"
}
switch myfltcat {
case "VFR":
mymetarImage = "sun.max.circle.fill"
case "MVFR":
mymetarImage = "cloud.sun.circle.fill"
case "IFR":
mymetarImage = "cloud.fog.circle.fill"
case "LIFR":
mymetarImage = "smoke.circle.fill"
default:
mymetarImage = "person.crop.circle.badge.questionmark"
}
if (xml["response"]["data"]["METAR"]["observation_time"].element?.text) != nil {
myobstime = xml["response"]["data"]["METAR"]["observation_time"].element!.text as NSString
if myobstime.length < 16 {
myobstime = "MISNG"
} else {
myobstime = myobstime.substring(with: NSRange(location: 11, length: 5)) as NSString
}
}
if (xml["response"]["data"]["METAR"]["visibility_statute_mi"].element?.text) != nil {
myvis = xml["response"]["data"]["METAR"]["visibility_statute_mi"].element!.text
let intVis = (myvis as NSString) .integerValue
myvis = String(intVis) + "SM"
} else {
myvis = "0"
}
if (xml["response"]["data"]["METAR"]["wind_dir_degrees"].element?.text) != nil {
mywinddir = xml["response"]["data"]["METAR"]["wind_dir_degrees"].element!.text
if mywinddir.contains("VRB") {
mywinddir = "VRB"
} else
if mywinddir.count <= 2 && mywinddir.count > 0 {
mywinddir = "0" + mywinddir
}
} else {
mywinddir = "MISNG"
}
if (xml["response"]["data"]["METAR"]["wind_speed_kt"].element?.text) != nil {
mywindspd = xml["response"]["data"]["METAR"]["wind_speed_kt"].element!.text
if mywindspd == "0" {
mywind = "Calm"
} else if mywindspd.count == 1 {
mywindspd = "0" + mywindspd
mywind = mywinddir + "/" + mywindspd + "KT"
} else if mywindspd.count > 1 {
mywind = mywinddir + "/" + mywindspd + "KT"
}
} else {
mywind = "MISNG"
}
}
self.tempDataModel.append(MetarDataModel(metarImage: mymetarImage, mairport: myairport, mobstime: myobstime as String, mfltcat: myfltcat, mvis: myvis, mwind: mywind))
self.metarDataModel = self.tempDataModel
tempDataModel = []
return metarDataModel
}
func readMetarApts() -> [String] {
let defaults = UserDefaults(suiteName: "group.userdefaults.shared.FRAT")!
if ((defaults.value(forKey: "icaoIdent") as! String).isEmpty) {
defaultairport = "KSFO"
} else {
defaultairport = defaults.value(forKey: "icaoIdent") as! String
}
wxAirport1 = defaults.value(forKey: "wxAirport1") as! String
wxAirport2 = defaults.value(forKey: "wxAirport2") as! String
wxAirport3 = defaults.value(forKey: "wxAirport3") as! String
wxAirport4 = defaults.value(forKey: "wxAirport4") as! String
wxAirport5 = defaults.value(forKey: "wxAirport5") as! String
wxAirport6 = defaults.value(forKey: "wxAirport6") as! String
metarArray.append(defaultairport)
metarArray.append(wxAirport1)
metarArray.append(wxAirport2)
metarArray.append(wxAirport3)
metarArray.append(wxAirport4)
metarArray.append(wxAirport5)
metarArray.append(wxAirport6)
metarArray = metarArray.sorted()
let returnArray = metarArray
metarArray = []
return returnArray
}// end of readAirports function
nonisolated func getAirports() -> ([MetarDataModel] ){
// transData = []
Task{
let tempArray = await readMetarApts()
for apts in tempArray {
let zData = try await self.fetchMetars(metarAPTs: apts)
if zData .isEmpty {
let errorData = MetarDataModel(metarImage: "sun.max.circle.fill", mairport: "DATA", mobstime: "CHK", mfltcat: "MSNG", mvis: "WiFi", mwind: "")
tempData = [errorData]
} else {
transData.append(contentsOf: zData)
tempData = transData
} // else Closure
} //Task Closure
//transData.removeAll()
} // apts in tempArray Closure
tempData = transData
// transData = []
return tempData.sorted()
} // end of getAirports function
} // end of MetarService Class
I have tried different solutions found on stack overflow, reddit, medium and others. But no matter what approach I take, if I try and empty the appended array in preparation for the next timeline update, it will always eventually return without data. At first I thought it was the app group shared container losing reference as I got the much debated 'kCFPreferencesAnyUser, ByHost: Yes, Container: (null)): Using kCFPreferencesAnyUser with a container is only allowed for System Containers, detaching from cfprefsd' in the log, but Apple says this does not indicate that particular condition? And, I use the container elsewhere with no issues. I am new to the async await and the new URLSession.shared.data(from: ) and maybe I'm not understanding the flow and timing of how the data is fetched and returned? I just need to append the array, display the data, then empty the array and wait for the next timeline to fetch new data. I've put the removeAll() and tried [] as an alternative in many different places in my code (at the start of the function and at the end of the function). Stumped!!
Related
I am trying to learn swift. I am currently looking at operations. I can queue up a series of operations and they run. Where I run into a problem is trying to pass data between operations. My getVOOCSVData operation retrieves the correct data which I can confirm by placing a print statement in its completion block. Unfortunately, I cannot figure out how to pass this retrieved data onto the next operation. The next operation (convertStringToArray) runs but the data I attempt to pass in is empty. Below are my operation setup and ConvertStringToArray2 class which is assigned to the convertStringToArray variable.
// operation setup
struct VOOModel {
init() {
let operationQueue = OperationQueue()
let checkForUpdatedVOOData = IsUpdatedVOODataAvailable()
checkForUpdatedVOOData.completionBlock = {
print("Updated CSV Data = \(checkForUpdatedVOOData.updatedCSVData)")
print("Latest Date = \(checkForUpdatedVOOData.latestCSVDate)")
}
let getVOOCSVData = GetVOOCSVData()
getVOOCSVData.addDependency(checkForUpdatedVOOData)
getVOOCSVData.completionBlock = {
print("completion block get voo csv data")
print("\(getVOOCSVData.csvData)") // prints out all of the data correctly
}
let convertStringToArray = ConvertStringToArray2(csvData: getVOOCSVData.csvData)
convertStringToArray.addDependency(getVOOCSVData)
convertStringToArray.completionBlock = {
print("completion block convert string to array")
}
operationQueue.addOperations([checkForUpdatedVOOData, getVOOCSVData, convertStringToArray], waitUntilFinished: true)
} // end init
} // end structure
// operation class
final class ConvertStringToArray2: Operation {
var localData: String
var sortedDataArray : [IndexFund] = []
init(csvData: String) {
localData = csvData
}
override func main() {
var dataArray = [IndexFund]()
var rows = localData.components(separatedBy: "\n") // gives rows a value of 1
rows.removeFirst()
for row in rows {
let columns = row.components(separatedBy: ",")
if columns.count == 6 {
let theDate = columns[0].replacingOccurrences(of: "-", with: "")
let timeStamp = stringDateFormatter.date(from: theDate)!
let open = Double(columns[1])!
let high = Double(columns[2])!
let low = Double(columns[3])!
let close = Double(columns[4])!
let theVolume = columns[5].trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let volume = Int64(theVolume)
let fundValuesByDay = IndexFund(
timeStamp: timeStamp,
open: open,
high: high,
low: low,
close: close,
volume: volume!)
dataArray.append(fundValuesByDay)
}
sortedDataArray = dataArray.sorted {
$0.timeStamp < $1.timeStamp
}
}
}
}
After some additional research I found that a block operation allows me to pass data between operations. Below is the updated code. It all runs in the init of my model as above. The link below is the site used to understand how to utilize the block operation.
https://medium.com/#marcosantadev/4-ways-to-pass-data-between-operations-with-swift-2fa5b3a3d561
let operationQueue = OperationQueue()
let updatedVOOData = IsUpdatedVOODataAvailable()
updatedVOOData.completionBlock = {
if updatedVOOData.updatedCSVData == true {
let fetchVOO = GetVOOCSVData()
let updateVOO = UpdateVOOPersistentStore()
let vooAdapter = BlockOperation() { [unowned updateVOO, unowned fetchVOO] in
updateVOO.dataArray = fetchVOO.dataArray
}
vooAdapter.addDependency(fetchVOO)
updateVOO.addDependency(vooAdapter)
operationQueue.addOperations([fetchVOO, updateVOO, vooAdapter], waitUntilFinished: true)
} // end if block
print("Updated CSV Data = \(updatedVOOData.updatedCSVData)")
print("Latest Date = \(updatedVOOData.latestCSVDate)")
} // end completion block
operationQueue.addOperations([updatedVOOData], waitUntilFinished: true)
I am having an issue where I run this function:
static func getAllJokes(reset: Bool, completion: #escaping () -> Void) {
Utilities.getDadJokes(reset: reset) {
print("Dad Jokes Pass, array = \(DadJokes.dadJokes)")
Utilities.getAssistantJokes(reset: reset) {
print("Assistant Jokes Pass, array = \(AssistantJokes.assistantJokes)")
Utilities.getKnockKnockJokes(reset: reset) {
print("Knock Knock Jokes Pass, array = \(KnockKnockJokes.knockKnockJokes)")
Utilities.getRandomJokes(reset: reset) {
print("Random Jokes Pass, array = \(RandomJokes.randomJokes)")
completion()
}
}
}
}
}
getDadJokes:
db.collection("jokes").document("Dad Jokes").addSnapshotListener { document, error in
//check for error
if error == nil {
//check if document exists
if document != nil && document!.exists {
if let JokeNum = document!.get("JokeNum") as? Int {
self.countDadJokes = JokeNum
UserDefaults.standard.setValue(JokeNum, forKey: "countDadJokes")
print("DadJokeNum = \(self.countDadJokes)")
}
// var DadJokes.dadJokes.count = 1
print("count = \(DadJokes.dadJokes.count)/\(self.countDadJokes)")
print("countDadJoke = \(self.countDadJokes)")
self.jokes.removeAll()
if reset == true {
DadJokes.dadJokes.removeAll()
}
// for _ in 0...self.countDadJokes {
while DadJokes.dadJokes.count <= self.countDadJokes {
// print("count = \(DadJokes.dadJokes.count)/\(self.countDadJokes)")
if let Joke = document!.get("\(DadJokes.dadJokes.count + 1)") as? String {
print("DadJokeNum = \(self.countDadJokes)")
if Utilities.jokes.contains("\(Joke) - From Dad Jokes") {}else {
Utilities.jokes.append("\(Joke) - From Dad Jokes")
DadJokes.dadJokes.append(Joke)
UserDefaults.standard.set(DadJokes.dadJokes, forKey: defaults.dadJokes.rawValue)
Utilities.updateJokesDefaults()
print("countDadJokesSaved = \(DadJokes.dadJokes.count)")
print("DadJokesSaved = \(DadJokes.dadJokes)")
}
print("Dad Joke: \(Joke)")
//print("count = \(DadJokes.dadJokes.count)/\(self.countDadJokes)")
if DadJokes.dadJokes.count == self.countDadJokes {
completion()
}
}
}
}
}
}
runs fine, but then when I run getAssistantJoke (the same thing):
print("assistant get running")
db.collection("jokes").document("Assistant Jokes").addSnapshotListener { document, error in
//check for error
if error == nil {
//check if document exists
if document != nil && document!.exists {
if let JokeNum = document!.get("JokeNum") as? Int {
self.countAssistantJokes = JokeNum
UserDefaults.standard.setValue(JokeNum, forKey: defaults.countAssistantJokes.rawValue)
print("DadJokeNum = \(self.countDadJokes)")
}
// var DadJokes.dadJokes.count = 1
print("count = \(AssistantJokes.assistantJokes.count)/\(self.countAssistantJokes)")
print("countAssistantJokes = \(self.countAssistantJokes)")
if reset == true {
AssistantJokes.assistantJokes.removeAll()
}
// for _ in 0...self.countDadJokes {
while AssistantJokes.assistantJokes.count <= self.countAssistantJokes {
// print("count = \(AssistantJokes.assistantJokes.count)/\(self.countAssistantJokes)")
if let Joke = document!.get("\(DadJokes.dadJokes.count + 1)") as? String {
print("AssistantJokeNum = \(self.countAssistantJokes)")
if Utilities.jokes.contains("\(Joke) - From Assistant Jokes") {}else {
Utilities.jokes.append("\(Joke) - From Assistant Jokes")
AssistantJokes.assistantJokes.append(Joke)
UserDefaults.standard.set(AssistantJokes.assistantJokes, forKey: defaults.assistantJokes.rawValue)
Utilities.updateJokesDefaults()
print("countAssistantJokesSaved = \(AssistantJokes.assistantJokes.count)")
print("AssistantJokesSaved = \(AssistantJokes.assistantJokes)")
}
print("Assistant Joke: \(Joke)")
// print("count = \(AssistantJokes.assistantJokes.count)/\(self.countAssistantJokes)")
if AssistantJokes.assistantJokes.count == self.countAssistantJokes {
completion()
}
}
}
}
}
}
}
It stops at firebase query. The documents and everything exist, therefor, what's the issue? Everything exists and I am truly stumped. My goal is to retrieve a ton of data and display a random subject. I feel like this should be easier than it is.
Here is my firebase setup:
I feel like the main problem here is the way in which you are modelling the data in Firestore.
You have a collection called jokes but then a document called Assistant Jokes, Dad Jokes, etc...
I think a much better structure would be for each document to be a single joke inside the jokes collection. Use an "auto id" for each joke and then put a type inside the joke document.
The fact that you have to maintain a JokeNum next to your list of jokes is a sign that something is wrong.
By structuring this way you would then be able to get all jokes or filter the jokes by a particular type etc...
Your code is super complex at the moment but I think the best approach is to structure your data in a way that helps your code.
I was using a pod for ftp picture upload issue. But it is giving an error after I build the app with Swift 5.0.
Here is the error:
deinitialize()' is unavailable: the default argument to deinitialize(count:) has been removed, please specify the count explicitly.
Here is the Swift file belongs to pod:
import Foundation
/* Resource type, values defined in `sys/dirent.h`. */
public enum ResourceType: String {
case Unknown = "Unknown" // DT_UNKNOWN
case Directory = "Directory" // DT_DIR
case RegularFile = "RegularFile" // DT_REG
case SymbolicLink = "SymbolicLink" // DT_LNK
case NamedPipe = "NamedPipe" // DT_FIFO
case CharacterDevice = "CharacterDevice" // DT_CHR
case BlockDevice = "BlockDevice" // DT_BLK
case LocalDomainSocket = "LocalDomainSocket" // DT_SOCK
case Whiteout = "Whiteout" // DT_WHT
}
open class ResourceItem: CustomStringConvertible {
open var type: ResourceType = .Unknown
open var name: String = ""
open var link: String = ""
open var date: Date = Date()
open var size: Int = 0
open var mode: Int = 0
open var owner: String = ""
open var group: String = ""
open var path: String = "/"
open var description: String {
get {
return "\nResourceItem: \(name), \(type.rawValue)"
}
}
}
private let _resourceTypeMap: [Int:ResourceType] = [
Int(DT_UNKNOWN): ResourceType.Unknown,
Int(DT_FIFO): ResourceType.NamedPipe,
Int(DT_SOCK): ResourceType.LocalDomainSocket,
Int(DT_CHR): ResourceType.CharacterDevice,
Int(DT_DIR): ResourceType.Directory,
Int(DT_BLK): ResourceType.BlockDevice,
Int(DT_REG): ResourceType.RegularFile,
Int(DT_LNK): ResourceType.SymbolicLink,
Int(DT_WHT): ResourceType.Whiteout
]
/** Operation for resource listing. */
internal class ResourceListOperation: ReadStreamOperation {
fileprivate var inputData: NSMutableData?
var resources: [ResourceItem]?
override func streamEventEnd(_ aStream: Stream) -> (Bool, NSError?) {
var offset = 0
let bytes = self.inputData!.bytes.bindMemory(to: UInt8.self, capacity: (self.inputData?.length)!)
let totalBytes = CFIndex(self.inputData!.length)
var parsedBytes = CFIndex(0)
let entity = UnsafeMutablePointer<Unmanaged<CFDictionary>?>.allocate(capacity: 1)
var resources = [ResourceItem]()
repeat {
parsedBytes = CFFTPCreateParsedResourceListing(nil, bytes.advanced(by: offset), totalBytes - offset, entity)
if parsedBytes > 0 {
let value = entity.pointee?.takeUnretainedValue()
if let fptResource = value {
resources.append(self.mapFTPResources(fptResource))
}
offset += parsedBytes
}
} while parsedBytes > 0
self.resources = resources
entity.deinitialize()
return (true, nil)
}
fileprivate func mapFTPResources(_ ftpResources: NSDictionary) -> ResourceItem {
let item = ResourceItem()
if let mode = ftpResources[kCFFTPResourceMode as String] as? Int {
item.mode = mode
}
if let name = ftpResources[kCFFTPResourceName as String] as? String {
// CFFTPCreateParsedResourceListing assumes that teh names are in MacRoman.
// To fix it we create data from string and read it with correct encoding.
// https://devforums.apple.com/message/155626#155626
if configuration.encoding == String.Encoding.macOSRoman {
item.name = name
} else if let nameData = name.data(using: String.Encoding.macOSRoman) {
if let encodedName = NSString(data: nameData, encoding: self.configuration.encoding.rawValue) {
item.name = encodedName as String
}
}
item.path = self.path! + item.name
}
if let owner = ftpResources[kCFFTPResourceOwner as String] as? String {
item.owner = owner
}
if let group = ftpResources[kCFFTPResourceGroup as String] as? String {
item.group = group
}
if let link = ftpResources[kCFFTPResourceLink as String] as? String {
item.link = link
}
if let size = ftpResources[kCFFTPResourceSize as String] as? Int {
item.size = size
}
if let type = ftpResources[kCFFTPResourceType as String] as? Int {
if let resourceType = _resourceTypeMap[type] {
item.type = resourceType
}
}
if let date = ftpResources[kCFFTPResourceModDate as String] as? Date {
item.date = date
}
return item
}
override func streamEventHasBytes(_ aStream: Stream) -> (Bool, NSError?) {
if let inputStream = aStream as? InputStream {
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: 1024)
let result = inputStream.read(buffer, maxLength: 1024)
if result > 0 {
if self.inputData == nil {
self.inputData = NSMutableData(bytes: buffer, length: result)
} else {
self.inputData!.append(buffer, length: result)
}
}
buffer.deinitialize()
}
return (true, nil)
}
}
Can you help me how can I fix these 2 below lines:
buffer.deinitialize()
entity.deinitialize()
And is it okay if we fix these two lines? I mean does the pod work after we fix these two lines?
deinitialize now requires a count parameter indicating how many values you want to deinitialise.
From the context, the code is probably trying to deinitialise everything the pointer references, so the number of values we deinitialise will be equal to the number of values we allocate. This will be 1024 for buffer and 1 for entity.
You should replace those lines with:
buffer.deinitialize(count: 1024)
// and
entity.deinitialize(count: 1)
respectively
However, since this is code from a pod that you are modifying, make sure to check the terms in the licence of the pod to make sure you are not violating anything.
You should also inform the author of the pod that the pod needs updating. This API change is made in Swift 4.1, I think, so it's quite old.
I developed the below algorithm to sort the array of dictionary items.
guard var imageUrlString = anyImage.value as? [String:AnyObject] else { return }
var values = [AnyObject]()
var keys = [String]()
var done = false
var j = 1
while !done {
for i in imageUrlString {
print(i.key, " this is the key")
if "media\(j)" == i.key {
values.append(i.value)
keys.append(i.key)
print(i, " This is teh i for in if ")
if imageUrlString.count == j {
done = true
break;
}
j+=1
} else {
print("No,,.")
}
}
}
The problem is that sometimes, for example, every time the first media is an image, it will loop forever. How can I solve that so that the algorithm can effectively sort the under all conditions?
It looks like you're really creating two parallel arrays: keys and values.
I went with creating those two arrays, sorted. Here's an example
var imageUrlString = [String: AnyObject]()
imageUrlString["media3"] = "whatever 3 content" as AnyObject
imageUrlString["media7"] = "whatever 7 content" as AnyObject
imageUrlString["media1"] = "whatever 1 content" as AnyObject
let keys = Array(imageUrlString.keys).sorted()
var values = [AnyObject]()
keys.forEach {
values.append(imageUrlString[$0]!)
}
print(keys)
print(values)
How would i get in correct way last record from the data base ? For now I am getting last record but i can not cast it. The warning says "Cast from '[Class]' to unrelated type 'Class' always fails". Is it other way to get properties from this result ?
let _context = DataBaseController.getContext()
let _fetchRequest:NSFetchRequest<Class> = Class.fetchRequest()
do {
let _allElements = try _context.count(for: _fetchRequest)
_fetchRequest.fetchLimit = 1
if _allElements == 1 {
_fetchRequest.fetchOffset = _allElements
} else {
_fetchRequest.fetchOffset = _allElements - 1
}
_fetchRequest.returnsObjectsAsFaults = false
do {
let _result = try DataBaseController.getContext().fetch(_fetchRequest) as! Ping
} catch {
print("Error \(error)")
}
} catch {
print("Error \(error)")
}
Thanks in advance!
A Core Data fetch returns always an array of managed objects
let _result = try DataBaseController.getContext().fetch(_fetchRequest) as! [Ping]
if !_result.isEmpty {
let lastItem to _result[0]
}