3

I would like to use a custom URL Scheme for my app, but having trouble getting the URL scheme configured.

When I create a WKWebView object programmatically (with initWithFrame:...) I am able to specify my own WKWebViewConfiguration object, but when WKWebView in Nib/Storyboard is created automatically (via initWithCoder:) I am not able to specify a configuration. The configuration is read-only so I can't change it after the fact. I really would prefer to avoid creating the WkWebView programmatically if possible.

Note: there is an existing question related to this (here), but the answer that was accepted there only solves a subset of the actual program (it involves adding a user script instead of an configuration object, so a custom URL scheme handler cannot be added). Other answers there either do not help with this issue, so I have opened another question.

Update: I wanted to give some clarification here based on a comment I received.

WKWebViewConfiguration is a "copy" property, as defined here:

*@property (nonatomic, readonly, copy) WKWebViewConfiguration configuration;

I tried to print it out multiple times and I see it is changing:

2019-05-30 15:02:25.724312-0700 MyTest [916:72204] configuration = 0x101b06b50

2019-05-30 15:02:25.724499-0700 MyTest [916:72204] configuration = 0x101a37110

Using LLDB gives the same result (different each time):

(lldb) print [self configuration] (WKWebViewConfiguration *) $17 = 0x0000000102e0b990

(lldb) print [self configuration] (WKWebViewConfiguration *) $18 = 0x0000000102f3bd40

Community
  • 1
  • 1
Locksleyu
  • 5,192
  • 8
  • 52
  • 77
  • Maybe stupid comment but... why don't you just, in awakeFromNib method, read the configuration that was created during the view creation (yourView.configuration), and then use setUrlSchemeHandler on it ? – AirXygène Jun 02 '19 at 11:23
  • The problem is that when that configuration is read, it is a copy. So if I add a URLSchemeHandler to that it will not get added to the actual configuration associated with the WKWebView. – Locksleyu Jun 05 '19 at 14:24
  • Mmmhhh... I am quite sure that's not the case. "copy" only applies to the setter, not to the getter (I agree that "readonly, copy" is "strange"), at least in Objective-C. So as the property is readonly, you can't change the configuration, but if the use the getter, you'll get the right configuration on which you can use setUrlSchemeHandler. Just do that simple test : call the getter several times and log the result. You'll see it is not changing. you are not getting a different copy every time. – AirXygène Jun 05 '19 at 17:50
  • I tried what you said and the value is different on subsequent calls. I added extra details in the question above. – Locksleyu Jun 05 '19 at 19:22
  • Strange. However, I did some tests, and indeed, didn't get the setUrlSchemeHandler method to work when the view is from story board (though my configuration variable is not changing through calls). See my posted answer for full code and suggeestion (a poor answer, I agree) – AirXygène Jun 05 '19 at 19:28

2 Answers2

0

How about creating a custom view that contains WKWebView. You can use this view in Storyboard in stead of WKWebView.

final class MyWebView: UIView {
    var webView: WKWebView!
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        let config = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: config)
        addSubview(webView)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        webView.frame = bounds
    }
}

To customize other properties in Storyboard, add @IBInspectable to wrapper view and pass the value to internal configuration.

The code below lacks some properties that WKWebView in storyboard has, but it'll be possible to implement other properties like this.

