I am using share sheet to open some of the native apps - i.e. Mail, Reminders, Notes, Messages. Below is my code to open share sheet after clicking on the share button.
I am looking for a way to identify a user click on share sheet i.e. an API that gets called when user selects an option on the share sheet. I searched and couldn't find anything, so not sure if there is a way to identify user selection?
func presentShareSheet(content: String, subject: String = "", shareButton: UIView? = nil, presentingVC: UIViewController? = nil) -> Guarantee<Void> {
return Guarantee { seal in
let itemsToShare: [Any] = [someItems]
let activityViewController = UIActivityViewController(activityItems: itemsToShare, applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = shareButton
activityViewController.completionWithItemsHandler = { activityType, completed, items, error in
// update color of navigation bar
seal(())
}
if let vc = presentingVC ?? UIApplication.shared.topMostViewController() {
vc.present(activityViewController, animated: true)
} else {
seal(())
}
}
}
Related
When I use UIActivityViewController to share an image the close button becomes transparent. I can click on its frame button but it is invisible for the user. And you cannot click outside to close on the sheet.
let activityItemMetadata = LinkMetadataManager(qrImage: image)
let activityVC = UIActivityViewController(
activityItems: [activityItemMetadata],
applicationActivities: nil)
activityVC.completionWithItemsHandler = {(activityType: UIActivityType?, completed: Bool, returnedItems: [Any]?, error: Error?) in
}
activityVC.activityItemsConfiguration = [
UIActivity.ActivityType.mail,
UIActivity.ActivityType.copyToPasteboard,
UIActivity.ActivityType.airDrop,
UIActivity.ActivityType.message
] as? UIActivityItemsConfigurationReading
activityVC.isModalInPresentation = false
self.present(activityVC, animated: true)
Close button appear like this:
Most of your UIActivityViewController code is wrong.
You should not be creating a LinkMetadataManager from the image and then using it as the item you wish to share.
You are trying to create an array of activities and setting those as the activity configuration. This is all wrong.
Your code should look more like the following:
let activityVC = UIActivityViewController(activityItems: [ image ], applicationActivities: nil)
activityVC.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
if completed || activityType == nil {
// activity view is being dismissed
}
}
present(activityVC, animated: true)
If you have a need to exclude certain activities you can add a line like the following:
activityVC.excludedActivityTypes = [ .assignToContact ] // Optionally exclude specific activities
If you want a bit more control you make use of UIActivityItemsConfiguration. The following is an example:
let configuration = UIActivityItemsConfiguration(objects: [ image ])
configuration.perItemMetadataProvider = { (index, key) in
switch key {
case .linkPresentationMetadata:
// Maybe make use of your LinkMetadataManager class here
var info = LPLinkMetadata()
info.title = "Some Title"
return info
default:
return nil
}
}
let activityVC = UIActivityViewController(activityItemsConfiguration: configuration)
activityVC.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
if completed || activityType == nil {
// activity view is being dismissed
}
}
present(activityVC, animated: true)
I'm trying to share my app using UIActivityViewController but I can't reproduce the same effect as when I share an app from the App Store, meaning :
When I clicked the share button in the App Store I have something that looks like this :
But when I try to share my app I have this :
The code that I used was :
if let logo = UIImage(named: "myLogo"), let websiteURL = URL(string: "https://itunes.apple.com/app/idxxxxxxxxxx") {
let objectsToShare = ["My App Name", websiteURL, logo] as [Any]
let activityVC = UIActivityViewController(activityItems: objectsToShare, applicationActivities: [])
if let popoverController = activityVC.popoverPresentationController {
popoverController.sourceView = self.view
popoverController.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
}
present(activityVC, animated: true)
}
The if let popoverController = ... loop is for preventing a crash when using iPads.
What do I have to change in order to have the effect as the App Store? (to have an image with a title and a subtitle)
Moreover, once I share the app with Messages for instance, this is the difference :
How can I have the same effect? (A single image with the title and subtitle, an as a bonus, a video). I'm not sure if this is an iOS 13 problem, since all similar questions don't have the same app sharing popover.
You have to use the new LinkPresentation framework.
Which essentially involves UIActivityItemSource conformance then retrieving the metadata that will encompass the Activity View and the data you are sharing. Data can be retrieved locally or downloaded.
ExampleController: UIViewController {
var metadata: LPLinkMetadata?
func share() {
let activityView = UIActivityViewController(activityItems: [self], applicationActivities: nil)
present(activityView, animated: true)
}
...
}
extension ExampleController: UIActivityItemSource {
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return metadata
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return metadata
}
func activityViewControllerLinkMetadata(_: UIActivityViewController) -> LPLinkMetadata? {
metadata = LPLinkMetadata()
metadata.title = "Title"
metadata.originalURL = URL(string: "Description")
metadata.url = metadata.originalURL
// Using a locally stored item
metadata.iconProvider = NSItemProvider(object: UIImage(named: "image")!)
metadata.imageProvider = NSItemProvider.init(contentsOf:
Bundle.main.url(forResource: "image", withExtension: "JPG"))
return metadata
}
}
Docs:
https://developer.apple.com/documentation/uikit/uiactivityitemsource/3144571-activityviewcontrollerlinkmetada
WWDC Presentation:
https://developer.apple.com/videos/play/wwdc2019/262/
activityViewController.popoverPresentationController?.sourceView = self.view // so that iPads won't crash
You can use this.
I Created and extension for sharing text along with Image.
Here is the extension class
extension UIViewController {
func shareScreen(shareText:String?,shareImage:UIImage?){
var objectsToShare = [AnyObject]()
if let shareTextObj = shareText{
objectsToShare.append(shareTextObj as AnyObject)
}
if let shareImageObj = shareImage{
objectsToShare.append(shareImageObj)
}
if shareText != nil || shareImage != nil{
let activityViewController = UIActivityViewController(activityItems: objectsToShare, applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = self.view
present(activityViewController, animated: true, completion: nil)
}else{
print("There is nothing to share")
}
}
}
share if its contains images only.
guard loadImage == nil else {
self.shareScreen(shareText: "AppName", shareImage: loadImage)
return
}
For Other share accessories link (Message,Email,Skype,Facebook) it worked.
When Try to share via (LinkedIn & WhatsApp) the image is not showing?
The above code which I tried, Can any one let me know is there any provision to share text along with Image including with linkedIn & WhatsApp.
I've 2 siri shortcuts in my App.
I use NSUserActivity to donate these shortcuts. I've also created 2 NSUserActivityTypes in my info.plist.
There are 2 view controllers which handle these shortcuts (1 view controller for 1 shortcut).
If I add 1 siri shortcut from 1 view controller and then go to 2nd view controller the native siri shortcut button (INUIAddVoiceShortcutButton) on 2nd view controller automatically picks the first shortcut (created from 1st view controller) and shows "Added to Siri" with suggested phrase instead of showing "Add to Siri" button. I double checked that each NSUserActivity has different identifier but still somehow its picks the wrong shortcut.
View Controller 1:
let userActivity = NSUserActivity(activityType: "com.activity.type1")
userActivity.isEligibleForSearch = true
userActivity.isEligibleForPrediction = true
userActivity.title = shortcut.title
userActivity.suggestedInvocationPhrase = suggestedPhrase
let attributes = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
attributes.contentDescription = description
userActivity.contentAttributeSet = attributes
let shortcut = INShortcut(userActivity: userActivity)
let siriButton = INUIAddVoiceShortcutButton(style: .whiteOutline)
siriButton.translatesAutoresizingMaskIntoConstraints = false
siriButton.shortcut = shortcut
self.view.addSubview(siriButton)
View Controller 2:
let userActivity2 = NSUserActivity(activityType: "com.activity.type2")
userActivity2.isEligibleForSearch = true
userActivity2.isEligibleForPrediction = true
userActivity2.title = shortcut.title
userActivity2.suggestedInvocationPhrase = suggestedPhrase
let attributes = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
attributes.contentDescription = description
userActivity2.contentAttributeSet = attributes
let shortcut = INShortcut(userActivity: userActivity2)
let siriButton = INUIAddVoiceShortcutButton(style: .whiteOutline)
siriButton.translatesAutoresizingMaskIntoConstraints = false
siriButton.shortcut = shortcut
self.view.addSubview(siriButton)
A similar thing happens when I delete the App and reinstall without deleting the shortcuts from Phone's Settings App.
Seems like its an IOS bug. I figured out a workaround for this problem. You have to create a new siri button every time the user add/edit the siri shortcut. Before creating siri button do the following things
1- Get all the voice shortcuts from INVoiceShortcutCenter by calling the function. Note that this happens asynchronously, so you need to do it some time before you need the data (e.g. in your AppDelegate). You'll also need to re-load this whenever the user adds a Siri Shortcut (probably in the INUIAddVoiceShortcutViewControllerDelegate.addVoiceShortcutViewController(_:didFinishWith:error) method).
INVoiceShortcutCenter.shared.getAllVoiceShortcuts { (voiceShortcutsFromCenter, error) in
guard let voiceShortcutsFromCenter = voiceShortcutsFromCenter else {
if let error = error as NSError? {
os_log("Failed to fetch voice shortcuts with error: %#", log: OSLog.default, type: .error, error)
}
return
}
self.voiceShortcuts = voiceShortcutsFromCenter
}
2- In View Controller-1 check if the shortcut is already added or not by iterating all the voice shortcuts
let voiceShorcut = voiceShortcuts.first { (voiceShortcut) -> Bool in
if let activity = voiceShortcut.shortcut.userActivity, activity.activityType == "com.activity.type1" {
return true
}
return false
}
3- If your voice shortcut is registered then pass the INShortcut to siri button otherwise don't set it.
if voiceShorcut != nil {
let shortcut = INShortcut(userActivity: userActivity1)
siriButton.shortcut = shortcut
}
Do the same thing in Second View Controller.
It's iOS 12.0 bug.
You can fix it by update INUIAddVoiceShortcutButton.voiceShortcut with correct value.
Use KVO to observe "voiceShortcut" property and when it change assign correct value to it.
I've moved to intents setup now and I find that even having just one intent setup and working the INUIAddVoiceShortcutButton is not able to track my shortcut. Once phrase is recorded it shows the Added to Siri with phrase.
But every time the app relaunches the Add to Siri button shows up instead of the Added to Siri button with recorded phrase.
I have tried going by Bilal's suggestion and although I can see the INVoiceShortcutCenter showing me my shortcut as present it doesn't loaded it into the Siri button.
My code looks like this for the button itself.
private func addSiriButton() {
let addShortcutButton = INUIAddVoiceShortcutButton(style: .blackOutline)
addShortcutButton.delegate = self
addShortcutButton.shortcut = INShortcut(intent: engine.intent )
addShortcutButton.translatesAutoresizingMaskIntoConstraints = false
siriButtonSubView.addSubview(addShortcutButton)
siriButtonSubView.centerXAnchor.constraint(equalTo: addShortcutButton.centerXAnchor).isActive = true
siriButtonSubView.centerYAnchor.constraint(equalTo: addShortcutButton.centerYAnchor).isActive = true
}
I have all the protocols implement and I had a close look at the Soup app but just can't figure out what drives this inaccuracy.
Funny enough, even British Airways app developers have given up on that as their button has exactly the same fault behaviour.
Update: I've built another test project with minimal amount implementation for the Intent and the Add to Siri and Added to Siri works perfectly. I'm guessing at this point that there is something in my own apps codebase that is causing this unwanted behaviour.
update 2 Just wanted to let everyone know I have fixed the issue. Using intents works fine but there is definitely a little sensitivity in the Intents definition file itself. All I had to do is create a new intent which then was generated and that worked. Seems my initial intent was somehow corrupt but there were no errors. After creating another intent and re-assigning intent handling function to that it all worked as intended. (pun intended)
I encountered this error when I had an existing intent and working configuration, but added a new parameter. However, in my Intent configuration, I had not added the new parameter name to a supported combination under the Shortcuts app section.
For example, if I had two properties myId and myName, and specified them as such:
let intent = MyIntent()
intent.myId = 1234
intent.myName = "banana"
Then I would need a supported combination of myId, myName in my intents definition file. In my particular case, I had forgotten myName so the INUIAddVoiceShortcutButton was attempting to do a lookup using myId, myName but didn't know how.
I just fixed this issue myself by changing my implementation (originally based on the soupchef app) to this code sample provided by apple (https://developer.apple.com/documentation/sirikit/inuiaddvoiceshortcutbutton):
EDIT: I added code that shows how I create and pass in the shortcutObject (INShortcut) for both UserActivity and custom Intent shortcuts.
The Shortcut class is an enum that contains a computed property called intent that returns an instantiation of the custom intent.
private func addShortcutButton(shortcut: Shortcut, parentViewController: UIViewController, shortcutViewControllerDelegate: INUIAddVoiceShortcutViewControllerDelegate) {
guard let view = parentViewController.view else { return }
if let intent = shortcut.intent {
shortcutObject = INShortcut(intent: intent)
} else if let userActivity = view.userActivity {
shortcutObject = INShortcut(userActivity: userActivity)
}
self.shortcutViewControllerDelegate = shortcutViewControllerDelegate
addSiriButton(to: shortcutButtonContainer)
}
func addSiriButton(to view: UIView) {
let button = INUIAddVoiceShortcutButton(style: .whiteOutline)
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
view.centerXAnchor.constraint(equalTo: button.centerXAnchor).isActive = true
view.centerYAnchor.constraint(equalTo: button.centerYAnchor).isActive = true
button.addTarget(self, action: #selector(addToSiri(_:)), for: .touchUpInside)
}
// Present the Add Shortcut view controller after the
// user taps the "Add to Siri" button.
#objc
func addToSiri(_ sender: Any) {
guard let shortcutObject = shortcutObject else { return }
let viewController = INUIAddVoiceShortcutViewController(shortcut: shortcutObject)
viewController.modalPresentationStyle = .formSheet
viewController.delegate = shortcutViewControllerDelegate
parentViewController?.present(viewController, animated: true, completion: nil)
}
So we can't use the default Siri button, you have to use custom UIButton.
The class VoiceShortcutsManager will check all the voice intents and then we can search that list check if exist one match if yes so we should suggest edition if not we should suggest adding.
public class VoiceShortcutsManager {
private var voiceShortcuts: [INVoiceShortcut] = []
public init() {
updateVoiceShortcuts(completion: nil)
}
public func voiceShortcut(for order: DeviceIntent, powerState: State) -> INVoiceShortcut? {
for element in voiceShortcuts {
guard let intent = element.shortcut.intent as? ToggleStateIntent else {
continue
}
let deviceIntent = DeviceIntent(identifier: intent.device?.identifier, display: intent.device?.displayString ?? "")
if(order == deviceIntent && powerState == intent.state) {
return element
}
}
return nil
}
public func updateVoiceShortcuts(completion: (() -> Void)?) {
INVoiceShortcutCenter.shared.getAllVoiceShortcuts { (voiceShortcutsFromCenter, error) in
guard let voiceShortcutsFromCenter = voiceShortcutsFromCenter else {
if let error = error {
print("Failed to fetch voice shortcuts with error: \(error.localizedDescription)")
}
return
}
self.voiceShortcuts = voiceShortcutsFromCenter
if let completion = completion {
completion()
}
}
}
}
And then implement in your ViewController
class SiriAddViewController: ViewController {
let voiceShortcutManager = VoiceShortcutsManager.init()
override func viewDidLoad() {
super.viewDidLoad()
contentView.btnTest.addTarget(self, action: #selector(self.testBtn), for: .touchUpInside)
}
...
#objc func testBtn() {
let deviceIntent = DeviceIntent(identifier: smartPlug.deviceID, display: smartPlug.alias)
//is action already has a shortcut, update shortcut else create shortcut
if let shortcut = voiceShortcutManager.voiceShortcut(for: deviceIntent, powerState: .off) {
let editVoiceShortcutViewController = INUIEditVoiceShortcutViewController(voiceShortcut: shortcut)
editVoiceShortcutViewController.delegate = self
present(editVoiceShortcutViewController, animated: true, completion: nil)
} else if let shortcut = INShortcut(intent: intentTurnOff) {
let addVoiceShortcutVC = INUIAddVoiceShortcutViewController(shortcut: shortcut)
addVoiceShortcutVC.delegate = self
present(addVoiceShortcutVC, animated: true, completion: nil)
}
}
}
#available(iOS 12.0, *)
extension SiriAddViewController: INUIAddVoiceShortcutButtonDelegate {
func present(_ addVoiceShortcutViewController: INUIAddVoiceShortcutViewController, for addVoiceShortcutButton: INUIAddVoiceShortcutButton) {
addVoiceShortcutViewController.delegate = self
addVoiceShortcutViewController.modalPresentationStyle = .formSheet
present(addVoiceShortcutViewController, animated: true, completion: nil)
}
func present(_ editVoiceShortcutViewController: INUIEditVoiceShortcutViewController, for addVoiceShortcutButton: INUIAddVoiceShortcutButton) {
editVoiceShortcutViewController.delegate = self
editVoiceShortcutViewController.modalPresentationStyle = .formSheet
present(editVoiceShortcutViewController, animated: true, completion: nil)
}
}
#available(iOS 12.0, *)
extension SiriAddViewController: INUIAddVoiceShortcutViewControllerDelegate {
func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) {
voiceShortcutManager.updateVoiceShortcuts(completion: nil)
controller.dismiss(animated: true, completion: nil)
}
func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) {
controller.dismiss(animated: true, completion: nil)
}
}
#available(iOS 12.0, *)
extension SiriAddViewController: INUIEditVoiceShortcutViewControllerDelegate {
func editVoiceShortcutViewController(_ controller: INUIEditVoiceShortcutViewController, didUpdate voiceShortcut: INVoiceShortcut?, error: Error?) {
voiceShortcutManager.updateVoiceShortcuts(completion: nil)
controller.dismiss(animated: true, completion: nil)
}
func editVoiceShortcutViewController(_ controller: INUIEditVoiceShortcutViewController, didDeleteVoiceShortcutWithIdentifier deletedVoiceShortcutIdentifier: UUID) {
voiceShortcutManager.updateVoiceShortcuts(completion: nil)
controller.dismiss(animated: true, completion: nil)
}
func editVoiceShortcutViewControllerDidCancel(_ controller: INUIEditVoiceShortcutViewController) {
voiceShortcutManager.updateVoiceShortcuts(completion: nil)
controller.dismiss(animated: true, completion: nil)
}
}
}
This code was inspired/copy from this webpage:
https://www.nodesagency.com/test-drive-a-siri-shortcuts-intro/
My experience with solving this was a little different. Some intents added via the Add to Siri button worked, which adjusted to "Added to Siri", while others didn't. I realised the actions that worked didn't require parameters.
After setting default values for intents that exposed parameters, which are passed into INShortcut (and then assigned to INUIAddVoiceShortcutButton), all buttons updated their state correctly!
My app is not Live yet. I got the app ID from the App Store Connect. I want to share the app link on social media apps. I used the UIActivityViewController:
let string1 = "itms-apps://itunes.apple.com/app/idXXXXXXX"
let url = NSURL(string: string1)
let shareItems = [UIApplication.sharedApplication().openURL(url!)]
let activityViewController = UIActivityViewController(activityItems: shareItems, applicationActivities: nil)
self.presentViewController(activityViewController, animated: true, completion: nil)
Problem: It is not showing some social media apps like WhatsApp.
Solution for Swift 4 or better:
This solution also works on iPad (the solution above crashes on iPad):
if let urlStr = NSURL(string: "https://apps.apple.com/us/app/idxxxxxxxx?ls=1&mt=8") {
let objectsToShare = [urlStr]
let activityVC = UIActivityViewController(activityItems: objectsToShare, applicationActivities: nil)
if UIDevice.current.userInterfaceIdiom == .pad {
if let popup = activityVC.popoverPresentationController {
popup.sourceView = self.view
popup.sourceRect = CGRect(x: self.view.frame.size.width / 2, y: self.view.frame.size.height / 4, width: 0, height: 0)
}
}
self.present(activityVC, animated: true, completion: nil)
}
This is used to open the site, not to share the app:
[UIApplication.sharedApplication().openURL(url!)]
Do this instead:
if let name = URL(string: "https://itunes.apple.com/us/app/myapp/idxxxxxxxx?ls=1&mt=8"), !name.absoluteString.isEmpty {
let objectsToShare = [name]
let activityVC = UIActivityViewController(activityItems: objectsToShare, applicationActivities: nil)
self.present(activityVC, animated: true, completion: nil)
} else {
// show alert for not available
}
for sample see this
The solutions here are all good but it's worth considering implementing the UIActivityItemSource protocol and LinkPresentation framework.
My solution achieves the following:
App icon and title showing at the top of the UIActivityViewController
Direct link to App Store for AirDrop ActivityType
Custom text for messages and emails, including an opportunity to add a link to the app on Google Play if required
Subject for emails
Doesn't use the LPMetaDataProvider fetch request (as described in this WWDC 2019 262 video) so faster to load
0. Init the UIActivityViewController:
Set the items to self:
let activityVC = UIActivityViewController(activityItems: [self], applicationActivities: nil)
Exclude certain ActivityTypes which don't apply:
activityVC.excludedActivityTypes = [.addToReadingList, .assignToContact, .markupAsPDF, .openInIBooks, .saveToCameraRoll]
For iPad set the popoverPresentationController.sourceView or .barButtonItem (this is ignored on iPhone):
activityVC.popoverPresentationController?.sourceView = myButton
Present it:
present(activityVC, animated: true, completion: nil)
1. Implement the required UIActivityItemSource methods
https://developer.apple.com/documentation/uikit/uiactivityitemsource
You must implement the placeholder method which according to the docs:
Placeholder objects do not have to contain any real data but should be configured as closely as possible to the actual data object you intend to provide.
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return ""
}
And the actual data, returning a link to the app for AirDrop and text for everything else:
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
if activityType == .airDrop {
return URL(string: "APP_STORE_URL")!
}
return "Check out the APP_NAME on the App Store: APP_STORE_URL or on the Google Play Store: PLAY_STORE_URL"
}
2. Implement the subject method
From the docs:
For activities that support a subject field, returns the subject for the item.
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
return "EMAIL_SUBJECT" // e.g. App name
}
3. Implement the LPLinkMetaData method
From the docs:
Returns metadata to display in the preview header of the share sheet.
#available(iOS 13.0, *)
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let metadata = LPLinkMetadata()
metadata.title = "APP_NAME"
return metadata
}
iPhone solution for Swift 5+
let url = URL(string: "https://apps.apple.com/us/app/id1535629801")!
let vc = UIActivityViewController(activityItems: [url], applicationActivities: nil)
present(vc, animated: true)