Limit what opens appear while sharing contact - iphone

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?

Related

Why does my app crashes when I tap "Save Image" option on UIActivityViewController?

So, in the 3D Touch peek controller, I have this computed var where I setup the UIPreviewAction:
override lazy var previewActionItems: [UIPreviewActionItem] = {
let save = UIPreviewAction.init(title: "Share Image", style: UIPreviewAction.Style.default, handler: { (action, controller) in
let renderer = UIGraphicsImageRenderer(size: self.view.bounds.size)
let imageAux = renderer.image { ctx in
self.view.drawHierarchy(in: self.view.bounds, afterScreenUpdates: true)
}
self.delegate!.shareImage(image: imageAux, url: self.data.url)
})
return [save]
}()
and in the delegate controller I have this method to present the UIActivityViewController:
func shareImage(image: UIImage, url: String) {
if let data = image.pngData() {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let filename = paths[0].appendingPathComponent(url)
try? data.write(to: filename)
}
let activityViewController = UIActivityViewController(activityItems: [image] , applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = self.view
self.present(activityViewController, animated: true, completion: nil)
}
everything works fine, i can share with any apps, and can store using the File Explore, but if I press "Save Image", the app crashes. Looks like I need gallery permission, but I don't know how to implement it in the UIPreviewAction to check if I have the permission or not, so that if I do it saves, and if I do not it asks for permission.
Add new records in your new InfoPlist.strings file.
<key>NSPhotoLibraryAddUsageDescription</key>
<string>$(PRODUCT_NAME)</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>$(PRODUCT_NAME)</string>
delete your app from simulator and run again.

Swift 4 vCard on touch add to contacts

I have scoured the internet looking to find a way to save a persons contact information. Currently on my mobile website I have a vCard that when clicked on a phone asks the user if they would like to add the contact to their contacts. In swift however loading that same link generates a 404 error page. So how would I go about doing this? I want when the user clicks on the icon a pop up displays asking the user if they want to save the contact to their phone. The data is being pulled in through a json api. I assume I need to take this data and format it in a specific way. Any suggestions or pointers in a direction to take this is much appreciated.
Thanks
Update: Here is my attempt at some code for this. When this prints to the console I get the vcard output, but an error is thrown on the JSONSerialization. Maybe someone can point me in the right direction.
#IBAction func contactTapped(_ sender: Any) {
let contact = createContact()
do {
try shareContacts(contacts: [contact])
} catch {
print("Error printing contact")
}
}
func shareContacts(contacts: [CNContact]) throws {
guard let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
return
}
var filename = NSUUID().uuidString
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)
.appendingPathComponent("vcf")
let data = try CNContactVCardSerialization.data(with: contacts)
print("filename: \(filename)")
print("contact: \(String(describing: String(data: data, encoding: String.Encoding.utf8)))")
try JSONSerialization.data(withJSONObject: JSONEncoder(), options: JSONSerialization.WritingOptions.prettyPrinted)
let activityViewController = UIActivityViewController(
activityItems: [fileURL],
applicationActivities: nil
)
present(activityViewController, animated: true, completion: {})
}
func createContact() -> CNContact {
// Creating a mutable object to add to the contact
let contact = CNMutableContact()
contact.imageData = NSData() as Data // The profile picture as a NSData object
contact.givenName = fullNameLbl.text!
//contact.familyName = "Appleseed"
let workEmail = CNLabeledValue(label:CNLabelWork, value: emailLbl.text! as NSString)
contact.emailAddresses = [workEmail]
contact.phoneNumbers = [CNLabeledValue(
label:CNLabelPhoneNumberiPhone,
value:CNPhoneNumber(stringValue: phoneLabel.text!))]
let store = CNContactStore()
let saveRequest = CNSaveRequest()
saveRequest.add(contact, toContainerWithIdentifier: nil)
try! store.execute(saveRequest)
return contact
}

Send mail with file attachment

