I do have a UIWebView included where a public URL is loaded; unfortunately, vcard and ical-Links are not handled, i.e. nothing happens when I click on them.
I tried to set all data detectors, no luck unfortunately.
In the Xcode-log, I get this here when clicking on such a link:
2017-07-14 13:43:00.982413+0200 xxx[2208:967973] WF: _userSettingsForUser mobile: {
filterBlacklist = (
);
filterWhitelist = (
);
restrictWeb = 1;
useContentFilter = 0;
useContentFilterOverrides = 0;
whitelistEnabled = 0;
}
In Safari, the same stuff works as expected.
If I use UIApplication.shared.openURL(icsOrVcardUrl) Safari gets opened and from there everything works as expected again, but I don't want the user to leave the app...
EDIT
This doesn't work either:
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if let url = request.url {
if url.absoluteString.contains("=vcard&") || url.absoluteString.contains("/ical/") {
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:url)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
DispatchQueue.main.async {
self.documentController.url = tempLocalUrl
self.documentController.presentPreview(animated: true)
}
}
}
task.resume()
return false
}
}
return true
}
Use a UIDocumentInteractionController to preview without leaving your app.
I tested it quickly with an .ics file and it works fine.
Implement the UIDocumentInteractionControllerDelegate protocol
extension MainViewController: UIDocumentInteractionControllerDelegate {
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return self;
}
}
Create an instance of the interaction controller:
let documentController = UIDocumentInteractionController()
Intercept the clicks in your UIWebView in shouldStartLoadWithRequest, return false for links you want to handle with the in-app preview and true for all the rest. And finally:
func previewDocument(_ url: URL) {
documentController.url = url
documentController.presentPreview(animated: true)
}
Here it is in the simulator
EDIT:
In response to the comment to this answer:
The reason it doesn't work for you is because the UIDocumentInteractionController depends on the file extension. The extension of the temp file is .tmp
Renaming the file after the download solves the problem. Quick and dirty example:
let task = session.downloadTask(with: url!) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
do {
let filemgr = FileManager.default
let newUrl = tempLocalUrl.appendingPathExtension("ics")
try filemgr.moveItem(at: tempLocalUrl, to: newUrl)
DispatchQueue.main.async {
self.documentController.url = newUrl
self.documentController.presentPreview(animated: true)
}
} catch let error {
print("Error!!!: \(error.localizedDescription)")
}
}
}
task.resume()
In this case it is advisable to clean after yourself, because the file won't be deleted after the task completes although the OS will delete it eventually, when space is needed. If you often access the same urls, Library/Caches/ may be a better place for this files, just come up with good naming schema, and check if the file doesn't exist already.
Related
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)
}
}
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?
I was doing download system with the code from https://stackoverflow.com/a/32322851/7789222. It was a great and complete code but I can find a way to pass foldername from view controller to download file to specific folder. Can anyone help me with it please. I am using swift 3 xcode 8.
If I hard code the custom directory in func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) , every file will be downloaded to same folder. I want to pass the foldername from view controller so I can download files to different folder. I cant hardcode because I retrieve file name and foldername from server
The destination URL in the example is given by
let destinationURL = try manager.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
).appendingPathComponent(url.lastPathComponent)
(Line 17)
You can just pass a destination folder URL to the initializer of DownloadOperation which replaces the destination URL in the example:
let destinationURL = yourDestinationFolder.appendingPathComponent(url.lastPathComponent)
Your modified DownloadOperation would look something like this:
class DownloadOperation : AsynchronousOperation {
var task: URLSessionTask!
let destinationFolder: URL
init(session: URLSession, url: URL, destinationFolder: URL) {
super.init()
self.destinationFolder = destinationFolder
task = session.downloadTask(with: url) { temporaryURL, response, error in
defer { self.completeOperation() }
guard error == nil && temporaryURL != nil else {
print("\(error)")
return
}
do {
let manager = FileManager.default
let destinationURL = destinationFolder.appendingPathComponent(url.lastPathComponent)
_ = try? manager.removeItem(at: destinationURL) // remove the old one, if any
try manager.moveItem(at: temporaryURL!, to: destinationURL) // move new one there
} catch let moveError {
print("\(moveError)")
}
}
}
...
}
The code for adding operations is then
queue.addOperation(DownloadOperation(session: session, url: url, destinationFolder: destinationFolder))
If you want to use the DownloadManager:
class DownloadManager {
#discardableResult
func addDownload(_ url: URL, to destinationFolder: URL) -> DownloadOperation {
let operation = DownloadOperation(session: session, url: url, destinationFolder: destinationFolder)
operations[operation.task.taskIdentifier] = operation
queue.addOperation(operation)
return operation
}
...
}
The extension:
extension DownloadOperation: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let manager = FileManager.default
let destinationURL = destinationFolder.appendingPathComponent(downloadTask.originalRequest!.url!.lastPathComponent)
if manager.fileExists(atPath: destinationURL.path) {
try manager.removeItem(at: destinationURL)
}
try manager.moveItem(at: location, to: destinationURL)
} catch {
print("\(error)")
}
}
...
}
Then you can add downloads with
downloadManager.addDownload(url, to: destinationFolder)
My project had been getting the URL string for the medium sized profile pic using this code:
let downloadMediumPicTask = session.dataTask(with: mediumProfPictureURL) { (data, response, error)
in
// The download has finished.
if let e2 = error {
print("Error downloading profile picture: \(e2)")
} else {
if let res2 = response as? HTTPURLResponse {
print("Downloaded medium profile picture with response code \(res2.statusCode)")
if let imageData2 = data {
mediumProfilePictureUIImageFile = UIImage(data: imageData2)!
print("mediumProfilePictureUIImageFile has now been defined as: \(mediumProfilePictureUIImageFile).")
} else {
print("Couldn't get image: Image is nil")
}
} else {
print("Couldn't get response code for some reason")
}
}
}
downloadMediumPicTask.resume()
It crashes here giving a 403 response code. The URL that is being referenced is an expired signature URL from Facebook. Firebase doesn't adjust to get the new appropriate URL, and it was from Firebase that I had been getting the URL. I can't figure out how to get it directly as tried below:
func getUrlOfMediumProfilePic(){
if (FBSDKAccessToken.current() != nil) {
let graphPathPart2 = "me/picture"
let paramsPart2 = ["type":"medium", "redirect":"false"]
let completionHandlerPart2 = { (connection: FBSDKGraphRequestConnection?, result: Any?, error: Error?) in
if let error = error {
print("Medium picture graph call contained an error: \(error.localizedDescription)")
return
} else {
guard connection != nil else {
print("getURLOfLargeProfilePic() function aborted bc connection failed.")
return
}
let results = result! as! NSDictionary
let dataDict = results["data"] as! NSDictionary
stringOfMediumProfilePicURLaka100x100 = dataDict["url"] as! String
print("medium picture graph call results define stringOfMediumProfilePicURLaka100x100 as: \(stringOfMediumProfilePicURLaka100x100)")
}
}
let graphRequest = FBSDKGraphRequest(graphPath: graphPathPart2, parameters: paramsPart2)!
graphRequest.start(completionHandler: completionHandlerPart2)
}else{
print("User not logged in when getURLOfMediumProfilePic() function was run.")
return
}
}
This code yields an error with code 8.
Have you tried this:
https://graph.facebook.com/{id}/picture?width=100&height=100
I don't know swift, so I can't help about syntax. I think you can make http request to url and get image.
Hope this help :)
I would like to know when the function below has finished downloading the data.
I know I can use a completion handler, but this is in an NSObject and I'd like to know when to return the completion handler for this function, making sure the video is downloaded completely and ready to go, before the function returns the completion handler, and my View Controller resumes it's logic.
Thanks :)
func downloadVideo(identity:JSON_Video){
// use guard to make sure you have a valid url
let videoId = identity.video_id!
let videoString:String = "\(Constants.endPoint_video)\(videoId).mp4"
guard let videoURL = URL(string: videoString) else { return }
let library_url = self.findVideo(video: identity)
if self.findVideo(video: identity) != nil{
print("Video exists. No need to download")
print("Existing video \(library_url!)")
}else{
print("Video not found. Downloading now")
// Variables to input on request
let loginString = Constants.loginString
let loginData = loginString.data(using: String.Encoding.utf8)!
let base64LoginString = loginData.base64EncodedString()
// URL request
var urlRequest = URLRequest(url: videoURL)
urlRequest.httpMethod = "GET"
urlRequest.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization")
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: urlRequest, completionHandler: { (data, response, error) in
// do stuff with response, data & error here
if let error = error{
print("Data Session Error: \(error.localizedDescription)")
print(response ?? "No response")
return
}
if let data = data {
print("Incoming video....")
print("Data: \(data.description)")
if let finalDatabaseURL = self.videosBaseUrl()?.appendingPathComponent("\(identity.video_id!).mp4"){
print("Copying from: \(videoURL)")
print("Copying to: \(finalDatabaseURL)")
do {
try
data.write(to: finalDatabaseURL)
print("Writing data to file")
// *****
// How do I know when data finished writing?
// Completion handler goes here ?
// *****
}catch{
print("Error writing data to file")
print(error.localizedDescription)
}
}
}
}
})
task.resume()
}
}