We have .zip file in app, tagged as on demand resource. On downloading it we get success in NSBundleResourceRequest completion handler, but unable to find path of downloaded file(.zip). It works fine for png and jpg file but fails for .zip files. Also .zip file download works fine in our testing devices and fails only on App Reviewer devices.
Any alternative for .zip in iOS will work on ODR?
Are you using conditionalyBeginAccessingResources method before beginAccessingResources ?
Resources:
Check this nice ODR ios tutorial from Ray, and this book from Vandad (it contains a section for propper ODR fetching).
From the Ray's tutorial:
class ODRManager {
// MARK: - Properties
static let shared = ODRManager()
var currentRequest: NSBundleResourceRequest?
// MARK: - Methods
func requestFileWith(tag: String,
onSuccess: #escaping () -> Void,
onFailure: #escaping (NSError) -> Void) {
currentRequest = NSBundleResourceRequest(tags: [tag])
guard let request = currentRequest else { return }
request.endAccessingResources()
request.loadingPriority =
NSBundleResourceRequestLoadingPriorityUrgent
request.beginAccessingResources { (error: Error?) in
if let error = error {
onFailure(error as NSError)
return
}
onSuccess()
}
}
}
In use:
ODRManager.shared.requestFileWith(tag: "<#Your tag#>", onSuccess: {
// load it through Bundle
}, onFailure: { (error) in
let controller = UIAlertController(title: "Error", message: "There was a problem.", preferredStyle: .alert)
switch error.code {
case NSBundleOnDemandResourceOutOfSpaceError:
controller.message = "You don't have enough space available to download this resource."
case NSBundleOnDemandResourceExceededMaximumSizeError:
controller.message = "The bundle resource was too big."
case NSBundleOnDemandResourceInvalidTagError:
controller.message = "The requested tag does not exist."
default:
controller.message = error.description
}
controller.addAction(UIAlertAction(title: "Dismiss", style: .default, handler: nil))
guard let rootViewController = self.view?.window?.rootViewController else { return }
rootViewController.present(controller, animated: true)
})
From the book:
let tag = "<#tagString#>"
var currentResourcePack: NSBundleResourceRequest? = NSBundleResourceRequest(tags: [tag])
guard let req = currentResourcePack else { return }
req.conditionallyBeginAccessingResources { available in
if available {
self.displayImagesForResourceTag(tag)
} else {
// this usualy means that the resources are not downloaded so you need to download them first
req.beginAccessingResources { error in
guard error == nil else {
<#/* TODO: you can handle the error here*/#>
return
}
self.displayImagesForResourceTag(tag)
}
}
}
func displayImagesForResourceTag(_ tag: String) {
OperationQueue.main.addOperation {
for n in 0..<self.imageViews.count {
self.imageViews[n].image = UIImage(named: tag + "-\(n+1)")
}
}
}
So, maybe you can dig out the source of zip there?
Alternative way
Another solution is to download the zip, extract it and start using resources from the extract sandbox destination folder by using the FileManager, and use Bundle only when ODR are not or can't be downloaded.
GL
Related
We have a requirement where we have to block the user for 24 hours if Face ID or Touch ID fails for 5 consecutive time.
In Face ID, I am using the fallback to call authentication manager again, and after total of 6 attempts (2 in each call), I block the user in the fallback method itself.
In Touch ID, on third failure, I get a call back to Authentication failed. I call authentication manager again, and on 5th try, I get a call back to Lockout in which I can block the user.
Is there any way common in both Face ID and Touch ID where I can get a callback after every single individual failure so that I can block the user on 5th failure itself?
//MARK:- Check if user has valid biometry, if yes, validate, else, show error
fileprivate func biometricAuthentication(completion: #escaping ((Bool) -> ())){
// addBlurredBackground()
//Check if device have Biometric sensor
if biometryRetryCount == 2 {
authenticationContext.localizedFallbackTitle = "Ok"
} else {
authenticationContext.localizedFallbackTitle = "Retry"
}
let isValidSensor : Bool = authenticationContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
if isValidSensor {
//Device have BiometricSensor
authenticationContext.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: biometryRetryCount == 2 ? "You have been blocked from using the application for next 24 hours. Please come back later" : "Touch / Face ID authentication",
reply: { [unowned self] (success, error) -> Void in
if(success) {
// Touch or Face ID recognized
// self.removeBlurredBackground()
completion(true)
} else {
//If not recognized then
if let error = error {
let msgAndAction = self.errorMessage(errorCode: error._code)
if msgAndAction.0 != "" {
UIApplication.topViewController()?.showAlert(withTitle: "Error", andMessage: msgAndAction.0, andActions: msgAndAction.1)
}
}
completion(false)
}
})
} else {
let msgAndAction = self.errorMessage(errorCode: (error?._code)!)
if msgAndAction.0 != ""{
UIApplication.topViewController()?.showAlert(withTitle: "Error", andMessage: msgAndAction.0, andActions: msgAndAction.1)
}
}
}
The error methods:
//MARK: TouchID error
fileprivate func errorMessage(errorCode:Int) -> (strMessage: String, action: [UIAlertAction]){
var strMessage = ""
let cancelAction = UIAlertAction.init(title: Const.Localize.Common().kCancel, style: .cancel) { (cancelAction) in
}
var actions: [UIAlertAction] = [cancelAction]
switch errorCode {
case LAError.Code.authenticationFailed.rawValue:
biometricAuthentication { (success) in
//
}
case LAError.Code.userCancel.rawValue:
if biometryRetryCount == 2 {
blockUserFor24Hours()
} else {
showAlertOnCancelTapAction()
}
case LAError.Code.passcodeNotSet.rawValue:
strMessage = "Please goto the Settings & Turn On Passcode"
case LAError.Code.userFallback.rawValue:
biometryRetryCount -= 2
if biometryRetryCount == 0 {
blockUserFor24Hours()
} else {
biometricAuthentication { (success) in
//
}
}
default:
strMessage = evaluatePolicyFailErrorMessageForLA(errorCode: errorCode).strMessage
actions = evaluatePolicyFailErrorMessageForLA(errorCode: errorCode).action
}
return (strMessage, actions)
}
func evaluatePolicyFailErrorMessageForLA(errorCode: Int) -> (strMessage: String, action: [UIAlertAction]){
let cancelAction = UIAlertAction.init(title: Const.Localize.Common().kCancel, style: .cancel) { (cancelAction) in
}
var actions: [UIAlertAction] = [cancelAction]
var message = ""
if #available(iOS 11.0, macOS 10.13, *) {
switch errorCode {
case LAError.biometryNotAvailable.rawValue:
message = "Authentication could not start because the device does not support biometric authentication."
case LAError.biometryLockout.rawValue:
showPasscodeScreen()
case LAError.biometryNotEnrolled.rawValue:
message = "You do not have a registered biometric authentication. Kindly go to the settings and setup one"
let settingsAction = UIAlertAction.init(title: Const.Localize.Common().kSetting, style: .default) { (settingsAction) in
UIApplication.shared.openURL(URL(string: UIApplicationOpenSettingsURLString)!)
}
actions.append(settingsAction)
default:
message = "Did not find error code on LAError object"
}
} else {
switch errorCode {
case LAError.touchIDLockout.rawValue:
showPasscodeScreen()
case LAError.touchIDNotAvailable.rawValue:
message = "TouchID is not available on the device"
case LAError.touchIDNotEnrolled.rawValue:
message = "You do not have a registered biometric authentication. Kindly go to the settings and setup one"
let settingsAction = UIAlertAction.init(title: Const.Localize.Common().kSetting, style: .default) { (settingsAction) in
UIApplication.shared.openURL(URL(string: UIApplicationOpenSettingsURLString)!)
}
actions.append(settingsAction)
default:
message = "Did not find error code on LAError object"
}
}
return (message, actions)
}
When trying to load a different file type (eg. PDF, PNG, JPEG), it works perfectly.
Also tried loading the .doc data into a UIWebview and it also works fine. I am getting the data from a server using JSON encoded to Base64.
let webview = WKWebView()
webview.load(data, mimeType: "application/msword", characterEncodingName: "UTF-8", baseURL: NSURL() as URL)
Has anyone faced this issue as well?
The file should not save locally.
It is the condition.
You need to use Quicklook for this, it is native feature of iOS:-
import Foundation
import UIKit
import QuickLook
class ClassQuickLookFilePreviewHandler {
static let shared = ClassQuickLookFilePreviewHandler()
var url: URL?
var tempURL: URL?
func previewFile(vc: UIViewController, url: URL, fileName: String) {
let previewController = QLPreviewController()
self.url = url
previewController.dataSource = self
previewController.view.tintColor = UIColor(hexString: "#ff3366")
tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
previewController.currentPreviewItemIndex = 0
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
// in case of failure to download your data you need to present alert to the user and update the UI from the main thread
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
let alert = UIAlertController(title: "Alert", message: error?.localizedDescription ?? "Failed to download the pdf!!!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
vc.present(alert, animated: true)
}
return
}
// write the downloaded data to a temporary folder or to the document directory if you want to keep the pdf for later usage
do {
try data.write(to: self.tempURL!, options: .atomic) // atomic option overwrites it if needed
// you neeed to check if the downloaded data is a valid pdf
// and present your controller from the main thread
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
vc.present(previewController, animated: true)
}
} catch {
print(error)
return
}
}.resume()
UIApplication.shared.isNetworkActivityIndicatorVisible = true
}
}
extension ClassQuickLookFilePreviewHandler: QLPreviewControllerDataSource {
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return tempURL! as QLPreviewItem
}
}
Usage:-
ClassQuickLookFilePreviewHandler.shared.previewFile(vc: self, url: URL(string: ?*documentURL)!, fileName: ?*documentFileName)
Hope it helps :)
This is my first app and I'm wondering if I have made a mistake with regards to using URLSession.shared dataTask because I am not seeing my app get new data which is frequently updated. I see the new json instantly in my browser when I refresh, but, my app apparently does not see it.
Will it ever get the new json data from my server without uninstalling the app?
There are some some similar question topics, such as How to disable caching from NSURLSessionTask however, I do not want to disable all caching. Instead, I want to know how this default object behaves in this scenario - How long is it going to cache it? If indeed the answer is forever, or until they update or reinstall the app, then I will want to know how to reproduce the normal browser based cache behavior, using if-modified-since header, but that is not my question here.
I call my download() function below gratuitously after the launch sequence.
func download(_ ch: #escaping (_ data: Data?, _ respone: URLResponse?, _ error: Error?) -> (), completionHandler: #escaping (_ sessionError: Error?) -> ()) {
let myFileURL: URL? = getFileURL(filename: self.getFilename(self.jsonTestName))
let myTestURL = URL(string:getURLString(jsonTestName))
let session = URLSession.shared
// now we call dataTask and we see a CH and it calls My CH
let task = session.dataTask(with: myTestURL!) { (data, response, error) // generic CH for dataTask
in
// my special CH
ch(data,response,error) // make sure the file gets written in this ch
}
task.resume() // no such thing as status in async here
}
Within the completion handler which I pass to download, I save the data with this code from "ch":
DispatchQueue.main.async {
let documentController = UIDocumentInteractionController.init(url: myFileURL!)
documentController.delegate = self as UIDocumentInteractionControllerDelegate
}
and then finally, I read the data within that same completion handler from disk as such:
let data = try Data(contentsOf: myFileURL!)
For clarification, my complete calling function from which I call download() with completion handler code.
func get_test(){ // download new tests
let t = testOrganizer
let myFileURL: URL? = t.getFileURL(filename:t.getFilename(t.jsonTestName))
t.download( { (data,response,error)
in
var status: Int! = 0
status = (response as? HTTPURLResponse)?.statusCode
if(status == nil) {
status = 0
}
if(error != nil || (status != 200 && status != 304)) {
let alertController = UIAlertController(title: "Error downloading", message:"Could not download updated test data. HTTP Status: \(status!)", preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default,handler: nil))
self.present(alertController, animated: true, completion: nil)
self.p.print("END OF COMPLETION HANDLER")
}
else {
let status = (response as! HTTPURLResponse).statusCode
print("Success: status = ", status)
self.p.print("WRITING FILE IN COMPLETION HANDLER")
do {
try data!.write(to: myFileURL!)
DispatchQueue.main.async {
let documentController = UIDocumentInteractionController.init(url: myFileURL!)
documentController.delegate = self as UIDocumentInteractionControllerDelegate
}
} catch {
// // _ = completionHandler(NSError(domain:"Write failed", code:190, userInfo:nil))
print("error writing file \(myFileURL!) : \(error)")
}
self.myJson = self.testOrganizer.readJson()
self.p.print("END OF COMPLETION HANDLER")
}
}, completionHandler: {
sessionError in
if(sessionError == nil) {
print("Downloaded and saved file successfully")
} else {
let alertController = UIAlertController(title: "get_tests", message:
"Failed to download new tests - " + sessionError.debugDescription, preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default,handler: nil))
self.present(alertController, animated: true, completion: nil)
}
})
}
I'm trying to open, modify, and save a file in iCloud Drive using UIDocument. When I call save(to:for:completionHandler:) with the file location and using .forOverwriting for the UIDocumentSaveOperation, it completes with a status of success = true. However, the iCloud file (as seen in both desktop and iOS file browser) does not update, and when reopening the file, the changes are not shown. I've verified that contents(forType:) returns the correct (modified) file contents when saving.
(Note: I've already looked at this question, but it wasn't very helpful 😕)
Here are the relevant sections of code:
MainViewController.swift:
var saveFile: SBDocument?
#IBAction func bbiOpen_pressed(_ sender: UIBarButtonItem) {
if saveFile == nil {
let importMenu = UIDocumentMenuViewController(documentTypes: self.UTIs, in: .import)
importMenu.delegate = self
importMenu.popoverPresentationController?.barButtonItem = bbiOpen
self.present(importMenu, animated: true, completion: nil)
} else {
willClose()
}
}
func willClose(_ action: UIAlertAction?) {
if saveFile!.hasUnsavedChanges {
dlgYesNoCancel(self, title: "Save Changes?", message: "Would you like to save the changes to your document before closing?", onYes: doSaveAndClose, onNo: doClose, onCancel: nil)
} else {
doSaveAndClose(action)
}
}
func doSaveAndClose(_ action: UIAlertAction?) {
saveFile?.save(to: saveFileURL!, for: .forOverwriting, completionHandler: { Void in
self.saveFile?.close(completionHandler: self.didClose)
})
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) {
saveFile = SBDocument(fileURL: url)
saveFile!.open(completionHandler: { success in self.finishOpen(didCompleteSuccessfully: success) })
}
func finishOpen(didCompleteSuccessfully result: Bool) {
if result {
print(saveFile!.localizedName)
saveFileURL = saveFile!.fileURL
saveFileName = saveFile!.localizedName
self.navTitleBar.prompt = saveFileName
bbiOpen.title = NSLocalizedString("titleClose", comment: "Close")
bbiOpen.style = .plain
} else {
saveFile = nil
}
}
#IBAction func bbiSave_pressed(_ sender: UIBarButtonItem) {
self.saveFile!.save(to: self.saveFileURL!, for: .forOverwriting, completionHandler: self.didSave)
}
func didSave(_ success: Bool) {
guard success else {
print("Error saving soundboard file to \(String(describing: saveFileURL))")
return
}
print("File saved successfully")
}
SBDocument.swift:
class SBDocument: UIDocument {
override var fileType: String? { get { return "com.whitehatenterprises.SoundBoardFX.sbd" } }
override var savingFileType: String? { get { return "com.whitehatenterprises.SoundBoardFX.sbd" } }
override init(fileURL url: URL) {
super.init(fileURL: url)
}
override func contents(forType typeName: String) throws -> Any {
let arr = NSArray(array: SoundEffects)
let data: NSData = NSKeyedArchiver.archivedData(withRootObject: arr) as NSData
return data
}
}
Update:
I really need help with this, and I've tried everything I can think of to fix this. Any assistance you could give me would be greatly appreciated.
The way the initial file generation works for me is:
let doc = YourUIDocumentClass(fileURL: fileURL)
doc.save(to: fileURL, for: .forCreating) { success in
...
}
Then modify the file and then do:
doc.save(to: fileURL, for: .forOverwriting) { success in
...
}
when done. And subsequent accesses to the file are done by:
doc.open() { success in
...
}
doc.close() { success in
...
}
You might also need to do a:
doc.updateChangeCount(.done)
while the file is open to tell the document there are unsaved changes. Just setting this will cause a save after a few seconds. You don't even need the close to do that.
The ... means that you either have to nest all these or make sure there is enough time between them so they are completed.
In addition to the above answers, another cause of this can be that there's an error during the save process unrelated to contents(forType:).
For example, if you implement fileAttributesToWrite(to:for:) and throw an error, then this can cause a UIDocumentState.savingError even though contents(forType:) returns the correct data.
So according to
https://developer.apple.com/reference/uikit/uidocument
It looks like the save function isn't actually for saving a document. My understanding from reading it is that save is only for creating a new document. I understand that you are using the .forOverwriting to just save over it but there may be something in iCloud that wont let the complete overwrite happen.
In your doSaveAndClose method try calling
self.saveFile?.close(completionHandler: self.didClose)
by itself. You may have to do some type of if query where you check if the file exist. If it doesn't then call the .save(), else call the .close function. It seems that no matter what when the document it closed it saves changes.
I have ios project written in swift 2.3 that is integrated with AWS by following the instructions from AWS mobile hub integration page. On the page that explains about downloading file from s3 bucket, it provides a function, and that function takes AWSContent type parameter. I'd like to use that function to download a particular file from S3 bucket. I tried using contentWithKey member of AWSUserFileManager with my file name and path but getting "found nil while unwrapping optional value" error. Could anyone point me to the right direction? Thanks.
private var manager: AWSUserFileManager!
class S3Access: NSObject {
func setupS3Acess() {
let x = manager.contentWithKey("public/GMG.csv")
downloadContent(x, pinOnCompletion: false)
}
// This code is from Amazon MobileHub integration page
private func downloadContent(content: AWSContent, pinOnCompletion: Bool) {
content.downloadWithDownloadType(
.IfNewerExists,
pinOnCompletion: pinOnCompletion,
progressBlock: {[weak self](content: AWSContent?, progress: NSProgress?) -> Void in
guard self != nil else { return }
/* Show progress in UI. */
},
completionHandler: {[weak self](content: AWSContent?, data: NSData?, error: NSError?) -> Void in
guard self != nil else { return }
if let error = error {
print("Failed to download a content from a server. \(error)")
return
}
print("Object download complete.")
})
}
}
I found the culprit. I need to assign manager variable with default user file manager like "manager = AWSUserFileManager.defaultUserFileManager()". I'm just posting this for dev like myself who already integrated with AWS libraries to the project and does not require additional authentication setup.
private var manager: AWSUserFileManager!
class S3Access: NSObject {
func setupS3Acess() {
manager = AWSUserFileManager.defaultUserFileManager()
let x = manager.contentWithKey("public/GMG.csv")
downloadContent(x, pinOnCompletion: false)
}
// This code is from Amazon MobileHub integration page
private func downloadContent(content: AWSContent, pinOnCompletion: Bool) {
content.downloadWithDownloadType(
.IfNewerExists,
pinOnCompletion: pinOnCompletion,
progressBlock: {[weak self](content: AWSContent?, progress: NSProgress?) -> Void in
guard self != nil else { return }
/* Show progress in UI. */
},
completionHandler: {[weak self](content: AWSContent?, data: NSData?, error: NSError?) -> Void in
guard self != nil else { return }
if let error = error {
print("Failed to download a content from a server. \(error)")
return
}
print("Object download complete.")
})
}
}