Scenario:
I wish to reduce the size of individual videos from my iTouch photo library.
1. Collect videoAssets from library.
2. Get a thumbnail of the PHAsset - works.
3. Get the actual video from the library.
4. Request the AVAssetForVideo from the library.
5. Convert the video via ExportSessions... loading assorted parameters.
6. Attempt to run the export into a tmp directory for use.
* FAILS *
Here's the debug output:
Here's the error message:
func getVideoFromPhotoLibrary() {
let videoAssets = PHAsset.fetchAssetsWithMediaType(.Video, options:nil)
videoAssets.enumerateObjectsUsingBlock {
(obj:AnyObject!, index:Int, stop:UnsafeMutablePointer<ObjCBool>) in
let mySize = CGSizeMake(120,120)
let myAsset = obj as! PHAsset
let imageManager = PHImageManager.defaultManager()
var myVideo:BlissMedium?
// Request the poster frame or the image of the video
imageManager.requestImageForAsset(myAsset, targetSize:mySize, contentMode: .AspectFit, options: nil) {
(imageResult, info) in
let thumbnail = UIImage(named:"videoRed")
myVideo = BlissMedium(blissImage: imageResult, creationDate:myAsset.creationDate)
myVideo!.mediumType = .video
}
// Actual Video:
imageManager.requestAVAssetForVideo(myAsset, options: nil, resultHandler: {result, audio, info in
let asset = result as! AVURLAsset
let mediaURL = asset.URL
let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetMediumQuality)
let filename = "composition.mp4"
session.outputURL = NSURL(string: NSTemporaryDirectory());
session.outputFileType = AVFileTypeQuickTimeMovie;
session.exportAsynchronouslyWithCompletionHandler({ () -> Void in
dispatch_async(dispatch_get_main_queue(), {
if session.status == AVAssetExportSessionStatus.Completed {
println("Success")
}
else {
println(session.error?.localizedDescription)
//The requested URL was not found on this server.
}
})
})
})
if nil != myVideo {
self.gBlissVideoMedia.append(myVideo!)
}
}
}
I checked to be sure the target path/file exist; then I added the 'AVFileTypeMPEG4' output type to match the intended .mp4:
let targetDir = createTempDirectory("bliss/composition.mp4") as String?
if NSFileManager.defaultManager().fileExistsAtPath(targetDir!) {
println("*** file exists! ***")
} else {
return
}
session.outputURL = NSURL(string: targetDir!);
session.outputFileType = AVFileTypeMPEG4
I'm still having problems:
* file exists! *
Optional("The operation could not be completed")
What am I doing wrong; what's missing?
Update:
I'm able to successfully run the export to my NSHomeDirectory() vs NSTemporaryDictory() in Objective-C.
However... the same code written in Swift fails.
I notice a change in absolute path to the target output in Swift, not found in Objective-C:
Perhaps it's a Swift 1.2 bug???
I am not sure if you can save in the root of the temp directory, I normally use this function to create a new temp directory that I can use:
func createTempDirectory(myDir: String) -> String? {
let tempDirectoryTemplate = NSTemporaryDirectory().stringByAppendingPathComponent(myDir)
let fileManager = NSFileManager.defaultManager()
var err: NSErrorPointer = nil
if fileManager.createDirectoryAtPath(tempDirectoryTemplate, withIntermediateDirectories: true, attributes: nil, error: err) {
return tempDirectoryTemplate
} else {
return nil
}
}
Try to make your conversion in the directory returned by this function.
I hope that helps you!
I didn't quite understand what that last part of code did, where you find out if a file exists or not. Which file is it you are locating?
Since I didn't understand that then this might be irrelevant, but in your topmost code I notice that you set the filename to composition.mp4, but let the outputURL be NSURL(string: NSTemporaryDirectory()). With my lack of Swiftness I might be missing something, but it seems to me as if you're not using the filename at all, and are trying to write the file as a folder. I believe setting a proper URL might fix the problem but I'm not sure. An Objective-c-example of this could be:
NSURL * outputURL = [[NSURL alloc]
initFileURLWithPath:[NSString pathWithComponents:
#[NSTemporaryDirectory(), #"composition.mp4"]]];
The outputURL is supposed to point to the actual file, not the folder it lies in. I think..
Anyway, if that doesn't work I do have a few other thoughts as well.
Have you tried it on an actual device? There may be a problem with the simulator.
Also, sadly, I have gotten the error -12780 countless times with different root-problems, so that doesn't help very much.
And, I see you check if session.status == AVAssetExportSessionStatus.Completed, have you checked what the actual status is? Is it .Failed, or perhaps .Unknown? There are several statuses.
This might be a long shot, but in one of my apps I am using the camera to capture video/audio, then encode/convert it using AVAssetExportSession. There were strange errors when starting to record, as well as after recording(exporting). I found out that I could change the AVAudioSession, which apparently has something to do with how the device handles media.
I have no idea how to Swift, but here's my code (in viewDidAppear of the relevant view)
NSError *error;
AVAudioSession *aSession = [AVAudioSession sharedInstance];
[aSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
[aSession setMode:AVAudioSessionModeVideoRecording error:&error];
[aSession setActive: YES error: &error];
The category PlayAndRecord allowed me to start the camera much faster, as well as getting rid of the occasional hanging AVAssetExportSessionStatus.Unknown and the occasional crash .Failed (which also threw the -12780-error).
Related
I have written a code to download HLS video and play it in offline mode.
This code works fine for encoded video. Now I have a video which is AES encrypted and we are having custom encryption key for it. After downloading AES encrypted HLS video I am using below given code to supply key for decryption of video.
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
NSString *scheme = loadingRequest.request.URL.scheme;
if ([scheme isEqualToString:#"ckey"]) {
NSString *request = loadingRequest.request.URL.host;
NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:request];
if (data) {
[loadingRequest.dataRequest respondWithData:data];
[loadingRequest finishLoading];
} else {
// Data loading fail
}
}
return NO; }
I am intercepting a request for a key and passing key stored in UserDefaults for decryption.
This AES encrypted HLS video with custom key plays well when my device's wifi or data connection is off.
If I start playing this video when my device's wifi or data connection is enabled or if
I enable my device's wifi or data connection while playing video; video stops playing immediately without any error and never plays again.
I have checked accessLog and errorLog of playerItem but haven't found anything helpful.
To provide a custom URL key after downloading of HLS content I am updating a content of .m3u8 file by replacing
URI="..."
string with
URI="ckey://..."
Is this a correct way to provide key for AES encrypted video?
and what could be the reason of this behaviour and how to solve this issue?
Thanks in advance.
Finally I managed to solve this issue. Rough package structure of downloaded HLS video is like given below:
HLS.movpkg
|_ 0-12345
|_ 123.m3u8
|_ StreamInfoBoot.xml
|_ StreamInfoRoot.xml
|_ <>.frag
|_ boot.xml
boot.xml contains network URL for HLS (which is https: based)
StreamBootInfo.xml contains mapping between HLS URL (which is https: based) and .frag file downloaded locally.
In offline mode HLS video was playing perfectly. But when network connection was enabled it was referring to https: URL instead of local .frag files.
I replaced https: scheme in these files with custom scheme (fakehttps:) to restrict AVPlayer going online for resources.
This thing solved my issue but I don't know the exact reason behind it and how HLS is played by AVPlayer.
I referred this and got some idea so tried something .
I am updating this answer further to explain how to play encrypted video in offline mode.
Get the key required for video decryption.
Save that key some where.
You can save that key as NSData or Data object in UserDefault I am using video file name as key to save key data in UserDefaults.
Use FileManager API to iterate over all the files inside .movpkg.
Get the content of each .m3u8 file and replace URI="some key url" with URI="ckey://keyusedToSaveKeyDataInUserDefaults"
You can refer code given below for this process.
if let url = asset.asset?.url, let data = data {
let keyFileName = "\(asset.contentCode!).key"
UserDefaults.standard.set(data, forKey: keyFileName)
do {
// ***** Create key file *****
let keyFilePath = "ckey://\(keyFileName)"
let subDirectories = try fileManager.contentsOfDirectory(at: url,
includingPropertiesForKeys: nil, options: .skipsSubdirectoryDescendants)
for url in subDirectories {
var isDirectory: ObjCBool = false
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
if isDirectory.boolValue {
let path = url.path as NSString
let folderName = path.lastPathComponent
let playlistFilePath = path.appendingPathComponent("\(folderName).m3u8")
if fileManager.fileExists(atPath: playlistFilePath) {
var fileContent = try String.init(contentsOf: URL.init(fileURLWithPath: playlistFilePath))
let stringArray = self.matches(for: "URI=\"(.+?)\"", in: fileContent)
for pattern in stringArray {
fileContent = fileContent.replacingOccurrences(of: pattern, with: "URI=\"\(keyFilePath)\"")
}
try fileContent.write(toFile: playlistFilePath, atomically: true, encoding: .utf8)
}
let streamInfoXML = path.appendingPathComponent("StreamInfoBoot.xml")
if fileManager.fileExists(atPath: streamInfoXML) {
var fileContent = try String.init(contentsOf: URL.init(fileURLWithPath: streamInfoXML))
fileContent = fileContent.replacingOccurrences(of: "https:", with: "fakehttps:")
try fileContent.write(toFile: streamInfoXML, atomically: true, encoding: .utf8)
}
} else {
if url.lastPathComponent == "boot.xml" {
let bootXML = url.path
if fileManager.fileExists(atPath: bootXML) {
var fileContent = try String.init(contentsOf: URL.init(fileURLWithPath: bootXML))
fileContent = fileContent.replacingOccurrences(of: "https:", with: "fakehttps:")
try fileContent.write(toFile: bootXML, atomically: true, encoding: .utf8)
}
}
}
}
}
userInfo[Asset.Keys.state] = Asset.State.downloaded.rawValue
// Update download status to db
let user = RoboUser.sharedObject()
let sqlDBManager = RoboSQLiteDatabaseManager.init(databaseManagerForCourseCode: user?.lastSelectedCourse)
sqlDBManager?.updateContentDownloadStatus(downloaded, forContentCode: asset.contentCode!)
self.notifyServerAboutContentDownload(asset: asset)
NotificationCenter.default.post(name: AssetDownloadStateChangedNotification, object: nil, userInfo: userInfo)
} catch {
}
}
func matches(for regex: String, in text: String) -> [String] {
do {
let regex = try NSRegularExpression(pattern: regex)
let nsString = text as NSString
let results = regex.matches(in: text, range: NSRange(location: 0, length: nsString.length))
return results.map { nsString.substring(with: $0.range)}
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
This will update your download package structure for playing encrypted video in offline mode.
Now last thing to do is implement below given method of AVAssetResourceLoader class as follows
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
NSString *scheme = loadingRequest.request.URL.scheme;
if ([scheme isEqualToString:#"ckey"]) {
NSString *request = loadingRequest.request.URL.host;
NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:request];
if (data) {
loadingRequest.contentInformationRequest.contentType = AVStreamingKeyDeliveryPersistentContentKeyType;
loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
loadingRequest.contentInformationRequest.contentLength = data.length;
[loadingRequest.dataRequest respondWithData:data];
[loadingRequest finishLoading];
} else {
// Data loading fail
}
}
return YES;
}
This method will provide key to video while playing to decrypt it.
So the following code is being used to attach an image from local storage url of an image. I check in Terminal to see if the image is stored and it does store the image without any issues. So ruling out any issues with the url itself.
do {
let attachment = try UNNotificationAttachment(identifier: imageTag, url: url, options: nil)
content.attachments = [attachment]
} catch {
print("The attachment was not loaded.")
}
Other code that goes with the creation of UserNotification works fine as it triggers at the correct specified time.
The code always goes to the catch block. Can anybody please point me to the mistake if there is any in the implementation. Please help. Thank you.
Edit: with print(error.localizedDescription) error message is Invalid attachment file URL.
Edit2 : with print(error) error message is Error Domain=UNErrorDomain Code=100 "Invalid attachment file URL" UserInfo={NSLocalizedDescription=Invalid attachment file URL}
I have found the real issue behind it. In apple documentation it is written that the url should be a file url and because of which you might be facing issue.
To solve this I have added image to temporary directory and then added to UNNotificationAttachment .
Please find the code below. (For my use case I was getting an imageURL)
extension UNNotificationAttachment {
/// Save the image to disk
static func create(imageFileIdentifier: String, data: NSData, options: [NSObject : AnyObject]?) -> UNNotificationAttachment? {
let fileManager = FileManager.default
let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString
let tmpSubFolderURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true)
do {
try fileManager.createDirectory(at: tmpSubFolderURL!, withIntermediateDirectories: true, attributes: nil)
let fileURL = tmpSubFolderURL?.appendingPathComponent(imageFileIdentifier)
try data.write(to: fileURL!, options: [])
let imageAttachment = try UNNotificationAttachment.init(identifier: imageFileIdentifier, url: fileURL!, options: options)
return imageAttachment
} catch let error {
print("error \(error)")
}
return nil
}
}
data in the argument of this function is Data of image . Below is how did I call this method.
let imageData = NSData(contentsOf: url)
guard let attachment = UNNotificationAttachment.create(imageFileIdentifier: "img.jpeg", data: imageData!, options: nil) else { return }
bestAttemptContent?.attachments = [attachment]
I also found important and quite weird behaviour of initialization of UNNotificationAttachment object. It was happening to me that I was getting error:
"Invalid attachment file URL"
But it was not happening always. It was happening in case when I used for some notifications same image for attachment. When I made a standalone copy of image for each attachment, it never happened. Then I checked directory when images should be copied ( because I wanted to clean it up ), but I was surprised that there were no images.
It seems that UNNotificationAttachment initialization process is deleting files at given URLs. So when you try to reuse some images, they can be deleted ( probably asynchronously, because I was checking existence of that images and it always returned me true - that file exists ). But UNNotificationAttachment ended up with error you can see above. In my opinion only logic explanation of this error is that file at given URL is deleted during the process of UNNotificationAttachment initialization.
Apple actually makes a statement in their documentation (https://developer.apple.com/documentation/usernotifications/unnotificationattachment)
Apple Docs for UNNotificationAttachment:
...
The system validates attachments before displaying the associated notification.
...
Once validated, attached files are MOVED into the attachment data store so that they can be accessed by all of the appropriate
processes. Attachments located inside an app’s bundle are copied
instead of moved.
So, above answers with copying an attachment (image) first into a temporary location before adding as an attachment seem to be the expected solution.
In Swift 5. The below code works for me. Hope this helps somebody.
let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
let imageURL = URL(fileURLWithPath: paths.first!).appendingPathComponent("\(fileName).jpg")
let image = UIImage(contentsOfFile: imageURL.path)
let imageData = image?.pngData()
if let unwrappedImageData = imageData, let attachement = try? UNNotificationAttachment(data: unwrappedImageData, options: nil) {
content.attachments = [ attachement ]
}
When I use method .fileExists(atPath:)to judge whether the file is exist in file system, the method always return false to me. I checked the file system and the file do exist. Here is my code:
let filePath = url?.path
var isDir : ObjCBool = false
if(self.fileManager.fileExists(atPath: filePath!, isDirectory: &isDir)){
let result = NSData(contentsOfFile: filePath!)
}
or
let filePath = url?.path
if(self.fileManager.fileExists(atPath: filePath!)){
let result = NSData(contentsOfFile: filePath!)
}
the if clause will always be skipped.
I assume your url is an URL type. If so try this out:
let filePath = url?.path // always try to work with URL when accessing Files
if(FileManager.default.fileExists(atPath: filePath!)){ // just use String when you have to check for existence of your file
let result = NSData(contentsOf: url!) // use URL instead of String
}
Saying enough, you should change your implementation like this:
if(FileManager.default.fileExists(atPath: (url?.path)!)){ // just use String when you have to check for existence of your file
let result = NSData(contentsOf: url!) // use URL instead of String
}
EDIT: 1
There is even more better way, you can call it swift-way (:D). You don't have to explicitly check for file existence.
guard let result = NSData(contentsOf: fileURL) else {
// No data in your fileURL. So no data is received. Do your task if you got no data
// Keep in mind that you don't have access to your result here.
// You can return from here.
return
}
// You got your data successfully that was in your fileURL location. Do your task with your result.
// You can have access to your result variable here. You can do further with result constant.
print(result)
Update for Swift 3.0+ without the Objective-Cish NS prefix:
do {
let result = try Data(contentsOf: fileURL)
print(result)
} catch {
print(error)
}
in swift 3
just in case anyone gets confused like i did, here's the full snippets:
let str = "file:///Users/martian2049/Library/Developer/CoreSimulator/Devices/67D744AA-6EEC-4AFD-A840-366F4D78A18C/data/Containers/Data/Application/DD96F423-AF9F-4F4D-B370-94ADE7D6D0A5/Documents/72b8b0fb-7f71-7f31-ac9b-f9cc95dfe90d.mp3"
let url = URL(string: str)
print(url!.path,"\n")
if FileManager.default.fileExists(atPath: url!.path) {
print("FILE Yes AVAILABLE")
} else {
print("FILE NOT AVAILABLE")
}
this prints
/Users/martian2049/Library/Developer/CoreSimulator/Devices/67D744AA-6EEC-4AFD-A840-366F4D78A18C/data/Containers/Data/Application/DD96F423-AF9F-4F4D-B370-94ADE7D6D0A5/Documents/72b8b0fb-7f71-7f31-ac9b-f9cc95dfe90d.mp3
FILE Yes AVAILABLE
notice how the 'file://' got chopped off?
I want to share my experience, in case anyone else gets baffled by this.
Tested on iOS 10-11, Xcode 9.2 and Swift 3.2.
Short answer: if you save a file path to disk, you may solve by not including the Documents directory in it.
Instead, every time you need to retrieve the file with the saved path, get the Documents directory and append the path.
For an iOS app, I was saving an image to .../Documents/Pictures through the relative URL, let's say url.
As the image was saved, a path, let's say url.path, was saved too in a Core Data entity.
When I later tried retrieving the image through FileManager.default.fileExists(atPath: url.path), it always returned false.
I was testing the app on my iPhone. It turned out that, for some reason, every time I ran the app from Xcode, the app identifier folder changed!!
So:
App opened from Xcode -> Image saved -> app closed -> app opened from physical device ->
fileExists -> TRUE
App opened from Xcode -> Image saved -> app closed -> app opened from Xcode -> fileExists -> FALSE
You can check if this is your case by getting and printing the Document folder path (or URL, it doesn't matter) and comparing it with the saved path (or URL). If you get something like this:
/var/mobile/Containers/Data/Application/5D4632AE-C432-4D37-A3F7-ECD05716AD8A/Documents..
/var/mobile/Containers/Data/Application/D09904C3-D80D-48EB-ACFB-1E42D878AFA4/Documents..
you found the issue.
Just use path instead of absoluteString to remove file://
FileManager.default.fileExists(atPath: URL.init(string: "your_url")!.path)
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true);
var path = paths[0] as String;
path = path + "/YourFilePath"
if((NSFileManager.defaultManager().fileExistsAtPath(path))) {
let result = NSData(contentsOfFile: filePath!)}
Try the above code and check again
I had the same problem this worked for me
filePath.replacingOccurrences(of: "file://", with: "")
First, what does your file path looks like? If the path begins with a ~,then it must be expanded with expandingTildeInPath;
Check if the path is inaccessible to your app. iOS App can only visits its sandbox directories.
I have an app that downloads and displays a lot of images from parse. An image is added to the database almost every minute. Since PFFile is automatically cached with no expiration date, even though I only need to display recent images, the older images still stay in cache thus occupying a lot of storage space. Because of this now the app takes about 5GB of storage on my iPhone. I have been doing a lot of research on this issue and found out that Parse does not have a public api for cleaning up PFFile Cache and also it doesn't allow setting expiration date on the cached files. Is there a workaround on this where I could manually delete older cache data?
Thank you in advance.
Here is a method you can use to clean up the PFFile Cache. If you call it when your app starts, before initializing Parse, I think it should be safe. You can check the file creation date in the for loop if you don't want to remove everything.
+ (void)cleanUpPFFileCacheDirectory
{
NSError *error = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *cacheDirectoryURL = [[fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject];
NSURL *PFFileCacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:#"Parse/PFFileCache" isDirectory:YES];
NSArray *PFFileCacheDirectory = [fileManager contentsOfDirectoryAtURL:PFFileCacheDirectoryURL includingPropertiesForKeys:nil options:0 error:&error];
if (!PFFileCacheDirectory || error) {
if (error && error.code != NSFileReadNoSuchFileError) {
NSLog(#"Error : Retrieving content of directory at URL %# failed with error : %#", PFFileCacheDirectoryURL, error);
}
return;
}
for (NSURL *fileURL in PFFileCacheDirectory) {
BOOL success = [fileManager removeItemAtURL:fileURL error:&error];
if (!success || error) {
NSLog(#"Error : Removing item at URL %# failed with error : %#", fileURL, error);
error = nil;
}
}
}
TimWhiting's answer translated to Swift 2.1:
Note: I have to say thought that is better to use file urls and use your own cache system as Matt S says, I'm using this just for testing purposes. I wish also that Parse could provide us with the correct path instead of having to hardcode it, that's why I think is better to use URLs.
func cleanUpParseDirectory(){
let parseFilesCache = "Parse/PFFileCache"
let fileManager = NSFileManager.defaultManager()
let cacheDirectoryURL = fileManager.URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask)
let PFFileCacheDirectoryURL = cacheDirectoryURL[0].URLByAppendingPathComponent(parseFilesCache, isDirectory: true)
do {
let PFFileCacheDirectory = try fileManager.contentsOfDirectoryAtURL(PFFileCacheDirectoryURL, includingPropertiesForKeys: nil, options: [])
print("number of cached files: \(PFFileCacheDirectory.count)")
for fileURL in PFFileCacheDirectory {
try fileManager.removeItemAtURL(fileURL)
}
print("Success removing items")
} catch let error {
print("Error: \(error)")
}
}
Per this answer from Hector: https://www.parse.com/questions/pffile-cache-size you can manually clear your applications ~/Library/Caches folder if you are insistent upon it. However, I'm fairly certain this will also impact things like NSURL/AFNetworking caches, amongst others.
My suggestion? Don't use PFFile to download the file. PFFile gives you back the remote URL to where it's hosted on Parse's site, so you can pass that to something like AFNetworking or NSURLSession to actually download the image for you, and then you can then assign cache lifetimes (or manage it yourself) since those systems actually support that, unlike PFFile.
deadbeef's answer translated to Swift for anyone that needs it.
func cleanUpParseDirectory(){
var error: NSError?
var fileManager: NSFileManager = NSFileManager.defaultManager()
var cacheDirectoryURL: [NSURL] = fileManager.URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask) as! [NSURL]
var PFFileCacheDirectoryURL: NSURL = cacheDirectoryURL[0].URLByAppendingPathComponent("Parse/PFFileCache", isDirectory: true)
var PFFileCacheDirectory: [AnyObject]? = fileManager.contentsOfDirectoryAtURL(PFFileCacheDirectoryURL, includingPropertiesForKeys: nil, options: NSDirectoryEnumerationOptions.allZeros, error: &error)// [fileManager contentsOfDirectoryAtURL:PFFileCacheDirectoryURL includingPropertiesForKeys:nil options:0 error:&error];
if (PFFileCacheDirectory == nil || error != nil) {
if ((error != NSFileReadNoSuchFileError && error!.code != NSFileReadNoSuchFileError)) {
println("error finding path")
} else {
println("no error finding path")
}
return
}
println("number of cached files: \(PFFileCacheDirectory!.count)")
for fileURL in PFFileCacheDirectory! {
var success: Bool = fileManager.removeItemAtURL(fileURL as! NSURL, error: &error)
if ((!success != false || error != nil) ) {
println("error removing item")
error = nil
} else {
println("success removing item")
}
}
}
For previous iOS 8 betas, load a local web app (in Bundle) and it works fine for both UIWebView and WKWebView, and I even ported a web game using the new WKWebView API.
var url = NSURL(fileURLWithPath:NSBundle.mainBundle().pathForResource("car", ofType:"html"))
webView = WKWebView(frame:view.frame)
webView!.loadRequest(NSURLRequest(URL:url))
view.addSubview(webView)
But in beta 4, I just got a blank white screen (UIWebView still work), looks like nothing is loaded or executed. I saw an error in the log:
Could not create a sandbox extension for /
Any help to guide me to the right direction? Thanks!
They finally solved the bug! Now we can use -[WKWebView loadFileURL:allowingReadAccessToURL:].
Apparently the fix was worth some seconds in WWDC 2015 video 504 Introducing Safari View Controller
For iOS8 ~ iOS10 (Swift 3)
As Dan Fabulish's answer states this is a bug of WKWebView which apparently is not being solved any time soon and as he said there is a work-around :)
I am answering just because I wanted to show the work-around here. IMO code shown in https://github.com/shazron/WKWebViewFIleUrlTest is full of unrelated details most people are probably not interested in.
The work-around is 20 lines of code, error handling and comments included, no need of a server :)
func fileURLForBuggyWKWebView8(fileURL: URL) throws -> URL {
// Some safety checks
if !fileURL.isFileURL {
throw NSError(
domain: "BuggyWKWebViewDomain",
code: 1001,
userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("URL must be a file URL.", comment:"")])
}
try! fileURL.checkResourceIsReachable()
// Create "/temp/www" directory
let fm = FileManager.default
let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("www")
try! fm.createDirectory(at: tmpDirURL, withIntermediateDirectories: true, attributes: nil)
// Now copy given file to the temp directory
let dstURL = tmpDirURL.appendingPathComponent(fileURL.lastPathComponent)
let _ = try? fm.removeItem(at: dstURL)
try! fm.copyItem(at: fileURL, to: dstURL)
// Files in "/temp/www" load flawlesly :)
return dstURL
}
And can be used as:
override func viewDidLoad() {
super.viewDidLoad()
var fileURL = URL(fileURLWithPath: Bundle.main.path(forResource:"file", ofType: "pdf")!)
if #available(iOS 9.0, *) {
// iOS9 and above. One year later things are OK.
webView.loadFileURL(fileURL, allowingReadAccessTo: fileURL)
} else {
// iOS8. Things can (sometimes) be workaround-ed
// Brave people can do just this
// fileURL = try! pathForBuggyWKWebView8(fileURL: fileURL)
// webView.load(URLRequest(url: fileURL))
do {
fileURL = try fileURLForBuggyWKWebView8(fileURL: fileURL)
webView.load(URLRequest(url: fileURL))
} catch let error as NSError {
print("Error: " + error.debugDescription)
}
}
}
WKWebView can't load content from file: URLs via its loadRequest: method. http://www.openradar.me/18039024
You can load content via loadHTMLString:, but if your baseURL is a file: URL, then it still won't work.
iOS 9 has a new API that will do what you want, [WKWebView loadFileURL:allowingReadAccessToURL:].
There is a workaround for iOS 8, demonstrated by shazron in Objective-C here https://github.com/shazron/WKWebViewFIleUrlTest to copy files into /tmp/www and load them from there.
If you're working in Swift, you could try nachos4d's sample instead. (It's also much shorter than shazron's sample, so if you're having trouble with shazron's code, give that a try instead.)
An example of how to use [WKWebView loadFileURL:allowingReadAccessToURL:] on iOS 9.
When you are moving the web folder to a project, select "Create folder references"
Then use code that is something like this(Swift 2):
if let filePath = NSBundle.mainBundle().resourcePath?.stringByAppendingString("/WebApp/index.html"){
let url = NSURL(fileURLWithPath: filePath)
if let webAppPath = NSBundle.mainBundle().resourcePath?.stringByAppendingString("/WebApp") {
let webAppUrl = NSURL(fileURLWithPath: webAppPath, isDirectory: true)
webView.loadFileURL(url, allowingReadAccessToURL: webAppUrl)
}
}
In the html file use filepaths like this
<link href="bootstrap/css/bootstrap.min.css" rel="stylesheet">
not like this
<link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet">
An example of directory that is moved to a xcode project.
Temporary workaround: I'm using GCDWebServer, as suggested by GuidoMB.
I first find the path of my bundled "www/" folder (which contains an "index.html"):
NSString *docRoot = [[NSBundle mainBundle] pathForResource:#"index" ofType:#"html" inDirectory:#"www"].stringByDeletingLastPathComponent;
... then start it up like so:
_webServer = [[GCDWebServer alloc] init];
[_webServer addGETHandlerForBasePath:#"/" directoryPath:docRoot indexFilename:#"index.html" cacheAge:3600 allowRangeRequests:YES];
[_webServer startWithPort:port bonjourName:nil];
To stop it:
[_webServer stop];
_webServer = nil;
Performance appears fine, even on an iPad 2.
I did notice a crash after the app goes into the background, so I stop it on applicationDidEnterBackground: and applicationWillTerminate:; I start/restart it on application:didFinishLaunching... and applicationWillEnterForeground:.
[configuration.preferences setValue:#"TRUE" forKey:#"allowFileAccessFromFileURLs"];
This solved the problem for me
iOS 8.0+ dev.apple.com
also this seems to worked just fine too...
NSString* FILE_PATH = [[[NSBundle mainBundle] resourcePath]
stringByAppendingPathComponent:#"htmlapp/FILE"];
[self.webView
loadFileURL: [NSURL fileURLWithPath:FILE_PATH]
allowingReadAccessToURL: [NSURL fileURLWithPath:FILE_PATH]
];
Besides solutions mentioned by Dan Fabulich, XWebView is another workaround. [WKWebView loadFileURL:allowingReadAccessToURL:] is implemented through extension.
I cannot comment yet, so I am posting this as a separate answer.
This is an objective-c version of nacho4d's solution. The best workaround I've seen so far.
- (NSString *)pathForWKWebViewSandboxBugWithOriginalPath:(NSString *)filePath
{
NSFileManager *manager = [NSFileManager defaultManager];
NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:#"www"];
NSError *error = nil;
if (![manager createDirectoryAtPath:tempPath withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLog(#"Could not create www directory. Error: %#", error);
return nil;
}
NSString *destPath = [tempPath stringByAppendingPathComponent:filePath.lastPathComponent];
if (![manager fileExistsAtPath:destPath]) {
if (![manager copyItemAtPath:filePath toPath:destPath error:&error]) {
NSLog(#"Couldn't copy file to /tmp/www. Error: %#", error);
return nil;
}
}
return destPath;
}
In the case that you are trying to display a local image in the middle of a larger HTML string like: <img src="file://...">, it still does not appear on device so I loaded the image file into NSData and was able to display it by replacing the src string with the data itself. Sample code to help build the HTML string to load into WKWebView, where result is what will replace what's inside the quotes of src="":
Swift:
let pathURL = NSURL.fileURLWithPath(attachmentFilePath)
guard let path = pathURL.path else {
return // throw error
}
guard let data = NSFileManager.defaultManager().contentsAtPath(path) else {
return // throw error
}
let image = UIImage.init(data: data)
let base64String = data.base64EncodedStringWithOptions(.Encoding64CharacterLineLength)
result += "data:image/" + attachmentType + "base64," + base64String
var widthHeightString = "\""
if let image = image {
widthHeightString += " width=\"\(image.size.width)\" height=\"\(image.size.height)\""
}
result += widthHeightString
Objective-C:
NSURL *pathURL = [NSURL fileURLWithPath:attachmentFilePath];
NSString *path = [pathURL path];
NSData *data = [[NSFileManager defaultManager] contentsAtPath:path];
UIImage *image = [UIImage imageWithData:data];
NSString *base64String = [data base64EncodedStringWithOptions:0];
[result appendString:#"data:image/"];
[result appendString:attachmentType]; // jpg, gif etc.
[result appendString:#";base64,"];
[result appendString:base64String];
NSString *widthHeightString = #"\"";
if (image) {
widthHeightString = [NSString stringWithFormat:#"\" width=\"%f\" height=\"%f\"", image.size.width, image.size.height];
}
[result appendString:widthHeightString];
I'm using the below. Has some extra stuff I'm working on but you can see where I've commented out the loadRequest and am substituting loadHTMLString call. Hope this helps until they fix the bug.
import UIKit
import WebKit
class ViewController: UIViewController, WKScriptMessageHandler {
var theWebView: WKWebView?
override func viewDidLoad() {
super.viewDidLoad()
var path = NSBundle.mainBundle().pathForResource("index", ofType: "html", inDirectory:"www" )
var url = NSURL(fileURLWithPath:path)
var request = NSURLRequest(URL:url)
var theConfiguration = WKWebViewConfiguration()
theConfiguration.userContentController.addScriptMessageHandler(self, name: "interOp")
theWebView = WKWebView(frame:self.view.frame, configuration: theConfiguration)
let text2 = String.stringWithContentsOfFile(path, encoding: NSUTF8StringEncoding, error: nil)
theWebView!.loadHTMLString(text2, baseURL: nil)
//theWebView!.loadRequest(request)
self.view.addSubview(theWebView)
}
func appWillEnterForeground() {
}
func appDidEnterBackground() {
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func userContentController(userContentController: WKUserContentController!, didReceiveScriptMessage message: WKScriptMessage!){
println("got message: \(message.body)")
}
}
For who must workaround this issue under iOS8:
If your page is not complicated, you might choose to make the page as a Single Page Application.
In other words, to embed all the resources into the html file.
To do:
1. copy your js/css file's content into / tags in the html file respectively;
2. convert your image files into svg to replace the accordingly.
3. load the page as before, using [webView loadHTMLString: baseURL:], for example
It was a bit different to styling a svg image, but it should not block you so much.
It seemed that the page render performance decreased a bit, but it was worthy to have such a simple workaround worked under iOS8/9/10.
In the same line of GCDWebServer, I am using SImpleHttpServer (http://www.andyjamesdavies.com/blog/javascript/simple-http-server-on-mac-os-x-in-seconds) and then loadRequest with the localhost url. With this approach you do not have to add any library, but the website files won't be in the bundle so It will not be deliverable. Because of that, this would be more appropriate for Debug cases.
I’ve managed to use PHP’s web server on OS X. Copying to the temporary/www directory did not work for me. The Python SimpleHTTPServer complained about wanting to read MIME types, probably a sandboxing issue.
Here’s a server using php -S:
let portNumber = 8080
let task = NSTask()
task.launchPath = "/usr/bin/php"
task.arguments = ["-S", "localhost:\(portNumber)", "-t", directoryURL.path!]
// Hide the output from the PHP server
task.standardOutput = NSPipe()
task.standardError = NSPipe()
task.launch()
#nacho4d solution is good. I want to change it a little but I don't know how to change it in your post. So I put it here I hope you don't mind. thanks.
In case you have a www folder there are many other files such as png, css, js etc. Then you have to copy all files to tmp/www folder.
for example, you have a www folder like this:
then in Swift 2.0:
override func viewDidLoad() {
super.viewDidLoad()
let path = NSBundle.mainBundle().resourcePath! + "/www";
var fileURL = NSURL(fileURLWithPath: path)
if #available(iOS 9.0, *) {
let path = NSBundle.mainBundle().pathForResource("index", ofType: "html", inDirectory: "www")
let url = NSURL(fileURLWithPath: path!)
self.webView!.loadRequest(NSURLRequest(URL: url))
} else {
do {
fileURL = try fileURLForBuggyWKWebView8(fileURL)
let url = NSURL(fileURLWithPath: fileURL.path! + "/index.html")
self.webView!.loadRequest( NSURLRequest(URL: url))
} catch let error as NSError {
print("Error: \(error.debugDescription)")
}
}
}
the function fileURLForBuggyWKWebView8 is copied from #nacho4d:
func fileURLForBuggyWKWebView8(fileURL: NSURL) throws -> NSURL {
// Some safety checks
var error:NSError? = nil;
if (!fileURL.fileURL || !fileURL.checkResourceIsReachableAndReturnError(&error)) {
throw error ?? NSError(
domain: "BuggyWKWebViewDomain",
code: 1001,
userInfo: [NSLocalizedDescriptionKey: NSLocalizedString("URL must be a file URL.", comment:"")])
}
// Create "/temp/www" directory
let fm = NSFileManager.defaultManager()
let tmpDirURL = NSURL.fileURLWithPath(NSTemporaryDirectory())
try! fm.createDirectoryAtURL(tmpDirURL, withIntermediateDirectories: true, attributes: nil)
// Now copy given file to the temp directory
let dstURL = tmpDirURL.URLByAppendingPathComponent(fileURL.lastPathComponent!)
let _ = try? fm.removeItemAtURL(dstURL)
try! fm.copyItemAtURL(fileURL, toURL: dstURL)
// Files in "/temp/www" load flawlesly :)
return dstURL
}
Try using
[webView loadHTMLString:htmlFileContent baseURL:baseURL];
Seems it's still working. Yet.