final class MyWebView: UIView {
    @IBInspectable var userAgent: String?
    @IBInspectable var appName: String? {
        didSet {
            config.applicationNameForUserAgent = appName
        }
    }
    private var types: WKDataDetectorTypes = [
        .phoneNumber,
        .link,
        .address,
        .calendarEvent,
        .trackingNumber,
        .flightNumber,
        .lookupSuggestion
    ]
    @IBInspectable var phoneNumber: Bool = true {
        didSet {
            if phoneNumber {
                types.insert(.phoneNumber)
            } else {
                types.remove(.phoneNumber)
            }
            config.dataDetectorTypes = types
        }
    }
    @IBInspectable var link: Bool = true {
        didSet {
            if link {
                types.insert(.link)
            } else {
                types.remove(.link)
            }
            config.dataDetectorTypes = types
        }
    }
    @IBInspectable var address: Bool = true {
        didSet {
            if address {
                types.insert(.address)
            } else {
                types.remove(.address)
            }
            config.dataDetectorTypes = types
        }
    }
    @IBInspectable var calendarEvent: Bool = true {
        didSet {
            if calendarEvent {
                types.insert(.calendarEvent)
            } else {
                types.remove(.calendarEvent)
            }
            config.dataDetectorTypes = types
        }
    }
    @IBInspectable var trackingNumber: Bool = true {
        didSet {
            if trackingNumber {
                types.insert(.trackingNumber)
            } else {
                types.remove(.trackingNumber)
            }
            config.dataDetectorTypes = types
        }
    }
    @IBInspectable var flightNumber: Bool = true {
        didSet {
            if flightNumber {
                types.insert(.flightNumber)

            } else {
                types.remove(.flightNumber)
            }
            config.dataDetectorTypes = types
        }
    }
    @IBInspectable var lookupSuggestion: Bool = true {
        didSet {
            if phoneNumber {
                types.insert(.lookupSuggestion)
            } else {
                types.remove(.lookupSuggestion)

            }
            config.dataDetectorTypes = types
        }
    }
    @IBInspectable var javaScriptEnabled: Bool = true {
        didSet {
            config.preferences.javaScriptEnabled = javaScriptEnabled
        }
    }
    @IBInspectable var canAutoOpenWindows: Bool = false {
        didSet {
            config.preferences.javaScriptCanOpenWindowsAutomatically = canAutoOpenWindows
        }
    }

    lazy var webView: WKWebView = {
        let webView = WKWebView(frame: .zero, configuration: config)
        webView.customUserAgent = userAgent
        addSubview(webView)
        return webView
    }()
    private var config = WKWebViewConfiguration()

    override func layoutSubviews() {
        super.layoutSubviews()
        webView.frame = bounds
    }
}
pompopo
  • 929
  • 5
  • 10
  • The idea of using a "mother" view to contain the WKWebView was already mentioned in AirXygène's answer. The problem with this is that I still loose much of the flexibility of the Storyboard. For example, if I change attributes of this containing view, I don't think they will all apply to the WkWebView inside. – Locksleyu Jun 12 '19 at 18:19
  • @Locksleyu I updated my answer, but this may not help you. That make possible to change some attributes on webview inside. Though, it seems impossible to apply container's attributes to webview's attribute like `alpha` or `hidden`. Because when you change some attributes in Storyboard, webview should be already initialized and there is no chance to configure WKWebViewConfiguration. – pompopo Jun 13 '19 at 04:54
  • Thanks. This is good to know but I really would like to find a solution that allows using all the attributes from the Storyboard, otherwise the advantages of Storyboard are lost. I already knew how to do this programmatically so am looking for a way to use Storyboard fully, though I guess Apple just may not support it. I'm hoping there is some trick though. – Locksleyu Jun 13 '19 at 13:54
-1

So, I did some tests :

@interface  ViewController  ()

@property   (strong)    IBOutlet    WKWebView   *webView ;

@end

@implementation ViewController

- (void)    awakeFromNib
{
    static  BOOL    hasBeenDone    = NO ;   //  because otherwise, awakeFromNib is called twice

    if (!hasBeenDone)
    {
        hasBeenDone = YES ;
    
        //  Create the scheme handler
        SchemeHandler           *mySchemeHandler    = [SchemeHandler new] ;
        NSLog(@"scheme handler : %lx",mySchemeHandler) ;
    
        if (!(self.webView))
        {
            //  Case 1 - we don't have a web view from the StoryBoard, we need to create it
            NSLog(@"Case 1") ;

            //  Add the scheme
            WKWebViewConfiguration  *configuration      = [WKWebViewConfiguration new] ;
            [configuration setURLSchemeHandler:mySchemeHandler
                              forURLScheme:@"stef"] ;
        
            //  Create and set the web view
            self.webView                                = [[WKWebView alloc] initWithFrame:NSZeroRect
                                                                             configuration:configuration] ;
        }
        else
        {
            //  Case 2 - we have a web view from the story board, just set the URL scheme handler
            //  of the configuration
            NSLog(@"Case 2") ;

            WKWebViewConfiguration  *configuration      = self.webView.configuration ;
            [configuration setURLSchemeHandler:mySchemeHandler
                              forURLScheme:@"stef"] ;
        }
        
        //  Log the view configuration
        NSLog(@"View configuration : %lx",self.webView.configuration) ;
        NSLog(@"URL handler for scheme : %lx",[self.webView.configuration     urlSchemeHandlerForURLScheme:@"stef"]) ;
    }
}


