Edited at

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

More than 1 year has 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で書き直しました。