I am trying to recreate an existing motivational quote app as practice.
On this app, you get a notification which displays the contents, and if you click it the home page provides you with the rest of the quote (screenshots below). I have seen this type of app a lot – they all use collection views and the notification and homepage content always match.
I have tried mimicking this by setting up local notifications for the next 7 days to provide the user with a random quote from my array:
func configureNotifications() {
let shuffled = quotes.shuffled()
for i in 1...7 {
let content = UNMutableNotificationContent()
content.title = “Daily quotes”
content.body = shuffled[i].text
let alertDate = Date().byAdding(days: i) // scheduled for next 7 days added
var alertComponents = Calendar.current.dateComponents([.day, .month, .year], from: alertDate)
alertComponents.hour = 10
Etc
}
func updateQuoteHome() {
guard let selectedQuote = quotes.randomElement() else {
fatalError("Unable to read a quote.")
}
updateLabels(with: selectedQuote)
}
However, once the notification is clicked, or if the user just randomly opens the app, the content displayed is obviously completely different.
What is the method to link these two together – is it even possible with local notification or are these apps utilising push notifications?
Is there something about this in the documentation?
Thank you ~
You should use Push Notifications for this feature. If you have little or no backend experience you can use Firebase Functions and Firebase Messaging to achieve this.
In a legacy extension it was possible to iterate over safari.application.activeBrowserWindow.tabs to send a message to all tabs registered with the extension.
Is there any equivalent available with the new safari app extensions?
I've been trough the docs but did not find any hints on how to achieve this very basic thing.
A horrible workaround would be to have all tabs ping the Swift background, but really this is such a basic thing it seems absurd that it is not available or covered by the docs, am I missing something?
I also tried keeping a weak map of all "page" instances as seen by "messageReceived" handler in the hope the SFSafariPage reference would be kept until a tab is closed but they are instead lost almost immediately, suggesting they are more message channels than actual Safari pages.
The way should be next:
in injected.js you send the message to your app-ext, e.g.
document.addEventListener("DOMContentLoaded", function (event) {
safari.extension.dispatchMessage('REGISTER_PAGE')
})
And in app-ext handle it with smth like this:
var pages: [SFSafariPage] = []
class SafariExtensionHandler: SFSafariExtensionHandler {
override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String : Any]?) {
switch messageName {
case "REGISTER_PAGE":
if !pages.contains(page) {
pages.append(page)
}
default:
return
}
}
}
Well, then you can send the message to all opened pages during runtime by smth like this:
for p in pages {
p.dispatchMessageToScript(withName: "message name", userInfo: userInfo)
}
It looks hacky but yet workable. Enjoy :)
I implemented a simple UIActivityViewController as below
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
activityController.completionWithItemsHandler = { activityType, completed, returnedItems, activityError in
if let error = activityError where !completed && activityType != nil {
// report the error
}
}
I thought the Add Account screen for Mail could be handled automatically by the UIActivityViewController instance if the user have no mail accounts, but not, it's not doing it.
The first thing I tried was to analyze the completion closure: the activityError instance is nil and the completed value is false. Great, I put this workaround (i want to open from my aap. "settings >> mail >> add account " (the add account page) is it possible.?) whenever this closure with those values is called. But it's called also when the user press Cancel on the mail composer.
So, how do Photos and other not-Apple apps they handle this scenario? Thanks
Seems like Apple doesn't handle well a NSURL type in the activityItems.
I solved it by using absoluteString value of that instance.
I'm using .net ui automation framework to capture user clicks of links when they are viewing a message in an outlook application.
The problem is that, I'm not able to get the link as an AutomationElement(as I can in a web page in IE window). I can only get the document pane as a whole.
Is there any way to do it?
Well, I got it myself.
Though I'm not able to get the link directly, I can locate the link and get it from the TextPattern of the document element.
Suppose element is the document element that directly gets the focus or clicked:
if (element.Current.LocalizedControlType == "document")
{
var point = new System.Windows.Point(Cursor.Position.X, Cursor.Position.Y);
object textPattern;
if (element.TryGetCurrentPattern(TextPattern.Pattern, out textPattern))
{
var range = ((TextPattern)textPattern).RangeFromPoint(point); //it's an empty range
var e = range.GetEnclosingElement(); //get the enclosing AutomationElement
if (e.Current.LocalizedControlType == "link" || e.Current.LocalizedControlType == "hyperlink")
{
//use e
}
}
}
Is synchronous communication between JavaScript and Swift/Obj-C native code possible using the WKWebView?
These are the approaches I have tried and have failed.
Approach 1: Using script handlers
WKWebView's new way of receiving JS messages is by using the delegate method userContentController:didReceiveScriptMessage: which is invoked from JS by window.webkit.messageHandlers.myMsgHandler.postMessage('What's the meaning of life, native code?')
The problem with this approach is that during execution of the native delegate method, JS execution is not blocked, so we can't return a value by immediately invoking webView.evaluateJavaScript("something = 42", completionHandler: nil).
Example (JavaScript)
var something;
function getSomething() {
window.webkit.messageHandlers.myMsgHandler.postMessage("What's the meaning of life, native code?"); // Execution NOT blocking here :(
return something;
}
getSomething(); // Returns undefined
Example (Swift)
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
webView.evaluateJavaScript("something = 42", completionHandler: nil)
}
Approach 2: Using a custom URL scheme
In JS, redirecting using window.location = "js://webView?hello=world" invokes the native WKNavigationDelegate methods, where the URL query parameters can be extracted. However, unlike the UIWebView, the delegate method is not blocking the JS execution, so immediately invoking evaluateJavaScript to pass a value back to the JS doesn't work here either.
Example (JavaScript)
var something;
function getSomething() {
window.location = "js://webView?question=meaning" // Execution NOT blocking here either :(
return something;
}
getSomething(); // Returns undefined
Example (Swift)
func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler decisionHandler: (WKNavigationActionPolicy) -> Void) {
webView.evaluateJavaScript("something = 42", completionHandler: nil)
decisionHandler(WKNavigationActionPolicy.Allow)
}
Approach 3: Using a custom URL scheme and an IFRAME
This approach only differs in the way that window.location is assigned. Instead of assigning it directly, the src attribute of an empty iframe is used.
Example (JavaScript)
var something;
function getSomething() {
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "js://webView?hello=world");
document.documentElement.appendChild(iframe); // Execution NOT blocking here either :(
iframe.parentNode.removeChild(iframe);
iframe = null;
return something;
}
getSomething();
This nonetheless, is not a solution either, it invokes the same native method as Approach 2, which is not synchronous.
Appendix: How to achieve this with the old UIWebView
Example (JavaScript)
var something;
function getSomething() {
// window.location = "js://webView?question=meaning" // Execution is NOT blocking if you use this.
// Execution IS BLOCKING if you use this.
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "js://webView?question=meaning");
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
return something;
}
getSomething(); // Returns 42
Example (Swift)
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
webView.stringByEvaluatingJavaScriptFromString("something = 42")
}
No I don't believe it is possible due to the multi-process architecture of WKWebView. WKWebView runs in the same process as your application but it communicates with WebKit which runs in its own process (Introducing the Modern WebKit API). The JavaScript code will be running in the WebKit process. So essentially you are asking to have synchronous communication between two different processes which goes against their design.
I found a hack for doing synchronous communication but haven't tried it yet: https://stackoverflow.com/a/49474323/2870783
Edit: Basically you can use the the JS prompt() to carry your payload from the js side to the native side. In the native WKWebView will have to intercept the prompt call and decide if it is a normal call or if it is a jsbridge call. Then you can return your result as a callback to the prompt call. Because the prompt call is implemented in such a way that it waits for user input your javascript-native communication will be synchronous. The downside is that you can only communicate trough strings.
I also investigated this issue, and failed as you. To workaround, you have to pass a JavaScript function as a callback. Native function needs to evaluate the callback function to return result. Actually, this is the JavaScript way, because JavaScript never wait. Blocking JavaScript thread may cause ANR, it's very bad.
I have created a project named XWebView which can establish a bridge between native and JavaScript. It offers binding styled API for calling native from JavaScript and vice versa. There is a sample app.
It's possible to synchronously wait for the result of evaluateJavaScript by polling the current RunLoop's acceptInput method. What this does is allow your UI thread to respond to input while you wait for the the Javascript to finish.
Please read warnings before you blindly paste this into your code
//
// WKWebView.swift
//
// Created by Andrew Rondeau on 7/18/21.
//
import Cocoa
import WebKit
extension WKWebView {
func evaluateJavaScript(_ javaScriptString: String) throws -> Any? {
var result: Any? = nil
var error: Error? = nil
var waiting = true
self.evaluateJavaScript(javaScriptString) { (r, e) in
result = r
error = e
waiting = false
}
while waiting {
RunLoop.current.acceptInput(forMode: RunLoop.Mode.default, before: Date.distantFuture)
}
if let error = error {
throw error
}
return result
}
}
What happens is that, while the Javascript is executing, the thread calls RunLoop.current.acceptInput until waiting is false. This allows your application's UI to be responsive.
Some warnings:
Buttons, ect, on your UI will still respond. If you don't want someone to push a button while your Javascript is running, you should probably disable interacting with your UI. (This is especially the case if you're calling out to another server in Javascript.)
The multi-process nature of calling evaluateJavaScript may be slower than you expect. If you're calling code that is "instant," things may still slow down if you make repeated calls into Javascript in a tight loop.
I've only tested this on the Main UI thread. I don't know how this code will work on a background thread. If there are problems, investigate using a NSCondition.
I've only tested this on macOS. I do not know if this works on iOS.
I was facing a similar issue, i resolved it by storing promise callbacks.
The js that you load in your web view via WKUserContentController::addUserScript
var webClient = {
id: 1,
handlers: {},
};
webClient.onMessageReceive = (handle, error, data) => {
if (error && webClient.handlers[handle].reject) {
webClient.handlers[handle].reject(data);
} else if (webClient.handlers[handle].resolve){
webClient.handlers[handle].resolve(data);
}
delete webClient.handlers[handle];
};
webClient.sendMessage = (data) => {
return new Promise((resolve, reject) => {
const handle = 'm' + webClient.id++;
webClient.handlers[handle] = { resolve, reject };
window.webkit.messageHandlers.<message_handler_name>.postMessage({data: data, id: handle});
});
}
Perform Js Request like
webClient.sendMessage(<request_data>).then((response) => {
...
}).catch((reason) => {
...
});
Receive request in userContentController :didReceiveScriptMessage
Call evaluateJavaScript with webClient.onMessageReceive(handle, error, response_data).