Swift - save video from url - swift

I would like to download a movie from a URL and save to iPhone.
I use this function
func downloadVideo(videoUrl: URL, name: String) {
let sampleURL = videoUrl.absoluteString
DispatchQueue.global(qos: .background).async {
if let url = URL(string: sampleURL), let urlData = NSData(contentsOf: videoUrl) {
let galleryPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0];
let filePath="\(galleryPath)/" + name + ".MOV"
DispatchQueue.main.async {
urlData.write(toFile: filePath, atomically: true)
}
}
}
}
But I got this error message: "Can't end BackgroundTask: no background task exists with identifier 16 (0x10), or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug."
I added a symbolic breakpoint for UIApplicationEndBackgroundTaskError, but I don't understand what is wrong. What should I do to solve this problem?
Screenshot from breakpoint:

But I got this error message: "Can't end BackgroundTask: no background task exists with identifier 16 (0x10), or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug."
This is a new warning only in the Simulator in Xcode 11. It’s unimportant. Ignore it. It happens all the time, nothing to do with your code.

Related

How do you fix the "found nil while unwrapping optional value" error when trying to play sound?

I'm making a function to play sound
func playSound(soundName: String) {
let url = Bundle.main.url(forResource: soundName, withExtension: "wav")
player = try! AVAudioPlayer(contentsOf: url!)
player.play()
}
Then call this function in an IBAction that contains all my buttons
#IBAction func buttonPiano(_ sender: UIButton) {
playSound(soundName: String(sender.currentTitle!))
sender.backgroundColor = UIColor.white
sender.alpha = 0.3
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300), execute: {
sender.backgroundColor = UIColor.systemBackground
sender.alpha = 1
})
}
Running the app I can do. But whenever you press a button, it crashes and gives me this error:
Fatal error: Unexpectedly found nil while unwrapping an Optional value: file /Users/administrator/Desktop/Xcode Projects/pianoButtons/pianoButtons/ViewController.swift, line 37
The optional value seems to be url! from my sound function.
I've tried all I could, but no luck. How do I avoid this error and play the sound without crashes?
Make sure that your soundName.wave file is shown inside the copy bundle resources. You can find that by clicking on your project > selecting your target > Build Phases > Copy Bundle Resources. If you do not see it there, click the plus button to add it.
var soundPlayer: AVAudioPlayer?
func playSentSound() {
DispatchQueue.main.async{
let path = Bundle.main.path(forResource: "soundName.mp3", ofType: nil)!
let url = URL(fileURLWithPath: path)
do {
self.soundPlayer = try AVAudioPlayer(contentsOf: url)
print("Playing")
self.soundPlayer?.play()
} catch {
// couldn't load file :(
print("Cant Load File")
}
}
}
Your code is essentially calling this
try! AVAudioPlayer(contentsOf: Bundle.main.url(forResource: String(button.currentTitle!), withExtension: "wav")!)
Each ! is a potential crash. This is when they would occur
The button may not have a current title at the time the action is triggered.
The main bundle may not have a resource with that name/extension
The audio player may not be able to play the contents of that file
In your specific case it seems like it is failing at 2. The nicer way to handle this is like so
if let url = Bundle.main.url(forResource: soundName, withExtension: "wav") {
player = try! AVAudioPlayer(contentsOf: url)
} else {
print("No resouce named \(soundName).wav")
}
First of all your app will just not play a sound instead of crashing, second you will get a helpful log message which might show you why the resource isn't being found.
Ideally all of your ! should be replaced with similar constructs, to log errors or perform some fallback action instead of crashing.

Writing iOS application logs on a text file

I am using this answer to log messages in my app.
import Foundation
class Log: TextOutputStream {
func write(_ string: String) {
let fm = FileManager.default
let log = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("log.txt")
if let handle = try? FileHandle(forWritingTo: log) {
handle.seekToEndOfFile()
handle.write(string.data(using: .utf8)!)
handle.closeFile()
} else {
do {
try string.data(using: .utf8)?.write(to: log)
} catch {
print(error)
}
}
}
static var log: Log = Log()
private init() {}
}
Used as follows using the Singleton pattern,
print("\(#function) Test Log", to: &Log.log)
This would append the String to the log.txt file. I cannot see the file being created in the Files.app and it doesn't produce an error either. If I print the path of the file where it's being saved it shows,
file:///var/mobile/Containers/Data/Application/00EBA5E5-7132-495E-B90E-E6CF32BA3EA7/Documents/
Where should it be saved? Do I have to do any prior setup? I can't seem to make this work. Do I have to do do something before to create the folder? Nothing shows up in the Files.app.
EDIT: I am not using the Simulator, I need to use a real device.
Okay I got confused and I totally forgot this document is not supposed to show up in the Files.app. It's stored inside the app's container. If you want to share it from the documents sheet and send it to another device via AirDrop or whatever add this action to trigger when you tap a button intended to share the document.
let fm = FileManager.default
let fileUrl = fm.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("log.txt")
var filesToShare = [Any]()
filesToShare.append(fileUrl)
let activityViewController = UIActivityViewController(activityItems: filesToShare, applicationActivities: nil)
self.present(activityViewController, animated: true, completion: nil)

Any information on MCMErrorDomain error 44?

