JavaScriptプログラマがSwift iOSアプリを2週間で作って公開してみた〜その14 UIWebView〜


JavaScriptプログラマー(JSer)がSwiftデビューして、ただ作りたいアプリを作ってみたシリーズ第14回目です。

前回はユーザーが何かアクションを起こした後に表示するトーストのポップアップとかローディングマークについてでした。

今回はlinkを含むtweetのcellをタップした際に、in app browser(UIWebView)を使ってwebページを表示する方法の紹介です。

まず完成イメージ図。

S__71589899

例えば、この画面の上から2番目の記事をタップすると、

S__71589898

こんな感じでブラウザで表示されます。

余談ですが、最近iPhone女史というサイトでこのアプリを紹介していただきました!(この記者の方はどうやってこのアプリに辿り着いたのだろうか。。)

Twitterでシェアした記事の既読管理が1発で出来る『TwitStocker』
http://www.iphone-girl.jp/2015/02/405494/

さて実装の話に戻ると、このアプリ内のブラウザを起動する方法は従来のUIWebViewを使った方法と、最近登場したWKWebViewを使った方法があります。性能に関してはWKWebViewの方が断然優れているそうなのですが、今回とりあえず作ることを目的としていたので、実装のための情報が豊富だったUIWebViewを使って実装します。

まずはtweetのcellがタップされた時のハンドラーを定義します。
実は第11回目Infinite Scrollの回で登場していたコードでtweet viewのdelegateにselfをセットしていました。それがtweetのcellがタップされた時の挙動を実装するためのものです。

BaseTweetViewController.swift

// UITableViewDataSourceを実装する
extension BaseTweetViewController : UITableViewDataSource {
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tweets.count
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell") as TWTRTweetTableViewCell
        if tweets.count > indexPath.row {
            // for TWTRTweetViewDelegate to handling on select
            cell.tweetView.delegate = self
            let tweet = tweets[indexPath.row]
            cell.tag = indexPath.row
            cell.configureWithTweet(tweet)
            // load more data by showing a last table cell
            if (tweets.count - 1) == indexPath.row && self.maxIdStr != "" {
                self.loadMore({() -> () in }, errcb: {() -> () in })
            }
        }
        return cell
    }
}

この”for TWTRTweetViewDelegate to handling on select”とコメントしているところです。それによって、TWTRTweetViewDelegateを実装する必要があります。

BaseTweetViewController.swift

extension BaseTweetViewController : TWTRTweetViewDelegate {
    // tap a cell
    func tweetView(tweetView: TWTRTweetView!, didSelectTweet tweet: TWTRTweet!) {
        var url: String = ""
        // URLTweetにcastしてurlを取り出す
        if let urlTweet = tweet as? URLTweet {
            url = urlTweet.url
        }
        if url == "" {
            return
        }
        self.openWebView(NSURL(string: url)!)
    }
    
    // tap a link in cell
    func tweetView(tweetView: TWTRTweetView!, didTapURL url: NSURL!) {
        self.openWebView(url)
    }
    
    func openWebView(url: NSURL){
        let webviewController = StockWebViewController()
        webviewController.url = url
        webviewController.hidesBottomBarWhenPushed = true
        self.navigationController?.pushViewController(webviewController, animated: true)
    }
}

TWTRTweetViewDelegateのtweetViewを実装するわけですが、1つ目の方はcell自体がクリックされた時(URLTweetの部分は後述します)、2つ目のcellの中のリンクがクリックされた時のためのものです。

cell自体もしくはcell内のlinkがtapされた時はurlをopenWebViewというfuncに渡してStockWebViewControllerを生成します。self.navigationController?.pushViewControllerの処理でNavigationされてアプリ内のブラウザのviewが出現します。

このStockWebViewControllerは自身のViewの中にブラウザと下の部分にツールバー(戻る、進む、リロード、safariで開く)を持つviewを扱います。コードの一部を抜粋します。

StockWebViewController.swift

import Foundation
import UIKit

class StockWebViewController : UIViewController, UIWebViewDelegate, UIActionSheetDelegate {
    
    var webView: UIWebView = UIWebView()
    var toolBar: UIToolbar = UIToolbar()
    var backButton: UIBarButtonItem!
    var forwardButton: UIBarButtonItem!
    var refreshButton: UIBarButtonItem!
    var safariButton: UIBarButtonItem!
    var url : NSURL?
    let toolBarHeight: CGFloat = 50.0
    
    override init() {
        super.init(nibName: nil, bundle: nil)
        let selfFrame: CGRect = self.view.frame
        self.webView.frame = CGRect(x: 0, y: 0, width: selfFrame.size.width, height: selfFrame.size.height-toolBarHeight)
        self.webView.delegate = self
        self.view.addSubview(self.webView)
        self.toolBar.frame = CGRect(x: 0, y: selfFrame.size.height - toolBarHeight, width: selfFrame.size.width, height: toolBarHeight)
        self.toolBar.backgroundColor = Constants.Theme.base()
        self.toolBar.tintColor = Constants.Theme.twitter()
        self.view.addSubview(self.toolBar)
    }
    
