OrientationObserver in UIWebView - javascript

I have the following structure
UIViewController1
--> UIWebView
I run a local HTML File in WebView from Documents-Folder. Within the HTML I have a short JavaScript snippet, which checks the interface Orientation via orientationObserver.
The UIViewController1 is locked to landscape.
OrientationObserver always returns "portrait", should return "landscape". I don't know what causes the wrong return value.
What could cause the orientationObserver to return the wrong orientation in this constellation ?
I'm really stuck here, and I need to get the right orientation returned.
Any help is appreciated.

Finally, after four days of banging my head against the iMac i found a solution for this.
In UIWebView there is no window.orientation available (i wonder why!?). If a JavaScript request the value, it gets "0" what means the orientation is "portrait".
You can go around the problem if you add the "orientation" Setter to the window.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)req navigationType:(UIWebViewNavigationType)navigationType {
[webView stringByEvaluatingJavaScriptFromString:#"window.__defineGetter__('orientation', function(){return 90;});"];
return YES;
}
This implements a getter within window and the JavaScript get's the return value. If you want your app to respond to interface orientation changes, you can set the values in the delegates again to the correct values (-90, 0, 90 or 180).

Related

Access javascripts functions from source WKWebView (window.opener)

In my iOS app, I have 2 WKWebView controls to simulate parent/child relationship of 2 tab/window in our web app so that we can reuse all those javascripts. Problem is that there is no generic communication channel between these 2 WKWebViews.
Web Architecture:
- The first/master window loads our internal javascript functions with links opening in second/child window.
- The second/child window loads third party web content.
- The third party content communicates with our system using javascript form source window (using window.opener / window.parent API bridge).
Now, in the native app, this is what I'm doing to reuse existing javascripts and simulate web archtecture -
a. Below js call from the webpage in 1st WKWebView -
window.open("http://localhost/child.html", "myWindow", "width=200,height=100");
b. The WKUIDelegate intercepts this call (a) and opens 2nd WKWebView -
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
// Here, window.open js calls are handled by loading the request in another/new web view
UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
WebViewController *webViewController = [mainStoryboard instantiateViewControllerWithIdentifier:#"WebViewController"];
webViewController.loadURLString = navigationAction.request.URL.absoluteString;
// Show controller without interfering the current flow of API call, thus dispatch after a fraction of second
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.navigationController pushViewController:webViewController animated:YES];
});
return nil;
}
c. In web/computer browser, the child window of web app is able to access source js functions using window.opener and window.parent
How to achieve the same functionality in WKWebViews? In other words, js functions in 2nd WKWebView could call js functions from parent/source WKWebView?
You definitely should not return nil from webView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:, but an instance of your WKWebView.
Unfortunately, I have no way to verify this right now. But as far as I remember, the trick is also to not ignore WKWebViewConfiguration argument from webView:createWebViewWithConfiguration:forNavigationAction:windowFeatures: delegate method, but create a second WKWebView instance, passing this argument as one of the initWithFrame:configuration: parameters.
Also, according Apple documentation, looks like you don't need to load the request yourself if you're returning correct web view instance. WebKit should handle this for you.
Overall your code should look similar to:
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
WKWebView *childWebView = [[WKWebView alloc] initWithFrame:CGFrameZero configuration:configuration];
WebViewController *webViewController = [[WebViewController alloc] initWithWebView: childWebView];
// looks like this is no longer required, but I can't check it now :(
// [childWebView loadRequest:navigationAction.request];
// Show controller without interfering the current flow of API call, thus dispatch after a fraction of second
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.navigationController pushViewController:webViewController animated:YES];
});
return childWebView;
}
A simpler way to accomplish your task will be to have one WKWebView, and use HTML to separate header and detail. The simplest solution is to have one HTML document with the header and all of the details, but if you need to add details dynamically that can also be done use the DOM. Reusing Javascript for both the header and detail will be easiest with this approach.

Injecting Javascript inside IOS webview depending on scenario like in Android

