#Storyboardを使わずシンプルなタブブラウザを作る
##はじめに
いままではObjective-CでiOSアプリを作っていましたが
最近Swiftの勉強を始めました。
ポチポチWeb上のサンプルコードを叩いていましたが
何か目的を決めないと捗らないな・・・ということで
WKWebViewを使ってタブブラウザを作ってみようと思います。
今回はStoryboardは使わずコードのみで作ります。
実装する機能はシンプルに
・トップページにGoogleを表示
・タブ切り替え
・ブックマーク機能
・履歴閲覧機能
あたりで、3~4回くらいに分けて進めていきます。
##作業の流れ
↓の感じで進めます
1.WKWebViewでGoogleを表示
2.検索バーの機能実装
3.タブの一覧画面実装(タブの追加・削除)
4.ブックマークの一覧画面実装(追加・編集・削除)
5.履歴の表示機能実装(削除)
6.微調整
ブックマーク・履歴はSQLiteDBで管理します。
今回の投稿では1~2ぐらいまで進めたいと思います。
↓↓↓
##WKWebViewでGoogleを表示する
早速SingleViewアプリケーションで新規プロジェクトを作りました。
まずAppDelegateを↓のように修正します
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」を作成します。
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をちょと弄ります
App Transport Security Setting -> Allow Arbitrary Loads を YESにします。
一旦ここで実行してみます
↓↓↓
無事Googleが表示できました。
戻る・進むのフリップアクションも問題ないようです!
次にツールバーを作ります。
##ツールバーを実装
まずアイコンを用意してプロジェクトに追加します。
今回はこちらのサイトから拝借しました。
ではBrowserVcのviewDidLoad()末尾に追記していきます
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)
}
次にボタンをタップした時のアクションメソッドを実装します。
実装は先になりますが、ブックマークやタブボタンのイベントもメソッドだけ先に用意しておきます。
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) {
// タブ一覧
}
実行!!
↓↓↓
良さそうなので次は検索バーを作ります
↓
##検索バーを実装
BrowserVcのviewDidLoad()末尾に追記していきます
// ↓追加
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)
}
サーチバーで検索ボタンをおした時のデリゲートメソッドを実装します。
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を追記
class BrowserVC: UIViewController ,UISearchBarDelegate{
次にプログレスバーを実装します
##プログレスバーを実装
クラス宣言の後にKNavigationDelegateとWKUIDelegateを追加します
class BrowserVC: UIViewController, UISearchBarDelegate, WKNavigationDelegate, WKUIDelegate{
// ↓追加
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)
// 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
}
更新ボタンのイベントメソッドを実装します
func onClickReload(sender : UIButton){
// ページを再読み込み
self.webView.reload()
}
func onClickStop(sender : UIButton){
// ページを読み込み中止
self.webView.stopLoading()
}
実行↓↓
進捗に合わせてプログレスバーが表示されました。
##Alertを表示できるようにする。
WKWebViewは標準ではjavascriptのalertを表示する機能がありません。
自前で実装する必要があります。
furu8maさんの記事を参考にさせていただきました。
Swift版 WKWebViewでJavaScript(Alert,Confirm,Prompt)の処理
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のリンクをストアアプリで開く
// 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で書き直しました。