-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
940 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,161 @@ | ||
# pythonista-webview | ||
WKWebView implementation for Pythonista | ||
|
||
WKWebView implementation for Pythonista. | ||
|
||
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](https://github.com/mikaelho/youey/blob/master/youey/wkwebview.py) 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 (Pythonista forum handles). | ||
|
||
## 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 | ||
|
||
### Synchronous vs. asynchronous JS evaluation | ||
|
||
Apple's WKWebView only provides an async Javascript evaliation function. This is available as an `eval_js_async` method, with an optional `callback` argument that will be called with a single argument containing the result of the JS evaluation (or None). | ||
|
||
We also provide a synchronous `eval_js` method, which essentially waits for the callback before returning the result. For this to work, you have to call the `eval_js` method outside the main UI thread, e.g. from a method decorated with `ui.in_background`. | ||
|
||
### Handling page scaling | ||
|
||
UIWebView had a property called `scales_page_to_fit`, WKWebView does not. See below for the various `disable` methods that can be used instead. | ||
|
||
## Additional features and notes | ||
|
||
### http allowed | ||
|
||
Looks like Pythonista has the specific plist entry required to allow fetching non-secure http urls. | ||
|
||
### Other url schemes | ||
|
||
If you try to open a url not natively supported by WKWebView, such as `tel:` for phone numbers, the `webbrowser` module is used to open it. | ||
|
||
### 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 Apple data detectors are active for WKWebView. You can activate them by including one or a tuple of the following values as the `data_detectors` argument to the constructor: NONE, PHONE_NUMBER, LINK, ADDRESS, CALENDAR_EVENT, TRACKING_NUMBER, FLIGHT_NUMBER, LOOKUP_SUGGESTION, ALL. | ||
|
||
For example, activating just the phone and link detectors: | ||
|
||
v = WKWebView(data_detectors=(WKWebView.PHONE_NUMBER, WKWebView.LINK)) | ||
|
||
### 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 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 JS postMessage must have a parameter, and the message argument to the Python handler is always a string version of that parameter. 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. | ||
|
||
Following two convenience methods are also available: | ||
|
||
* `add_style(css)` to add a style tag containing the given CSS style definition. | ||
* `add_meta(name, content)` to add a meta tag with the given name and content. | ||
|
||
### Making a web page behave more like an app | ||
|
||
These methods set various style and meta tags to disable typical web interaction modes: | ||
|
||
* `disable_zoom` | ||
* `disable_user_selection` | ||
* `disable_font_resizing` | ||
* `disable_scrolling` (alias for setting `scroll_enabled` to False) | ||
|
||
There is also a convenience method, `disable_all`, which calls all of the above. | ||
|
||
Note that disabling user selection will also disable the automatic data detection of e.g. phone numbers, described earlier. | ||
|
||
### Javascript debugging | ||
|
||
Javascript errors and console messages are sent to Python side and printed to Pythonista console. Supported JS console methods are `log`, `info`, `warn` and `error`. | ||
|
||
For further JS debugging and experimentation, there is a simple convenience command-line utility that can be used to evaluate load URLs and evaluate javascript. If you `present` your app as a 'panel', you can easily switch between the tabs for your web page and this console. | ||
|
||
Or you can just create a WKWebView manually for quick experimentation, like in the usage example below. | ||
|
||
>>> from wkwebview import * | ||
>>> v = WKWebView(name='Demo') | ||
>>> WKWebView.console() | ||
Welcome to WKWebView console. Evaluate javascript in any active WKWebView. Special commands: list, switch #, load <url>, quit | ||
js> list | ||
0 - Demo - | ||
js> load http://omz-software.com/pythonista/ | ||
js> list | ||
0 - Demo - Pythonista for iOS | ||
js> document.title | ||
Pythonista for iOS | ||
js> quit | ||
>>> v2 = WKWebView(name='Other view') | ||
>>> WKWebView.console() | ||
Welcome to WKWebView console. Evaluate javascript in any active WKWebView. Special commands: list, switch #, load <url>, quit | ||
js> list | ||
0 - Demo - Pythonista for iOS | ||
1 - Other view - | ||
js> switch 1 | ||
js> load https://www.python.org | ||
js> document.title | ||
Welcome to Python.org | ||
js> window.doesNotExist.wrongFunction() | ||
ERROR: TypeError: undefined is not an object (evaluating 'window.doesNotExist.wrongFunction') (https://www.python.org/, line: 1, column: 20) | ||
None | ||
js> quit | ||
|
||
### Setting a custom user agent | ||
|
||
WKWebView has a `user_agent` property that can be used to retrieve or set a value reported to the server when requesting pages. | ||
|
||
### 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 |
Oops, something went wrong.