I am currently trying to port an existing Android application that we have to an IOS app. The issue here is that a large portion of our Android app was made using a lot of webviews with custom Javascript code called from Android.
If anyone is not familiar with it, the code for android would go something like this:
mWebView.addJavascriptInterface(WebInterface(this), "Android");
// more settings or whatever
mWebView.loadUrl("yourUrl.html.whatever");
Then the interface to the Javascript code reads like this:
public class WebInterface {
Context mContext;
public WebInterface(Context c) { this.mContext = c;}
#JavascriptInterface
public String returnMessage() {
Toast.makeText(mContext, "This is being called from the interface", Toast.LENGTH_SHORT).show();
return "This is being called from the interface";
}
}
At this point any time we want to access the code for the returnMessage() method inside of the webview we simply call:
var getContentsFromIntefrace = Android.returnMessage()
// getcontentsFromInterface is now "This is being called from the interface"
These targeted calls(Android.returnMessage()) allow me to call the interface code only when I need to do so, in contrast, if I was to use Android APIĀ“s evaluate javascript methods I would have to watch for the code executing all over the place e.g if my evaluatejavascript Android API method sets a variable to something it will do so in any other part of the code where said variable exist(please do correct me if I am wrong, it seems fuzzy at first but it is an issue thus far)
===== The issue I am having with IOS
The above is merely an explanation of what I need to do, to show what I am attempting on IOS I will demonstrate the code:
In IOS if I want to se functions that call on swift code inside my controller I have to set it in JS as:
function testButton() {
webkit.messageHandlers.callbackHandlerTestSend.postMessage({action: "testButtonAction", data: {name: "E.A.P"}});
}
The above sends a named message('callbackHandlerTestSend') with the key value pairs of action and data which can be parsed on the appropriate swift delegate as:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if (message.name == "callbackHandlerTestSend") {
guard let body = message.body as? [String:Any] else {
print("Unable to do connection")
return
}
for(key, value) in body {
print("\(key) : \(value)")
if(key == "action"){
if(value as! String == "testButtonAction") {
intCounter += 1
print("The testButtonAction message action has been called")
webView.evaluateJavaScript("receiverP.innerHTML = 'It works \(intCounter)'", completionHandler: nil)
webView.evaluateJavaScript("receiverFunction('Message test')", completionHandler: nil)
// FIXME: evaluate JS might call on any other instance of receiverP, this is an issue
}
}
}
}// body of if
}// end of delegate
This is currently the only way in which I have been able to send Swift code back to the webview, by injecting the Javascript code with webView.evaluateJavascript()
My problem is that I need to capture everything with the whole:
webkit.messageHandlers.callbackHandlerTestSend.postMessage({action: "testButtonAction", data: {name: "E.A.P"}});
And it becomes bothersome to continue to do it as well as to have to individually parse each call for the proper parameters to be set while at the same time trying to not inject javascript code without messing something else up. So calling javascript and sending javascript code seems extremely cumbersome at this point and I would like to know if there is something I could do for the code to look more like the Android counterpart.
Any advice on what cool external libs out there I could use or in how could I modify the code to this would be greatly appreciated, I have been trying to find something in the IOS docs but have not found anything that I could use.
-- Edit:
At this time I am seriously considering an alternative such as React Native since it seems as the fastest route to develop an application that requires heavy usage of web like content as well as having limited knowledge of IOS in contrast to Android. I would still want to know if there is a viable solution to the above.

Suspend Javascript execution in UIWebView