I am running into "MCMErrorDomain error 44" about 50% of the time on app.launch() when loading a container to my app. I have no idea what is the cause and I can't find any information about this error.
This is the code I am using to load container to the app.
extension AppDelegate {
func loadAppData(appDataPath: String) {
let loaderUrl = URL(fileURLWithPath: #file)
let bundleUrl = URL(fileURLWithPath: appDataPath, relativeTo: loaderUrl).appendingPathExtension("xcappdata")
let contentsURL = bundleUrl.appendingPathComponent("AppData")
let fileManager = FileManager.default
let enumerator = fileManager.enumerator(at: contentsURL,
includingPropertiesForKeys: [.isDirectoryKey],
options: [],
//swiftlint:disable:next force_unwrapping
errorHandler: nil)!
//swiftlint:disable:next force_unwrapping
let destinationRoot = fileManager.urls(for: .libraryDirectory, in: .userDomainMask).last!.deletingLastPathComponent()
let test = fileManager.enumerator(at: destinationRoot,
includingPropertiesForKeys: [.isDirectoryKey],
options: [],
//swiftlint:disable:next force_unwrapping
errorHandler: nil)!
while let lol = test.nextObject() as? URL {
do {
try fileManager.removeItem(at: lol)
} catch {
print("✌️ \(error)")
}
}
print("✌️ \(destinationRoot)")
let sourceRoot = contentsURL.standardizedFileURL.path
while let sourceUrl = enumerator.nextObject() as? URL {
guard let resourceValues = try? sourceUrl.resourceValues(forKeys: [.isDirectoryKey]),
let isDirectory = resourceValues.isDirectory,
!isDirectory else {
continue
}
let path = sourceUrl.standardizedFileURL.path.replacingOccurrences(of: sourceRoot, with: "")
let destinationURL = destinationRoot.appendingPathComponent(path)
do {
try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(),
withIntermediateDirectories: true,
attributes: nil)
try fileManager.copyItem(at: sourceUrl,
to: destinationURL)
} catch {
print("✌️ \(error)")
do {
_ = try fileManager.replaceItemAt(destinationURL, withItemAt: sourceUrl)
} catch {
print("✌️ \(error)")
}
}
}
print("done")
}
}
Yes, MCMErrorDomain is frustrating, as it not documented by Apple.
When I encounter it, the full description reads:
The test runner encountered an error (Failed to install or launch the
test runner. (Underlying error: The operation couldn’t be completed.
(MCMErrorDomain error 44.)))
The workaround I found is to delete the app from simulator and re-run the test.
So far, here is what I am learning about this particular error:
it looks as it is coming from MobileContainerManager.framework
often caused by interrupting a running test on simulator.
once it happens the simulator often in permanently "damaged" state until app is deleted.
only happens for Application tests, that require host app, never for Logic tests.
happens only at the beginning of a test suite
often caused by Xcode Bot trying to use the same simulator that Xcode is already using.
Framework Location:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/MobileContainerManager.framework
shows entries for MCMErrorDomain in MobileContainerManager binary and in containermanagerd

Swift - Check if the file download from a URL is completed

I am downloading mp4 clips from a M3U8 manifest which can have around 700 clips. Everything works fine to download them but what would be the best to check individual downloads are finished? When all the clips are downloaded, I merge them into one but I need to know when all my clips have been downloaded first.
This is the code snippet I use to download the video clip.
func download(video: String){
DispatchQueue.global(qos: .background).async {
if let url = URL(string: "http://SERVER/storage/sessions/SESSIONID/mp4_segments/\(video)"),
let urlData = NSData(contentsOf: url) {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0];
let fileName = video
let filePath = "\(documentsPath)/SegmentVideos/\(fileName)"
urlData.write(toFile: filePath, atomically: true)
}
}
}
This is the code snippet that reads the M3U8 file and splits it so I can grab the video clip's name.
func checkM3U8forClips(){
guard let url = url else {return}
do {
let contents = try String(contentsOf: url)
let splitContent = contents.components(separatedBy: "\n")
for split in splitContent {
if split.hasSuffix("mp4") {
download(video: split)
}
}
} catch {
print("error with mp4 segments: \(error.localizedDescription)")
}
}
One reason you are in a quandary is that this code is wrong:
if let url = URL(string: "http://SERVER/storage/sessions/SESSIONID/mp4_segments/\(video)"),
let urlData = NSData(contentsOf: url) {
You must never use NSData(contentsOf:) to do networking. If you want to network, then network: use URLSession and a proper data task or download task. Now you get the callbacks you need; if you do it in the full form you get a full set of delegate callbacks that tell you exactly when a download has succeeded and completed (or failed).
As for your overall question, i.e. how can I know when multiple asynchronous operations have all finished, that is what things like DispatchGroup, or operation dependencies, or the new Combine framework are for.

Swift: NSData(contentsOfURL) crashing on XCode 6.1

Before upgrading to XCode6.1 I was using the method NSData.dataWithContents() and it was working perfectly all the images were downloading. Today I have updated to XCode 6.1 and it forces me to use the function like this:
NSData(contentsOfURL: NSURL(string: completeUrl)!)!
and when I run the application it crashes on it with message:
fatal error: unexpectedly found nil while unwrapping an Optional value
I have tried many things but nothing works. I am looking for any simple alternative for this to download images from a given URL.
Since the initalization of NSURL may fail due to several reasons you should better use it this way:
if let url = NSURL(string: completeUrl) {
if let data = NSData(contentsOfURL: url) { // may return nil, too
// do something with data
}
}
More better way to download files is:
let request:NSURLRequest = NSURLRequest(URL: NSURL(string: completeUrl)!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: { (response:NSURLResponse!, imageData:NSData!, error:NSError!) -> Void in
var filePath:String = pathString + "/" + fileName
imageData.writeToFile(filePath, atomically: true)
})
It is working very nicely and also it gives you more control on the request.