この記事では、ドワンゴから今秋リリースされた 「ニコニコ漫画」iOSアプリ の開発での取り組みや内部の設計、ニコニコ漫画アプリのこれからについてご紹介します。あまり変わったことはやっていないとは思いますが、よくあるアプリ開発の一例としてご覧いただければと思います。
なお、一応お約束として書いておくと、この記事は個人の見解であり、所属する組織の公式見解ではありません。
はじめに
"ニコニコ漫画"は、縦スクロールでサクサク、マンガを読めるのはもちろんのこと、新感覚のダイナミックな見開きページ表現を楽しめたり1、紙芝居のような形式のマンガが読めたりするなど2、かなり**アグレッシブなマンガアプリ3**です。
読み手はもちろん、マンガの作り手が作品を公開して嬉しい気持ちになれるサービスになっているかと思います。まだ使ったことがないという方は、この機会にご利用いただければ幸いです(宣伝)。
サービスもアグレッシブなのですが、開発もアグレッシブにすすめています。この記事では、ニコニコ漫画 iOS アプリの開発について、以下の3つのトピックを切り出し、ご紹介させていただきます。
- ニコニコ漫画 iOS アプリの開発体制
- ニコニコ漫画 iOS アプリの構造
- ニコニコ漫画 iOS アプリのこれから
ニコニコ漫画 iOS アプリの開発体制
ニコニコ漫画アプリは Android 版 が9月15日に先行リリースされています。開発自体も Android 版が先行してスタートし、基本的にはそこで決定された仕様をもとに、iOS 版の開発を行いました。Android 版のデザインと開発の流れなどについてはまた 別の記事 がありますので、興味がある方は是非ご覧ください。
基本的な開発体制
体制としては、ディレクター 1 名、エンジニア 2 名、デザイナー 1 名でクイックに開発を行っています。少人数チームの開発ではありますが、困った時にはドワンゴの抱える他のアプリ開発者と Slack などを用いて密にコミュニケーションをとることができ、安心して開発を進めることができます。
開発言語は、これからの iOS 開発のスタンダードになるであろう Swift
を採用しています。Xcode6 系、Swift 1.2 系での開発をスタートさせたため、今後 Swift 2 系へのマイグレーションが必要となる点は、少々苦しいところです。しかしながら、Objective-C に比べれば、平易なシンタックスや優れた言語機能を持つ Swift を選択したのは正しかったと考えています。
またビューの構成には、基本的に 1 画面を 1 ストーリーボードに対応させる方針 で開発を進めています。特に複雑なビューを持つニコニコ漫画アプリでは、Auto Layout で期待したとおりの画面を構成するのは、なかなか難易度の高い作業でした。このあたりについては、まだまだどういった手法がベストプラクティスなのか、探り探りやっている段階です。
アプリのプロジェクト構成
ニコニコ漫画アプリのプロジェクトは、プロダクトコード、単体テストコード、結合テストコードの3つのターゲットから構成されています。通信部分などアプリケーションの肝となる部分は、しっかりとテストを書いておきたいという気持ちから主に通信部分についてのテストを結合テストコードとして分離しています。後述するように、ニコニコ漫画 iOS アプリでは Alamofire
のAPI や NSURLSession
を直接扱うのではなく、ラップしたHTTPクライアントを実装して利用しています。またAPIごとにさらに抽象化を行っています。したがって、各レイヤーの境界でしっかりと結合できているかの確認を自動化できるようにするためにも、結合テストを書いておくことは重要なことでした。
結合テスト用のサーバーは Java
+ Spark Framework
製のモックおよび、ニコニコ静画のステージング環境のAPI を利用しています。モック APIを外部に置いたのは、 Swift の足回りはまだまだ不安定で今後も激しいアップデートが予想されることが大きな理由となります。たとえば、言語レベルのマイグレーションのときに、 Swift で記述したモックAPIが原因でテストが落ちているのか、プロダクトコードが原因でテストが落ちているのかの判断がつかないのではないかと思い、こういう形態をとっています。また、個人的にも Swift ばかり書いていると、味気ないという理由で Java で書いていたりします。
synx によるソースコードファイルの整理
ドワンゴでは、全社的に GitHub Enterprise を用いた pull request ベースのチーム開発が当たり前になっており、わたしたちのチームも同様の形態をとっています。したがって GitHub 上でコードを参照する機会も多いのですが、困ったことに Xcode プロジェクトのグループ構造とソースコードファイルのディレクトリ構造は独立しています。GitHub で見ると、ひとつのフォルダにずらっとファイルが並んでお粗末な感じになります。
そこで、わたしたちのチームでは synx
というツールを利用しています。これは Xcode プロジェクトのグルーピングと実際のディレクトリ構造を一致させてくれるツールです。導入も Bundler
を用いて、以下のように簡単に行うことができます。
# プロジェクトルートに移動後、以下を実行
$ bundle init
$ echo 'gem '\''synx'\'', '\''~> 0.1.1'\''' >> Gemfile
$ bundle install
$ bundle exec synx `プロジェクト名`.xcodeproj/
Gemfile.lock
ごと git 管理下におけば、開発者全員の synx
のバージョンを揃えるのも簡単です。なお、開発をしているなかで一回だけ、synx
の実行に失敗し、変更が失われてしまうということが発生したため、synx
実行前にとりあえず変更をコミットしておくことをおすすめします。
CocoaPods による依存ライブラリ管理
iOS開発をしている方ではもうすでにおなじみかもしれませんが、依存ライブラリ管理には CocoaPods
を利用しています。こちらも Bundler
を用いて、以下のように一発で導入することができます。CocoaPods はアップデートが頻繁に行われているため、こちらに関しては特に Gemfile
で CocoaPods のバージョンを指定してあげたり、Gemfile.lock
を git 管理下におくことを強くおすすめします。
# プロジェクトルートに移動後、以下を実行
$ echo 'gem '\''cocoapods'\'', '\''~> 0.39.0'\''' >> Gemfile
$ bundle install
ニコニコ漫画 iOS アプリの構造
ニコニコ漫画 iOS アプリでは、各クラスをレイヤリングしています。いわゆるレイヤードアーキテクチャという形になるかと思います。アプリの大まかな設計図は以下のようになっています。
コード量がそれなりに大きな規模になることが予想できたため、最初にこの図のような主要なモジュールとその責務をざっくりと決め、各モジュールを構成するクラスのプロトコル(他言語でいうところのインターフェース)を定める作業を行いました。
この作業を行うことにより、各部品の実装をそのまま小さなタスクとして取り扱うことができ、進捗管理の目安にできたり、チームメンバー同士での作業のコンフリクトを防げたりと、開発をスムーズに進めることに貢献できたのではないかと思います。
利用しているライブラリも、このときに定めたプロトコルの実装の内側に閉じ込めることにより、いつでも自前の実装や他のライブラリに差し替えられるようにしています。とくに Swift は言語ができて間もないのでライブラリ自体の安定性も低い状態であることが多いです。サッとライブラリを修正してプルリクエストするのが好ましいのですが、なかなかうまくいかないときにこういった形で設計されていると、差し替えが容易で安心できます。
RxSwiftを用いたアプリ開発の実践
前述の全体の大まかな設計を行なっている際に、 Swift でリアクティブプログラミングをサポートするライブラリである RxSwift
を全面的に取り入れるか非常に苦慮しました。最終的には、Swift でのエラーを伝播させる仕組みとして Rx の Observable
というものが非常に有用だという点が大きな理由となって、利用することにしました。
Swift 2系からは、言語レベルで例外処理がサポートされたので、そちらを利用するのも良いかもしれませんが、個人的には処理のシーケンスを乱す例外を積極的に使っていくのは気が進みません。
ここからは少しだけ、RxSwiftの基本的な使い方をご紹介した上で、ニコニコ漫画アプリへの RxSwift
の組み込み方の実例をご紹介していきます。
RxSwift, RxCocoaの導入
まずは RxSwift
, RxCocoa
の導入方法とその使い方について、簡単に解説を行います。ライブラリのレポジトリにもサンプルプロジェクトが豊富にあるのでそちらを見るのもおすすめです。また、RxSwiftに関するスライド をアップしているのでそちらを見るのも良いかもしれません。
導入は CocoaPods を用いると簡単です。Xcode7 環境下では以下のとおりに実行して、.xcworkspace
ファイルを開けば導入が完了です。
$ bundle exec pod init
$ vi Podfile
# Podfile の プロダクトコードのターゲットに以下の記述を追加する
pod 'RxSwift', '2.0.0-beta4'
pod 'RxCocoa', '2.0.0-beta4'
$ bundle exec pod install
なお Xcode6 系の方は、バージョン指定を 1.9.1
あたりにすると良いでしょう。Swift2系の登場に合わせて、RxSwiftのインターフェースが大きく変更されているので、ご注意ください。マイグレーションガイドはここにあります。
RxSwift 2.0.0 リリースにともなう追記
2015/01/02 に RxSwift 2.0.0 がリリースされました
以下の記事の内容はほとんど問題なく使うことができると思いますが、以下の記事に主だった変更点をまとめました。
http://qiita.com/gomi_ningen/items/dd7b6cab657fd4476906
RxSwift, RxCocoa を用いたイベント処理
RxSwift は驚くほど簡単に用いることができます。「リアクティブプログラミング」とは何かを理解せずとも何の問題もないので臆せず使ってみましょう。まずは UIイベント処理でありがちな例の RxSwift を用いた解決策をご紹介します。
ボタンのタップイベント処理
UIButtonのタップイベント処理についてみていきましょう。とりあえず、細かな話は抜きにして、適当なビューに UIButton
を作成して ViewController
の viewWillAppear
に次のようなコードを記述してください。
import UIKit
import RxSwift
class ViewController: UIViewController {
private var disposeBag = DisposeBag()
@IBOutlet weak var button: UIButton!
override func viewWillAppear(animeted: Bool) {
super.viewWillAppear(animated)
button.rx_tap
.subscribeNext { NSLog("チマメ隊") }
.addDisposableTo(disposeBag)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
disposeBag = DisposeBag()
}
}
これを実行して、ボタンをタップすると 「チマメ隊」 というログ出力がされるはずです。ボタンのタップイベントの処理方法は多々ありますが、Rxを使ったパターンはわりあい見通しの良いものになっているかと思います。
テキストフィールドの入力イベント処理
続いてテキストフィールドの文字入力イベントを拾う例を考えてみましょう。適当なビューに UITextField
を作成して、 viewWillAppear
に以下のようなコードを記述してみてください。
textField.rx_text
.map { "はぁ... " + $0 + "さん..." }
.subscribeNext { NSLog($0) }
.addDisposableTo(disposeBag)
すると以下のように、文字入力や削除のたびにイベントが発生して、NSLog
でログが出力されるのがわかると思います。
もしテキストフィールドに特定の値が来たときだけのイベントを拾いたい際は、例えば以下のように filter
をしてあげれば良い感じになります。
textField.rx_text
.filter { $0 == "ココア" } //=> テキストフィールドに「ココア」と入力されたときだけ以後のステップに進む
.map { "はぁ... " + $0 + "さん..." }
.subscribeNext { NSLog($0) }
.addDisposableTo(disposeBag)
スライダーの値に応じてラベルのフォントサイズを変更する
さて、もっとインタラクティブな例を見てみましょう。スライダーの値に応じてラベルの文字サイズを変更するような処理を書きたいとします。このとき、以下のように UISlider
の値の変更イベントを UILabel
の rx_attributedText
に bind
してあげれば OK です。
slider.rx_value
.map {
NSAttributedString(
string: "特殊相対性理論",
attributes: [NSFontAttributeName: UIFont(name: "HiraginoSans-W3", size: CGFloat($0))!]
)
}
.bindTo(label.rx_attributedText)
.addDisposableTo(disposeBag)
UISlider
の上限値と下限値をうまい感じに設定すれば、以下のようにスライダーの値に応じてラベルのサイズが変化するような実装が簡単に行えます。
RxSwiftを使ったイベント処理の利点
以上の例を見ていただければ、だいたいのUIコンポーネントのイベントは RxSwift
を使って処理できることが分かっていただけたのではないでしょうか。
iOS開発でのUIイベント処理は、コールバックやデリゲートで対応していくパターンが多いと思いますが、RxSwiftを使うと宣言的に書くことができるようになっています。特に、コールバックを避けることによりイベント処理を各Viewのクラスに散らさずに済むという点は非常に良いです。これにより、コードの見通しが格段に向上するのではないかと考えています。
また、map
, filter
をはじめてとして様々な高階関数を利用できる点も優れています。これらをチェインでつなぐことにより、イベントデータの変形や抽出などが自由自在にできます。
以上の内容は「Reactive Programmingとは何か」などということは知っていなくても、理解できたのではないでしょうか? RxSwift
を使おうか迷っている方は、巷に出回っている難しい記事をみて頭をひねるより、まず使ってみることをお勧めします。
ここでご紹介した RxSwift
の利用例は、GitHub にて公開していますので是非 clone
して動かしてみてください。Xcode7.2 で動作を確認しています。
$ git clone git@github.com:53ningen/rxswift-examples.git
$ cd ./rxswift-examples
$ bundle install
$ bundle exec pod install
$ open ./rxswift-examples.xcoworkspace
RxSwiftを用いた基本的な通信処理
簡単な通信処理
NSURLSession
のインスタンスに rx_response
というプロパティが生えているので、これを使えば以下のように簡単に通信処理が行えます。
import RxSwift
import UIKit
class SimpleNetworkingSampleViewController: UIViewController {
private var disposeBag: DisposeBag = DisposeBag()
private let backgroundWorkScheduler = ConcurrentDispatchQueueScheduler(globalConcurrentQueuePriority: .High)
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let url = NSURL(string: "http://gochiusa.com")!
let request = NSURLRequest(URL: url)
NSURLSession.sharedSession().rx_response(request)
.subscribeOn(backgroundWorkScheduler)
.observeOn(MainScheduler.sharedInstance)
.subscribeNext { (data, response) -> Void in
if let str = String(data: data, encoding: NSUTF8StringEncoding) {
print(str)
}
}
.addDisposableTo(disposeBag)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
disposeBag = DisposeBag()
}
}
subscribeNext
で渡しているクロージャの中身を見れば大体察しがつくかと思いますが、rx_response
は (NSData, NSHTTPURLResponse)
を返してくれるものになってます。
RxSwiftを用いた通信エラー処理
さて、上記の例に加えてエラー処理をしたいケースを考えてみましょう。たとえば、存在しないページにアクセスした場合は、404のステータスコードとページが見つからない旨のレスポンスが返ってくると思います。
let url = NSURL(string: "http://gochiusa.com/hogehoge")!
let request = NSURLRequest(URL: url)
NSURLSession.sharedSession().rx_response(request)
.subscribeOn(backgroundWorkScheduler)
.observeOn(MainScheduler.sharedInstance)
.subscribeNext { (data, response) -> Void in
if let str = String(data: data, encoding: NSUTF8StringEncoding) {
print("status code: " + String(response.statusCode))
print("response body: " + str)
}
}
.addDisposableTo(disposeBag)
// => ログ出力
// status code: 404
// response body: <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
// <html><head>
// <title>404 Not Found</title>
// </head><body>
// <h1>Not Found</h1>
// <p>The requested URL /hoge was not found on this server.</p>
// </body></html>
Rxにはエラー的な状況をハンドリングする仕組みがあります。例えば、ステータスコードが 200 のときには、レスポンスを String
で、それ以外のときには NSError
を流すには次のように書けば OK です。
NSURLSession.sharedSession().rx_response(request)
.flatMap({ (data, response) -> Observable<String> in
if let string = String(data: data, encoding: NSUTF8StringEncoding) where response.statusCode == 200 {
// 成功値を流すには flatMap の中で just(値) を返せばよい
return just(string)
} else {
// エラーを流すには flatMap の中で failWith(ErrorType) を返せばよい
return failWith(NSError(domain: "ConnectionError", code: response.statusCode, userInfo: nil))
}
})
.subscribeOn(backgroundWorkScheduler)
.observeOn(MainScheduler.sharedInstance)
.subscribe({ (event) -> Void in
switch event {
case .Next(let string): print(string) // 値の処理
case .Error(let e): self.showDialog(e) // エラー処理
case .Completed: break
}
})
.addDisposableTo(disposeBag)
コードからわかるように、flatMapの中で just(値)
を返すと成功値を流すことができます。また failWith(ErrorType)
を返すとエラーを伝播させることができます。ニコニコ漫画iOSアプリでは、基本的にこの仕組みを利用してエラー処理を行っています。
通信処理の階層化
簡単なアプリであれば、上記の NSURLSession.rx_response
を ViewController
から直接呼び出してしまっても良いのですが、ちょっと複雑な画面やロジックが入るようなアプリを作る際にこれをやってしまうと、たちまちどこに何が書いてあるのかが分からなくなります。
そこでニコニコ漫画 iOS アプリでは、通信処理とそのレスポンスの加工を、以下の3つの要素に分離して実装を行っています。
- HTTPリクエストを組みたて通信を行う責務を持つ
HttpClient
- 利用するウェブAPIに与えるパラメータとそのレスポンスを抽象化した
ApiClient
- ウェブAPIからのレスポンスやキャッシュなどからデータを取得し、クライアント側からコレクションのように操作を加えられるようにした
Repository
こうすることにより、例えばリクエストの組み立てなどの部分で、同じロジックを繰り返し書くことを防げます。またスタブを利用したテストを書けば、各処理部単位で正しい実装が行われていることを保証することもできます。また、アプリ開発においてはデザイン確認用のレスポンスを返すスタブを作ると便利かもしれません。
次節からは HttpClient
と ApiClient
の実装についてみていきます。
RxSwiftを用いたHttpClientの構造例
HTTPリクエストを組み立てる機能単位を HttpClient にまとめることを考えます。こうすることによりクエリパラメータのURLエンコード処理やリクエストヘッダーを作る部分の共通処理をまとめることができます。また、HttpClient
としてプロトコル(他言語でいうところのインターフェース)を切っておくことにより、実際の通信時に用いるライブラリやSwiftのAPIへの依存を外側に晒さずにすむという効果もあります。作るものの全体像は下図のようなものになります4。
RequestParameter
と RequestHeader
は、単純に Key-Value 的なただのデータ構造です。
実装的には以下のように typealias としてあげるのが一番簡単ですが、個別に struct
を定義しても大丈夫です。
RxHttpClient
のプロトコル自体もそのまま書き下せば良いかと思います。
import RxSwift
import Foundation
public typealias RequestParameter = (key: String, value: String)
public typealias RequestHeader = (key: String, value: String)
public protocol RxHttpClient {
func get(url: NSURL, parameters: [RequestParameter], headers: [RequestHeader]) -> Observable<(NSData, NSHTTPURLResponse)>
func post(url: NSURL, parameters: [RequestParameter], headers: [RequestHeader]) -> Observable<(NSData, NSHTTPURLResponse)>
func put(url: NSURL, parameters: [RequestParameter], headers: [RequestHeader]) -> Observable<(NSData, NSHTTPURLResponse)>
func delete(url: NSURL, parameters: [RequestParameter], headers: [RequestHeader]) -> Observable<(NSData, NSHTTPURLResponse)>
}
ここで定義したプロトコルの実装クラスは、ちゃんと動けばどんな感じにしても良いかと思いますが、Alamofireなどライブラリに依存しない形で実装した一例を、GitHub に公開していますのでそちらをご覧いただければ幸いです。
通信関連のエラー処理はこのあたりの層でハンドリングしてあげると以後の層の実装がすっきりすると思いますので、以前に紹介している flatMap
関数などを使い、よしなにエラー処理を記述すると使い勝手がなかなか良い HttpClient
になるのではないでしょうか?
RxSwiftを用いたApiClientの構造例
続いて、アプリで用いるウェブAPIの操作を抽象化した ApiClient
について考えてみましょう。使うエンドポイントに対して Controller
で個別にリクエストパラメータやヘッダを指定するのは煩わしいですし、コード自体の見通しも悪くなります。例として Qiita API: https://qiita.com/api/v2/docs の記事リストを取得するAPI: GET /api/v2/items
を扱ってみます。今回実装する ApiClient
の構造は以下の図のような形となります。
QiitaApiClient
の持つメソッドを見ると、指定すべきパラメータがはっきりと理解できるかと思います。こうしてあげることによって、クライアントコードでAPIのデータを取得する際に迷わずパラメータを指定することができます。図中に出てくる登場人物を確認しましょう。
HttpClient
は前節で作成したものと同じものとなります。今回作成したい ApiClient
は、メソッドの引数をいい感じに加工して RequestParameter
や RequestHeader
を生成し、 HttpClient
に与えることにより、APIへの通信を行うという仕組みになっています。ItemRecord
は Qiita の投稿記事(item)レスポンスのデータ構造を定義したクラスです。Qiita APIは、JSON形式でレスポンスを返してくれますが、Controller
などでJSONをそのまま扱うのは見通しが悪いので、こういったクラスを作成して、そのインスタンスにマッピングしてあげるのが良いかと思います。
プロトコルとデータ構造のコードはだいたい以下のようになります。
public protocol QiitaApiClient {
func getItems(offset: Int, limit: Int) -> Observable<[ItemRecord]>
func getItems(keyword: String, offset: Int, limit: Int) -> Observable<[ItemRecord]>
}
public struct ItemRecord {
let id: String
let title: String
let url: String
// 以下同様に...
}
ここで定義したプロトコルの実装クラスでは、JSONを ItemRecord
にマッピングするなど細かい作業が多くなってきます。RxSwiftを用いた ApiClient の作り方とはまた離れた話になってきますので、GitHub にコードを上げてあります。興味がある方は、そちらをご覧ください。
オブジェクトの構築
前述のとおり、ニコニコ漫画 iOS アプリでは、通信処理を HttpClient
, ApiClient
, Repository
といった具合に責務を小さくしたクラスを組み合わせて構築しています。このときに Repository
の実装は ApiClient
に依存して、 ApiClient
の実装は HttpClient
に依存するなどということが発生すると思います。
ニコニコ漫画 iOS アプリではそれぞれの依存オブジェクトは、「コンストラクタインジェクション」という形で解決しています。これは単純に、コンストラクタで依存オブジェクトを注入できるようにしてるというだけです。
APIClient
を例にとれば、plistなどの設定値をみて ApiClient
の向き先を開発環境やステージング環境、本番環境に差し替えたいなどということや、依存している HttpClient
をスタブに差し替えたいということがあると思います。そのために各プロトコルを持つインスタンスを取得するためのファクトリを作成しています。
本来であれば、Java でいうところの Google Guice のような DIコンテナを用いたいところでしたが、事前調査の段階では、今回の開発に適用できそうなライブラリを見つけたり、実装する余裕がなかったためこういう形になっています。この点については今後の改善を行っていきたいと考えています。また、Swinject
という Swift で DI をサポートするライブラリの開発が、最近活発に行われているようなので今後の動向を追っていくつもりです。
その他、開発の際に気をつけていること
Optional 型の扱い
Swift の Optional 型のアンラップにはかなり気を使っています。できるだけ if-let
構文の Optional Bindings
を用いたり、map
や filter
などのコンビネータを利用したりなどの方法をとっています。こうしているのは、安全なアンラップかどうかをコードレビューでしっかりとチェックするのは難しいという点や、多くの場合に強制アンラップの必要がないという点が理由になります。プログラミング的にはなんてことない部分だとは思いますが、クラッシュの原因を生み出しやすい箇所だけに、このあたりには細心の注意を払っています。
どうしても強制アンラップが必要になる部分や強制アンラップしたほうが圧倒的に簡潔になる場合は、なるべく該当箇所のテストを記述するようにしています。たとえば、ストーリーボードに紐付いた ViewController を取得するなどという部分にこういった形の対応をしています。
immutableなオブジェクトと副作用について
UI層やキャッシュ関連のクラスを除いて、基本的にはすべてのオブジェクトは不変(immutable)にしてあります。オブジェクトはできるだけ不変にするということは、古事記 にもそう書かれているので、特筆すべきことではないかもしれません。不変なオブジェクトは内部状態を考慮せずに利用することができ、単純にコーディングする際にロジックに集中することができ、非常に良いと考えています。配列の操作や Objective-C 由来の操作で副作用が生じてしまうようなコードについても、なるべくprivate
な関数内に閉じ込めるように心がけました。
開発中に見ていたアニメ
みなさんは「ご注文はうさぎですか?」という作品をご存知ですか? もし知らない方がいらっしゃいましたら、Googleで「1」で検索すると情報が出てくると思いますのでお試しください。
統計を取った結果、なんとニコニコ漫画アプリ開発者の50%がこのアニメを見ながら開発を行っていたことがわかりました(当社調べ)。このことから、やはりiOSアプリを開発するには、良いアニメに触れている必要があるということが浮き彫りとなりました。
原作も素晴らしいので是非、紙の書籍を2冊と、電子書籍1冊の計3冊各巻を購入することをおすすめします。紙で読む「ごちうさ」も素晴らしいのですが、持ち歩きの際に本を傷つけてしまう恐れがあり、心ぐるしいです。したがって私は iPad に「ニコニコ静画(電子書籍)アプリ」を入れて、購入した「ご注文はうさぎですか?」の電子書籍を繰り返し読んでいます。
まだ原作をご覧になっていない方、アニメをご覧になっていない方は、是非一度見てみることを強くお勧めいたします。
ニコニコ漫画アプリの今後について
ニコニコ漫画アプリでは今後、お気に入り作品の更新PUSH通知機能の追加や、魅力的な作品の拡充など、マンガの読み手・書き手のみなさんがもっと楽しめるようなアプリにするべく、精力的に開発を進めて参ります。拙い開発ではございますが、今後ともニコニコ漫画アプリとニコニコ静画をよろしくお願いします。
ニコニコ静画広告
ニコニコ漫画は、少年、青年、少女、4コマなど、幅広いジャンルの作品が毎日更新され、その数は国内最大級です。ここから商業デビューを果たした作品も多数あり、魅力的な作品が満載です。ぜひ、使ってみてください!
ニコニコ漫画について詳しくは こちら
アプリのダウンロードはこちら:【Android版】 / 【iOS版】
また、ドワンゴでは アプリ開発エンジニアを募集しているそうなので、我こそはという方は、是非応募してみてください。