Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

はじめに

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


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

アプリのイメージ

名称未設定.png
こんな感じ(笑)

作業の流れ

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

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

↓↓↓

WKWebViewでGoogleを表示する

早速SingleViewアプリケーションで新規プロジェクトを作りました。
スクリーンショット 2016-03-07 23.43.47.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
App Transport Security Setting -> Allow Arbitrary Loads を YESにします。

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

スクリーンショット 2016-03-08 0.20.43.png

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

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

ツールバーを実装

まずアイコンを用意してプロジェクトに追加します。
今回はこちらのサイトから拝借しました。

スクリーンショット 2016-03-08 1.22.51.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

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

検索バーを実装

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

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

プログレスバーを実装

クラス宣言の後に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

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

Alertを表示できるようにする。

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

furu8maさんの記事を参考にさせていただきました。
Swift版 WKWebViewでJavaScript(Alert,Confirm,Prompt)の処理

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で書き直しました。

penguin1121
よろしくお願いします!
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away