iOS
Swift
RxSwift
WebCollector
Renewal

6年前に開発したiOSアプリ「WebCollector」をリニューアルした

iOSアプリ開発の勉強を始めた6年前に作ったアプリ「WebCollector」をリニューアルしました。
当時は主にPHPで開発業務に携わっていたので、いつか仕事でiOSアプリが作れる様になりたいと個人的に勉強していた中でリリースしたアプリです。
ブラウザではAwesome Screenshot等を利用すれば全画面のスクリーンショットは撮影出来ましたがiPhoneではその様なアプリはありませんでした。
Awesome Screenshotは既にiPhone版もあるので実はリニューアルするかどうかを長い間悩んでいました。。


WebCollectorとは

フルサイズでWebページのスクリーンショットが撮影する事が出来るiOSアプリケーションです。
ダウンロード数は約30万件です。(2018年4月現在)

Screen Shot 2018-04-07 at 17.52.23.png


いくつか有名なメディアにも取り上げて頂きました🙇🏻‍♂️


リニューアルしようと思った理由

ある日、1通のメールをいただきました。

こんにちは。
web collecterの件でメッセージしました。

他アプリでのエラーでソフトウェアのアップデートを余儀なくされした所、開かなくなり困っています…。

今も毎日のように使っているのでアップデートしていただけたら幸いです。。
よろしくお願いいたします。

僕の個人ウェブサイトの問い合わせよりこの様なメールを頂いてしまい、先述した様にずっとリニューアルするか悩んでいたんですがこちらのメールをきっかけに重い腰をあげる事にしました。


リニューアルが必要だった理由

  1. 32bitしか対応していなかったので64bitに対応する必要があった
  2. 多数のライブラリが古過ぎた
  3. 学習し始めのObjective-Cのコードで出来ているのでさすがにSwiftにしたかった

などが主な理由です。


リニューアルにあたって

チャレンジした事


サーバーは構築せずバックエンドは全てFirebase

# TypeではバックエンドはRealm Object Serverを利用してデータ同期を行う仕組みを導入しています。
こちらのデメリットとしてサーバーの運用管理を自分で行う必要がある、サーバー費用を払い続ける必要がある辺りが挙げられるかと思います。
Firebaseであれば暫くは無料で利用する事が出来るので一旦無料枠で様子を見る事にしました。
データは全てFirestore、画像はFirebase Storageに上げる様にしています。
また、アプリの中に問い合わせ機能があるんですが、こちらもFirebase Cloud Functionsにnodemailerを導入して僕にメールを送信する機能を作成しました。


マイグレーション

過去にWebCollectorを利用してくれていた人も沢山いるかと思いますが、データの移行機能は用意していませんでした。
今回から先述した様にFirebaseを利用する様になったのでアカウント登録さえすれば今後はログインすればデータが引き継げる様にしました。
アプリ内に保存していたデータはSQLite上にデータを保存していましたが、SwiftからSQLiteのデータを取得する為だけにライブラリを入れるのも面倒だったのでその部分だけ過去に利用していたObjective-Cのファイルを持ってきてSwiftからアクセス出来る様にしてFirebaseへの移行を行いました。


RxSwift

弊社ではこちらの記事にある様にMVC以外のアーキテクチャを採用していません。
しかし、個人的に技術力を広げて深めていく為には色んなアーキテクチャを知り、綺麗な設計やテストについての知見を得続ける必要はあるなと感じていました。
個人的にはVIPERやClean Architectureに興味はありますが、一番敬遠していたのがRxSwiftを利用する事でした。
理由としては、先述した記事にもありますが、

  • ライブラリにロックインされる
  • Swiftのアップデートなどへの追従が心配
  • 学習コストが高い

辺りがあります。
とはいえ、ずっと触らないまま過ごすのももやもやしていたので、アプリをリニューアルするタイミングで導入してみる事にしました。


RxSwiftを導入してみて

正直そんなに複雑なイベントを伝搬する様な画面が無かったのであまり必要性は感じませんでした。
ただ、アドレス入力バーの部分をUIStackViewで作成し、RxWebKitを利用してバインディングすればボタンの出し分けをライブラリに任せる事が出来たのはとても簡単で直感的で感動しました。

stackview.png

