Problems with migration to Swift3, compilation error with URLSession.GET - swift3

I'm trying to migrate from Swift2 to Swift3 and I'm having an issue with URLSession.GET
I went through the migration tool provided by XCode, and changed my "NSURLSession" calls into "URLSession" but I keep having this error message:
"Use of Instance member 'GET' on type 'URLSession'; did you mean to use a value of type 'URLSession' instead?"
Here is my code:
open static func getData() -> Promise<[Station]> {
return URLSession.GET("http://api.xxx.gov/api/").asDataAndResponse().then { (data: NSData, _) -> [Station] in
var stations = [Station]()
// rest of the code to retrieve the data
...
...
return stations
}
}
Update:
After some digging a found a solution, see answer below for more details and helpful links

After some more digging I found a solution :
First I have to init URLSession with a configuration:
let URLSession1 = URLSession(configuration: .default)
Then I can call the 'GET' method and I had to use (data,_ ) instead of (data:NSData,_ ):
open static func getData() -> Promise<[Station]> {
return URLSession1.GET("http://api.xxx.com/api/").asDataAndResponse().then { (data, _) -> [Station] in
var stations = [Station]()
// rest of the code to retrieve the data
...
...
return stations
}
}
Some helpful StackOverflow links that helped me find the solution:
NSURLSession dataTaskForRequest:completion: unrecognized selector sent to instance

Related

Swiftui FileManager/URLSession not writing to documentDirectory when running as background task

Hope you're doing well!
I've built an app that generates a view from a .csv file that I have hosted on my website. I've previously managed to get everything working as expected where I called the csv from the website and wrote the contents directly to a variable and then processed it from there. Obviously this wasn't good practice as the app started mis-behaving when the internet couldn't be accessed (despite writing in connectivity checks).
I've now built out the app to call the URL, save the csv with Filemanager, then when the app refreshes, it will use FileManager.default.replaceItemAt to replace the previous version if there is internet connectivity, if not the app builds from the previously stored .csv
This all works fine when the app is running, however I'm running into issues with the background processing task. It seems the app doesn't have permissions to write with FileManager when it is executed from the background task. Is there an additional step I'm missing when using this in background tasks? I've attempted to use FileManager.default.removeItem followed by FileManager.default.copyItem instead of replaceItemAt but it doesn't seem to make a difference as expected.
UPDATE 22/06 - Still scouring the internet for similar issues or examples I think I might be going down the wrong rabbit hole here. This could be issues with the way the new background task has been configured for retrieving data from my website, although the background tasks worked fine before there seems to be a bit more legwork needed for this method to work as a background task.
func handleAppRefresh(task: BGProcessingTask) {
//Schedules another refresh
scheduleAppRefresh()
DispatchQueue.global(qos: .background).async {
pullData()
print("BG Background Task fired")
}
pullData() will call loadCSV() and then do some data processing. At the moment I'm just using a print straight after loadCSV() is called to validate if the downloads etc are successful.
// Function to pass the string above into variables set in the csvevent struct
func loadCSV(from csvName: String) -> [CSVEvent] {
var csvToStruct = [CSVEvent]()
// Creates destination filepath & filename
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")
//Create URL to the source file to be downloaded
let fileURL = URL(string: "https://example.com/testcsv.csv")!
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("File downloaded Successfully. Response: \(statusCode)")
}
do {
let _ = try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: tempLocalUrl)
} catch (let writeError) {
print("Error creating a file \(destinationFileUrl) : \(writeError)")
}
} else {
print("Error" )
}
}
task.resume()
let data = readCSV(inputFile: "testcsv.csv")
var rows = data.components(separatedBy: "\n")
rows.removeFirst()
// Iterates through each row and sets values
for row in rows {
let csvColumns = row.components(separatedBy: ",")
let csveventStruct = CSVEvent.init(raw: csvColumns)
csvToStruct.append(csveventStruct)
}
print("LoadCSV has run and created testcsv.csv")
return csvToStruct
}
Any help or pointers to why these files aren't being updated in background tasks but are working fine in app would be massively appreciated!
Thanks in advance.
EDIT: adding new BGProcessingTask
func handleAppRefresh(task: BGProcessingTask) {
//Schedules another refresh
print("BG Background Task fired")
scheduleAppRefresh()
Task.detached {
do {
let events = try await loadCSV(from: "Eventtest").filter { !dateInPast(value: $0.date) }
print(events)
pullData(events: events)
} catch {
print(error)
}
}
}
The problem is not the background task per se, the problem is the asynchronous behavior of downloadTask. readCSV is executed before the data is downloaded.
In Swift 5.5 and later async/await provides asynchronous behavior but the code can be written continuously.
func loadCSV(from csvName: String) async throws -> [CSVEvent] {
var csvToStruct = [CSVEvent]()
// Creates destination filepath & filename
let documentsUrl:URL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL?)!
let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv")
//Create URL to the source file to be downloaded
let fileURL = URL(string: "https://example.com/testcsv.csv")!
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL)
let (url, response) = try await session.download(for: request)
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("File downloaded Successfully. Response: \(statusCode)")
}
let _ = try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: url)
let data = readCSV(inputFile: "testcsv.csv")
var rows = data.components(separatedBy: "\n")
rows.removeFirst()
// Iterates through each row and sets values
for row in rows {
let csvColumns = row.components(separatedBy: ",")
let csveventStruct = CSVEvent.init(raw: csvColumns)
csvToStruct.append(csveventStruct)
}
print("LoadCSV has run and created testcsv.csv")
return csvToStruct
}
To call the function you have to wrap it in a detached Task which replaces the GCD queue
Task.detached {
do {
let events = try await loadCSV(csvName: "Foo")
print("BG Background Task fired")
} catch {
print(error)
}
}

