let uiwebview as wkwebview

63
let UIWebView as WKWebView @taketo1024 2015/02/14 iOSオールスターズ勉強会

Upload: taketo-sano

Post on 14-Jul-2015

18.720 views

Category:

Software


5 download

TRANSCRIPT

let UIWebView as WKWebView

@taketo1024

2015/02/14 iOSオールスターズ勉強会

 本題

from: WWDC2014 Introducing the Modern WebKit API

from: WWDC2014 Introducing the Modern WebKit API

• iOS 2.0 からお馴染み • JavaScript が Safari より遅い

from: WWDC2014 Introducing the Modern WebKit API

• iOS 2.0 からお馴染み • JavaScript が Safari より遅い

• iOS 8.0 から導入 • Safari と同じ JavaScript エンジン! • ページ遷移ジェスチャーにも対応 • その他も何かと高性能で良い

WKWebView を使いたい!

でもまだ iOS 7 を切るわけには行かない…

UIWebView (親: UIView) WKWebView (親: UIView)var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var title: String? { get } var URL: NSURL? { get } var estimatedProgress: Double { get }

func loadRequest(request: NSURLRequest) func reload() func stopLoading() func goBack() func goForward()

func loadRequest(request: NSURLRequest) -> WKNavigation?

func reload() -> WKNavigation? func stopLoading() func goBack() -> WKNavigation? func goForward() -> WKNavigation?

func stringByEvaluatingJavaScriptFromString (script: String) -> String?

func evaluateJavaScript(javaScriptString: String, completionHandler: ((AnyObject!, NSError!) -> Void)?)

weak var delegate: UIWebViewDelegate? weak var navigationDelegate: WKNavigationDelegate?

weak var UIDelegate: WKUIDelegate?

インターフェースは似ているが互換性はない

UIWebView (親: UIView) WKWebView (親: UIView)var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var title: String? { get } var URL: NSURL? { get } var estimatedProgress: Double { get }

func loadRequest(request: NSURLRequest) func reload() func stopLoading() func goBack() func goForward()

func loadRequest(request: NSURLRequest) -> WKNavigation?

func reload() -> WKNavigation? func stopLoading() func goBack() -> WKNavigation? func goForward() -> WKNavigation?

func stringByEvaluatingJavaScriptFromString (script: String) -> String?

func evaluateJavaScript(javaScriptString: String, completionHandler: ((AnyObject!, NSError!) -> Void)?)

weak var delegate: UIWebViewDelegate? weak var navigationDelegate: WKNavigationDelegate?

weak var UIDelegate: WKUIDelegate?

インターフェースは似ているが互換性はない

WKWebView にしかないプロパティ

UIWebView (親: UIView) WKWebView (親: UIView)var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var title: String? { get } var URL: NSURL? { get } var estimatedProgress: Double { get }

func loadRequest(request: NSURLRequest) func reload() func stopLoading() func goBack() func goForward()

func loadRequest(request: NSURLRequest) -> WKNavigation?

func reload() -> WKNavigation? func stopLoading() func goBack() -> WKNavigation? func goForward() -> WKNavigation?

func stringByEvaluatingJavaScriptFromString (script: String) -> String?

func evaluateJavaScript(javaScriptString: String, completionHandler: ((AnyObject!, NSError!) -> Void)?)

weak var delegate: UIWebViewDelegate? weak var navigationDelegate: WKNavigationDelegate?

weak var UIDelegate: WKUIDelegate?

インターフェースは似ているが互換性はない

load系 は WKNavigation? 型の戻り値がある

UIWebView (親: UIView) WKWebView (親: UIView)var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var title: String? { get } var URL: NSURL? { get } var estimatedProgress: Double { get }

func loadRequest(request: NSURLRequest) func reload() func stopLoading() func goBack() func goForward()

func loadRequest(request: NSURLRequest) -> WKNavigation?

func reload() -> WKNavigation? func stopLoading() func goBack() -> WKNavigation? func goForward() -> WKNavigation?

func stringByEvaluatingJavaScriptFromString (script: String) -> String?

func evaluateJavaScript(javaScriptString: String, completionHandler: ((AnyObject!, NSError!) -> Void)?)

weak var delegate: UIWebViewDelegate? weak var navigationDelegate: WKNavigationDelegate?

weak var UIDelegate: WKUIDelegate?

インターフェースは似ているが互換性はない

JavaScript はコールバック形式

UIWebView (親: UIView) WKWebView (親: UIView)var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var title: String? { get } var URL: NSURL? { get } var estimatedProgress: Double { get }