実際のコードはこんな感じ

webView.rx.canGoBack.map { !$0 }.bind(to: topToolbar.backButton.rx.isHidden).disposed(by: disposeBag)

webView.rx.canGoForward.map { !$0 }.bind(to: topToolbar.forwardButton.rx.isHidden).disposed(by: disposeBag)

webView.rx.loading.bind(to: topToolbar.reloadButton.rx.isHidden).disposed(by: disposeBag)

webView.rx.loading.map { !$0 }.bind(to: topToolbar.stopButton.rx.isHidden).disposed(by: disposeBag)

.
.
.

UIテストで全画面スクリーンショット撮影

僕はiPhone 6 Plus(iOS 10.3), iPhone X(iOS 11.3), iPad Pro 10.5inch(iOS 11.3)が手元にあるんですが、お気づきの様にiPhone 5系、iPhone 6系の端末が手元に無いんです。
一応シミュレーターで動作確認は可能ですが画面サイズ・OS毎に都度ビルドするのも大変なのと、基本的には画面崩れさえ無ければ機能の確認は手元にある端末で十分出来ると考えた為、全画面をUIテストでスクリーンショットを撮影して撮影後の画像さえ確認すれば十分ではないかと考えました。

しかし、テストをする為には本番データにアクセスしたり端末内のUserDefaultsに影響を及ぼす可能性があるので、沢山のMockを用意する必要があるのでは、と思っており少し躊躇していましたが、某 @d_date さんの一言で思い切ってテストを回す事にしました。(UIテストを回す為に本番とは別のターゲットを作成し、Firebaseのプロジェクトも別で作成しました)

というわけでUIテストのターゲット内で XCTestCase に下記の様な Extension を生やしました。

extension XCTestCase {
    func screenshot(_ named: String) {
        XCTContext.runActivity(named: named, block: { activity in
            let screenshot = XCUIScreen.main.screenshot()
            let attachment = XCTAttachment(screenshot: screenshot)
            attachment.lifetime = .keepAlways
            activity.add(attachment)
        })
    }
}

Lunchを利用して各画面が直接表示出来る様にし、各UIテストで screenshot("画面名") の様に呼べばスクリーンショットが保存出来ます。

その他ページのテストコードの一部

import XCTest
import LunchTest

final class OtherViewControllerTest: XCTestCase, ViewControllerTestable {
    var viewControllerName: String { return ViewControllerName.otherViewController.rawValue }

    override func setUp() {
        super.setUp()

        continueAfterFailure = false
    }

    func launchOtherView(with locale: String) -> XCUIApplication {
        let launcher = Launcher(targetViewController: self, locale: locale)
        return launcher.launch()
    }

