Refactor MobileMiniBrowser into an application framework to allow external XCTesting
[WebKit-https.git] / Tools / MobileMiniBrowser / MobileMiniBrowserFramework / WebViewController.m
1 /*
2  * Copyright (C) 2016 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "WebViewController.h"
27
28 #import "TabViewController.h"
29 #import <WebKit/WKNavigation.h>
30 #import <WebKit/WKNavigationDelegate.h>
31 #import <WebKit/WKWebView.h>
32 #import <WebKit/WKWebViewConfiguration.h>
33
34 @implementation NSURL (BundleURLMethods)
35 + (NSURL *)__bundleURLForFileURL:(NSURL *)url bundle:(NSBundle *)bundle
36 {
37     if (![url.scheme isEqualToString:@"file"])
38         return nil;
39     NSString *resourcePath = [bundle.resourcePath stringByAppendingString:@"/"];
40     if (![url.path hasPrefix:resourcePath])
41         return nil;
42     NSURLComponents *bundleComponents = [[NSURLComponents alloc] init];
43     bundleComponents.scheme = @"bundle";
44     bundleComponents.path = [url.path substringFromIndex:resourcePath.length];
45     return [bundleComponents.URL copy];
46 }
47
48 + (NSURL *)__fileURLForBundleURL:(NSURL *)url bundle:(NSBundle *)bundle
49 {
50     if (![url.scheme isEqualToString:@"bundle"])
51         return nil;
52     return [bundle.resourceURL URLByAppendingPathComponent:url.path];
53 }
54 @end
55
56 @interface WebViewController () <WKNavigationDelegate> {
57     WKWebView *_currentWebView;
58 }
59 - (WKWebView *)createWebView;
60 - (void)removeWebView:(WKWebView *)webView;
61 - (void)setCurrentWebView:(WKWebView *)webView;
62 @end
63
64 void* EstimatedProgressContext = &EstimatedProgressContext;
65 void* TitleContext = &TitleContext;
66 void* URLContext = &URLContext;
67
68 @implementation WebViewController
69
70 - (void)viewDidLoad
71 {
72     [super viewDidLoad];
73     self.webViews = [[NSMutableArray alloc] initWithCapacity:1];
74     self.tabViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"idTabViewController"];
75     self.tabViewController.parent = self;
76     self.tabViewController.modalPresentationStyle = UIModalPresentationPopover;
77
78     [self setCurrentWebView:[self createWebView]];
79 }
80
81
82 - (void)didReceiveMemoryWarning
83 {
84     [super didReceiveMemoryWarning];
85 }
86
87 #pragma mark Actions
88
89 - (IBAction)reload:(id)sender
90 {
91     [self.currentWebView reload];
92 }
93
94 - (IBAction)goBack:(id)sender
95 {
96     [self.currentWebView goBack];
97 }
98
99 - (IBAction)goForward:(id)sender
100 {
101     [self.currentWebView goForward];
102 }
103
104 - (IBAction)urlFieldEditingBegan:(id)sender
105 {
106     self.urlField.selectedTextRange = [self.urlField textRangeFromPosition:self.urlField.beginningOfDocument toPosition:self.urlField.endOfDocument];
107 }
108
109 - (IBAction)navigateTo:(id)sender
110 {
111     [self.urlField resignFirstResponder];
112     NSString* requestedDestination = self.urlField.text;
113     if ([requestedDestination rangeOfString:@"^[\\p{Alphabetic}]+:" options:(NSRegularExpressionSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location == NSNotFound)
114         requestedDestination = [@"http://" stringByAppendingString:requestedDestination];
115     NSURL* requestedURL = [NSURL URLWithString:requestedDestination];
116     if ([requestedURL.scheme isEqualToString:@"bundle"]) {
117         NSBundle *frameworkBundle = [NSBundle bundleForClass:[WebViewController class]];
118         requestedURL = [NSURL __fileURLForBundleURL:requestedURL bundle:frameworkBundle];
119         [self.currentWebView loadFileURL:requestedURL allowingReadAccessToURL:frameworkBundle.resourceURL];
120     }
121     [self.currentWebView loadRequest:[NSURLRequest requestWithURL:requestedURL]];
122 }
123
124 - (IBAction)showTabs:(id)sender
125 {
126     [self presentViewController:self.tabViewController animated:YES completion:nil];
127     self.tabViewController.popoverPresentationController.barButtonItem = self.tabButton;
128 }
129
130 #pragma mark Public methods
131
132 @dynamic currentWebView;
133
134 - (WKWebView *)currentWebView
135 {
136     return _currentWebView;
137 }
138
139 - (void)setCurrentWebView:(WKWebView *)webView
140 {
141     [_currentWebView removeObserver:self forKeyPath:@"estimatedProgress" context:EstimatedProgressContext];
142     [_currentWebView removeObserver:self forKeyPath:@"URL" context:URLContext];
143     [_currentWebView removeFromSuperview];
144
145     _currentWebView = webView;
146
147     [_currentWebView addObserver:self forKeyPath:@"estimatedProgress" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:EstimatedProgressContext];
148     [_currentWebView addObserver:self forKeyPath:@"URL" options:(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:URLContext];
149     [self.webViewContainer addSubview:_currentWebView];
150 }
151
152 - (void)selectWebViewAtIndex:(NSUInteger)index
153 {
154     if (index >= self.webViews.count)
155         return;
156     self.currentWebView = self.webViews[index];
157 }
158
159 - (void)removeWebViewAtIndex:(NSUInteger)index
160 {
161     if (index >= self.webViews.count)
162         return;
163     [self removeWebView:self.webViews[index]];
164 }
165
166 - (void)addWebView
167 {
168     self.currentWebView = [self createWebView];
169 }
170
171 #pragma mark Internal methods
172
173 - (WKWebView *)createWebView
174 {
175     WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
176     WKWebView *webView = [[WKWebView alloc] initWithFrame:self.webViewContainer.bounds configuration:configuration];
177     webView.navigationDelegate = self;
178     webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
179     [webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:TitleContext];
180     [self.webViews addObject:webView];
181
182     [self.tabViewController.tableView reloadData];
183
184     return webView;
185 }
186
187 - (void)removeWebView:(WKWebView *)webView
188 {
189     NSUInteger index = [self.webViews indexOfObject:webView];
190     if (index != NSNotFound) {
191         [self.webViews[index] removeObserver:self forKeyPath:@"title" context:TitleContext];
192         [self.webViews removeObjectAtIndex:index];
193     } if (!self.webViews.count)
194         [self createWebView];
195     if (index >= self.webViews.count)
196         index = self.webViews.count - 1;
197     [self setCurrentWebView:self.webViews[index]];
198
199     [self.tabViewController.tableView reloadData];
200 }
201
202 #pragma mark Navigation Delegate
203
204 - (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error
205 {
206     [webView loadHTMLString:[error description] baseURL:nil];
207 }
208
209 #pragma mark KVO
210
211 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
212 {
213     if (context == EstimatedProgressContext) {
214         float value = [[change valueForKey:NSKeyValueChangeNewKey] floatValue];
215         [self.progressView setProgress:value animated:YES];
216     } else if (context == URLContext) {
217         NSURL *newURLValue = [change valueForKey:NSKeyValueChangeNewKey];
218         if ([newURLValue isKindOfClass:[NSURL class]]) {
219             if ([newURLValue.scheme isEqualToString:@"file"])
220                 newURLValue = [NSURL __bundleURLForFileURL:newURLValue bundle:[NSBundle bundleForClass:[WebViewController class]]];
221             self.urlField.text = [newURLValue absoluteString];
222         } else if ([newURLValue isKindOfClass:[NSNull class]])
223             self.urlField.text = @"";
224     } else if (context == TitleContext)
225         [self.tabViewController.tableView reloadData];
226     else
227         [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
228 }
229
230 @end