I have a WKWebView in my app. Everything is running smooth except the website I show has the social media logon buttons. The problem is, they display (or try to display) a popup where you allow it to have access to your social media account. I have researched this and tried a few things, but none seem to fit. Here is what I tried:
in viewDidLoad, I tried to enable Javascript on the init:
WKWebViewConfiguration *theConfiguration = [[WKWebViewConfiguration alloc] init];
WKPreferences *thePreferences = [[WKPreferences alloc] init];
thePreferences.javaScriptCanOpenWindowsAutomatically = YES;
thePreferences.javaScriptEnabled = YES;
theConfiguration.preferences = thePreferences;
self.wkWeb = [[WKWebView alloc] initWithFrame:screenRect configuration:theConfiguration];
However, this didn't help me. Then I tried to play with the delegates and go that route. I tried playing around with the createWebViewWithConfiguration method, but that seems like overkill because I had to filter out if they are at the login URL then configure a popup and display it. And then this still wasn't working. I also come in here for the if not main frame logic, cancel the request and reload it in the main view, and that is an easy one-line fix where as this was ballooning into 20+ lines.
This seems like a common problem, but I can't seem to find a lot of information out there. Any ideas?
EDIT - Addition
After playing around with Armands answer, I get closer. This is my createWebViewWithConfig method now, which just displays a white screen overlay. It's like I need something to tell the new popup to load the content. Also - I assume I can place this modal window in my current .m file and it doesn't need a completely new file?
NSURL *url = navigationAction.request.URL;
if(!navigationAction.targetFrame.isMainFrame && [url.absoluteString rangeOfString:#"initiate_"].location != NSNotFound) {
//open new modal webview
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.processPool = [appDelegate getSharedPool];
self.popupLogin = [[WKWebView alloc] initWithFrame:self.wkWeb.bounds configuration:config];
self.popupLogin.frame = CGRectMake(self.wkWeb.frame.origin.x,
self.wkWeb.frame.origin.y,
self.wkWeb.frame.size.width,
self.wkWeb.frame.size.height);
self.popupLogin.navigationDelegate = self;
self.popupLogin.UIDelegate = self;
[webView addSubview:self.popupLogin];
[self.popupLogin loadRequest:navigationAction.request];
return nil;
}
Swift 4 and Swift 5
Create two instance of WKWebView
var webView : WKWebView!
var newWebviewPopupWindow: WKWebView?
Now implement the methods
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
newWebviewPopupWindow = WKWebView(frame: view.bounds, configuration: configuration)
newWebviewPopupWindow!.autoresizingMask = [.flexibleWidth, .flexibleHeight]
newWebviewPopupWindow!.navigationDelegate = self
newWebviewPopupWindow!.uiDelegate = self
view.addSubview(newWebviewPopupWindow!)
return newWebviewPopupWindow!
}
func webViewDidClose(_ webView: WKWebView) {
webView.removeFromSuperview()
newWebviewPopupWindow = nil
}
Also make sure this string in the Info.plist if your code is still not working.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Those login buttons tries to open a new tab, which is not supported by WKWebView. As far as I know, there's only one way to do this.
First add WKUIDelegate method:
-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
NSURL *url = navigationAction.request.URL;
if (navigationAction.targetFrame == nil && [url.absoluteString rangeOfString:#"facebook.com/dialog"].location != NSNotFound) {
//Open new modal WKWebView
}
return nil;
}
In the modal WKWebView add this:
-(void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
{
NSURL *url = navigationResponse.response.URL;
if ([url.absoluteString rangeOfString:#"close_popup.php"].location != NSNotFound) {
//Close viewController
}
decisionHandler(WKNavigationResponsePolicyAllow);
}
close_popup.php is for Facebook. If you need to support multiple social networks, find something unique in their URLs.
In order for this to work, you will have to share Cookies between WKWebViews. To do this you have to init them with shared WKProcessPool.
I prefer to store it in AppDelegate.
WKWebView can be created like this:
- (void)createWebView
{
AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.processPool = delegate.webViewProcessPool;
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
self.webView.UIDelegate = self;
self.webView.navigationDelegate = self;
[self.view addSubview:self.webView];
}
In AppDelegate just lazy load it:
-(id)webViewProcessPool
{
if (!_webViewProcessPool) {
_webViewProcessPool = [[WKProcessPool alloc] init];
}
return _webViewProcessPool;
}
Armands' answer is on the right track but can be improved significantly.
It's important to return the newly created webview (and not nil) if you want the webviews to be able to work with each other (e.g., via window.opener)
Sharing the process pool happens automatically if you use the configuration passed in to the create call
You don't need the extra checks in the createWebViewWith call. (You should still whitelist URLs if you want to prevent arbitrary popup windows or create specific WebViewController types.)
In your main WebViewController (where webView.uiDelegate = self):
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
let webView = WKWebView.init(frame: .zero, configuration: configuration)
//OPTIONAL: check navigationAction.request.url to see what site is being opened as a popup
//TODO HERE: Launch new instance of your WebViewController (within a nav controller)
//and pass it this newly created webView to be added as main subview
return webView
}
You do still want to have the close listener in the new instance of your popup WebViewController (where webView.navigationDelegate = self):
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
if let url = navigationResponse.response.url, url.absoluteString.hasPrefix("https://example.com/something-unique-to-this-popup-when-it-closes") {
dismiss(animated: true, completion: nil)
}
decisionHandler(.allow)
}
Swift4/5 snippet - using MyWebView: extended WkWebView:
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures) -> WKWebView? {
if navigationAction.targetFrame == nil {
let newView = MyWebView.init(frame: webView.frame, configuration: configuration)
newView.customUserAgent = webView.customUserAgent
newView.navigationDelegate = self
newView.uiDelegate = self
self.view.addSubview(newView)
newView.fit(newView.superview!)
newView.configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
newView.configuration.preferences.javaScriptEnabled = true
newView.configuration.preferences.plugInsEnabled = true
newView.load(navigationAction.request)
return newView
}
Add the following handlers and the popup window loads fine.
# pragma mark WKUIDelegate
- (nullable WKWebView *)webView:(WKWebView *)webView
createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration
forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures
(WKWindowFeatures *)windowFeatures
{
if (navigationAction.targetFrame == nil) {
_popupWebViewObject = [[WKWebView alloc] initWithFrame:webView.frame configuration:configuration];
_popupWebViewObject.customUserAgent = webView.customUserAgent;
_popupWebViewObject.UIDelegate = self;
_popupWebViewObject.navigationDelegate = self;
[self.window.contentView addSubview:_popupWebViewObject];
_popupWebViewObject.configuration.preferences.javaScriptCanOpenWindowsAutomatically = YES;
_popupWebViewObject.configuration.preferences.javaScriptEnabled = YES;
_popupWebViewObject.configuration.preferences.plugInsEnabled = YES;
[_popupWebViewObject loadRequest:[navigationAction.request copy]];
return _popupWebViewObject;
}
return nil;
}
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
NSLog(#"%s", __FUNCTION__);
NSLog(#"%# TOKEN : ",webView.URL.absoluteString.lowercaseString);
[self dismissPopWindow:webView];
}
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
[self dismissPopWindow:webView];
decisionHandler(WKNavigationActionPolicyAllow);
}
Related
I am implementing the web view through react-native. Therefore, I use the react-native-webview library. However, "window.open" and "window.close" are not implemented in the react-native-webview.
I need to apply that part of the code for social login. So I found the swift code document. However, I don't know how to change this document to an objective-c code.
object-c partial code of react-native-webview
swift document
// webView list management
var webViews = [WKWebView]()
...
func webView(_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures
) -> WKWebView? {
guard let frame = self.webViews.last?.frame else {
return nil
}
//Creating and returning a web view creates a parent relationship with the current web view.
return createWebView(frame: frame, configuration: configuration)
}
/// ---------- popup close ----------
func webViewDidClose(_ webView: WKWebView) {
destroyCurrentWebView()
}
// Examples of Web View Generation Methods
func createWebView(frame: CGRect, configuration: WKWebViewConfiguration) -> WKWebView {
let webView = WKWebView(frame: frame, configuration: configuration)
// set delegate
webView.uiDelegate = self
webView.navigationDelegate = self
// add view
self.view.addSubview(webView)
self.webViews.append(webView)
return webView
}
// Examples of webview deletion methods
func destroyCurrentWebView() {
// remove from webview lists and screens
self.webViews.popLast()?.removeFromSuperview()
}
How can I apply this code to suit the react-native-webview?
EDIT
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}
WKWebView *popUpWebView = [[WKWebView alloc] initWithFrame: navigationAction.targetFrame configuration: configuration];
popUpWebView.uiDelegate = self;
popUpWebView.navigationDelegate = self;
[_webView addSubview:popUpWebView];
return nil;
}
- (void)webViewDidClose:(WKWebView *)webView {
[_webView removeFromSuperview];
}
I looked at the document and changed it as follows. However, an error occurs when building. I don't know what the problem is.
As I mentioned in my comments, I am not sure if this will work for React Native but this Obj-C code is the same as your swift code and should compile
In your .h file
Your .h file will probably need to be the same as RNCWebView.h and you might need to remove anything unwanted / unused
In your .m file
Similarly, your .m will be similar to RNCWebView.m and remove what you don't use.
Then as per your swift code, these are the updated Obj C versions of those functions
- (WKWebView *)webView:(WKWebView *)webView
createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration
forNavigationAction:(WKNavigationAction *)navigationAction
windowFeatures:(WKWindowFeatures *)windowFeatures
{
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}
if ([webViews count] == 0) {
return nil;
}
WKWebView *currentWebView = [webViews lastObject];
WKWebView *popUpWebView = [[WKWebView alloc] initWithFrame: currentWebView.frame
configuration: configuration];
popUpWebView.UIDelegate = self;
popUpWebView.navigationDelegate = self;
[webView addSubview:popUpWebView];
return popUpWebView;
}
- (void)webViewDidClose:(WKWebView *)webView
{
[webView removeFromSuperview];
}
Update
If the webViews variable from the original swift code is unused / not needed, you probably need to update the webView createWebViewWithConfiguration as follows:
- (WKWebView *)webView:(WKWebView *)webView
createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration
forNavigationAction:(WKNavigationAction *)navigationAction
windowFeatures:(WKWindowFeatures *)windowFeatures
{
if (!navigationAction.targetFrame.isMainFrame) {
[webView loadRequest:navigationAction.request];
}
WKWebView *popUpWebView = [[WKWebView alloc] initWithFrame: webView.bounds
configuration: configuration];
popUpWebView.UIDelegate = self;
popUpWebView.navigationDelegate = self;
[webView addSubview:popUpWebView];
return popUpWebView;
}
Finally, just to clarify:
The header does not need to be the same as mine, I just gave you an example if you were subclassing a UIViewController. You probably need to follow the header and implementation file defined here
My goal was to convert your swift code into Obj C code that would compile, I cannot say if it is right for React Native however.
How i can remove or hide a page counter from WKWebview when load a PDF file?
I tried the solutions that are on this link (using iOS 13.3, Swift 4.2.), but they don't work.
With the help of a friend, we found a solution.
In the delegate method, we can hide the UIView which contains the page counter:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
hidePDFPageCount(webView)
}
func hidePDFPageCount(_ webView: WKWebView){
guard let last = webView.subviews.last else {
return
}
last.isHidden = true
}
Objective-C version, tested on iOS 14:
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
UIView *lastView = webView.subviews.lastObject;
if (lastView != nil && ![lastView isKindOfClass:[UIScrollView class]]) {
lastView.hidden = YES;
}
}
Compared to the accepted answer I had to add:
![lastView isKindOfClass:[UIScrollView class]]
Because the last view is not always the page counter.
Is there a way to disable interactions on a webview?
So that the user can not go any further than the webview that is loaded?
EDIT:
Disabling UserInteractions is not a solution because the website still has to be scrollable.
Implement the WKNavigationDelegate protocol:
#interface ViewController () <WKNavigationDelegate>
Set your WKWebView's navigationDelegate property:
self.wkWebView.navigationDelegate = self;
Then implement the policy for the URL(s) that you want to restrict:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if ([navigationAction.request.URL.absoluteString containsString:#"somedomain.com/url/here"]) {
decisionHandler(WKNavigationActionPolicyAllow);
}
else {
decisionHandler(WKNavigationActionPolicyCancel);
}
}
The WKNavigationDelegate solution only prevents the user from following links. I also have form controls that I want to prevent interaction with, while still allowing the page to be scrolled. Eventually I figured out that this could be achieved by disabling the subviews of the web view's scroll view:
Swift
self.webView.scrollView.subviews.forEach { $0.isUserInteractionEnabled = false }
Objective-C
for (UIView *subview in self.webView.scrollView.subviews)
{
subview.userInteractionEnabled = NO;
}
First, you have to give the delegate to your webkit then add below code.
Swift 5.0
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
Activity.stopAnimating()
let javascriptStyle = "var css = '*{-webkit-touch-callout:none;-webkit-user-select:none}'; var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; style.appendChild(document.createTextNode(css)); head.appendChild(style);"
webView.evaluateJavaScript(javascriptStyle, completionHandler: nil)
}
What this code will do, we add programmatically css that will disable interaction in webview
These javascript lines will disable long presses and link touches by overriding the HTML. Frame based things like embedded youtube videos will still work.
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.style.webkitUserSelect='none'")
webView.evaluateJavaScript("document.documentElement.style.webkitTouchCallout='none'")
webView.evaluateJavaScript("var elems = document.getElementsByTagName('a'); for (var i = 0; i < elems.length; i++) { elems[i]['href'] = 'javascript:(void)'; }")
}
You can do it as follow.
//-----------------------------------------------------------------------
#pragma mark - UIWebView Methods
//-----------------------------------------------------------------------
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
return YES;
}
//----------------------------------------------------------------
- (void)webViewDidStartLoad:(UIWebView *)webView {
//disable user interaction
}
//----------------------------------------------------------------
- (void)webViewDidFinishLoad:(UIWebView *)webView{
//enable user interaction
}
//----------------------------------------------------------------
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{
//enable user interaction
}
In my webView's delegate method I am checking for the condition when webview loading fully completes by using this code:
if ([[webView stringByEvaluatingJavaScriptFromString:#"document.readyState"] isEqualToString:#"complete"]) {
//Code to handle other things
//Sometimes after this block shouldStartLoadWithRequest is called
}
Sometimes even control goes to if block even then -shouldStartLoadWithRequest this method is being called and my code breaks.
Is their any other alternative? I also tried webview.isLoading but this also doesn't work.
You could use javascript and the userContentController as a way for your page to message back to your native code that it is loaded and ready. When you setup your WkWebView, setup the handler like this:
WKWebViewConfiguration *theConfiguration = [[WKWebViewConfiguration alloc] init];
WKUserContentController *controller = [[WKUserContentController alloc] init];
[controller addScriptMessageHandler:self name:#"observe"];
theConfiguration.userContentController = controller;
CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
CGRect frame = self.view.frame;
frame.origin.y = statusBarFrame.size.height;
frame.size.height -= statusBarFrame.size.height;
self.primaryWebView = [[WKWebView alloc] initWithFrame:frame configuration:theConfiguration];
Then add a method to your ViewController called userContentController, like this:
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Check the details in message.body for what your javascript said
NSLog(#"javascript message: %#", message.body);
});
}
I have found one workaround to stop any further execution of scripts after didFinishLoading.
I used following condition to stop reqeust:
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
if(navigationType == UIWebViewNavigationTypeLinkClicked)
{
//my code
}
}
I m trying to log an error when a page component loaded in a WKWebView fail, but didFailProvisionalNavigation is not called.
I removed a css file from my page to test with it, but no result.
When I load the page on Google Chrome I can find the missing file
The WKWebView is initialised as:
#interface MyWebView : UIViewController <WKNavigationDelegate>
#property(strong,nonatomic) WKWebView *loginWebView;
#end
The .m file
#implementation MyWebView
- (void) initWebView {
// Set and Load the WebView
self.webView = [[WKWebView alloc] initWithFrame:self.view.frame];
self.webView.navigationDelegate = self;
[self.webView loadRequest:request];
[self.view addSubview:self.webView];
}
Then I implemented the methods bellow:
webView:decidePolicyForNavigationAction:decisionHandler:
webView:didStartProvisionalNavigation:
webView:didFinishNavigation:
webView:didFailNavigation:withError:
webView:didFailProvisionalNavigation:withError:
The first three methods get called, but neither didFailNavigation nor didFailProvisionalNavigation are called
By looking at the documentation for didFailProvisionalNavigation we have
Called when an error occurs while the web view is loading content.
Well, a content has failed (css file) and the method is not called, how can I do this?
PS : I also tried using UIWebView!
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSInteger statusCode = ((NSHTTPURLResponse *)navigationResponse.response).statusCode;
NSLog(#"statusCode:%ld", statusCode);
if (statusCode/100 == 4 || statusCode/100 == 5) {
NSLog(#"webview error:%#", navigationResponse.response);
}
decisionHandler(WKNavigationResponsePolicyAllow);
}
this works!