iOS Contextual Menu API can't display Link Presentation content - ios

I am attempting to display the preview, using the LinkPresentation API, of a website inside of a UIContextualMenu when the user force or long presses on a link, similar to how peek and pop used to work for links. However, the LP API loads the metadata from the website asynchronously, and when I force touch on the link, the preview controller displayed by the context menu is blank. The following is the delegate method for the UIContextMenuConfiguration:
public func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
guard let url = shouldShowContextualMenu(location: location) else { return nil }
return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
self.getMetadataForUrl(url: url ) { linkView in
self.previewController = LinkPreviewViewController(linkView: linkView)
}
return self.previewController
}, actionProvider: nil)
}

Related

iOS App Intents UI Extension calling configureView multiple times and displaying my view multiple times

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.

Can you detect which component was tapped in an Intents UI extension?

I have a UI extension for a Siri Shortcut and the I want the view I'm displaying within Siri to do different things when different parts are clicked. For example, I am displaying some contact information for a contact from a different system, and I'd like to call the phone number when the phone number is clicked, and to open the map with directions when the address is clicked. I'm able to detect when the app is opened because anything in the view was clicked, but I can't detect the specific element that was clicked.
This is the gist of my code, with parts removed for brevity.
class IntentViewController: UIViewController, INUIHostedViewControlling {
func configureView(for parameters: Set<INParameter>, of interaction: INInteraction, interactiveBehavior: INUIInteractiveBehavior, context: INUIHostedViewContext, completion: #escaping (Bool, Set<INParameter>, CGSize) -> Void) {
let viewSize = configureUI(with: intent, of: interaction)
completion(true, [], viewSize)
}
private func configureUI(with intent: PersonInfoIntent, of interaction: INInteraction) -> CGSize {
let vc = UIViewController()
addChild(vc)
view.addSubview(vc.view)
let nameLabel = UILabel(text: name)
vc.view.addSubview(nameLabel)
let phoneLabel = UILabel(text: phoneNumber)
vc.view.addSubview(phoneLabel)
let addressLabel = UILabel(text: address)
vc.view.addSubview(address)
// This does not result in openMap being called
// let addressTapped = UITapGestureRecognizer(target: self, action: #selector(openMap))
// addressLabel.addGestureRecognizer(addressTapped)
return self.extensionContext!.hostedViewMaximumAllowedSize
}
}
I've tried using a UIButton and addTarget rather than a UILabel with a gesture recognizer, but that also never triggers openMap.
I'd like to be able to detect which element was tapped so I can set it on my activity.userInfo and pick it up in my AppDelegate.application(_:,continue:,restorationHandler:), where I can perform actions like opening the map with directions, or calling a phone number.

Back link to previous app from containing app progmatically in case of deeplinks

