在iOS中更改JavaScript警告对话框标题

36

在 iPhone 的 UIWebview 中,是否有可能更改 JavaScript 警告框的标题?


4
我想在iOS中更改网页的JavaScript警告对话框标题。什么是BIJ? - VK.Dev
1
这是一个非常好的问题,而且很难找到关于这个主题的信息。如果您给了负评,能否提供一些有关如何改进问题的信息? - Robert Karl
7个回答

58

基本上您不能更改标题,但最近我发现一种方法可以显示一个没有标题的警告对话框。

var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", 'data:text/plain,');
document.documentElement.appendChild(iframe);
window.frames[0].window.alert('hello');
iframe.parentNode.removeChild(iframe);

2
在iOS Safari浏览器中运行良好...但在应用程序中表现不佳。 - MizAkita
它在iOS 7中运行良好,但在iOS8中会以粗体字显示文本。有什么想法吗? - Ramesh Sangili
在Mozilla(版本41.0.2)中会抛出“未捕获的异常:内存不足”的错误。有什么想法吗? - Nits
有时在 iOS Webview 中使用 "removeChild(iframe)" 会改变已存在的 div 的顺序。 - Alan Wang
对于那些想知道的人,MacOS Safari 没有警报的标题。这也不会影响 MacOS Chrome 的警报。 - IcyIcicle

15

我提供了三种解决这个问题的方案。生产代码中最好的解决方案是由@Martin H.描述的那个。我的唯一补充是提供了一个具体的示例,展示如何实现它。

我的其他解决方案依赖于UIWebView的未记录行为,但不需要对内容/JS进行任何更改。

解决方案1: 这里有一个具体的示例,展示了@Martin H.提出的技术。这很可能是最好的解决方案,因为它不依赖于UIWebView/UIAlertView未记录的行为。

@interface TSViewController () <UIWebViewDelegate>
@end

@implementation TSViewController

- (UIWebView*) webView
{
    return (UIWebView*) self.view;
}

- (void) loadView
{
    UIWebView* wv = [[UIWebView alloc] initWithFrame: CGRectMake(0, 0, 320, 480)];
    wv.delegate = self;
    wv.scalesPageToFit = YES;

    self.view = wv;
}

- (void) viewDidLoad
{
    [super viewDidLoad];

    [self.webView loadRequest: [NSURLRequest requestWithURL: [NSURL URLWithString: @"http://www.craigslist.org"]]];
}

- (void) webViewDidFinishLoad: (UIWebView *) webView
{
    // inject some js to re-map window.alert()
    // ideally just do this in the html/js itself if you have control of that.
    NSString* js = @"window.alert = function(message) { window.location = \"http://action/alert?message=\" + message; }";
    [webView stringByEvaluatingJavaScriptFromString: js];

    // trigger an alert.  for demonstration only:
    [webView stringByEvaluatingJavaScriptFromString: @"alert('hello, world');" ];
}


- (BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL* url = request.URL;

    // look for our custom action to come through:
    if ( [url.host isEqualToString: @"action"] && [url.path isEqualToString: @"/alert"] )
    {
        // parse out the message
        NSString* message = [[[[url query] componentsSeparatedByString: @"="] lastObject] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

        // show our alert
        UIAlertView* av = [[UIAlertView alloc] initWithTitle: @"My Custom Title"
                                                     message: message
                                                    delegate: nil
                                           cancelButtonTitle: @"OK"
                                           otherButtonTitles: nil];

        [av show];

        return NO;
    }
    return YES;
}

解决方案2:首先,我要说的是我个人永远不会在生产代码中使用以下解决方案。要实现此功能的最佳方法是执行@Martin H在他的答案中建议的操作,或者可能是执行@twk所建议的操作(如果确实需要空标题)。如果您无法控制Web内容本身,也许您可以在内容加载后注入@Martin H的代码。

尽管如此,如果我们作出一些假设,这仍然可以实现。首先,JavaScript alert()方法确实映射到真正的UIAlertView。(实际上确实是这样!)其次,我们可以想出某种机制来区分由alert()引起的UIAlertView和其他应用程序源的UIAlertViews。(我们可以!)

我的方法是调换UIAlertView。我知道——调换很糟糕,并且有各种各样的缺点。如果我能避免,我不会在生产应用程序中进行调换。但是对于这个科学项目,我们将交换UIAlertView setDelegate:方法。

我发现的是1)UIWebView将构造一个UIAlertView来显示JavaScript警报,2)UIAlertView标题在设置UIAlertView delegate之前设置。通过将UIAlertView setDelegate:与我的自定义方法交换,我可以完成两件事:1)我可以确定UIAlertView是代表UIWebView委托的(只需检查委托即可...),并且2)我有机会在显示警报视图之前重新设置标题。