I search solution to send a mail with attachment.
I have this code but the file is not attached...
if let url = URL(string: "mailto:\(email)?subject=report&body=see_attachment&attachment=/Users/myname/Desktop/report.txt") {
NSWorkspace.shared().open(url)
}
I have see it maybe work with MessageUI, but I can't import this framework I don't know why. I get this error message : No such module 'MessageUI'
I checked in General > Linked Frameworks and Libraries, but there are not MessageUI...
Anyone have a solution to add file in mail?
Thanks
It seems that attachment in mailto: URLs are not supported on macOS (not always at least...details seems sketchy dependent on where you look on the internet :))
What you can use instead I found out from this blog post, is an instance of NSSharingService documented here
Here is an example demonstrating how to use it.
And in your case you could do something like:
let email = "your email here"
let path = "/Users/myname/Desktop/report.txt"
let fileURL = URL(fileURLWithPath: path)
let sharingService = NSSharingService(named: NSSharingServiceNameComposeEmail)
sharingService?.recipients = [email] //could be more than one
sharingService?.subject = "subject"
let items: [Any] = ["see attachment", fileURL] //the interesting part, here you add body text as well as URL for the document you'd like to share
sharingService?.perform(withItems: items)
Update
So #Spire mentioned in a comment below that this won't attach a file.
It seems there is a gotcha to be aware of.
For this to work you need to look into your App Capabilities.
You can either:
disable App Sandbox
enable read access for the folders from where you would like to fetch content.
I've attached a couple of screenshots.
Here is how this looks if I have disabled App Sandbox under Capabilities
And here is an image where I have enabled App Sandbox and allowed my app to read content in my Downloads folder
If I do the above, I can access my file called document.txt, located in my Downloads folder, using this URL
let path = "/Users/thatsme/Downloads/document.txt"
let fileURL = URL(fileURLWithPath: path)
And attach that to a mail
Hope that helps you.
import MessageUI
class ViewController: UIViewController,MFMailComposeViewControllerDelegate {
func sendMail() {
if( MFMailComposeViewController.canSendMail()){
print("Can send email.")
let mailComposer = MFMailComposeViewController()
mailComposer.mailComposeDelegate = self
//Set to recipients
mailComposer.setToRecipients(["yakupad#yandex.com"])
//Set the subject
mailComposer.setSubject("email with document pdf")
//set mail body
mailComposer.setMessageBody("This is what they sound like.", isHTML: true)
let pathPDF = "\(NSTemporaryDirectory())contract.pdf"
if let fileData = NSData(contentsOfFile: pathPDF)
{
print("File data loaded.")
mailComposer.addAttachmentData(fileData as Data, mimeType: "application/pdf", fileName: "contract.pdf")
}
//this will compose and present mail to user
self.present(mailComposer, animated: true, completion: nil)
}
else
{
print("email is not supported")
}
func mailComposeController(_ didFinishWithcontroller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?)
{
self.dismiss(animated: true, completion: nil)
}
}
First of all you should import import MessageUI. For this add framework to the project.
Example:
After investigate MFMailComposeViewControllerDelegate for knowing when you end sending email.
Example of the creating of the email:
if( MFMailComposeViewController.canSendMail() ) {
println("Can send email.")
let mailComposer = MFMailComposeViewController()
mailComposer.mailComposeDelegate = self
//Set the subject and message of the email
mailComposer.setSubject("Have you heard a swift?")
mailComposer.setMessageBody("This is what they sound like.", isHTML: false)
if let filePath = NSBundle.mainBundle().pathForResource("swifts", ofType: "wav") {
println("File path loaded.")
if let fileData = NSData(contentsOfFile: filePath) {
println("File data loaded.")
mailComposer.addAttachmentData(fileData, mimeType: "audio/wav", fileName: "swifts")
}
}
self.presentViewController(mailComposer, animated: true, completion: nil)
}
Working example is presented by this link.

Save/Copy a file from Bundle to Desktop using NSSavePanel

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.

Add Instagram to UIActivityViewController

I'm trying to share an image using standard UIActivityViewController, it's fine to share on Facebook, Twitter and Save Image using this code:
let firstActivityItem = "foo text"
let secondActivityItem : UIImage = image!
let activityViewController : UIActivityViewController = UIActivityViewController(
activityItems: [firstActivityItem, secondActivityItem], applicationActivities: nil)
activityViewController.excludedActivityTypes = [
UIActivityTypePostToWeibo,
UIActivityTypePrint,
UIActivityTypeAssignToContact,
UIActivityTypeAddToReadingList,
UIActivityTypePostToVimeo,
UIActivityTypePostToTencentWeibo
]
self.presentViewController(activityViewController, animated: true, completion: nil)
I need one more thing, Instagram:
If UIApplication.sharedApplication().canOpenURL(instagramURL!) {
// Success
var img = image!
var savePath: String = NSHomeDirectory().stringByAppendingPathComponent("Documents/Test.igo")
UIImageJPEGRepresentation(img, 1).writeToFile(savePath, atomically: true)
var imgURL = NSURL(string: NSString(format: "file://%#", savePath) as! String)
docController = UIDocumentInteractionController(URL: imgURL!) // 1
docController.UTI = "com.instagram.exclusivegram" // 2
docController.delegate = self
docController.annotation = ["InstagramCaption":"testsss"] // 3
docController.presentOpenInMenuFromRect(self.view.frame, inView: self.view, animated: true) // 4
} else {
// Error
}
Both these codes work fine separately, how can I add Instagram to the UIActivityViewController? Is it possible at all?
I think it would be very easier to add other social shares to the code you wrote for Instagram. The ".igo" extension is exclusive for Instagram so other apps will not support it. Just change this extension from ".igo" to ".ig" and other apps will read it:
var savePath: String = NSHomeDirectory().stringByAppendingPathComponent("Documents/Test.ig")
But Instagram also have an exclusive UTI to avoiding other apps to appear in the same Document Interaction View. So you will also need to change it from "exclusivegram" to "photo":
docController.UTI = "com.instagram.photo"
I have an app with a similar functionality and this is my original code:
#IBAction func shareOnIntagram(sender: UIButton) {
let finalImage: UIImage = UIImage.imageWithView(photoView)
let instagramURL = NSURL(string: "instagram://app")
if (UIApplication.sharedApplication().canOpenURL(instagramURL!)) {
let imageData = UIImageJPEGRepresentation(finalImage, 1)
let captionString = "caption"
let writePath = (NSTemporaryDirectory() as NSString).stringByAppendingPathComponent("instagram.ig")
if imageData?.writeToFile(writePath, atomically: true) == false {
return
} else {
let fileURL = NSURL(fileURLWithPath: writePath)
self.documentController = UIDocumentInteractionController(URL: fileURL)
self.documentController.delegate = self
self.documentController.UTI = "com.instagram.photo"
self.documentController.annotation = NSDictionary(object: captionString, forKey: "InstagramCaption")
self.documentController.presentOpenInMenuFromRect(self.view.frame, inView: self.view, animated: true)
}
} else {
print(" Instagram is not installed ")
}
}
To make this code work, don't forget to add UIDocumentInteractionControllerDelegate in the UIViewController class.
It seems it's not possible, because of .igo extension which is needed by Instagram.