Skip to content

Commit

Permalink
feat(ios): Add URLSchemeHandler for iOS 11+ (#221)
Browse files Browse the repository at this point in the history
* Add URLSchemeHandler for iOS 11+

* Make Scheme usage configurable
  • Loading branch information
jcesarmobile authored and imhoffd committed Dec 5, 2018
1 parent ade4f78 commit 4a973f4
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ Thumbs.db

node_modules
xcuserdata
package-lock.json

20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ The default port the server will listen on. _You should change this to a random

Preferences only available for iOS platform

#### UseScheme

`<preference name="UseScheme" value="true" />`

Default value is `false`.

On iOS 11 and newer it will use a `WKURLSchemeHandler` that loads the app from `ionic://` scheme instead of using the local web server and `https://` scheme.

On iOS 10 and older will continue using the local web server even if the preference is set to `true`.

#### HostName

`<preference name="HostName" value="myHostName" />`

Default value is `app`.

If `UseScheme` is set to yes, it will use the `HostName` value as the host of the starting url.

Example `ionic://app`

#### WKSuspendInBackground

```xml
Expand Down
3 changes: 3 additions & 0 deletions plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<config-file target="config.xml" parent="/*">
<allow-navigation href="http://localhost:8080/*"/>
<allow-navigation href="http://127.0.0.1:8080/*"/>
<allow-navigation href="ionic://*" />
<feature name="IonicWebView">
<param name="ios-package" value="CDVWKWebViewEngine"/>
</feature>
Expand All @@ -76,6 +77,8 @@
<source-file src="src/ios/CDVWKWebViewUIDelegate.m"/>
<header-file src="src/ios/CDVWKProcessPoolFactory.h"/>
<source-file src="src/ios/CDVWKProcessPoolFactory.m"/>
<header-file src="src/ios/IONAssetHandler.h"/>
<source-file src="src/ios/IONAssetHandler.m"/>
<asset src="src/ios/wk-plugin.js" target="wk-plugin.js"/>

<!--GCDWebServer headers-->
Expand Down
66 changes: 53 additions & 13 deletions src/ios/CDVWKWebViewEngine.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Licensed to the Apache Software Foundation (ASF) under one
#import "CDVWKProcessPoolFactory.h"
#import "GCDWebServer.h"
#import "GCDWebServerPrivate.h"
#import "IONAssetHandler.h"

#define CDV_BRIDGE_NAME @"cordova"
#define CDV_IONIC_STOP_SCROLL @"stopScroll"
Expand Down Expand Up @@ -107,6 +108,8 @@ @interface CDVWKWebViewEngine ()
@property (nonatomic, readwrite) CGRect frame;
@property (nonatomic, strong) NSString *userAgentCreds;
@property (nonatomic, assign) BOOL internalConnectionsOnly;
@property (nonatomic, assign) BOOL useScheme;
@property (nonatomic, strong) IONAssetHandler * handler;

@property (nonatomic, readwrite) NSString *CDV_LOCAL_SERVER;
@end
Expand Down Expand Up @@ -152,6 +155,13 @@ - (void)initWebServer
[GCDWebServer setLogLevel: kGCDWebServerLoggingLevel_Warning];
self.webServer = [[GCDWebServer alloc] init];

[self updateBindPath];
[self setServerPath:[self getStartPath]];

[self startServer];
}

-(NSString *) getStartPath {
NSString * wwwPath = [[NSBundle mainBundle] pathForResource:@"www" ofType: nil];

NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
Expand All @@ -162,11 +172,8 @@ - (void)initWebServer
NSString * snapshots = [cordovaDataDirectory stringByAppendingPathComponent:@"ionic_built_snapshots"];
wwwPath = [snapshots stringByAppendingPathComponent:[persistedPath lastPathComponent]];
}

[self updateBindPath];
[self setServerPath:wwwPath];

[self startServer];
self.basePath = wwwPath;
return wwwPath;
}

-(BOOL) isNewBinary
Expand Down Expand Up @@ -264,9 +271,22 @@ - (void)pluginInitialize
{
// viewController would be available now. we attempt to set all possible delegates to it, by default
NSDictionary* settings = self.commandDelegate.settings;
self.internalConnectionsOnly = [settings cordovaBoolSettingForKey:@"WKInternalConnectionsOnly" defaultValue:YES];
if (@available(iOS 11.0, *)) {
self.useScheme = [settings cordovaBoolSettingForKey:@"UseScheme" defaultValue:NO];
} else {
self.useScheme = NO;
}

[self initWebServer];
self.internalConnectionsOnly = [settings cordovaBoolSettingForKey:@"WKInternalConnectionsOnly" defaultValue:YES];
if (self.useScheme) {
NSString *bind = [settings cordovaSettingForKey:@"HostName"];
if(bind == nil){
bind = @"app";
}
self.CDV_LOCAL_SERVER = [NSString stringWithFormat:@"ionic://%@", bind];
} else {
[self initWebServer];
}

self.uiDelegate = [[CDVWKWebViewUIDelegate alloc] initWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]];

Expand Down Expand Up @@ -307,6 +327,15 @@ - (void)pluginInitialize
WKWebViewConfiguration* configuration = [self createConfigurationFromSettings:settings];
configuration.userContentController = userContentController;

if (@available(iOS 11.0, *)) {
if (self.useScheme) {
self.handler = [[IONAssetHandler alloc] init];
[self.handler setAssetPath:[self getStartPath]];
[configuration setURLSchemeHandler:self.handler forURLScheme:@"ionic"];
[configuration setURLSchemeHandler:self.handler forURLScheme:@"ionic-asset"];
}
}

// re-create WKWebView, since we need to update configuration
// remove from keyWindow before recreating
[self.engineWebView removeFromSuperview];
Expand Down Expand Up @@ -459,7 +488,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
if (context == KVOContext) {
if (object == [self webView] && [keyPath isEqualToString: @"URL"] && [object valueForKeyPath:keyPath] == nil){
NSLog(@"URL is nil. Reloading WKWebView");
if ([self.webServer isRunning]) {
if ([self isSafeToReload]) {
[(WKWebView*)_engineWebView reload];
} else {
[self loadErrorPage:nil];
Expand All @@ -472,7 +501,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N

- (void)onAppWillEnterForeground:(NSNotification *)notification {
if ([self shouldReloadWebView]) {
if ([self.webServer isRunning]) {
if ([self isSafeToReload]) {
NSLog(@"%@", @"CDVWKWebViewEngine reloading!");
[(WKWebView*)_engineWebView reload];
} else {
Expand Down Expand Up @@ -516,6 +545,11 @@ - (BOOL)shouldReloadWebView
return [self shouldReloadWebView:wkWebView.URL title:wkWebView.title];
}

- (BOOL)isSafeToReload
{
return [self.webServer isRunning] || self.useScheme;
}

- (BOOL)shouldReloadWebView:(NSURL *)location title:(NSString*)title
{
BOOL title_is_nil = (title == nil);
Expand Down Expand Up @@ -551,7 +585,7 @@ - (id)loadRequest:(NSURLRequest *)request
}
request = [NSURLRequest requestWithURL:url];
}
if ([self.webServer isRunning]) {
if ([self isSafeToReload]) {
return [(WKWebView*)_engineWebView loadRequest:request];
} else {
return [self loadErrorPage:request];
Expand Down Expand Up @@ -831,7 +865,7 @@ - (void)webView:(WKWebView*)theWebView didFailNavigation:(WKNavigation*)navigati

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView
{
if ([self.webServer isRunning]) {
if ([self isSafeToReload]) {
[webView reload];
} else {
[self loadErrorPage:nil];
Expand Down Expand Up @@ -912,9 +946,15 @@ -(void)getServerBasePath:(CDVInvokedUrlCommand*)command
-(void)setServerBasePath:(CDVInvokedUrlCommand*)command
{
NSString * path = [command argumentAtIndex:0];
[self setServerPath:path];
if (self.useScheme) {
self.basePath = path;
[self.handler setAssetPath:path];
} else {
[self setServerPath:path];
}

NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.CDV_LOCAL_SERVER]];
if ([self.webServer isRunning]) {
if ([self isSafeToReload]) {
[(WKWebView*)_engineWebView loadRequest:request];
} else {
[self loadErrorPage:request];
Expand Down
10 changes: 10 additions & 0 deletions src/ios/IONAssetHandler.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>

@interface IONAssetHandler : NSObject <WKURLSchemeHandler>

@property (nonatomic, strong) NSString * basePath;

-(void)setAssetPath:(NSString *)assetPath;

@end
74 changes: 74 additions & 0 deletions src/ios/IONAssetHandler.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#import "IONAssetHandler.h"
#import <MobileCoreServices/MobileCoreServices.h>

@implementation IONAssetHandler

-(void)setAssetPath:(NSString *)assetPath {
self.basePath = assetPath;
}

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask
API_AVAILABLE(ios(11.0)){
NSString * startPath = @"";
NSURL * url = urlSchemeTask.request.URL;
NSString * stringToLoad = url.path;
NSString * scheme = url.scheme;
if ([scheme isEqualToString:@"ionic"]) {
startPath = self.basePath;
if ([stringToLoad isEqualToString:@""] || !url.pathExtension) {
startPath = [startPath stringByAppendingString:@"/index.html"];
} else {
startPath = [startPath stringByAppendingString:stringToLoad];
}
} else {
if (![stringToLoad isEqualToString:@""]) {
startPath = stringToLoad;
}
}

NSData * data = [[NSData alloc] initWithContentsOfFile:startPath];
NSInteger statusCode = 200;
if (!data) {
statusCode = 404;
}
NSURL * localUrl = [NSURL URLWithString:url.absoluteString];
NSString * mimeType = [self getMimeType:url.pathExtension];
id response = nil;
if (data && [self isMediaExtension:url.pathExtension]) {
response = [[NSURLResponse alloc] initWithURL:localUrl MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil];
} else {
NSDictionary * headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache"};
response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers];
}

[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:data];
[urlSchemeTask didFinish];

}

- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask API_AVAILABLE(ios(11.0)){
NSLog(@"stop");
}

-(NSString *) getMimeType:(NSString *)fileExtension {
if (fileExtension && ![fileExtension isEqualToString:@""]) {
NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL);
NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
return contentType ? contentType : @"application/octet-stream";
} else {
return @"text/html";
}
}

-(BOOL) isMediaExtension:(NSString *) pathExtension {
NSArray * mediaExtensions = @[@"m4v", @"mov", @"mp4",
@"aac", @"ac3", @"aiff", @"au", @"flac", @"m4a", @"mp3", @"wav"];
if ([mediaExtensions containsObject:pathExtension]) {
return YES;
}
return NO;
}


@end

0 comments on commit 4a973f4

Please sign in to comment.