    override func loadView() {
        super.loadView()
        let spacer: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .FlexibleSpace, target: nil, action: nil)
        self.backButton = UIBarButtonItem(image: UIImage(named: "toolbar_back"), style: .Plain, target: self, action: Selector("back"))
        self.forwardButton = UIBarButtonItem(image: UIImage(named: "toolbar_forward"), style: .Plain, target: self, action: Selector("forward"))
        self.refreshButton = UIBarButtonItem(image: UIImage(named: "toolbar_reload"), style: .Plain, target: self, action: Selector("reload"))
        self.safariButton = UIBarButtonItem(image: UIImage(named: "toolbar_external"), style: .Plain, target: self, action: Selector("safari"))
        let items: NSArray = [spacer, self.backButton, spacer, self.forwardButton, spacer, self.refreshButton, spacer, self.safariButton, spacer]
        self.toolBar.items = items
    }
        
    override func viewDidLoad() {
        super.viewDidLoad()
        self.backButton.enabled = self.webView.canGoBack
        self.forwardButton.enabled = self.webView.canGoForward
        self.refreshButton.enabled = false
        self.safariButton.enabled = false
        
    }
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        self.view.makeToastActivity()
        let requestURL: NSURLRequest = NSURLRequest(URL: self.url!)
        self.webView.loadRequest(requestURL)
    }
    
    func back() {
        self.webView.goBack()
        self.backButton.enabled = self.webView.canGoBack
        self.forwardButton.enabled = self.webView.canGoForward
    }
    
    func forward() {
        self.webView.goForward()
        self.backButton.enabled = self.webView.canGoBack
        self.forwardButton.enabled = self.webView.canGoForward
    }
    
    func reload() {
        self.view.makeToastActivity()
        self.webView.reload()
    }
    ...
}

ブラウザであるUIWebViewと下部のツールバーであるUIToolbarを初期化時に生成して、以降各アクションボタンをUIBarButtonItemを生成し、toolbarのitemsにまとめてセットします。また、それぞれのアクションに対応したfunctionを定義すればOKです。

以上が、In AppのブラウザとしてUIWebViewを起動する方法の紹介でした。

ただし、Twitter Fabricを使ってTweet Cellをtapした時にin app browserをオープンする実装についてはもう少し拡張が必要です。それは、TWTRTweetというCellに対するデータを提供するクラスががtwitter APIのresponse内のlink URLの情報をもっていないからです。

今回はBaseTweetViewController.swiftのTWTRTweetViewDelegateの実装の中に出てきURLTweetというクラスを作成しました。

このクラスはFabricで提供しているTWTRTweetをextendしています。Twitter Search APIのResponseを受け取った時にurlというプロパティを追加したクラスです。

responseのJSONのパースをしてURLのentitiesが存在する場合は、この拡張したプロパティであるURLにセットしていきます。

URLTweet.swift

import Foundation
import TwitterKit

class URLTweet : TWTRTweet {
    
    var url : String = ""
    
    override init!(JSONDictionary dictionary: [NSObject : AnyObject]!) {
        super.init(JSONDictionary: dictionary)
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override class func tweetsWithJSONArray(array: [AnyObject]!) -> [AnyObject]! {
        var list: [AnyObject]!
        if array != nil {
            list = [AnyObject]()
            for status in array {
                let tweet = URLTweet(JSONDictionary: status as [NSObject : AnyObject])
                // set URL from json status -> entities -> urls -> expanded_url
                if let status = status as? NSDictionary {
                    if let entities = status["entities"] as? NSDictionary {
                        if let urls = entities["urls"] as? NSArray {
                            if urls.count > 0 {
                                if let url = urls[0] as? NSDictionary {
                                    if let expandedUrl = url["expanded_url"] as? String {
                                        tweet.url = expandedUrl
                                    }
                                }
                            }
                        }
                    }
                }
                list.append(tweet)
            }
        }
        return list
    }
}

API responseを受け取った側は以下のように、JSONからTWTRTweetへの変換をURLTweetのクラスメソッドを使ってURLTweetへ変換するだけです。

TwitterAPI.swift

var list: [TWTRTweet] = []
if let statuses = top["statuses"] as? NSArray {
    list = URLTweet.tweetsWithJSONArray(statuses) as [TWTRTweet]
}

上の例はTWTRTweetでキャストしていますが、実態はURLTweetです。そしたら、urlの取り出しが必要な時はURLTweetとしてcastしてやればOKです!
以上、今回は貼り付けているコードの量が多くなってしまいましたがFabricのtweet cellをタップした時にUIWebViewを起動する方法の紹介でした。次は、Xcodeを実機で試すところの解説予定。

TwitStockerのダウンロードはこちらから。
https://itunes.apple.com/en/app/twitstocker/id958798898?l=ja&ls=1&mt=8