I am successfuly using an invisible UIWebView as an execution engine for Javascript snippets. Contrary to what certain pages suggest, it does not require to be visible. If declared dead simply as
UIWebView* myJSExecutor = [UIWebView new];
not only it executes any stringByEvaluatingJavaScriptFromString: thrown at it, it even bubbles alert() to the desktop! (Not that it's something you would like to do often) Disclaimer: tested only on iOS 5+
Now, I have another normally visible UIWebView with normal webpage content, and I want it to suspend JavaScript execution: that is stop acting on timer and DOM events. I thought that removeFromSuperview with stopLoading and setting delegate = nil would do the trick. But no, as soon as I add the UIWebView back to some visible view, I can see that the timers were running all the time.
I understand the schizophreny of my requirement. I appreciate it working in the background on one hand (contrary to some observations), but I want to suspend it on the other hand. But still I would like to ask if there is any, perhaps even private way to suspend it. Safari for iOS is capable of suspending browser tabs in the background. Chrome for iOS can't, which may be a sad negative proof :(
If you're looking for crazy, I have an idea.
The only way I know to pause JavaScript is to show an alert, confirm, prompt, or sending an ajax request with async=false.
Is the alert hidden when the UIWebView is hidden? If so, you could send an alert when you want it to freeze. The user would have to dismiss it when you showed the view again:
[web stringByEvaluatingJavaScriptFromString:#"alert('Press OK to continue...')"];
Or maybe you could dismiss it programmatically. I'm sure you'd agree that this whole suggestion is bound for trouble, but it's a thought.
Do you need a full UIWebView? If not you could just invoke JavaScriptCore directly.
- (NSString *)runJS:(NSString *)aJSString
{
JSContextRef ctx = JSGlobalContextCreate(NULL);
JSStringRef scriptJS = JSStringCreateWithUTF8CString([aJSString UTF8String]);
JSValueRef exception = NULL;
JSValueRef result = JSEvaluateScript([self JSContext], scriptJS, NULL, NULL, 0, &exception);
JSGlobalContextRelease(ctx);
...
This would give you more control over the entire JS runtime, but unfortunately I've not found an API to suspend execution of timers other than releasing the whole context.
For a more complete example of how to use JavaScriptCore see https://github.com/jfahrenkrug/AddressBookSpy/blob/master/AddressBookSpy/AddressBookSpy/ABSEngine.m
This might sound obvious, but if you can edit the JS you're running in the UIWebView, can't you just store a global variable in the JS called 'paused', and check that when your timers are firing?
var paused = false;
var thread = setInterval(function() {
if(!paused) {
// normal timer checking etc
}
}, 300);
Then just fire [webView stringByEvaluatingJavaScriptFromString:#"paused = true;"] when you remove the webview, and set paused = false when you add it back to the view.

DOM is not being re-rendered after updating based on touch events (android, phone gap) [duplicate]

When ever I make a css class change, the changes don't always appear. This appears to happen when I have a touch event that adds something like a down class name to a button. The button doesn't update, and neither does anything else on the page. It's very inconstant in when it works. I've also noticed that sometimes my elements appear white with no content or anything. This is very frustrating!
Note:
There's a better solution as of Android 4.4+. It's a drop-in WebView replacement called CrossWalk. It uses the latest Chromium-kit and it's fantastic. You can read up about it here: crosswalk-project.org
Also, it appears that since Android 4.4, the invalidate() solution is no longer necessary and you can get away with using some of the other safer answer. I would only use this invalidate() approach as a last-ditch effort.
I'm answering my own question to hopefully help people out with the same issues as me.
I've tried several methods to making things better, even the all notorious -webkit-transform: translate3d(0,0,0); Even that didn't work all too well.
Let me share with you what did work.
First, use the most recent API. I'm using API 15. In your AndroidManifest.xml, make sure to enable hardware acceleration. If your version of API does not support this, then move on to the next bit.
If your version does support it, you can enable it by modifying your manifest:
<application
...
android:hardwareAccelerated="true">
Also, make sure that your manifest has the minimum supported API to the one that you are using. Since I'm using API 15, this is what my manifest has:
<uses-sdk
android:minSdkVersion="15"
android:targetSdkVersion="15" />
(Update: you should now modify that values in your build.gradle)
Now, in your primary CSS file for what will be presented in a WebView, add something like this:
body div {
-webkit-transform: translate3d(0,0,0);
}
Add onto the body div bit with any other element types you have; you can probably exclude images, ul, li, etc. The reason for applying this CSS style to everything is just by trial and error, and I found that brute-applying it to everything appears to work the best. With a larger DOM tree, you may need to be more-specific. I'm not sure what the specification would be, however.
When you instantiate your WebView, there are some settings you'll want to set:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.loadUrl("file:///android_asset/www/index.html");
appView.getSettings().setRenderPriority(RenderPriority.HIGH);
appView.getSettings()
.setPluginState(WebSettings.PluginState.ON_DEMAND);
}
Second to last, but crucial bit: I was reading through the source code for the WebView class and found this little tiny comment about force redrawing. There is a static final boolean, that when set to true will force the view to always redraw. I'm not huge on Java syntax, but I don't think you can directly change a static final attribute of a class. So what I ended up doing was I extended the class like so:
import org.apache.cordova.CordovaWebView;
import android.content.Context;
import android.graphics.Canvas;
public class MyWebView extends CordovaWebView {
public static final String TAG = "MyWebView";
public MyWebView(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Warning! This will cause the WebView to continuously be redrawn
// and will drain the devices battery while the view is displayed!
invalidate();
}
}
Keep in mind, I'm using Cordova/PhoneGap, so I had to extend from the CordovaWebView. If you see in the onDraw method, there is a call to invalidate. This will cause the view to redraw constantly. I highly recommend adding in logic to only redraw when you need it, however.
There is one final step, if you are using Cordova. You have to tell PhoneGap to use your new WebView class instead of their own WebView class. In your MainActivity class, add this:
public void init(){
CordovaWebView webView = new MyWebView(MainActivity.this);
super.init(webView, new CordovaWebViewClient(this, webView), new CordovaChromeClient(this, webView));
}
That's it! Try and run your app and see if everything is much smoother. Before doing all of these changes, the pages would appear white, no CSS changes would be applied until after tapping on the screen again, animations were super choppy or not even noticeable. I should mention that the animations are still choppy, but far much less choppy than before.
If anyone has anything to add to this, just comment under. I'm always open for optimizations; and I'm fully aware there may be better ways to do what I have just recommended.
If my above solution did not work for you, could you describe your specific situation and what results you are seeing with Androids WebView?
Lastly, I have enabled this answer as a "community wiki", so anyone and everyone, feel free to make adjustments.
Thanks!
Edit:
With the most latest PhoneGap, you'll need to have your init() method look more like this:
public void init(){
CordovaWebView webView = new MyWebView(MainActivity.this);
super.init(webView, new IceCreamCordovaWebViewClient(this, webView), new CordovaChromeClient(this, webView));
}
I implemented kyle's solution and it solved the problem. Howewer I noticed a huge battery drain on android 4.0.4 when the app was open. Also after the change I had users complaining that the swiftKey keyboard was not working with my app anymore.
Every change in my app are triggered by a user action so I came up with a modified version that only trigger invalidate() after a touchEvent:
Handler handler = new Handler();
public boolean onTouchEvent (MotionEvent event){
super.onTouchEvent(event);
handler.postDelayed(triggerInvalidate, 60);
handler.postDelayed(triggerInvalidate, 300);
return true;
}
private Runnable triggerInvalidate=new Runnable(){
public void run(){
invalidate();
}
};
Never did any programming with Java so there might be a better solution to do this.
re: the redraw problem, you can force a redraw by reading a property from the element
so say you do this:
$('#myElement').addClass('foo'); // youre not seeing this take effect
if you do this afterwards:
$('#myElement').width();
it will force a redraw.
This way you can be selective instead of redrawing the whole page all the time, which is expensive
For me this issue was only happening on Samsung devices. I was able to fix it by disabling Hardware Acceleration for WebViews:
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Hope it helps.
As pointed out above and elsewhere - overriding View.onDraw() and calling View.invalidate will make for an unhappy battery / app performance will drop. You can also do a manual invalidate call ever x ms like so
/**
* Due to bug in 4.2.2 sometimes the webView will not draw its contents after the data has loaded.
* Triggers redraw. Does not work in webView's onPageFinished callback for some reason
*/
private void forceWebViewRedraw()
{
mWebView.post(new Runnable() {
#Override
public void run()
{
mWebView.invalidate();
if(!isFinishing())
mWebView.postDelayed(this, 1000);
}
});
}
I tried putting an invalidate call in WebViewClient.onPageLoaded() but this does not seem to work. While my solution could be better its simple and it works for me (im just showing a twitter login)
Please also see my answer at WebView fails to render until touched Android 4.2.2 , The idea is to export #Olivier's function to JavaScript so you can trigger invalidates from JS in sensible parts ... My god, yet another more hack !! :)
I had this problem because onPageStared and onPageFinished I show and hide a loading animation.
Since was injecting JS do my menus and stuff from the original website and noticed that injecting JS on onPageFinished force the webview to draw contents. Here is a example of what you can add at the end of onPageFinished
view.loadUrl("javascript:(function() { var select = document.getElementsByClassName('something')[0]\r\n" +
" if(select)" +
" select.style.display = 'none';})()");
Just clear the cache of webView in oncreate() and it works for me.
webView.clearCache(true);
You should enable hardware by modifying your manifest:
<application
...
android:hardwareAccelerated="true">
And in your activity onResume()
webView.postDelayed(() -> {
if (!isFinishing() && webView != null) {
webView.setLayerType(View.LAYER_TYPE_NONE, null);
}
}, 1000);
And in your onPause()
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

Strange problem with stringByEvaluatingJavaScriptFromString not working with NSUSerDefault value...please help!

I am working on a bookmarking feature for a book reader iOS app I have. The way it is setup now, the user scrolls through a view, and when they want to save, or bookmark their spot before they leave, they hit a save button on the bottom toolbar.
When they hit this save button, the saved action is called:
-(IBAction) savePlace:(id)sender{
int pageYOffset = [[webView stringByEvaluatingJavaScriptFromString:#"window.pageYOffset"] intValue];
NSLog(#"%d Set Y Value", pageYOffset);
[[NSUserDefaults standardUserDefaults] setSavedSpot:pageYOffset];
}
Surprisingly enough, I have got this part working. I can hit the save button, the console will read say 200 for where I'm at on the screen, and then when I leave and come back, 200 is again printed out to the console thanks to this method that is called by NSUSerDefaults when the app loads:
- (void) setCurrentSpot:(NSUInteger)ySpot {
NSLog(#"%d Saved Y Value", ySpot);
[webView stringByEvaluatingJavaScriptFromString: [NSString stringWithFormat: #"window.scrollTo(0, %d);", ySpot]];
}
BUT! Nothing happens...I know for a fact that I am saving and correctly retrieving the correct Y-axis value, but when that JavaScript method is called, it won't fire.
To further complicate things, I went ahead and made a custom IBAction that accesses that same ySpot value, and used the exact same JavaScript method to move the view to position 200, and it works perfectly!
What am I missing? I don't see what is going on. Thanks
edit: misread your question,
Perhaps the method you are calling to call that javascript is being called too fast? and not giving the UIWebview time to initialize and react? if it works when you wait on the webview to come upand then call it, that is the only thing i could see.

Categories