diff --git a/.gitignore b/.gitignore index 30b09b4d..8ef01939 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ Thumbs.db node_modules xcuserdata +package-lock.json diff --git a/README.md b/README.md index 6e4bc269..19355761 100644 --- a/README.md +++ b/README.md @@ -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 + +`` + +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 + +`` + +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 diff --git a/plugin.xml b/plugin.xml index d606bd4c..4c6858c8 100644 --- a/plugin.xml +++ b/plugin.xml @@ -62,6 +62,7 @@ + @@ -76,6 +77,8 @@ + + diff --git a/src/ios/CDVWKWebViewEngine.m b/src/ios/CDVWKWebViewEngine.m index a0e27f78..e079f95c 100644 --- a/src/ios/CDVWKWebViewEngine.m +++ b/src/ios/CDVWKWebViewEngine.m @@ -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" @@ -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 @@ -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]; @@ -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 @@ -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"]]; @@ -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]; @@ -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]; @@ -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 { @@ -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); @@ -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]; @@ -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]; @@ -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]; diff --git a/src/ios/IONAssetHandler.h b/src/ios/IONAssetHandler.h new file mode 100644 index 00000000..61cab670 --- /dev/null +++ b/src/ios/IONAssetHandler.h @@ -0,0 +1,10 @@ +#import +#import + +@interface IONAssetHandler : NSObject + +@property (nonatomic, strong) NSString * basePath; + +-(void)setAssetPath:(NSString *)assetPath; + +@end diff --git a/src/ios/IONAssetHandler.m b/src/ios/IONAssetHandler.m new file mode 100644 index 00000000..f03db6cd --- /dev/null +++ b/src/ios/IONAssetHandler.m @@ -0,0 +1,74 @@ +#import "IONAssetHandler.h" +#import + +@implementation IONAssetHandler + +-(void)setAssetPath:(NSString *)assetPath { + self.basePath = assetPath; +} + +- (void)webView:(WKWebView *)webView startURLSchemeTask:(id )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)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