The underlying component used to implement ui.WebView in Pythonista is UIWebView, which has been deprecated since iOS 8. This module implements a Python webview API using the current iOS-provided view, WKWebView. Besides being Apple-supported, WKWebView brings other benefits such as better Javascript performance and an official communication channel from Javascript to Python. This implementation of a Python API also has the additional benefit of being inheritable.
Available as a single file on GitHub. Run the file as-is to try out some of the capabilities; check the end of the file for demo code.
Credits: This would not exist without @JonB and @mithrendal.
Basic usage
WKWebView matches ui.WebView API as defined in Pythonista docs. For example:
v = WKWebView()
v.present()
v.load_html('<body>Hello world</body>')
v.load_url('http://omz-software.com/pythonista/')
For compatibility, there is also the same delegate API that ui.WebView has, with webview_should_start_load etc. methods.
Mismatches with ui.WebView
eval_js – synchronous vs. asynchronous JS evaluation
Apple's WKWebView only provides an async Javascript evaliation function. This is available as an eval_js_async, with an optional callback argument that will called with a single argument containing the result of the JS evaluation (or None).
Here we also provide a synchronous eval_js method, which essentially waits for the callback behind the scenes before returning the result. For this to work, you have to call the method outside the main UI thread, e.g. from a method decorated with ui.in_background.
scales_page_to_fit
UIWebView had such a property, WKWebView does not. It is likely that I will implement an alternative with script injection.
Additional features and notes
http allowed
Looks like Pythonista has the specific plist entry required to allow fetching non-secure http urls.
Swipe navigation
There is a new property, swipe_navigation, False by default. If set to True, horizontal swipes navigate backwards and forwards in the browsing history.
Note that browsing history is only updated for calls to load_url - load_html is ignored (Apple feature that has some security justification).
Data detection
By default, no data detectors are active for WKWebView. If there is demand, it is easy to add support for activating e.g. turning phone numbers automatically into links.
Messages from JS to Python
WKWebView comes with support for JS to container messages. Use this by subclassing WKWebView and implementing methods that start with on_ and accept one message argument. These methods are then callable from JS with the pithy window.webkit.messageHandler.<name>.postMessage call, where <name> corresponds to whatever you have on the method name after the on_ prefix.
Here's a minimal yet working example:
class MagicWebView(WKWebView):
def on_magic(self, message):
print('WKWebView magic ' + message)
html = '<body><button onclick="window.webkit.messageHandlers.magic.postMessage(\'spell\')">Cast a spell</button></body>'
v = MagicWebView()
v.load_html(html)
Note that the message argument is always a string. For structured data, you need to use e.g. JSON at both ends.
User scripts a.k.a. script injection
WKWebView supports defining JS scripts that will be automatically loaded with every page.
Use the add_script(js_script, add_to_end=True) method for this.
Scripts are added to all frames. Removing scripts is currently not implemented.
Javascript exceptions
WKWebView uses both user scripts and JS-to-Python messaging to report Javascript errors to Python, where the errors are simply printed out.
Customize Javascript popups
Javascript alert, confirm and prompt dialogs are now implemented with simple Pythonista equivalents. If you need something fancier or e.g. internationalization support, subclass WKWebView and re-implement the following methods as needed:
def _javascript_alert(self, host, message):
console.alert(host, message, 'OK', hide_cancel_button=True)
def _javascript_confirm(self, host, message):
try:
console.alert(host, message, 'OK')
return True
except KeyboardInterrupt:
return False
def _javascript_prompt(self, host, prompt, default_text):
try:
return console.input_alert(host, prompt, default_text, 'OK')
except KeyboardInterrupt:
return None