func loadRequest(request: NSURLRequest) func reload() func stopLoading() func goBack() func goForward()

func loadRequest(request: NSURLRequest) -> WKNavigation?

func reload() -> WKNavigation? func stopLoading() func goBack() -> WKNavigation? func goForward() -> WKNavigation?

func stringByEvaluatingJavaScriptFromString (script: String) -> String?

func evaluateJavaScript(javaScriptString: String, completionHandler: ((AnyObject!, NSError!) -> Void)?)

weak var delegate: UIWebViewDelegate? weak var navigationDelegate: WKNavigationDelegate?

weak var UIDelegate: WKUIDelegate?

インターフェースは似ているが互換性はない

delegateが2種類ある(IFもだいぶ違う)

class MyViewController: UIViewController { var wkWebView: WKWebView? var uiWebView: UIWebView? override func viewDidLoad() { super.viewDidLoad() if NSClassFromString("WKWebView") != nil { wkWebView = WKWebView(frame: ) self.view.addSubview(wkWebView!) } else { uiWebView = UIWebView(frame: ) self.view.addSubview(uiWebView!) } } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) let URL = NSURL(string: "http://yahoo.co.jp") let req = NSURLRequest(URL: URL!) if(wkWebView != nil) { wkWebView!.loadRequest(req) } else { uiWebView!.loadRequest(req) } }

ナイーブな下位互換対応をすると…

class MyViewController: UIViewController { var wkWebView: WKWebView? var uiWebView: UIWebView? override func viewDidLoad() { super.viewDidLoad() if NSClassFromString("WKWebView") != nil { wkWebView = WKWebView(frame: ) self.view.addSubview(wkWebView!) } else { uiWebView = UIWebView(frame: ) self.view.addSubview(uiWebView!) } } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) let URL = NSURL(string: "http://yahoo.co.jp") let req = NSURLRequest(URL: URL!) if(wkWebView != nil) { wkWebView!.loadRequest(req) } else { uiWebView!.loadRequest(req) } }

ナイーブな下位互換対応をすると…

UIWebView / WKWebView の両方を変数で宣言

class MyViewController: UIViewController { var wkWebView: WKWebView? var uiWebView: UIWebView? override func viewDidLoad() { super.viewDidLoad() if NSClassFromString("WKWebView") != nil { wkWebView = WKWebView(frame: ) self.view.addSubview(wkWebView!) } else { uiWebView = UIWebView(frame: ) self.view.addSubview(uiWebView!) } } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) let URL = NSURL(string: "http://yahoo.co.jp") let req = NSURLRequest(URL: URL!) if(wkWebView != nil) { wkWebView!.loadRequest(req) } else { uiWebView!.loadRequest(req) } }

ナイーブな下位互換対応をすると…

対応/非対応に応じて一方にインスタンスを入れる

class MyViewController: UIViewController { var wkWebView: WKWebView? var uiWebView: UIWebView? override func viewDidLoad() { super.viewDidLoad() if NSClassFromString("WKWebView") != nil { wkWebView = WKWebView(frame: ) self.view.addSubview(wkWebView!) } else { uiWebView = UIWebView(frame: ) self.view.addSubview(uiWebView!) } } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) let URL = NSURL(string: "http://yahoo.co.jp") let req = NSURLRequest(URL: URL!) if(wkWebView != nil) { wkWebView!.loadRequest(req) } else { uiWebView!.loadRequest(req) } }

ナイーブな下位互換対応をすると…

そして毎回分岐して同じ処理を2度書くことに…

これはよくない

バージョン分岐だらけのコードは危険

• コントローラやモデルの本来の役割が見えにくくなる。

• 古いコードが山積するといずれ誰も手に負えなくなる。

• 下位バージョンのサポートを切るときにも不必要なコー

ドが残りがち。

「OSの最新バージョンだけサポートしてればいいじゃん」

2015/02 時点でも iOS 8 のシェアは 72%。 100万 ユーザいれば 28万 は iOS 7 以前。 大きなユーザ数を持つサービスには重大な問題。

As measured by the App Store on February 2, 2015.

どうするか?

「複雑性保存の法則」

タスクの複雑性はある点を超えて減らすことはできない。移動のみ可能である。

ラリー・テスラーインタラクションデザイナ

ソフトウェアの設計についてもあてはまる!

下位互換対応は「隠蔽」せよ

• コントローラやモデルなどの上位層からはバージョン分岐を意識しなくていいようにする。