您可能会问:“为什么不调换UIAlertview -show方法?”那太好了,但是在我的实验中,我发现UIWebView从未调用show。它必须调用某个其他的内部方法,而我没有进一步调查。

我的解决方案实现了UIAlertView的类别,其中添加了几个类方法。基本上,您可以使用这些方法注册UIWebView,并提供要使用的替换标题。或者,您可以提供一个回调块,当调用时返回标题。

我使用iOS6的NSMapTable类来保持UIWebView-to-Title映射的一组,其中UIWebView是弱键。这样,我永远不必注销我的UIWebView,一切都得到了良好的清理。因此,当前实现仅适用于iOS6。

以下是代码,显示在基本视图控制器中的使用情况:

#import <objc/runtime.h>

typedef NSString*(^TSAlertTitleBlock)(UIWebView* webView, NSString* defaultTitle );

@interface UIAlertView (TS)
@end

@implementation UIAlertView (TS)

NSMapTable* g_webViewMapTable;

+ (void) registerWebView: (UIWebView*) webView alertTitleStringOrBlock: (id) stringOrBlock
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        g_webViewMapTable = [NSMapTable weakToStrongObjectsMapTable];

        // $wizzle
        Method originalMethod = class_getInstanceMethod(self, @selector(setDelegate:));
        Method overrideMethod = class_getInstanceMethod(self, @selector(setDelegate_ts:));
        method_exchangeImplementations(originalMethod, overrideMethod);
    });

    [g_webViewMapTable setObject: [stringOrBlock copy] forKey: webView];
}

+ (void) registerWebView: (UIWebView*) webView alertTitle: (NSString*) title
{
    [self registerWebView: webView alertTitleStringOrBlock: title];
}

+ (void) registerWebView: (UIWebView*) webView alertTitleBlock: (TSAlertTitleBlock) alertTitleBlock
{
    [self registerWebView: webView alertTitleStringOrBlock: alertTitleBlock];
}

- (void) setDelegate_ts: (id) delegate
{
    // call the original implementation
    [self setDelegate_ts: delegate];

    // oooh - is a UIWebView asking for this UIAlertView????
    if ( [delegate isKindOfClass: [UIWebView class]] )
    {
        // see if we've registered a title/title-block
        for ( UIWebView* wv in g_webViewMapTable.keyEnumerator )
        {
            if ( wv != delegate)
                continue;

            id stringOrBlock = [g_webViewMapTable objectForKey: wv];

            if ( [stringOrBlock isKindOfClass: NSClassFromString( @"NSBlock" )] )
            {
                stringOrBlock = ((TSAlertTitleBlock)stringOrBlock)( wv, self.title );
            }

            NSParameterAssert( [stringOrBlock isKindOfClass: [NSString class]] );

            [self setTitle: stringOrBlock];
            break;
        }
    }
}

@end

@interface TSViewController () <UIWebViewDelegate>
@end

@implementation TSViewController

- (UIWebView*) webView
{
    return (UIWebView*) self.view;
}

- (void) loadView
{
    UIWebView* wv = [[UIWebView alloc] initWithFrame: CGRectMake(0, 0, 320, 480)];
    wv.delegate = self;
    wv.scalesPageToFit = YES;

    self.view = wv;
}

- (void) viewDidLoad
{
    [super viewDidLoad];

    // method 1: bind a title to a webview:
    [UIAlertView registerWebView: self.webView alertTitle: nil];

    /*
    // method 2: return a title each time it's needed:
    [UIAlertView registerWebView: self.webView
                 alertTitleBlock: ^NSString *(UIWebView *webView, NSString* defaultTitle) {

                     return @"Custom Title";
    }];
     */

    [self.webView loadRequest: [NSURLRequest requestWithURL: [NSURL URLWithString: @"http://www.craigslist.org"]]];
}

- (void) webViewDidFinishLoad: (UIWebView *) webView
{
    // trigger an alert
    [webView stringByEvaluatingJavaScriptFromString: @"alert('hello, world');" ];
}

@end

