I am using the IntentsExtension and IntentsUIExtension for a messaging app to allow a user to send messages using Siri.
It all works and the message is sent once, however when the extension UI is displayed, the view I define in configureView is displayed 3 times.
This is the default code intent handler. Either using this default code or my custom code the result is the same. UI extension IntentViewController configureView method called 3 times:
class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessagesIntentHandling, INSetMessageAttributeIntentHandling {
override func handler(for intent: INIntent) -> Any {
// This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent.
return self
}
// MARK: - INSendMessageIntentHandling
// Implement resolution methods to provide additional information about your intent (optional).
func resolveRecipients(for intent: INSendMessageIntent, with completion: #escaping ([INSendMessageRecipientResolutionResult]) -> Void) {
if let recipients = intent.recipients {
// If no recipients were provided we'll need to prompt for a value.
if recipients.count == 0 {
completion([INSendMessageRecipientResolutionResult.needsValue()])
return
}
var resolutionResults = [INSendMessageRecipientResolutionResult]()
for recipient in recipients {
let matchingContacts = [recipient] // Implement your contact matching logic here to create an array of matching contacts
switch matchingContacts.count {
case 2 ... Int.max:
// We need Siri's help to ask user to pick one from the matches.
resolutionResults += [INSendMessageRecipientResolutionResult.disambiguation(with: matchingContacts)]
case 1:
// We have exactly one matching contact
resolutionResults += [INSendMessageRecipientResolutionResult.success(with: recipient)]
case 0:
// We have no contacts matching the description provided
resolutionResults += [INSendMessageRecipientResolutionResult.unsupported()]
default:
break
}
}
completion(resolutionResults)
} else {
completion([INSendMessageRecipientResolutionResult.needsValue()])
}
}
func resolveContent(for intent: INSendMessageIntent, with completion: #escaping (INStringResolutionResult) -> Void) {
if let text = intent.content, !text.isEmpty {
completion(INStringResolutionResult.success(with: text))
} else {
completion(INStringResolutionResult.needsValue())
}
}
// Once resolution is completed, perform validation on the intent and provide confirmation (optional).
func confirm(intent: INSendMessageIntent, completion: #escaping (INSendMessageIntentResponse) -> Void) {
// Verify user is authenticated and your app is ready to send a message.
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
let response = INSendMessageIntentResponse(code: .ready, userActivity: userActivity)
completion(response)
}
// Handle the completed intent (required).
func handle(intent: INSendMessageIntent, completion: #escaping (INSendMessageIntentResponse) -> Void) {
// Implement your application logic to send a message here.
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
let response = INSendMessageIntentResponse(code: .success, userActivity: userActivity)
completion(response)
}
// Implement handlers for each intent you wish to handle. As an example for messages, you may wish to also handle searchForMessages and setMessageAttributes.
// MARK: - INSearchForMessagesIntentHandling
func handle(intent: INSearchForMessagesIntent, completion: #escaping (INSearchForMessagesIntentResponse) -> Void) {
// Implement your application logic to find a message that matches the information in the intent.
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self))
let response = INSearchForMessagesIntentResponse(code: .success, userActivity: userActivity)
// Initialize with found message's attributes
response.messages = [INMessage(
identifier: "identifier",
content: "I am so excited about SiriKit!",
dateSent: Date(),
sender: INPerson(personHandle: INPersonHandle(value: "sarah#example.com", type: .emailAddress), nameComponents: nil, displayName: "Sarah", image: nil, contactIdentifier: nil, customIdentifier: nil),
recipients: [INPerson(personHandle: INPersonHandle(value: "+1-415-555-5555", type: .phoneNumber), nameComponents: nil, displayName: "John", image: nil, contactIdentifier: nil, customIdentifier: nil)]
)]
completion(response)
}
// MARK: - INSetMessageAttributeIntentHandling
func handle(intent: INSetMessageAttributeIntent, completion: #escaping (INSetMessageAttributeIntentResponse) -> Void) {
// Implement your application logic to set the message attribute here.
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self))
let response = INSetMessageAttributeIntentResponse(code: .success, userActivity: userActivity)
completion(response)
}
}
and the UI code:
class IntentViewController: UIViewController, INUIHostedViewControlling {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
// MARK: - INUIHostedViewControlling
// Prepare your view controller for the interaction to handle.
func configureView(for parameters: Set<INParameter>, of interaction: INInteraction, interactiveBehavior: INUIInteractiveBehavior, context: INUIHostedViewContext, completion: #escaping (Bool, Set<INParameter>, CGSize) -> Void) {
// Do configuration here, including preparing views and calculating a desired size for presentation.
completion(true, parameters, self.desiredSize)
}
var desiredSize: CGSize {
return self.extensionContext!.hostedViewMaximumAllowedSize
}
}
Putting a breakpoint on the completion handler in configureView I can see it is called 3 times. In my app this causes the custom view to appear 3 times one after another stacked vertically.
I added UIMenu to the UIButton.menu with long title, but menu show not full text. Is there anyway to change it to show full text ? thanks for help
let _: [()] = (0...100).map {
childdrens.append(UIAction(title: "This is a very very very very very very very very very long title \($0)", handler: { _ in
}))
}
longCapMenuButton = UIMenu(title: "", options: .displayInline, children: childdrens )
longCapMenuButton.showsMenuAsPrimaryAction = true
Screenshot Attached below-
The problem
I was implementing UIContextMenuInteraction, and ended up with the behavior I can't explain or find fixes too. The issue as seen from the screen shot that menu items have checkmarks. This is not intended and those checkmarks added automatically. Ideally I'd like use SF Symbols, but any image I add ends up being this checkmark. Even if I set image to nil, it still adds this weird checkmark.
Additional steps taken: Reinstall SF Symbols and SF Pro, Clean build, Restart xCode / Simulator
Reproduced: Simulator iOS 13.3, iPhone 7 iOS 13.3
System: Catalina 10.15.1, xCode 11.3.1
Code:
import UIKit
class ViewController: UIViewController {
let sampleView = UIView(frame: CGRect(x: 50, y: 300, width: 300, height: 200))
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(sampleView)
sampleView.backgroundColor = .systemIndigo
let interaction = UIContextMenuInteraction(delegate: self)
sampleView.addInteraction(interaction)
}
}
extension ViewController: UIContextMenuInteractionDelegate {
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint
) -> UIContextMenuConfiguration? {
let actionProvider: UIContextMenuActionProvider = { [weak self] _ in
let like = UIAction(
title: "Like",
image: UIImage(systemName: "heart"),
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: .on
) { _ in
}
let copy = UIAction(
title: "Copy",
image: nil,
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: .on
) { _ in
}
let delete = UIAction(
title: "Delete",
image: UIImage(systemName: "trash"),
identifier: nil,
discoverabilityTitle: nil,
attributes: [.destructive],
state: .on
) { _ in
}
return UIMenu(
title: "",
image: nil,
identifier: nil,
options: [],
children: [
like, copy, delete
]
)
}
let config = UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: actionProvider)
return config
}
}
You need to change UIAction.state from .on to .off to get rid of the checkmark.
I decided to addUIContextMenuInteraction to my UITableViewCell, it works fine, but the title that has 9+ letters (without image) or 6+ letters(with image) is getting shortened like this:
Implementation of delegate method:
extension MyCustomCell: UIContextMenuInteractionDelegate {
#available(iOS 13.0, *)
func contextMenuInteraction(_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ -> UIMenu in
let first = UIAction(title: "8Letters") { _ in
print("8 letters")
}
let second = UIAction(title: "9Letters+") { _ in
print("9 letters")
}
let third = UIAction(title: "Hello", image: UIImage(systemName: "square.and.arrow.up")) { _ in
print("5 letters + image")
}
let fourth = UIAction(title: "Hello+", image: UIImage(systemName: "square.and.arrow.up")) { _ in
print("6 letters + image")
}
return UIMenu(title: "", children: [first, second, third, fourth])
}
}
}
Check if any third party framework added to your project for customising the UITableViewCell is breaking the UI. In my case, the issue is caused by third party framework ( "SkeletonView") which I had added to give shimmer effect to UITableViewCell
I have a view controller with map kit integrated. I need to shoot an alert before opening that map, asking to choose from all similar applications of maps to open it with. For instance, if google maps app is installed in my iPhone, there should be an option for it, along with the default mapkit view. Is there a possibility to achieve this functionality which scans every similar app from iphone and returns the result as options to open map with.
You can create an array of checks to map the installed apps using sumesh's answer [1]:
var installedNavigationApps : [String] = ["Apple Maps"] // Apple Maps is always installed
and with every navigation app you can think of:
if (UIApplication.sharedApplication().canOpenURL(url: NSURL)) {
self.installedNavigationApps.append(url)
} else {
// do nothing
}
Common navigation apps are:
Google Maps - NSURL(string:"comgooglemaps://")
Waze - NSURL(string:"waze://")
Navigon - NSURL(string:"navigon://")
TomTom - NSURL(string:"tomtomhome://")
A lot more can be found at: http://wiki.akosma.com/IPhone_URL_Schemes
After you created your list of installed navigation apps you can present an UIAlertController:
let alert = UIAlertController(title: "Selection", message: "Select Navigation App", preferredStyle: .ActionSheet)
for app in self.installNavigationApps {
let button = UIAlertAction(title: app, style: .Default, handler: nil)
alert.addAction(button)
}
self.presentViewController(alert, animated: true, completion: nil)
Of course you need to add the behavior of a button click in the handler with the specified urlscheme. For example if Google Maps is clicked use something like this:
UIApplication.sharedApplication().openURL(NSURL(string:
"comgooglemaps://?saddr=&daddr=\(place.latitude),\(place.longitude)&directionsmode=driving")!) // Also from sumesh's answer
With only Apple Maps and Google Maps installed this will yield something like this:
Swift 5+
Base on #Emptyless answer.
import MapKit
func openMapButtonAction() {
let latitude = 45.5088
let longitude = -73.554
let appleURL = "http://maps.apple.com/?daddr=\(latitude),\(longitude)"
let googleURL = "comgooglemaps://?daddr=\(latitude),\(longitude)&directionsmode=driving"
let wazeURL = "waze://?ll=\(latitude),\(longitude)&navigate=false"
let googleItem = ("Google Map", URL(string:googleURL)!)
let wazeItem = ("Waze", URL(string:wazeURL)!)
var installedNavigationApps = [("Apple Maps", URL(string:appleURL)!)]
if UIApplication.shared.canOpenURL(googleItem.1) {
installedNavigationApps.append(googleItem)
}
if UIApplication.shared.canOpenURL(wazeItem.1) {
installedNavigationApps.append(wazeItem)
}
let alert = UIAlertController(title: "Selection", message: "Select Navigation App", preferredStyle: .actionSheet)
for app in installedNavigationApps {
let button = UIAlertAction(title: app.0, style: .default, handler: { _ in
UIApplication.shared.open(app.1, options: [:], completionHandler: nil)
})
alert.addAction(button)
}
let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alert.addAction(cancel)
present(alert, animated: true)
}
Also put these in your info.plist:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlechromes</string>
<string>comgooglemaps</string>
<string>waze</string>
</array>
Cheers!
Swift 5+ solution based on previous answers, this one shows a selector between Apple Maps, Google Maps, Waze and City Mapper. It also allows for some optional location title (for those apps that support it) and presents the alert only if there are more than 1 option (it opens automatically if only 1, or does nothing if none).
func openMaps(latitude: Double, longitude: Double, title: String?) {
let application = UIApplication.shared
let coordinate = "\(latitude),\(longitude)"
let encodedTitle = title?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
let handlers = [
("Apple Maps", "http://maps.apple.com/?q=\(encodedTitle)&ll=\(coordinate)"),
("Google Maps", "comgooglemaps://?q=\(coordinate)"),
("Waze", "waze://?ll=\(coordinate)"),
("Citymapper", "citymapper://directions?endcoord=\(coordinate)&endname=\(encodedTitle)")
]
.compactMap { (name, address) in URL(string: address).map { (name, $0) } }
.filter { (_, url) in application.canOpenURL(url) }
guard handlers.count > 1 else {
if let (_, url) = handlers.first {
application.open(url, options: [:])
}
return
}
let alert = UIAlertController(title: R.string.localizable.select_map_app(), message: nil, preferredStyle: .actionSheet)
handlers.forEach { (name, url) in
alert.addAction(UIAlertAction(title: name, style: .default) { _ in
application.open(url, options: [:])
})
}
alert.addAction(UIAlertAction(title: R.string.localizable.cancel(), style: .cancel, handler: nil))
contextProvider.currentViewController.present(alert, animated: true, completion: nil)
}
Note this solution uses R.swift for string localization but you can replace those with NSLocalizedString normally, and it uses a contextProvider.currentViewController to get the presented UIViewController, but you can replace it with self if you are calling this in a view controller already.
As usual, you need to also add the following to your app Info.plist:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>citymapper</string>
<string>comgooglemaps</string>
<string>waze</string>
</array>
A SwiftUI approach based on #Angel G. Olloqui answer:
struct YourView: View {
#State private var showingSheet = false
var body: some View {
VStack {
Button(action: {
showingSheet = true
}) {
Text("Navigate")
}
}
.actionSheet(isPresented: $showingSheet) {
let latitude = 45.5088
let longitude = -73.554
let appleURL = "http://maps.apple.com/?daddr=\(latitude),\(longitude)"
let googleURL = "comgooglemaps://?daddr=\(latitude),\(longitude)&directionsmode=driving"
let wazeURL = "waze://?ll=\(latitude),\(longitude)&navigate=false"
let googleItem = ("Google Map", URL(string:googleURL)!)
let wazeItem = ("Waze", URL(string:wazeURL)!)
var installedNavigationApps = [("Apple Maps", URL(string:appleURL)!)]
if UIApplication.shared.canOpenURL(googleItem.1) {
installedNavigationApps.append(googleItem)
}
if UIApplication.shared.canOpenURL(wazeItem.1) {
installedNavigationApps.append(wazeItem)
}
var buttons: [ActionSheet.Button] = []
for app in installedNavigationApps {
let button: ActionSheet.Button = .default(Text(app.0)) {
UIApplication.shared.open(app.1, options: [:], completionHandler: nil)
}
buttons.append(button)
}
let cancel: ActionSheet.Button = .cancel()
buttons.append(cancel)
return ActionSheet(title: Text("Navigate"), message: Text("Select an app..."), buttons: buttons)
}
}
}
Also, add the following to your Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlechromes</string>
<string>comgooglemaps</string>
<string>waze</string>
</array>
For anyone else looking for something similar
you can now use UIActivityViewController, its the same UIControl Photos or Safari use when you click on the share button.
For apple maps and google maps you can add custom application activity to show alongside the other items. You need to subclass UIActivity and override the title and image methods. And the perform() function to handle the tap on our custom item
below is Objective C code i wrote for the same.
For Swift code you can refer UIActivityViewController swift
NSMutableArray *activityArray = [[NSMutableArray alloc] init];
// Check if google maps is installed and accordingly add it in menu
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:#"comgooglemaps://"]]) {
GoogleMapsActivityView *googleMapsActivity = [[GoogleMapsActivityView alloc] init];
[activityArray addObject:googleMapsActivity];
}
// Check if apple maps is installed and accordingly add it in menu
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:#"maps://"]]) {
AppleMapsActivityView *appleMapsActivity = [[AppleMapsActivityView alloc] init];
[activityArray addObject:appleMapsActivity];
}
NSArray *currentPlaces = [NSArray arrayWithObject:place];
UIActivityViewController *activityViewController =
[[UIActivityViewController alloc] initWithActivityItems:currentPlaces
applicationActivities:activityArray];
activityViewController.excludedActivityTypes = #[UIActivityTypePrint,
UIActivityTypeCopyToPasteboard,
UIActivityTypeAssignToContact,
UIActivityTypeSaveToCameraRoll,
UIActivityTypePostToWeibo,
UIActivityTypeAddToReadingList,
UIActivityTypePostToVimeo,
UIActivityTypeAirDrop];
[self presentViewController:activityViewController animated:YES completion:nil];
And Subclass the GoogleMapsActivity
#interface GoogleMapsActivityView: UIActivity
#end
#implementation GoogleMapsActivityView
- (NSString *)activityType {
return #"yourApp.openplace.googlemaps";
}
- (NSString *)activityTitle {
return NSLocalizedString(#"Open with Google Maps", #"Activity view title");
}
- (UIImage *)activityImage {
return [UIImage imageNamed:#"ic_google_maps_logo"];
}
- (UIActivityCategory)activityCategory {
return UIActivityCategoryAction;
}
- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems {
return YES;
}
- (void)performActivity {
CLLocationDegrees lat = 99999;
CLLocationDegrees lng = 99999;
NSString *latlong = [NSString stringWithFormat:#"%.7f,%#%.7f", lat, #"", lng];
NSString *urlString = [NSString stringWithFormat:#"comgooglemaps://?q=%#", latlong];
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:urlString]]) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString]
options:#{}
completionHandler:nil];
}
[self activityDidFinish:YES];
}
SwiftUI rewrite from previous solutions using enums and a view modifier
extension View {
func opensMap(at location: LocationCoordinate2D) -> some View {
return self.modifier(OpenMapViewModifier(location: location))
}
}
struct OpenMapViewModifier: ViewModifier {
enum MapApp: CaseIterable {
case apple, gmaps
var title: String {
switch self {
case .apple: return "Apple Maps"
case .gmaps: return "Google Maps"
}
}
var scheme: String {
switch self {
case .apple: return "http"
case .gmaps: return "comgooglemaps"
}
}
var isInstalled: Bool {
guard let url = URL(string: self.scheme.appending("://")) else { return false }
return UIApplication.shared.canOpenURL(url)
}
func url(for location: LocationCoordinate2D) -> URL? {
switch self {
case .apple:
return URL(string: "\(self.scheme)://maps.apple.com/?daddr=\(location.latitude),\(location.longitude)")
case .gmaps:
return URL(string: "\(self.scheme)://?daddr=\(location.latitude),\(location.longitude)&directionsmode=driving")
}
}
}
var location: LocationCoordinate2D
#State private var showingAlert: Bool = false
private let installedApps = MapApp.allCases.filter { $0.isInstalled }
func body(content: Content) -> some View {
Button(action: {
if installedApps.count > 1 {
showingAlert = true
} else if let app = installedApps.first, let url = app.url(for: location) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}) {
content.actionSheet(isPresented: $showingAlert) {
let appButtons: [ActionSheet.Button] = self.installedApps.compactMap { app in
guard let url = app.url(for: self.location) else { return nil }
return .default(Text(app.title)) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
return ActionSheet(title: Text("Navigate"), message: Text("Select an app..."), buttons: appButtons + [.cancel()])
}
}
}
}