How can I change the filename in a SwiftUI Document-based app?

I've been using the new template for a document-based SwiftUI app. While you get a lot of file-management "for free" in the new template, as it stands in the iOS version users have to back out of the file to the file browser to change the filename. I want to create an opportunity for the user to rename the file while it is open.
Here's a sample project focused on the issue: https://github.com/stevepvc/DocumentRenamer
In the code, I've added to the template code a simple UI with a textfield for the user to enter a new name. When the user hits the "rename" button, the app checks to see if the URL with that name component is available, appending a suffix if necessary to create a target url.
func getTargetURL() -> URL {
let baseURL = self.fileurl.deletingLastPathComponent()
print("filename: \(self.filename)")
print("fileURL: \(self.fileurl)")
print("BaseURL: \(baseURL)")
var target = URL(fileURLWithPath: baseURL.path + "/\(filename).exampletext")
var nameSuffix = 1
while (target as NSURL).checkPromisedItemIsReachableAndReturnError(nil) {
target = URL(fileURLWithPath: baseURL.path + "/\(filename)-\(nameSuffix).sermon")
print("Checking: \(target)")
nameSuffix += 1
}
print("Available Target: \(target)")
return target
}
It then attempts to rename the file, and this is when I am stuck. I have tried several methods, most recently the following:
func changeFilename(){
let target = getTargetURL()
var rv = URLResourceValues()
let newFileName = target.deletingPathExtension().lastPathComponent
rv.name = newFileName
do {
if fileurl.startAccessingSecurityScopedResource(){
try fileurl.setResourceValues(rv)
fileurl.stopAccessingSecurityScopedResource()
}
} catch {
print("Error:\(error)")
}
}
But I keep getting the following error:
Domain=NSCocoaErrorDomain Code=513 "You don’t have permission to save the file “Untitled” in the folder “DocumentRenamer”."
I have also tried this without the startAccessingSecurityScopedResource() check, and alternatively have tried creating a helper class as follows:
class FileMover: NSObject {
func moveFile(originalURL: URL, updatedURL:URL) -> Bool {
let coordinator = NSFileCoordinator(filePresenter: nil)
var writingError: NSError? = nil
var success : Bool = true
print("moving file")
coordinator.coordinate(writingItemAt: originalURL, options: NSFileCoordinator.WritingOptions.forMoving, error: &writingError, byAccessor: { (coordinatedURL) in
do {
try FileManager.default.moveItem(at: coordinatedURL, to: updatedURL)
success = true
print("file moved")
} catch {
print(error)
success = false
}
})
return success
}
}
But using this method locks up the app entirely. It's possible that there is something about iCloud permissions going on there, but I think I've have those set up appropriately.
It appears to work fine in the simulator, but not when run on a device.
What is the correct method for renaming a file in the app's container?

Confused about Alamofire sessionmanager