- (void)    viewDidLoad
{
    [super viewDidLoad] ;

    //  Log the view configuration
    NSLog(@"View configuration : %lx",self.webView.configuration) ;
    NSLog(@"URL handler for scheme : %lx",[self.webView.configuration urlSchemeHandlerForURLScheme:@"stef"]) ;

    //  Start loading a URL with the scheme - this should log "start" if everything works fine
    NSURL           *url        = [NSURL URLWithString:@"stef://willIWinTheBounty?"] ;
    NSURLRequest    *urlRequest = [NSURLRequest requestWithURL:url] ;
    [self.webView loadRequest:urlRequest] ;
}

@end

If you run that code with the IBOutlet webView unset set in the storyboard (Case 1), then the code creates the web view, configures it for the scheme and everything is fine.

scheme handler : 600000008e30

Case 1

View configuration : 600003d0c780

URL handler for scheme : 600000008e30

View configuration : 600003d0c780

URL handler for scheme : 600000008e30

scheme handler start

If you run that code with the IBOutlet webView set in the storyboard (Case 2), then indeed, setURLSchemeHandler:forURLScheme: is not working. The log of urlSchemeHandlerForURLScheme: returns nil in that case.

scheme handler : 600000005160

Case 2

View configuration : 600003d08d20

URL handler for scheme : 0

View configuration : 600003d08d20

URL handler for scheme : 0

Note that scheme handler start is not called

The reason is not that you get a different copy through the getter, as the log of configuration indicates it remains the same. It is just that the scheme handler is not set despite the call to setURLSchemeHandler:forURLScheme.

So I guess the only solution is to use case 1. Depending on your view setup, it might be more or less difficult to insert the view. I would suggest to have in your story board an empty "mother" view, and use the following code :

@interface  ViewController  ()

@property   (weak)      IBOutlet    NSView      *webViewMother ;
@property   (strong)                WKWebView   *webView ;

@end


@implementation ViewController

- (void)    awakeFromNib
{
    static  BOOL    hasBeenDone    = NO ;   //  because otherwise, awakeFromNib is called twice

    if (!hasBeenDone)
    {
        hasBeenDone = YES ;
    
        //  Create the scheme handler
        SchemeHandler           *mySchemeHandler    = [SchemeHandler new] ;

        //  Create the configuration
        WKWebViewConfiguration  *configuration      = [WKWebViewConfiguration new] ;
        [configuration setURLSchemeHandler:mySchemeHandler
                          forURLScheme:@"stef"] ;
    
        //  Create the web view at the size of its mother view
    self.webView                                = [[WKWebView alloc]     initWithFrame:self.webViewMother.frame
                                                                         configuration:configuration] ;
        [self.webViewMother addSubview:self.webView] ;

    //  Log the view configuration
    NSLog(@"View configuration : %lx",self.webView.configuration) ;
    NSLog(@"URL handler for scheme : %lx",[self.webView.configuration     urlSchemeHandlerForURLScheme:@"stef"]) ;
    }

}

@end

Working fine for me, but could be tricky if you have subviews to your web view.

View configuration : 600003d082d0

URL handler for scheme : 600000004c90

View configuration : 600003d082d0

URL handler for scheme : 600000004c90

scheme handler start

Note that configuration remains the same through calls...

Edit : added the run logs

Community
  • 1
  • 1
AirXygène
  • 2,409
  • 15
  • 34
  • Thanks for the detailed answer! Your suggestion to use #1 is essentially to create it programmatically, which I specified I wanted to avoid in the question. Your hybrid suggestion at the end is an interesting idea, but I think it will be hard to do this for my WKWebView (due to subviews, etc.), or at least a messy solution. Hopefully someone can provide a solution that does not require programmatically creating a WKWebView object. – Locksleyu Jun 05 '19 at 21:50
  • Case 1 and 2 were not solutions, but an analysis of where the problem lies. I think it shows the problem lies in setURLSchemeHandler: forURLScheme: behaviour, as it odes different things depending on who created the configuration. As a minimum, this should be documented by Apple, and they should explain how to solve your case without this. Maybe a radar would help. My only solution suggestion is indeed the hybrid solution, which I agree is not nice when you have subviews, and this is more a work-around, waiting for Apple to provide a real solution. Wish you good luck. – AirXygène Jun 06 '19 at 17:35
  • I spoke with Apple about this and they did not have a good solution or workaround, they said that it was not supported. I also opened a radar (though now it is called a different system). I'm still hoping someone can provide a better workaround, so going to leave this question open. – Locksleyu Jun 10 '19 at 13:24