解决方案3:经过反思,这里有一个比我在解决方案2中最初描述的更简单的技术。它仍然依赖于未公开的事实,即javascript alert是使用具有其委托设置为源UIWebView的UIAlertView实现的。对于此解决方案,只需子类化UIWebView并为UIAlertView实现自己的委托方法willPresentAlertView:,当调用它时,将标题重置为您希望的任何内容。

@interface TSWebView : UIWebView
@end

@implementation TSWebView

- (void) willPresentAlertView:(UIAlertView *)alertView
{
    if ( [self.superclass instancesRespondToSelector: @selector( willPresentAlertView:) ])
    {
        [super performSelector: @selector( willPresentAlertView:) withObject: alertView];
    }

    alertView.title = @"My Custom Title";
}

@end

@interface TSViewController () <UIWebViewDelegate>
@end

@implementation TSViewController

- (UIWebView*) webView
{
    return (UIWebView*) self.view;
}

- (void) loadView
{
    UIWebView* wv = [[TSWebView alloc] initWithFrame: CGRectMake(0, 0, 320, 480)];
    wv.delegate = self;
    wv.scalesPageToFit = YES;

    self.view = wv;
}

- (void) viewDidLoad
{
    [super viewDidLoad];

    [self.webView loadRequest: [NSURLRequest requestWithURL: [NSURL URLWithString: @"http://www.craigslist.org"]]];
}

- (void) webViewDidFinishLoad: (UIWebView *) webView
{
    // trigger an alert
    [webView stringByEvaluatingJavaScriptFromString: @"alert('hello, world');" ];
}

@end

1
从你的回答中,我了解到你在Objective-C方面非常熟练。我无法确定你是否曾经使用过Web视图进行任何工作,但如果你这样做了,你是否认为编写一个简洁的示例来展示Cordova/Phonegap使用JS与Objective-C之间桥接技术会更有用呢?我确信他们使用的技术之一是JavaScript中的window.location,并且在Objective-C中监视Web视图位置的变化,读取嵌入在URL中的参数。然后可能取消导航。我知道这个技术,但我不能编写有意义的Objective-C代码。 - Myrne Stol
1
我之所以这么说是因为现在你已经有了一个答案,而你自己却说你不会使用它。Phonegap技术肯定被广泛应用,所以这可能是一个合理的解决方案。 - Myrne Stol
@MerynStol - 好的,你明白了。我为window.location技术添加了一个简洁的示例。此外,还有一个更简单的变体,比我的原始解决方案更简单。 - TomSwift
非常有用的信息!你应该得到奖励。UIWebView的子类化看起来也很吸引人。 - Myrne Stol
@TomSwift 的解决方案 1 可以用于 alert() 函数,但无法在位置权限弹窗中使用!不推荐使用解决方案 2。解决方案 3 也不可行,我可以获取标题,但更改 alertView 的标题似乎没有效果。 - onmyway133
我现在才发现这个答案,我必须说这是迄今为止处理JavaScript警告框的最佳解释。非常感谢! - Giorgio Antonioli

10

不,你不能在JavaScript警报框中执行此操作。

但是,如果这个JavaScript属于你,那么你可以调用一个函数来调用Objective C并调用iOS本地警报,而不是调用警报框。

如果这个JavaScript不属于你,那么你可以使用UIWebView注入一些代码来覆盖默认行为,并将其更改为调用iOS本地警报,例如:

window.alert = function(message) {
  window.location = "myScheme://" + message"
};

然后在UIWebView的shouldStartLoadWithRequest方法中查找myScheme并提取消息,以下是从JavaScript调用Objective-C的标准方法。

如何从JavaScript调用Objective-C?


4

1
只需为您的UIWebView创建一个类,并在其中创建willPresentAlertView(alertView: UIAlertView)函数。之后设置alertView.title = "你喜欢的标题",就完成了。
    import UIKit

class webviewClass: UIWebView {

func willPresentAlertView(alertView: UIAlertView) {


    alertView.title = "my custum title"
}

}

此后,每当显示警告或确认对话框时,您的自定义标题将会显示。


0
使用 cordova-plugin-dialogs 插件。它允许创建本地对话框。
以下代码将创建本地警报:
navigator.notification.alert(message, alertCallback, [title], [buttonName])

0

根据@twk所写,我只是覆盖了alert函数。在iOS7上运行得很好。

function alert(words){
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", 'data:text/plain,');
document.documentElement.appendChild(iframe);
window.frames[0].window.alert(words);
iframe.parentNode.removeChild(iframe);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接