Using Alamofire 4.0 and Swift 3.0 this works:
Alamofire.request("http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json", method: .get).responseJSON {
(response) -> Void in
print("Success: \(response.result)")
}
Success: SUCCESS
However when I try to use the Sessionmanager so I can include a timeoutInterval, my requests always fail
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 15
let alamofireManager = Alamofire.SessionManager(configuration: configuration)
alamofireManager.request("http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json").validate().responseJSON {
response in
print("Success: \(response.result)")
print("Response String: \(response.result.value)")
}
Success: FAILURE
Would be grateful if someone could help point me in the right direction here.
By printing response.result.error I got:
Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLKey=http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json, NSLocalizedDescription=cancelled, NSErrorFailingURLStringKey=http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json}
Which lead me to this reference:
You need to make sure that the manager is retained. The difference
here is that the initialized manager is not owned, and is deallocated
shortly after it goes out of scope. As a result, any pending tasks are
cancelled.
Solution:
One way to solve the issue your having is by declaring the custom session manager outside of the class declaration as a global variable like so...
let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 15
return SessionManager(configuration: configuration)
}()
Now, within your class you can make the request.
class ViewController: UIViewController {
let url = "http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json"
override func viewDidLoad() {
super.viewDidLoad()
sessionManager.request(url).validate().responseJSON { response in
switch response.result {
case .success:
print(response.result.value as! NSDictionary)
break
case .failure:
print(response.result.error!)
break
}
}
}
}
Which shall give you what you're looking for. Hope that helps!

FileManager replaceItemAt() results in EXC_BAD_ACCESS

I've written an application that downloads images from a website.
If this image already exists on the device I'm trying to replace it.
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let userId = Int.init(downloadTask.taskDescription!)! // task description is definetly set in downloadImage() and is an Int
guard let target = imageFolder?.appendingPathComponent("\(userId).jpg") else {
delegate?.imageDownloadFailed(forUser: userId, error: "Could not create target URL.")
return
}
do {
if fileManager.fileExists(atPath: target.path) {
_ = try fileManager.replaceItemAt(target, withItemAt: location)
} else {
try fileManager.moveItem(at: location, to: target)
}
delegate?.savedImage(forUser: userId, at: target)
} catch let error {
delegate?.imageDownloadFailed(forUser: userId, error: error.localizedDescription)
}
}
The problem occurs in the if-statement:
_ = try fileManager.replaceItemAt(target, withItemAt: location)
I always got EXC_BAD_ACCESS and I can't find the error.
fileManager, target, and location are non-nil.
I've already tried to dispatch the code synchronous to the main thread, but the error still persists.
Any advices?
Edit:
Since I'm not the only one who got this error I decided to create a bug report at Apple.
The report is available at Open Radar; click
I've also uploaded a playground file at pastebin.com which demonstrates the error and provides a quick solution similar to the one of naudec.
Had the same issue. Ended up writing my own version:
let fileManager = FileManager.default
func copyItem(at srcURL: URL, to dstURL: URL) {
do {
try fileManager.copyItem(at: srcURL, to: dstURL)
} catch let error as NSError {
if error.code == NSFileWriteFileExistsError {
print("File exists. Trying to replace")
replaceItem(at: dstURL, with: srcURL)
}
}
}
func replaceItem(at dstURL: URL, with srcURL: URL) {
do {
try fileManager.removeItem(at: dstURL)
copyItem(at: srcURL, to: dstURL)
} catch let error as NSError {
print(error.localizedDescription)
}
}
I call copyItem first.
The class holding this method does not exist any more at the time your download finishes and did release your filemanager. Create the FileManager within your completion closure:
...
let localFilemanager = FileManager.default
do {
...

Swift 3 FileManagerDelegate: shouldRemoveItemAt compiler error

While using Xcode 8 and Swift 3, I am trying to implement the following method for the FileManagerDelegate protocol:
private func fileManager(_ fileManager: FileManager, shouldRemoveItemAt URL: URL) -> Bool {
var shouldDelete = true
let urlString = URL.absoluteString
if urlString?.range(of: "keepfiles") != nil {
shouldDelete = false
}
return shouldDelete
}
the compiler shows the following error:
Use of undeclared type: 'URL'
but it does not offer any solution to fix it. Because of this I cannot test the selective deletion. If I change the URL type declaration to NSURL, the error goes away, but the delegate never gets called and all the files get deleted.
Does anyone know why this is happening and how I can fix it?
You were trying to get the absoluteString from the type URL instead of instance url. Change your parameter name into a more readable format and do like this, the error will go away.
private func fileManager(_ fileManager: FileManager, shouldRemoveItemAt url: URL) -> Bool {
var shouldDelete = true
let urlString = url.absoluteString
if urlString?.range(of: "keepfiles") != nil {
shouldDelete = false
}
return shouldDelete
}