    func testContactJP() {
        let app = launchOtherView(with: "ja_JP")
        let otherPage = OtherPageObjects(app: app)
        otherPage.otherTableView.cells.element(boundBy: 3).tap()

        let page = ContactPageObject(app: app)
        XCTAssertTrue(page.emailTextField.exists)
        XCTAssertTrue(page.messageTextView.exists)
        XCTAssertTrue(page.sendButton.exists)

        screenshot(#function + " screenshot")
    }

    func testContactUS() {
        let app = launchOtherView(with: "en_US")
        let otherPage = OtherPageObjects(app: app)
        otherPage.otherTableView.cells.element(boundBy: 3).tap()

        let page = ContactPageObject(app: app)
        XCTAssertTrue(page.emailTextField.exists)
        XCTAssertTrue(page.messageTextView.exists)
        XCTAssertTrue(page.sendButton.exists)

        screenshot(#function + " screenshot")
    }

    .
    .
    .
}

UIテストはTerminalから実行します。

xcodebuild test -project WebCollector.xcodeproj -scheme WebCollectorDev -configuration Debug -destination "platform=iOS Simulator,name=iPhone SE,OS=10.3.1" -resultBundlePath ./result/iphonese.bundle
xcodebuild test -project WebCollector.xcodeproj -scheme WebCollectorDev -configuration Debug -destination "platform=iOS Simulator,name=iPhone X,OS=11.3" -resultBundlePath ./result/iphonex.bundle
xcodebuild test -project WebCollector.xcodeproj -scheme WebCollectorDev -configuration Debug -destination "platform=iOS Simulator,name=iPhone 8,OS=11.3" -resultBundlePath ./result/iphone8.bundle
xcodebuild test -project WebCollector.xcodeproj -scheme WebCollectorDev -configuration Debug -destination "platform=iOS Simulator,name=iPhone 8 Plus,OS=11.3" -resultBundlePath ./result/iphone8plus.bundle
.
.
.

暫くすればプロジェクトフォルダの result フォルダ配下に *.bundle ファイルが作成され、その中の Attachments フォルダにたくさんのスクリーンショットが保存されています。

Screen Shot 2018-04-07 at 19.26.10.png

一度作成してしまえば今後も使いまわせるので、今後の運用にも役立つ事を期待しています。


デザイン

LPは当時も作っていたんですが利用していたドメインを手放してしまった為にいつからか見れなくなってしまいました。
今回はTypeと同様にGithub Pagesを利用する様にしました。
かなり久々にHTML,CSSを書いた様な気がします。
最初無料のLPのテンプレート等を利用する事も検討したんですが必要ない機能がたくさんあったのと、自分で作った感が全然得られなかったのでSketchでデザインを起こして自分でゼロから書く事にしました。

Sketchはアプリのデザインやストアに掲載するスクリーンショット等全てを同じファイル内で作成しました。

Screen Shot 2018-04-07 at 19.50.58.png


妥協した事

多数のSDKを削除

6年前に開発した当時は UIActivityViewController なんて便利なものは無かったんですね。
なのでありとあらゆるサービスに写真を投稿する為に色んなSDKを利用する必要がありました。

主に対応していたのは

  • Twitter
  • Facebook
  • Dropbox
  • Evernote
  • etc...

辺りですね。が、現在は、

let activityController = UIActivityViewController(activityItems: [image], applicationActivities:[])
activityController.popoverPresentationController?.barButtonItem = actionButton
present(activityController, animated: true, completion: nil)

これだけで色んなサービスにポストする事が出来ますね。
もしかしたら個別に対応が必要なものもあるかもしれませんが、一旦全部削除して様子を見る事にしました。
(画像サイズが大きいとほとんどのサービスが受け付けてくれない問題は認識しています。。どうしようかなぁ🤔)


お絵かき機能の削除

有料機能としてお絵描き機能もあったんですがこちらも一旦削除する事にしました。理由としては、他の画像編集アプリに有能な物が沢山あるのでアプリの中で無理に作る必要はないのでは🤔と判断したからです。要望があればまた頑張って作るかもしれません。


その他必須ではない機能の削除

スクリーンショット撮影アプリなのにサイトのソースを閲覧する機能まであったりしたんですが、MVPを考えた時に一体何をするアプリなのか路頭に迷っていた感があったので一旦削除しました。
今後要望があれば削除した機能も復活するかもしれません。


ハマった事

スクリーンショットの撮影

リニューアル前は UIWebView 使ってたんですが、今は WKWebView ですよね。

CGRect baseFrame = webView.frame;

CGSize fittingSize = [webView sizeThatFits:CGSizeZero];

webView.frame = CGRectMake(0.0f, 44.0f, fittingSize.width, fittingSize.height);

UIGraphicsBeginImageContext(imageView.bounds.size);

[webView.layer renderInContext:UIGraphicsGetCurrentContext()];

UIImage* image = UIGraphicsGetImageFromCurrentImageContext();

webView.frame = baseFrame;

UIWebView だとこんな感じでフルサイズの撮影出来てたんですよね。
ところが WKWebView はメモリの効率化の為か同じ方法だとスクリーンショットが撮影出来ませんでした。(画面に表示されてる場所だけ取得出来た)
検索したらStack Overflowでそれっぽい物があったのですがあまりうまくいかず参考にして独自の方法で撮影する様にしました。


リジェクト

ヘルプの中に少しふざけた事を書いていたら見事に審査の時に見つかってしまって一度リジェクトされてしまいました。レビューガイドラインは審査提出前に見直しましょう(笑)


まとめ

ざっとリニューアルに際して工夫した事や気にしたことをまとめてみました。
随分お待たせした方もいるかと思いますが、今後もWebCollectorをよろしくお願いします🙇🏻‍♂️