I’m creating a macOS app which ships with some .zip files within its Bundle directory.
Users should be able to save these files from my app to a custom directory.
I found NSSavePanel and thought it is the right approach — that’s what I have so far:
#IBAction func buttonSaveFiles(_ sender: Any) {
let savePanel = NSSavePanel()
let bundleFile = Bundle.main.resourcePath!.appending("/MyCustom.zip")
let targetPath = NSHomeDirectory()
savePanel.directoryURL = URL(fileURLWithPath: targetPath.appending("/Desktop"))
// Is appeding 'Desktop' a good solution in terms of localisation?
savePanel.message = "My custom message."
savePanel.nameFieldStringValue = "MyFile"
savePanel.showsHiddenFiles = false
savePanel.showsTagField = false
savePanel.canCreateDirectories = true
savePanel.allowsOtherFileTypes = false
savePanel.isExtensionHidden = true
savePanel.beginSheetModal(for: self.view.window!, completionHandler: {_ in })
}
I couldn’t find out how to 'hand over' the bundleFile to the savePanel.
So my main question is: How can I save/copy a file from the app bundle to a custom directory?
Additional questions depending NSSavePanel: 1) It seems that it’s not localized by default (my Xcode scheme is set to German, but the panel appears in English), do I have to customize that by myself? 2) Is there a way to present the panel expanded by default?
You should use Bundle.main.url to get your existing file URL, then get the destination URL with the panel, then copy the file. The panel doesn't do anything to files, it just gets their URL.
Example:
// the panel is automatically displayed in the user's language if your project is localized
let savePanel = NSSavePanel()
let bundleFile = Bundle.main.url(forResource: "MyCustom", withExtension: "zip")!
// this is a preferred method to get the desktop URL
savePanel.directoryURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
savePanel.message = "My custom message."
savePanel.nameFieldStringValue = "MyFile"
savePanel.showsHiddenFiles = false
savePanel.showsTagField = false
savePanel.canCreateDirectories = true
savePanel.allowsOtherFileTypes = false
savePanel.isExtensionHidden = true
if let url = savePanel.url, savePanel.runModal() == NSFileHandlingPanelOKButton {
print("Now copying", bundleFile.path, "to", url.path)
// Do the actual copy:
do {
try FileManager().copyItem(at: bundleFile, to: url)
} catch {
print(error.localizedDescription)
}
} else {
print("canceled")
}
Also, note that the panel being expanded or not is a user selection, you can't force it from your app.
Related
Whenever I try to use .addAttachmentURL, it does not attach anything. The ViewController is presented with nothing within the body of the text. The URL is a path to the pdf data (I don't know if that makes a difference) in my file defaults. Is there any way I can send a PDF through text like this? I have not found anything by looking through documentation or StackOverflow. Also, I haven't implemented it yet, but I was wondering if there was a way to also attach PNGs to this message I am sending along with the PDF.
func getFileManager() -> NSString {
let filePath = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString)
return filePath
}
func displayMessageInterface() {
let composeVC = MFMessageComposeViewController()
composeVC.messageComposeDelegate = self
// Configure the fields of the interface.
composeVC.recipients = ["000000000"]
var url = URL(string: self.getFileManager() as String)!
url.appendPathComponent("my_report.pdf")
composeVC.addAttachmentURL(url, withAlternateFilename:
"this file")
// Present the view controller modally.
if MFMessageComposeViewController.canSendText() {
self.present(composeVC, animated: true, completion: nil)
} else {
print("Can't send messages.")
}
}
You are using the wrong URL initializer. URL(string:) initializer expects a scheme, in this case file://. You need to use URL(fileURLWithPath:) initializer or simply get the document directory URL using FileManager urls method:
extension URL {
static let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
}
let url = URL.documentDirectory.appendingPathComponent("my_report.pdf")
I am not sure what you mean when you say "The URL is a path to the pdf data in my file defaults". If you have included your file in your project Bundle you need to use its url(forResource:) method.
let url = Bundle.main.url(forResource: "my_report", withExtension: "pdf")!
I built an iOS app in Swift, and I'm adding some functionality for macOS Catalyst.
In my app I create a .txt file upon clicking a button. I want to present a UIDocumentPickerViewController that allows the user to specify the save directory, and file name. So far I am only able to display the UIDocumentPickerViewController without the option to name the file or save. Is UIDocumentPickerViewController the right view controller to accomplish this? If so, how do I specify the save directory and file name?
Here is the code I'm using to present the UIDocumentPickerViewController
#if targetEnvironment(macCatalyst)
let str = "Hello boxcutter"
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as NSURL
let fileURL = documentsURL.appendingPathComponent("boxCutter.txt")
try! str.write(to: fileURL!, atomically: true, encoding: String.Encoding.utf8)
let types: [String] = [kUTTypeFolder as String]
let documentPicker = UIDocumentPickerViewController(documentTypes: types, in: .open)
documentPicker.delegate = self
documentPicker.allowsMultipleSelection = false
documentPicker.modalPresentationStyle = .formSheet
self.present(documentPicker, animated: true, completion: nil)
#endif
You can use NSSavePanel to ask the user where to save the file. It's a macOS API that isn't directly accessible from Catalyst apps, but you can create a macOS plugin that has access to macOS API as explained here. Or use a library like Dynamic (Full disclosure: I'm the author) to achieve the same thing without a plugin:
let nsWindow = Dynamic.NSApplication.sharedApplication.delegate.hostWindowForUIWindow(view.window)
let panel = Dynamic.NSSavePanel()
panel.nameFieldStringValue = "boxCutter.txt"
panel.beginSheetModalForWindow(nsWindow, completionHandler: { response in
if response == 1 /*OK*/ {
print("file URL: ", panel.URL.asURL)
}
} as ResponseBlock)
typealias ResponseBlock = #convention(block) (_ response: Int) -> Void
I want to save a MIDI-file to a certain folder. But unfortunately just get an "Untitled" txt file.
I found this code which I tried:
let savePanel = NSSavePanel()
let bundleFile = Bundle.main.url(forResource: "Melody", withExtension: "mid")!
// this is a preferred method to get the desktop URL
savePanel.directoryURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
savePanel.message = "My custom message."
savePanel.nameFieldStringValue = "MyFile"
savePanel.showsHiddenFiles = false
savePanel.showsTagField = false
savePanel.canCreateDirectories = true
savePanel.allowsOtherFileTypes = false
savePanel.isExtensionHidden = false
if let url = savePanel.url, savePanel.runModal() == NSApplication.ModalResponse.OK {
print("Now copying", bundleFile.path, "to", url.path)
// Do the actual copy:
do {
try FileManager().copyItem(at: bundleFile, to: url)
} catch {
print(error.localizedDescription)
} else {
print("canceled")
}
What can I improve to copy the MIDI-File from the Application Bundle to the e.g. Desktop??
Thanks!
Looking over some old code I wrote to copy a file from the app bundle to a location on someone's Mac, I had to append the name of the file as an additional path component to the destination URL to get the file to copy properly. Using your code example, the code would look similar to the following:
let name = "Melody.mid"
// url is the URL the user chose from the Save panel.
destinationURL = url.appendingPathComponent(name)
// Use destinationURL instead of url as the to: argument in FileManager.copyItem.
I use the following code to retrieve icon of files or folders. Then show them in a menu. My problem is: some files, the icon is not displayed (for example .txt file). Icons of folders and some other files are still displayed. What is possible cause of this problem?
// menuItem.Title: display name for file/folder
// menuItem.Content: full path of file/url
let menuItem = NSMenuItem(title: item.Title, action: #selector(AppDelegate.openLocal(_:)), keyEquivalent: "")
let requiredAttributes = [URLResourceKey.effectiveIconKey]
if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: item.Content), includingPropertiesForKeys: requiredAttributes, options: [.skipsHiddenFiles, .skipsPackageDescendants, .skipsSubdirectoryDescendants], errorHandler: nil) {
while let url = enumerator.nextObject() as? URL {
do {
let properties = try (url as NSURL).resourceValues(forKeys: requiredAttributes)
let icon = properties[URLResourceKey.effectiveIconKey] as? NSImage ?? NSImage()
menuItem.image = icon
}
catch {
}
}
}
I am using the following code to get the icon for a file. This works pretty reliably:
static func getIconForUrl(_ path: String) -> NSImage?
{
return NSWorkspace.shared.icon(forFile: path)
}
I want to share a contact inside of my application but I only want to let the user do it via Message and Mail. Can I block out all other options on the alert sheet?
func shareContacts(contacts: [CNContact]) throws {
guard let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
return
}
var filename = NSUUID().uuidString
// Create a human friendly file name if sharing a single contact.
if let contact = contacts.first, contacts.count == 1 {
if let fullname = CNContactFormatter().string(from: contact) {
filename = fullname.components(separatedBy:" ").joined(separator: "")
}
}
let fileURL = directoryURL
.appendingPathComponent(filename)
.appendingPathExtension("vcf")
let data = try CNContactVCardSerialization.data(with: contacts)
try data.write(to:fileURL, options: [.atomicWrite])
let textToShare = "This is my clear captions text test"
let objectsToShare = [textToShare, fileURL] as [Any]
let activityViewController = UIActivityViewController(
activityItems: objectsToShare,
applicationActivities: nil
)
present(activityViewController, animated: true, completion: {})
}
It is not possible to simply exclude everything besides Mail and iMessage but you can do the following.
You can use a function to exclude options for the UIActivityViewController but there are only some apps you can disable. To disable more you would need a private API and you would violate the App Guidelines Apple has for all iOS Apps.
You are allowed to disable these types:
UIActivityTypePostToFacebook,
UIActivityTypePostToTwitter,
UIActivityTypePostToWeibo,
UIActivityTypeMessage,
UIActivityTypeMail,
UIActivityTypePrint,
UIActivityTypeCopyToPasteboard,
UIActivityTypeAssignToContact,
UIActivityTypeSaveToCameraRoll,
UIActivityTypeAddToReadingList,
UIActivityTypePostToFlickr,
UIActivityTypePostToVimeo,
UIActivityTypePostToTencentWeibo,
UIActivityTypeAirDrop
by using this code (Xcode suggests you the exact types):
activityController.excludedActivityTypes = [
UIActivityType.assignToContact,
// ... and all of the types you want to disable
// If you know the rawValue/BundleID of other types you can try to disable them too like this
UIActivityType(rawValue: "..."),
]
Apple Documentation about UIActivityViewController
Check out this question: How to exclude Notes and Reminders apps from the UIActivityViewController?