【Swift】Storyboardを使わずシンプルなタブブラウザを作る①

#Storyboardを使わずシンプルなタブブラウザを作る

##はじめに

いままではObjective-CでiOSアプリを作っていましたが
最近Swiftの勉強を始めました。
ポチポチWeb上のサンプルコードを叩いていましたが
何か目的を決めないと捗らないな・・・ということで
WKWebViewを使ってタブブラウザを作ってみようと思います。
今回はStoryboardは使わずコードのみで作ります。

<hr>

実装する機能はシンプルに
・トップページにGoogleを表示
・タブ切り替え
・ブックマーク機能
・履歴閲覧機能
あたりで、3~4回くらいに分けて進めていきます。


##アプリのイメージ
![名称未設定.png](https://qiita-image-store.s3.amazonaws.com/0/86259/be502ba1-7de4-9243-e85b-7f56f119d6ab.png)
こんな感じ(笑)

##作業の流れ
↓の感じで進めます
1.WKWebViewでGoogleを表示
2.検索バーの機能実装
3.タブの一覧画面実装(タブの追加・削除)
4.ブックマークの一覧画面実装(追加・編集・削除)
5.履歴の表示機能実装(削除)
6.微調整

ブックマーク・履歴はSQLiteDBで管理します。
今回の投稿では1~2ぐらいまで進めたいと思います。



↓↓↓


##WKWebViewでGoogleを表示する

早速SingleViewアプリケーションで新規プロジェクトを作りました。
![スクリーンショット 2016-03-07 23.43.47.png](https://qiita-image-store.s3.amazonaws.com/0/86259/d14e6b2b-b170-fada-21cd-0689eb97b2b8.png)

まずAppDelegateを↓のように修正します

``` AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var navigationController: UINavigationController?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        let viewController: BrowserVC = BrowserVC()
        navigationController = UINavigationController(rootViewController: viewController)
        
        self.window = UIWindow(frame: UIScreen.main.bounds)
        self.window?.rootViewController = navigationController
        self.window?.makeKeyAndVisible()
        
        return true
    }
}

```

次にWebViewを表示するViewController「BrowserVc.swift」を作成します。

``` BrowserVc.swift
import UIKit
import WebKit // WKWebViewを使うのでwebKitインポート

class BrowserVC: UIViewController {
    
    private var webView: WKWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // WKWebViewを生成
        webView = WKWebView(frame:CGRect(x:0, y:0, width:self.view.bounds.size.width, height:self.view.bounds.size.height - 40))
        
        // フリップで進む・戻るを許可
        webView.allowsBackForwardNavigationGestures = true
        
        // Googleを表示
        let urlString = "http://www.google.com"
        let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
        
        let url = NSURL(string: encodedUrlString!)
        let request = NSURLRequest(url: url! as URL)
        webView.load(request as URLRequest)
        
        // Viewに貼り付け
        self.view.addSubview(webView)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

```

iOS9からデフォルトではhttp通信ができなくなっているようなので
info.plistをちょと弄ります
![スクリーンショット 2016-03-08 0.10.38.png](https://qiita-image-store.s3.amazonaws.com/0/86259/4303c536-d937-8a3f-1dfa-5ea388486ffa.png)
App Transport Security Setting -> Allow Arbitrary Loads を YESにします。

一旦ここで実行してみます
↓↓↓

![スクリーンショット 2016-03-08 0.20.43.png](https://qiita-image-store.s3.amazonaws.com/0/86259/80eaf28d-c79f-8985-ee4f-ca578874d2fa.png)

無事Googleが表示できました。
戻る・進むのフリップアクションも問題ないようです！

次にツールバーを作ります。

##ツールバーを実装

まずアイコンを用意してプロジェクトに追加します。
今回は<a href="http://icooon-mono.com/">こちら</a>のサイトから拝借しました。

![スクリーンショット 2016-03-08 1.22.51.png](https://qiita-image-store.s3.amazonaws.com/0/86259/83bcf218-3643-c1ef-7ca5-55ef845f8c2c.png)


ではBrowserVcのviewDidLoad()末尾に追記していきます

``` BrowserVc.swift
override func viewDidLoad() {
    super.viewDidLoad()

    /*
        ~略~
    */

    // ツールバー
        let toolbar = UIToolbar(frame: CGRect(x:0, y:self.view.bounds.size.height - 44, width:self.view.bounds.size.width, height:40.0))
        toolbar.layer.position = CGPoint(x: self.view.bounds.width/2, y: self.view.bounds.height-20.0)
        toolbar.barStyle = .default
        toolbar.tintColor = UIColor.white
        
        // 戻るボタン
        let backBtnView = UIButton(frame: CGRect(x:0, y:0, width:24, height:24))
        backBtnView.setBackgroundImage(UIImage(named: "back"), for: .normal)
        backBtnView.addTarget(self, action: #selector(onClickBackBarButton), for: .touchUpInside)
        let backBtn = UIBarButtonItem(customView: backBtnView)
        
        // 進むボタン
        let forwardBtnView = UIButton(frame: CGRect(x:0, y:0, width:24, height:24))
        forwardBtnView.setBackgroundImage(UIImage(named: "forward"), for: .normal)
        forwardBtnView.addTarget(self, action: #selector(onClickForwardBarButton), for: .touchUpInside)
        let forwardBtn = UIBarButtonItem(customView: forwardBtnView)
        
        // ブックマークボタン
        let bookmarkBtnView = UIButton(frame: CGRect(x:0, y:0, width:24, height:24))
        bookmarkBtnView.setBackgroundImage(UIImage(named: "bookmark"), for: .normal)
        bookmarkBtnView.addTarget(self, action: #selector(onClickBookmarkBarButton), for: .touchUpInside)
        let bookmarkBtn = UIBarButtonItem(customView: bookmarkBtnView)
        
        // ブックマークボタン長押しのジェスチャー
        let bookmarkLongPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressBookmark))
        bookmarkLongPressGesture.minimumPressDuration = 1.0// 長押し-最低1秒間は長押しする.
        bookmarkLongPressGesture.allowableMovement = 150// 長押し-指のズレは15pxまで.
        bookmarkBtnView.addGestureRecognizer(bookmarkLongPressGesture)
        
        // タブボタン
        let tabBtnView = UIButton(frame: CGRect(x:0, y:0, width:24, height:24))
        tabBtnView.setBackgroundImage(UIImage(named: "tab"), for: .normal)
        tabBtnView.addTarget(self, action: #selector(onClickTabBarButton), for: .touchUpInside)
        let tabBtn = UIBarButtonItem(customView: tabBtnView)
        
        // スペーサー
        let flexibleItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.flexibleSpace, target: nil, action: nil)
        
        // ツールバーに追加する.
        toolbar.items = [backBtn, flexibleItem, forwardBtn, flexibleItem, bookmarkBtn, flexibleItem, tabBtn]
        self.view.addSubview(toolbar)

}
```

次にボタンをタップした時のアクションメソッドを実装します。
実装は先になりますが、ブックマークやタブボタンのイベントもメソッドだけ先に用意しておきます。

``` BrowserVc.swift
func onClickBackBarButton(sender: UIButton){
    // 前のページ
    self.webView.goBack()
}
func onClickForwardBarButton(sender: UIButton){
    // 次のページ
    self.webView.goForward()
}
func onClickBookmarkBarButton(sender: UIButton){
    // ブックマークリストを開く
}
func longPressBookmark(sender: UILongPressGestureRecognizer){
    // 長押し：ブックマーク追加
}
func onClickTabBarButton(sender: UIButton) {
    // タブ一覧
}
```

実行！！
↓↓↓

![スクリーンショット 2016-03-08 1.08.49.png](https://qiita-image-store.s3.amazonaws.com/0/86259/c2ac7c82-46b0-d4d5-6e97-c56945d03329.png)

良さそうなので次は検索バーを作ります

↓

##検索バーを実装
BrowserVcのviewDidLoad()末尾に追記していきます

``` BrowserVc.swift

// ↓追加
private var searchBar:UISearchBar!
private var reloadBtn:UIBarButtonItem!
private var stopBtn:UIBarButtonItem!

override func viewDidLoad() {
    super.viewDidLoad()

    /*
        ~略~
    */
        // 検索バーを作成する.
        searchBar = UISearchBar(frame:CGRect(x:0, y:0, width:270, height:80))
        searchBar.delegate = self
        searchBar.layer.position = CGPoint(x: self.view.bounds.width/2, y: 20)
        searchBar.searchBarStyle = UISearchBarStyle.minimal
        searchBar.placeholder = "URLまたは検索ワード"
        searchBar.tintColor = UIColor.cyan
        // 余計なボタンは非表示にする.
        searchBar.showsSearchResultsButton = false
        searchBar.showsCancelButton = false
        searchBar.showsBookmarkButton = false
        
        // UINavigationBar上に、UISearchBarを追加
        self.navigationItem.titleView = searchBar
        
        // Reloadボタン
        let reloadBtnView = UIButton(frame: CGRect(x:0, y:0, width:24, height:24))
        reloadBtnView.setBackgroundImage(UIImage(named: "reload"), for: .normal)
        reloadBtnView.addTarget(self, action: #selector(onClickReload), for: .touchUpInside)
        reloadBtn = UIBarButtonItem(customView: reloadBtnView)
        self.navigationItem.rightBarButtonItem = reloadBtn
        
        // Stopボタン
        let stopdBtnView = UIButton(frame: CGRect(x:0, y:0, width:24, height:24))
        stopdBtnView.setBackgroundImage(UIImage(named: "stop"), for: .normal)
        stopdBtnView.addTarget(self, action: #selector(onClickStop), for: .touchUpInside)
        stopBtn = UIBarButtonItem(customView: stopdBtnView)
}

```

サーチバーで検索ボタンをおした時のデリゲートメソッドを実装します。

``` BrowserVc.swift
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
    // ソフトウェアキーボードの検索ボタンが押された
    search(urlString: searchBar.text!)
    // キーボードを閉じる
    searchBar.resignFirstResponder()
}

func search( urlString:String)
{
    var urlString = urlString
    if(urlString == ""){
        return;
    }
    
    var strUrl: String
    var searchWord:String = ""
    let chkURL = urlString.components(separatedBy: ".")
    if chkURL.count > 1 {
        // URLの場合
        if urlString.hasPrefix("http://") || urlString.hasPrefix("https://") {
        } else {
            strUrl = "http://"
            strUrl = strUrl.appending(urlString)
        }
    } else {
        // 検索ワード
        urlString = urlString.replacingOccurrences(of: "?", with: " ")
        let words = urlString.components(separatedBy: " ")
        searchWord = words.joined(separator: "+")
        urlString = "https://www.google.co.jp/search?&q=\(searchWord)"
    }
    
    let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)
    let url = NSURL(string: encodedUrlString!)
    let request = NSURLRequest(url: url! as URL)
    self.webView.load(request as URLRequest)
}
```

そしてUISearchBarDelegateを使うのでクラス宣言の後にUISearchBarDelegateを追記

``` BrowserVc.swift
class BrowserVC: UIViewController ,UISearchBarDelegate{
```

実行↓↓↓
![スクリーンショット 2016-03-08 1.51.32.png](https://qiita-image-store.s3.amazonaws.com/0/86259/f41a43ec-724a-d1a3-c3b1-94c0d5cceeb5.png)

次にプログレスバーを実装します

##プログレスバーを実装

クラス宣言の後にKNavigationDelegateとWKUIDelegateを追加します

``` BrowserVc.swift
class BrowserVC: UIViewController, UISearchBarDelegate, WKNavigationDelegate, WKUIDelegate{
```

``` BrowserVc.swift

// ↓追加
private var progressView: UIProgressView!

override func viewDidLoad() {
    super.viewDidLoad()

    /*
        ~略~
    */

    // ProgressViewを作成する.
    progressView = UIProgressView(frame: CGRect(x:0, y:0, width:self.view.bounds.size.width * 2, height:20))
    progressView.progressTintColor = UIColor.green
    progressView.trackTintColor = UIColor.white
    progressView.layer.position = CGPoint(x:0, y:(self.navigationController?.navigationBar.frame.size.height)!)
    progressView.transform = CGAffineTransform(scaleX: 1.0, y: 2.0)
    self.navigationItem.titleView?.addSubview(progressView)

    // WebViewの読み込み状態を監視する
    self.webView.addObserver(self, forKeyPath:"estimatedProgress", options:.new, context:nil)
}
```

今回、シークバーの更新にはKVO(Key-Value Observing)を利用します。
KVOはあるオブジェクトに変更などが発生したことを検知して、別の命令を実行するデザインパターンです。
webView.estimatedProgressでWkWebViewの読み込み進捗が取得できます。(0.0〜1.0 ※ロード完了時が1.0)

``` BrowserVc.swift
// MARK: - プログレスバーの更新(KVO)
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
    if(keyPath == "estimatedProgress"){
        let progress : Float = Float(webView.estimatedProgress)
        if(progressView != nil){
            // プログレスバーの更新
            if(progress < 1.0){
                progressView.setProgress(progress, animated: true)
                UIApplication.shared.isNetworkActivityIndicatorVisible = true
                self.navigationItem.rightBarButtonItem = stopBtn
            }else{
                // 読み込み完了
                progressView.setProgress(0.0, animated: false)
                UIApplication.shared.isNetworkActivityIndicatorVisible = false
                self.navigationItem.rightBarButtonItem = reloadBtn
                searchBar.text = webView.url?.absoluteString
            }
        }
    }
}
deinit {
    self.webView?.removeObserver(self, forKeyPath: "estimatedProgress")
    self.webView.navigationDelegate = nil
    self.webView!.uiDelegate = nil
}   
```

更新ボタンのイベントメソッドを実装します

``` BrowserVc.swift
func onClickReload(sender : UIButton){
    // ページを再読み込み
    self.webView.reload()
}
func onClickStop(sender : UIButton){
    // ページを読み込み中止
    self.webView.stopLoading()
}
```

実行↓↓

![スクリーンショット 2016-03-08 2.28.36.png](https://qiita-image-store.s3.amazonaws.com/0/86259/6f72de73-72ea-0433-8908-4cf93b2cd2f0.png)

進捗に合わせてプログレスバーが表示されました。

##Alertを表示できるようにする。
WKWebViewは標準ではjavascriptのalertを表示する機能がありません。
自前で実装する必要があります。

furu8maさんの記事を参考にさせていただきました。
<a href="http://qiita.com/furu8ma/items/183f85a106ba827ad0ea">Swift版 WKWebViewでJavaScript(Alert,Confirm,Prompt)の処理</a>

``` BrowserVc.swift
override func viewDidLoad() {
    /*
      ~ 略 ~
    */
    // ↓追加
    self.webView.navigationDelegate = self
    self.webView!.uiDelegate = self
}
// MARK: Javascript関連
    func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {        let alertController = UIAlertController(title: "", message: message, preferredStyle: .alert)
        // display alert dialog
        let otherAction = UIAlertAction(title: "OK", style: .default) {
            action in completionHandler()
        }
        alertController.addAction(otherAction)
        present(alertController, animated: true, completion: nil)
    }
    
    
    func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
        // display confirm dialog
        let alertController = UIAlertController(title: "", message: message, preferredStyle: .alert)
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) {
            action in completionHandler(false)
        }
        let okAction = UIAlertAction(title: "OK", style: .default) {
            action in completionHandler(true)
        }
        alertController.addAction(cancelAction)
        alertController.addAction(okAction)
        present(alertController, animated: true, completion: nil)
    }
    
    
    func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
        // display prompt dialog
        let alertController = UIAlertController(title: "", message: prompt, preferredStyle: .alert)
        
        let okHandler: () -> Void = { handler in
            if let textField = alertController.textFields?.first {
                completionHandler(textField.text)
            } else {
                completionHandler("")
            }
        }
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) {
            action in completionHandler("")
        }
        let okAction = UIAlertAction(title: "OK", style: .default) {
            action in okHandler()
        }
        alertController.addTextField() { $0.text = defaultText }
        alertController.addAction(cancelAction)
        alertController.addAction(okAction)
        present(alertController, animated: true, completion: nil)
    }

    
```

##AppStoreのリンクをストアアプリで開く

``` BrowserVc.swift
// MARK: - WkWebViewの画面遷移をフック
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    let url = navigationAction.request.url
    let urlString = ((url) != nil) ? url!.absoluteString : ""
    
    if navigationAction.targetFrame == nil {
        // _blank Link
        webView.load(navigationAction.request)
    }else if isMatch(input: urlString,pattern: "\\/\\/itunes\\.apple\\.com\\/") {
        // AppStoreのリンクなら、ストアアプリで開く
        UIApplication.shared.openURL(url!)
        decisionHandler(WKNavigationActionPolicy.cancel)
    }
    decisionHandler(WKNavigationActionPolicy.allow)
}

// MARK: - 正規表現でマッチング
func isMatch(input: String, pattern:String) -> Bool {
    let regex = try! NSRegularExpression( pattern: pattern, options: NSRegularExpression.Options.caseInsensitive)
    let matches = regex.matches( in: input, options: [], range:NSMakeRange(0, input.characters.count) )
    return matches.count > 0
}
```

##今回はここまでです
次回は、タブ周りの機能を実装していきます。

※2017.4.18
 Swift3.0で書き直しました。