I created a keyboard extension with a scan button to open a barcode scanner in my containing app. When the scan is completed, it should navigate back to the initial app and the barcode data should be set as text to the textfield that initiated the keyboard and we clicked on scan button.
There is this app Scandit Wedge that does it the same way. But I couldn't find a way to achieve the same.
Please refer GIF below.
https://s3.amazonaws.com/id123-dev-ios/scandit.gif
Any help would be much appreciated.
There is no public API to switch to the previous app, here is the answer: https://stackoverflow.com/a/13447282/1433612
But you could do that if you know the app's bundle id and url scheme. You can find unofficial lists on internet. Assuming that you are able to recognize the source app you can do something like this in your AppDelegate:
public func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
guard let applicationBundleId = options[UIApplicationOpenURLOptionsKey.sourceApplication] as? String else {
return true
}
// Save your source application
sourceApplicationBundleId = applicationBundleId
return true
}
var sourceApplicationBundleId: String?
// Attempt to open application from which your app was opened
func openApplication() {
guard let applicationBundleId = sourceApplicationBundleId, let url = url(for: applicationBundleId) else {
return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
func url(for bundleId: String) -> URL? {
guard let scheme = knownUrlSchemes[bundleId] else {
return nil
}
return URL(string: scheme)!
}
// A list of known url schemes
var knownUrlSchemes: Dictionary<String, String> = {
return ["com.google.Maps": "comgooglemaps://",
"com.facebook.Facebook": "fb://"]
}()

Open the main app from a iOS Safari Share Extension

I have a Safari share extension where I want the ability to open the main app from within the extension. The user is presented with an alert where they have the option to open the app.
func openAppHandler() {
self.extensionContext?.completeRequest(returningItems: []) { (success) in
if let url = URL(string: "myapp://...") {
self.extensionContext?.open(url, completionHandler: nil)
}
}
}
The alert appears after the method didSelectPost() is called, and as you can see it occurs in the background priority completion block for the extension. The open method says in it's docs "In iOS 8, only the Today extension point (used for creating widgets) supports this method." I'm guessing it's still the case that it's still not supported in the Safari Share Extension.
Does anyone know of a way to open my main app from a share extension?
I found a solution here. I'm not sure if this is technically ok with Apple, but it works just as I need it to.
#objc func openURL(_ url: URL) {
return
}
func openContainerApp() {
var responder: UIResponder? = self as UIResponder
let selector = #selector(MyViewController.openURL(_:))
while responder != nil {
if responder!.responds(to: selector) && responder != self {
responder!.perform(selector, with: URL(string: "myapp://url")!)
return
}
responder = responder?.next
}
}

WKWebView in iOS: How can I intercept a click and retrieve the linked content instead?

In a WKWebView, when a user clicks a link that refers to certain file types (e.g. a VCF file for contacts, or an ICS file for calendar events), I'd like to intercept the link, i.e. cancel the navigation, and instead display the content using a specialized view controller.
For example, the CNContactViewController can be used to display contacts, the EKEventViewController can be used to display calendar events.
I can intercept the click by assigning a WKNavigationDelegate and using decidePolicyForNavigationAction:
// Swift 2
extension MyController: WKNavigationDelegate {
func webView(webView: WKWebView, decidePolicyForNavigationAction
navigationAction: WKNavigationAction,
decisionHandler: (WKNavigationActionPolicy) -> ()) {
let url = navigationAction.request.URL!
if url.pathExtension == "ics" {
decisionHandler(WKNavigationActionPolicy.Cancel)
// TODO: download data
// TODO: display with EKEventViewController
} else if url.pathExtension == "vcf" {
decisionHandler(WKNavigationActionPolicy.Cancel)
// TODO: download data
// TODO: display with CNContactViewController
} else {
decisionHandler(WKNavigationActionPolicy.Allow)
}
}
}
But in order to display the files the specialized controllers, I need to download the data from the given url first.
How can I do that?
Since the download requires authentication, the download needs to share the cookies with the WKWebView, or use another technique to share the already authenticated session.
If it helps: I've already got access to the web view's WKProcessPool and WKWebViewConfiguration. To my understanding, the cookies are somehow tied to the WKProcessPool. But I don't know how to use this to download the content, for example with a NSURLSession.
It feels hacky, but I solved this by having the WKWebView execute some javascript that retrieves the content via ajax and returns it to a completionHandler in swift.
Background
The WKWebView supports calling evaluateJavaScript, which passes the javascript's result to a completionHandler:
func evaluateJavaScript(_ javaScriptString: String,
completionHandler completionHandler: ((AnyObject?, NSError?) -> Void)?)
Since there's jQuery on the server side, I used this to send an ajax request like follows. But, of course, this can be done with vanilla javascript as well.
(function(url) {
var result = '';
$.ajax({
type: 'GET',
url: url,
success: function(r) {result = r},
failure: function() {result = null},
async: false
});
return result
})(url)
The url can be passed to javascript with swift's string interpolation.
Extend WKWebView
To easily use this, I've extended the WKWebViewclass.
// Views/WKWebView.swift
import WebKit
extension WKWebView {
func readUrlContent(url: NSURL, completionHandler: (result: String) -> Void) {
self.evaluateJavaScript("(function() { var result = ''; $.ajax({type: 'GET', url: '\(url)', success: function(r) {result = r}, failure: function() {result = null}, async: false }); return result })()", completionHandler: { (response, error) -> Void in
let result = response as! String
completionHandler(result: result)
})
}
}
Usage
From the question's example, this can be called like this:
let url = navigationAction.request.URL!
if url.pathExtension == "ics" {
decisionHandler(WKNavigationActionPolicy.Cancel)
webView.readUrlContent(url) { (result: String) in
print(result)
// TODO: display with EKEventViewController
}
}

Resources