こんにちわインケンです。
先月からswiftを始めました。Obj-Cもまともに触ったことないし、全くのiOS初心者です。
「はじめてのアプリ」みたいな初心者本はいくつか読んだのですが、もうちょっと突っ込んだ情報が欲しいなーと作りながら思っていたので、同じような境遇にいる初心者の方の参考になれば。
Xcodeのバージョンは6.3.1です。
完成はこんな感じ
ソースはこちら https://github.com/inkenkun/RssReader
ストーリーボードは使わず、カスタムセルのxibとコードのみで作りました。
タブというかメニュー型のカテゴリがあってスワイプすると、そのカテゴリの記事の一覧に切り替わります。
一覧の記事をタップすると右の詳細ページへ。詳細ページはWEBブラウザになっています。
使用するライブラリ
使用したライブラリはこれ
Alamofire
https://github.com/Alamofire/Alamofire
通信部分をいい感じに書きやすくしてくれるライブラリです。
RSSのAPIを叩いて結果を取得する部分を担っています
SwiftyJSON
https://github.com/SwiftyJSON/SwiftyJSON
JSONを簡単に扱うためのライブラリです。
RSSはXMLなのですがJSONの方が汎用性があるので、今回はGoogleのRSSをJSONにしてくれるAPIを通してから取得しています。
PageMenu
https://github.com/uacaps/PageMenu
メニューによってページの切り替えをいい感じにやってくれるライブラリです。
画面上部の「コンピューター」「海外」「地域」のメニュー切り替えを行っています。
HTMLReader
https://github.com/nolanw/HTMLReader
HTMLパーサーです。
一覧でのサムネイルの取得と、タイトルの下の内容文の取得で使っています。
SVProgressHUD
https://github.com/TransitApp/SVProgressHUD
ローディングのくるくるを超簡単に付けられるライブラリです。
TOWebViewController
https://github.com/TimOliver/TOWebViewController
ブラウザライブラリです。
標準のWebViewでもいいかなーと思ったのですが、なんとなく
PullToRefreshSwift
https://github.com/dekatotoro/PullToRefreshSwift
PullToRefreshを簡単に実装できるライブラリです。
あと、サムネイル画像がなかった時のNoPhotoアイコンはicon8のを使用しています。
1.プロジェクトを作る
まずはプロジェクトを作ります。
今回は記事一覧のtableviewはコードで書くので、Single View Applicationで作ります。
RssReaderなのにnewsってつけてしまいました、、、LanguageはもちろんSwift
で、作成されたらライブラリを入れるためプロジェクトを一旦閉じます。
2.ライブラリを入れる
ライブラリは Cocoa Pods ってので管理します。NodeJSのnpmみたいなもんですね。
Cocoa Podsの入れ方などはこの辺を参考に。
Pods入れたら、コンソールからプロジェクトのディレクトリで
$ pod init
をやるとPodfileが作られます。
Podfileの中身を以下のように変更します。
source 'https://github.com/CocoaPods/Specs.git' use_frameworks! platform :ios, '8.0' target 'news' do pod 'Alamofire' pod 'SwiftyJSON' pod 'SVProgressHUD' pod 'TOWebViewController' pod 'PageMenu' pod "HTMLReader" end target 'newsTests' do end
そして、
$ pod install
をすると、ライブラリがインストールされます。
ライブラリがインストールされると、news.xcworkspace ってファイルも作成されるので、news.xcodeproj の代わりにnews.xcworkspace を開いて開発していくことになります。
news.xcworkspace を開いて一旦ビルド(command + b)してみましょう。
pagemenuのCAPSPageMenu.swiftがエラーを大量に吐き出しました。執筆時点ではCocoaPodsに登録されているpagemenuのライブラリのバージョンが低いため、Xcode6.3.1だとエラーになります。
そこで、pagemenuのgitからCAPSPageMenu.swiftのソースをまるまるコピーしてきて上書きします。
これでエラーはなくなりました。
CocoaPodsに対応していないライブラリを入れる
残念ながらこれ書いてる時点では、PullToRefreshSwiftがCocoaPodsに対応していないので、これだけは手動で入れる必要があります。
と言ってもファイルをドラッグ&ドロップするだけです。
Copy Item if neededにして、必要であればgroupでまとめちゃいましょう
Objective-Cのライブラリをswiftで使えるようにする
今回使ったライブラリの中で、SVProgressHUDとTOWebViewControllerの2つがObj-Cで書かれているので、そのままでは使えずブリッジヘッダファイルを用意しなければなりません。
手動で作ることもできるみたいですが、Obj-Cファイルを作成すると「ブリッジングヘッダファイル作る?」と聞かれて勝手に作ってくれるのでこれを利用します
このファイルは消すので、適当な名前つけときます。
Nextを押すと以下のようにbridging heder作る?って聞かれます
Yesを押すと、自動的に news-Bridging-Header.h というファイルが作られ、
Build Setting にも自動的に追加してくれます。
ただ、Podsのパスは自動的に追加してくれないので、自分で追加します。
Build Settings の SearchPaths の User Header Search Pathsに以下の設定をします。
今作ったObj-Cのダミーファイルは削除しちゃいましょう。
news-Bridging-Header.h に以下を追加します。
#import "SVProgressHUD/SVProgressHUD.h" #import "TOWebViewController/TOWebViewController.h"
ついでにno photoの画像もimages.xcassetsにドラッグ&ドロップして追加しておきます
3.カスタムセルを作る
ライブラリが整ったところで早速コードと行きたいところですが、まずは記事一覧の一つ一つのセルのパーツを作ります。
この記事一覧の中身はコードで書くより画面で配置したほうがわかりやすいですしね
新規で Cocoa Touch Classを作ります。
Subclass of を UITableViewCell にして Also create XIB file にチェック入れます
これで CustomCell.swiftとCustomCell.xib ファイルが作られるので、CustomCell.xibを開いてパーツを配置していきます。
パーツを配置する前に、Show the Attributes InspectorのIdentifierを「Cell」にします。
セル全体の高さは140としておきます。
パーツは今回はUIImageViewとUILabelを使います
UIImageは以下のように設定します。
Imageには登録したno photoの画像を指定します。
Clip Subviewをチェックしないと画像がはみ出ます。
AutoLayoutのPinで右上のように設定します。画像のサイズは100 x 100に。
タイトルを表示するラベルも配置して設定していきます。
コンテンツはこんな感じ
パーツとコードを紐付ける
パーツを配置したらそれをコードと紐付けます。
マウスでびーって、こんな感じに。
これでカスタムセルの完成です。
4.必要なswiftファイルを作成
必要なライブラリ、パーツが揃ったので、コードを書いていくためのファイルを作成します。
まずは記事一覧ページの FeedTableViewController
Cocoa Touch Class で Subclass of : に UITableViewController を指定します。
次に一覧をタップした先の詳細ページの DetailViewController
Cocoa Touch Class で Subclass of : に UIViewController を指定します。
次にJSONのパースやスクレイピングなどの機能を別ファイルにするための parseFeed.swift (なんでこれだけ小文字始まりにしたんだろ、、)
Swift Fileで作ります。
5.パース関数を作る
parseFeed.swiftに、RSSのJSONをパースする関数と、URL先の画像と文章を取得する関数を作りました。
https://github.com/inkenkun/RssReader/blob/master/news/parseFeed.swift
/* RSSのJSONをパースする */ func parse (url: String, completion: (([JSON]?, NSError?) -> Void)){ var url = NSURL(string: url) Alamofire.request(.GET, url!, parameters: nil, encoding: .JSON) .responseJSON { (request, response, data, error) in let json = JSON(data!) let entries = json["responseData"]["feed"]["entries"].array completion(entries, error) } }
/* URL先の文章と画像を取得 */ func getContents (url: String, completion: ((AnyObject, NSError?) -> Void)){ var url = NSURL(string: url) var ret : Dictionary<String, String!> = [:] Alamofire.request(.GET, url!, parameters: nil) .responseString { (request, response, data, error) in var content = "" let html = HTMLDocument(string: data) if let ogTags = html.nodesMatchingSelector("meta[property=\"og:description\"]") { for tag in ogTags { content = (tag.attributes?["content"] as? String)! } } var image = "" if let imgTags = html.nodesMatchingSelector("img") { for img in imgTags { if(img.attributes?["data-src"] != nil){ image = (img.attributes?["data-src"] as? String)! } } } ret = [ "content": content , "image" : image ] completion(ret, error) } }
あとから言われて気づいたけど、これクラスにする必要がなかったみたい。
6.メインのViewControllerでPageMenuを設定する
最初から作られている ViewController.swift にはPageMenuライブラリの設定をしていきます。
https://github.com/inkenkun/RssReader/blob/master/news/ViewController.swift
PageMenuはいくつかのビューコントローラーを配列にして渡してやると、勝手にタブっぽくメニュー化してくれます。
var feedArray: [ Dictionary<String, String!> ] = [ [ "link" : "http://ajax.googleapis.com/ajax/services/feed/load?v=1.0&q=http://rss.dailynews.yahoo.co.jp/fc/computer/rss.xml&num=10" , "title" : "コンピュータ" ], [ "link" : "http://ajax.googleapis.com/ajax/services/feed/load?v=1.0&q=http://rss.dailynews.yahoo.co.jp/fc/world/rss.xml&num=10" , "title" : "海外" ], [ "link" : "http://ajax.googleapis.com/ajax/services/feed/load?v=1.0&q=http://rss.dailynews.yahoo.co.jp/fc/local/rss.xml&num=10" , "title" : "地域" ] ] for feed in feedArray { var feedController = FeedTableViewController() feedController.link = feed["link"]! feedController.parent = self feedController.title = feed["title"]! controllerArray.append(feedController) }
feedArrayにURLとタイトルを設定してます。URLはRSSのURLをgoogleのAPIに通したものを設定してます。これでRSSをJSONで取得できます。
その下のforで、URLとタイトルをこの後いじる TableViewController に渡しています。
あとはPageMenuの設定と、メニューを表示させる位置をナビゲーションバーの高さを考慮して設定してます。
7.記事の一覧ページ
FeedTableViewController.swift をいじっていきましょう。
https://github.com/inkenkun/RssReader/blob/master/news/FeedTableViewController.swift
TableViewControllerなので、よくあるTableViewControllerの記法に則って記載していきます
本とかには、以下のように記載されていることが多いと思うのですが、
class FeedTableViewController: UITableViewController, UITableViewDataSource, UITableViewDelegate { ....
これを今回はextensionを使って書いています。
class FeedTableViewController: UITableViewController { ... } extension FeedTableViewController : UITableViewDataSource { ... } extension FeedTableViewController : UITableViewDelegate { ... }
内容は一緒なのですが、extension使って書くことによって、可読性が上がっていいらしいです。
override func viewDidLoad() { super.viewDidLoad() SVProgressHUD.show() var nib:UINib = UINib(nibName: "CustomCell", bundle: nil) self.tableView.registerNib(nib, forCellReuseIdentifier: "Cell") parse.parse(self.link, completion: {(data,error) in self.entries = data! self.tableView.reloadData() SVProgressHUD.dismiss() }) self.tableView.addPullToRefresh({ [weak self] in self?.tableView.reloadData() self?.tableView.stopPullToRefresh() }) }
SVProgressHUD.show() でローディングのクルクルを出して、SVProgressHUD.dismiss() でクルクルを止めてます
self.tableView.addPullToRefresh で PullToRefreshの設定をしてます。
あとはカスタムセルの指定をしてます。
extension FeedTableViewController : UITableViewDataSource { override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.entries.count } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! CustomCell cell.title.text = self.entries[indexPath.row]["title"].string var contents = "" var image = "" cell.contents.text = "" cell.img.image = UIImage(named:"Picture")! parse.getContents(self.entries[indexPath.row]["link"].string!, completion: { (data, error) in contents = data["content"] as! String cell.contents.text = contents image = data["image"] as! String if(image != ""){ self.dispatch_async_global { let url = NSURL(string: image) var err: NSError?; var imageData :NSData = NSData(contentsOfURL: url!, options: NSDataReadingOptions.DataReadingMappedIfSafe, error: &err)! self.dispatch_async_main { cell.img.image = UIImage(data:imageData)! cell.layoutSubviews() } } }else{ cell.img.image = UIImage(named:"Picture")! } }) return cell } override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { return 140 } func dispatch_async_main(block: () -> ()) { dispatch_async(dispatch_get_main_queue(), block) } func dispatch_async_global(block: () -> ()) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block) } }
ここでは取得してきたJSONのデータをカスタムセルにセットしています。
画像のサムネイルを別スレッドで非同期で取得してきています。マルチスレッドはこの辺を参考にしました。
override func tableView(tableView: UITableView, heightForRowAtIndexPath … の部分はセルの高さですね。
extension FeedTableViewController : UITableViewDelegate { override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { let detailViewController = DetailViewController() detailViewController.entry = self.entries[indexPath.row].dictionary! parent.navigationController!.pushViewController(detailViewController , animated: true) } }
セルをタップした先の詳細画面の設定です。
ナビゲーションコントローラを使って画面遷移してます。
8.記事の詳細ページ、WebView
DetailViewController.swift では、WebViewの設定をしています。
https://github.com/inkenkun/RssReader/blob/master/news/DetailViewController.swift
TOWebViewController の使い方が最初良くわからなかったのですが、どうやらこんな感じで継承させれば良さそうです。
class DetailViewController: TOWebViewController {
9.ナビゲーションコントローラーの設定
このままだと一覧から詳細に遷移できないので、AppDelegate.swiftでナビゲーションコントローラーの設定をします。
https://github.com/inkenkun/RssReader/blob/master/news/AppDelegate.swift
追加したのは以下の部分
var myNavigationController: UINavigationController? func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { myNavigationController = UINavigationController(rootViewController: ViewController()) self.window = UIWindow(frame: UIScreen.mainScreen().bounds) self.window?.rootViewController = myNavigationController self.window?.makeKeyAndVisible() return true }
10.ビルド
以上で完成です。
初めてなので型でハマりまくったり、ライブラリの使い方がわからなかったりといろいろありましたが、なんとか動くものが出来ました。
もし、こういう書き方のがいいよとか、これ便利だよとかあったら教えて下さい。
【追記】2015-5-26
「Objective-Cのライブラリをswiftで使えるようにする」の部分を一部修正しました
この記事の動画もあるよ