• 混み入った分岐はどこかにまとめておいて、いつでも簡単に切り離せるようにしておく。

• 広範囲で使用されるものでも、依存性は最小限に。

2014-01-15 @ potatotips 3Method Swizzling で iOS 7 コードを iOS 6 でも動かす

今回のお題

UIWebView と WKWebView の分岐処理を隠蔽せよ

方針1: ラッパーで包むIF は WKWebView と共通にする

WKWebViewUIWebView

前処理

iOS 8 以上 iOS 7 以下

WKWebViewWrapper : UIView

class WKWebViewWrapper: UIView { // どちらか一方はnil var wkWebView: WKWebView? var uiWebView: UIWebView? override init(frame: CGRect) { super.init(frame: frame) if NSClassFromString("WKWebView") != nil { wkWebView = WKWebView(frame: self.bounds) self.addSubview(wkWebView!) } else { uiWebView = UIWebView(frame: self.bounds) self.addSubview(uiWebView!) } }

func loadRequest(request: NSURLRequest) -> WKNavigation? { if(wkWebView != nil) { return wkWebView!.loadRequest(request); } else { uiWebView!.loadRequest(request) return nil } }

内部の実装イメージ

class WKWebViewWrapper: UIView { // どちらか一方はnil var wkWebView: WKWebView? var uiWebView: UIWebView? override init(frame: CGRect) { super.init(frame: frame) if NSClassFromString("WKWebView") != nil { wkWebView = WKWebView(frame: self.bounds) self.addSubview(wkWebView!) } else { uiWebView = UIWebView(frame: self.bounds) self.addSubview(uiWebView!) } }

func loadRequest(request: NSURLRequest) -> WKNavigation? { if(wkWebView != nil) { return wkWebView!.loadRequest(request); } else { uiWebView!.loadRequest(request) return nil } }

内部の実装イメージ

UIWebView / WKWebView の両方を変数で宣言

class WKWebViewWrapper: UIView { // どちらか一方はnil var wkWebView: WKWebView? var uiWebView: UIWebView? override init(frame: CGRect) { super.init(frame: frame) if NSClassFromString("WKWebView") != nil { wkWebView = WKWebView(frame: self.bounds) self.addSubview(wkWebView!) } else { uiWebView = UIWebView(frame: self.bounds) self.addSubview(uiWebView!) } }

func loadRequest(request: NSURLRequest) -> WKNavigation? { if(wkWebView != nil) { return wkWebView!.loadRequest(request); } else { uiWebView!.loadRequest(request) return nil } }

内部の実装イメージ

対応/非対応に応じて一方にインスタンスを入れる

class WKWebViewWrapper: UIView { // どちらか一方はnil var wkWebView: WKWebView? var uiWebView: UIWebView? override init(frame: CGRect) { super.init(frame: frame) if NSClassFromString("WKWebView") != nil { wkWebView = WKWebView(frame: self.bounds) self.addSubview(wkWebView!) } else { uiWebView = UIWebView(frame: self.bounds) self.addSubview(uiWebView!) } }

func loadRequest(request: NSURLRequest) -> WKNavigation? { if(wkWebView != nil) { return wkWebView!.loadRequest(request); } else { uiWebView!.loadRequest(request) return nil } }

内部の実装イメージ

すべてのメソッドで分岐処理

目的は達成できているのだが、 全メソッドに分岐を入れるのが あまりエレガントな感じしない。

方針2:

UIWebView に

WKWebView のフリをさせる

WKWebView UIWebView

iOS 8 以上 iOS 7 以下

WKWebViewUIWebView

iOS 8 以上 iOS 7 以下

WKWebViewUIWebView

WKWebView の IF

iOS 8 以上 iOS 7 以下

WKWebViewUIWebView

WKWebView の IF

Controller

iOS 8 以上 iOS 7 以下

外からは同じ型に見える

Obj-C 先輩の力を借りる

えっ

Why Obj-C ?UIWebView WKWebView

var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var title: String? { get } var URL: NSURL? { get } var estimatedProgress: Double { get }

func loadRequest(request: NSURLRequest) func reload() func stopLoading() func goBack() func goForward()

func loadRequest(request: NSURLRequest) -> WKNavigation?

func reload() -> WKNavigation? func stopLoading() func goBack() -> WKNavigation? func goForward() -> WKNavigation?

func stringByEvaluatingJavaScriptFromString (script: String) -> String?

func evaluateJavaScript(javaScriptString: String, completionHandler: ((AnyObject!, NSError!) -> Void)?)

weak var delegate: UIWebViewDelegate? weak var navigationDelegate: WKNavigationDelegate?

weak var UIDelegate: WKUIDelegate?

Why Obj-C ?UIWebView WKWebView

var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var canGoBack: Bool { get } var canGoForward: Bool { get } var loading: Bool { get }

var title: String? { get } var URL: NSURL? { get } var estimatedProgress: Double { get }

func loadRequest(request: NSURLRequest) func reload() func stopLoading() func goBack() func goForward()

func loadRequest(request: NSURLRequest) -> WKNavigation?

func reload() -> WKNavigation? func stopLoading() func goBack() -> WKNavigation? func goForward() -> WKNavigation?

func stringByEvaluatingJavaScriptFromString (script: String) -> String?

func evaluateJavaScript(javaScriptString: String, completionHandler: ((AnyObject!, NSError!) -> Void)?)

weak var delegate: UIWebViewDelegate? weak var navigationDelegate: WKNavigationDelegate?

weak var UIDelegate: WKUIDelegate?

Swift はココの戻り値の型の違いを区別してしまう! (かなり頑張ったけど無理でした笑)

Obj-C の緩さに甘える (どうせ隠蔽するんだしいいでしょ)

…許す

手順1: WKWebView の IF を Protocol として切り出す

// WKWebViewProtocol.swift

#import <UIKit/UIKit.h> #import <WebKit/WebKit.h>

@protocol WKWebViewProtocol <NSObject>

@property (nonatomic, readonly, getter=canGoBack) BOOL canGoBack; @property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward; @property (nonatomic, readonly, getter=isLoading) BOOL loading;

@property (nonatomic, readonly) NSURL *URL; @property (nonatomic, readonly, copy) NSString *title;

- (void)loadRequest:(NSURLRequest *)request;

- (void)reload; - (void)stopLoading; - (void)goBack; - (void)goForward;

- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler;

@end

手順1: WKWebView の IF を Protocol として切り出す

// WKWebViewProtocol.swift

#import <UIKit/UIKit.h> #import <WebKit/WebKit.h>

@protocol WKWebViewProtocol <NSObject>

@property (nonatomic, readonly, getter=canGoBack) BOOL canGoBack; @property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward; @property (nonatomic, readonly, getter=isLoading) BOOL loading;

@property (nonatomic, readonly) NSURL *URL; @property (nonatomic, readonly, copy) NSString *title;

- (void)loadRequest:(NSURLRequest *)request;

- (void)reload; - (void)stopLoading; - (void)goBack; - (void)goForward;

- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler;

@endWKWebViewのヘッダファイルからコピペしてくる

手順2: WKWebView の拡張で Protocol に適合

// WKWebView+ProtocolConformed.h

#import "WKWebViewProtocol.h"

@interface WKWebView(YSSWebViewProtocol) <WKWebViewProtocol>

@end

// WKWebView+ProtocolConformed.m

#import "WKWebView+ProtocolConformed.h"

@implementation WKWebView(YSSWebViewProtocol)

@end

手順2: WKWebView の拡張で Protocol に適合

// WKWebView+ProtocolConformed.h

#import "WKWebViewProtocol.h"

@interface WKWebView(YSSWebViewProtocol) <WKWebViewProtocol>

@end

// WKWebView+ProtocolConformed.m

#import "WKWebView+ProtocolConformed.h"

@implementation WKWebView(YSSWebViewProtocol)

@end

Protocol の適合を宣言

手順2: WKWebView の拡張で Protocol に適合

// WKWebView+ProtocolConformed.h

#import "WKWebViewProtocol.h"

@interface WKWebView(YSSWebViewProtocol) <WKWebViewProtocol>

@end

// WKWebView+ProtocolConformed.m

#import "WKWebView+ProtocolConformed.h"

@implementation WKWebView(YSSWebViewProtocol)

@end

元から実装されているので拡張は何もいらない!

手順3: 同様に UIWebView の拡張も作成

// UIWebView+WKProtocolConformed.h

#import "WKWebViewProtocol.h"

@interface UIWebView (WKProtocolConformed) <WKWebViewProtocol>

@end

手順3: 同様に UIWebView の拡張も作成

// UIWebView+WKProtocolConformed.h

#import "WKWebViewProtocol.h"

@interface UIWebView (WKProtocolConformed) <WKWebViewProtocol>

@end 同様に WKWebView の Protocol 適合を宣言

UIWebView には足りないメソッドを実装する!

// UIWebView+WKProtocolConformed.m

#import "UIWebView+WKProtocolConformed.h"

@implementation UIWebView (WKProtocolConformed)

- (NSURL *)URL { NSString *URLString = [self stringByEvaluatingJavaScriptFromString:@"document.URL"]; return [NSURL URLWithString:URLString]; }

- (NSString *)title { return [self stringByEvaluatingJavaScriptFromString:@"document.title"]; }

- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler { NSString *result = [self stringByEvaluatingJavaScriptFromString:javaScriptString]; if(completionHandler) { completionHandler(result, nil); } }

@end

UIWebView には足りないメソッドを実装する!

// UIWebView+WKProtocolConformed.m

#import "UIWebView+WKProtocolConformed.h"

@implementation UIWebView (WKProtocolConformed)

- (NSURL *)URL { NSString *URLString = [self stringByEvaluatingJavaScriptFromString:@"document.URL"]; return [NSURL URLWithString:URLString]; }

- (NSString *)title { return [self stringByEvaluatingJavaScriptFromString:@"document.title"]; }

- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler { NSString *result = [self stringByEvaluatingJavaScriptFromString:javaScriptString]; if(completionHandler) { completionHandler(result, nil); } }

@end loadRequest: などは元から実装されているのでスルー!

準備はここまで ( Obj-C のことはもう忘れてOK)

Controller で WKWebViewProtocol 型として webView を保持// MyViewController.swift

import UIKit import WebKit

class ViewController: UIViewController { var webView: WKWebViewProtocol! override func viewDidLoad() { super.viewDidLoad() self.createWebView() } func createWebView() { if NSClassFromString("WKWebView") != nil { let wkWebView = WKWebView(frame: self.view.bounds) self.view.addSubview(wkWebView) webView = wkWebView } else { let uiWebView = UIWebView(frame: self.view.bounds) self.view.addSubview(uiWebView) webView = uiWebView as WKWebViewProtocol } }

…(続く)

Controller で WKWebViewProtocol 型として webView を保持// MyViewController.swift

import UIKit import WebKit

class ViewController: UIViewController { var webView: WKWebViewProtocol! override func viewDidLoad() { super.viewDidLoad() self.createWebView() } func createWebView() { if NSClassFromString("WKWebView") != nil { let wkWebView = WKWebView(frame: self.view.bounds) self.view.addSubview(wkWebView) webView = wkWebView } else { let uiWebView = UIWebView(frame: self.view.bounds) self.view.addSubview(uiWebView) webView = uiWebView as WKWebViewProtocol } }

…(続く)

WKWebViewProtocol! 型として1個だけ変数を保持!

Controller で WKWebViewProtocol 型として webView を保持// MyViewController.swift

import UIKit import WebKit

class ViewController: UIViewController { var webView: WKWebViewProtocol! override func viewDidLoad() { super.viewDidLoad() self.createWebView() } func createWebView() { if NSClassFromString("WKWebView") != nil { let wkWebView = WKWebView(frame: self.view.bounds) self.view.addSubview(wkWebView) webView = wkWebView } else { let uiWebView = UIWebView(frame: self.view.bounds) self.view.addSubview(uiWebView) webView = uiWebView as WKWebViewProtocol } }

…(続く)インスタンス化の後は、WK / UI 共に変数 webView に格納できる!

Controller で WKWebViewProtocol 型として webView を保持

// MyViewController.swift

…(続き)

override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) let URL = NSURL(string: "http://yahoo.co.jp") let req = NSURLRequest(URL: URL!) webView.loadRequest(req) } }

Controller で WKWebViewProtocol 型として webView を保持

// MyViewController.swift

…(続き)

override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) let URL = NSURL(string: "http://yahoo.co.jp") let req = NSURLRequest(URL: URL!) webView.loadRequest(req) } }

以後は何の区別もなく同じ webView として扱える!

DEMO

方針1: Wrapper で包む

方針2: UIWebViewをいじる

• 特別なことをしていないので安心。• だいぶ裏技っぽい。 (戻り値型を捻じ曲げてる辺り特に)

• 全メソッドを実装して、それぞれ分岐処理を書くのが面倒。

• UIWebViewで足りないメソッドだけ実装すれば良い。

いつか iOS 7 のサポートを切るとき、 UIWebView と気持ち良く別れられるように。

2014年12月 5日 Yahoo! JAPAN Tech Blog

let UIWebView as WKWebView

Thanks!

Blog: http://taketo1024.hateblo.jpTwitter: @taketo1024