will and way

ただの自分用メモを人に伝える形式で書くことでわかりやすくまとめてるはずのブログ

UITableViewCellの中身をRxで監視するときのtips

シナリオ

  1. テキスト入力をもつTableViewCellがある
  2. 画面には更新ボタンがあり、押した時に、キーボードを閉じたい

結論ソース

※諸々省略してます

// UITableViewCellにcellForRowAtIndexまでにunbindさせるための情報を定義する
extension Reactive where Base: UITableViewCell {
    var prepairForReuse: Observable<Void> {
        return methodInvoked(#selector(base.prepareForReuse))
            .map { _ -> Void in
                return Void()
        }
    }

    var obsolete: Observable<Void> {
        return Observable.merge(prepairForReuse, deallocated)
    }
}


extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as TextCell
        weak var weakCell = cell

        updateButton.rx.tap
            .takeUntil(cell.rx.obsolete)
            .subscribe(onNext: { [weak self] _ in
                weakCell?.closeKeyboard()
            })
            .disposed(by: disposeBag)
    }
}

sentMessageでprepairForReuseを監視

prepairForResuseとdeallocのどちらかのイベントを受け取るまで監視しておけば良いので、それらをまとめてobsoleteというイベントを生やしました。

これをしないと、tableViewをスクロールするたびにsubscribeが増えるので予期せぬ動作を引き起こす可能性がある。

2017年買ったもの

とりあえず、2017年買ったものの総評を書いていこうと思う。 あくまで個人的な感想です。

買ったもの

  • SITPACK
  • 婚約指輪/結婚指輪
  • ゲーミングチェア
  • 自作PC
    • ケース
    • マザボ
    • 電源
    • CPU
    • CPUクーラー
    • GPU
    • メモリ
    • キャプチャボード
    • モニターアーム
    • キーボード
    • マウス
    • マウスパッド
    • ディスプレイ
    • Windows
    • ゲーミングチェア
  • イケアのデスクセット
  • Nintendo Switch
    • ゼルダ
    • プロコン
    • Splatoon2+アミーボ+コロコロ
  • ヘッドホンハンガー
  • 1000BASE-T対応のスイッチングハブ
  • 1000BASE-T対応のUSB-LANコネクタ
  • iPad Pro 10.9 inch Cellularモデル 256GB
  • Apple Pencil
  • LinksMateのSim 5GBプラン
  • iPhone X
  • Apple Watch Series 3 Cellularモデル
  • MacBook Pro 15inch BTO(CPU3.1GB, SSD1TB, US Keyboard)
  • USB-C to USB-A
  • HDMI=DVIケーブル
  • JetBrains All in Pack

リストにすると、必要な物ではなく欲しいものを買いすぎているなぁ。。。という印象 来年は買う前の判断を厳しくしていこう。

SITPACK

f:id:matsuokah:20171224012014j:plain

一口総評: 「★2 活躍できていない」

2017年1月頃入手

一言で言うと、「超コンパクトな折りたたみ椅子」

Makuakeのクラウドファンディングで1年位前にファンディング したのがやっと届いた。
ファンディング時には遅くとも1年前には届いてる予定だったけど、生産体制ができてないのにファンディング枠を増やして受注してたとのことで、届かない状態が1年くらい続いてた。
本当にくるか心配だったけど1年越しで入手。

毎月のように来る謝罪メールを確認するのが日課になっていた。

1個5000円くらいしたのかな。4つセットで購入し2つは家庭用、2つは友人に
予定どおり来ていれば海外旅行に持っていくはずだったのだけど、機会を逃したため全く使えていない

1年の始まりとして、非常に香ばしいスタート

結婚指輪/婚約指輪

一口総評: 「★5 嫁が喜んでくれた。よかった」

1月末に注文し、3月中旬ごろに完成

プロボーズを1月に結構し、承諾してもらったため一緒に嫁と買いに行った。
表参道のとあるお店で嫁が気に入ったものがあったので即決。
手元に届くまで約2ヶ月.

もともとアクセサリー系はだめな人間で、つけ始めたときの違和感が異常だったけど、すぐに慣れた。

自作PC

f:id:matsuokah:20171224012123j:plain

一口総評: 「★5 PCゲームでも、開発用でも活躍」

2017年2月頃に購入

8年ぶりに自作PCをやりたくなったため、独身最後の贅沢や!とそこそこのスペックを揃えた。

スペック (リンクはAmazonに飛びます。)

CPU, メモリ, マザボ, SSD, グラボはド定番みたいなところをチョイス 2,3年はパーツを買い換えなくても大丈夫だと思う。

基本的にはPUBGやスプラトゥーンの録画・ライブ配信に使っているのですが、
Androidアプリのビルドサーバーとして使ったりUbuntu載せたりして楽しんでます。

PCケースが2月頃は品切れ中で、黒透という3000円くらいのケースを一時的に使っていた。
一時的とはいえ完全にケチったせいで、グラボが収まらなくて、しばらくPCの外にはみ出た状態で運用していたw

汎用性を持つものの購入はやはりいいですね。

本業ではゲームの生放送サービスのネイティブアプリのエンジニアをやっているので、自分の生活にシンクロした買い物だったと思う。

CPUクーラー CORSAIR H115i CW-9060027-WW

ちょっとラジエータがでかい。大きさ的には 50cm四方くらいのケースを付けてやっと搭載できるレベル

Intensity Pro 4K

f:id:matsuokah:20171224022718j:plain

1080p60fpsをキャプチャできるもので、一番XSplitやOBSと相性が良かった。
実は他のキャプボで試したところ映らず、1度も使わずして売った時は凹みました。

電源 CORSAIR HX850i

組んでるPCもミドルクラスなのでそれに合わせた感じ。将来、GPU2つかもしれないということでこの容量。そしてPlatinum。

キーボード RealForce 87UB

一口総評: 「★3 HHKBは駆逐されるのだろうか」

f:id:matsuokah:20171224023631j:plain

しぶい!HHKBに近い打鍵感 。安定感を生み出す1.4kg。

f:id:matsuokah:20171224023530j:plain
上がRealforce, 下がHHKB Professional2

矢印キーがないHHKBが一回り小さい印象。 打鍵中に安定するな〜と思っていたら、底面のすべり止めもまた大きめだった。

また、US配列だと半角/全角の切り替えが若干めんどくさかった。
自分は右下のaltをあまり使わないので、そこに半角/全角の切り替えを割り当てました。

IKEAのデスク

f:id:matsuokah:20171224013701j:plain
IKEAの公式ページより転載

一口総評: 「★3 機能性よりもシンプルさを選んでよかった」

2017年2月頃に購入

PCとモニターを設置するために購入。 リビングに置くので、見た目を嫁にも相談し、白のデスクにした。
掲載している画像のテーブルトップよりも薄く丈夫なものがあったのでそちらに変更して購入。

LERBERGという名前の脚にテーブルトップを載せる形式。可愛い感じ
ワゴンとか棚は無いのがシンプルで良い。

ゲーミングチェア

一口総評: 「★3 リクライニングは良い」

デスクときたら椅子!
本当は人間工学系のイスを買いたいけど、高いということで、ゲーミングチェアを選択。
メルカリで偶然良品(AKRacing)に出会ったため、元値の5, 6割の値段で新品同様の品を手に入れることができた。

リクライニングで完全フラットになるので、小休憩しやすい。

余談ですが、AKRacingのHPの"AKRacing フォトギャラリー"はギャグだと思うので覗いてみてほしい。
きれいな女性がオフィスにゲーミングチェアで座ってたり、暖炉の前でくつろいでたりしている異様な空気感w

モニターアーム

f:id:matsuokah:20171224020749j:plain

f:id:matsuokah:20171224013312j:plain

(配線はこの後きれいにしました)

一口総評: 「★5 デスクがかなり広くなった」

2017年12月頃購入

Amazonのサイバーマンデーセールでガス圧式で移動が容易なできるモニターアームが安くなっていた。1万が6000円くらいだったかな

頻繁に位置を変えないなら、2枚設置可能で4000円台で最低要件を満たすモニターアームを発見し、購入
設置してみるとモニターの脚がデスクを占める割合が大きかったんだな〜と痛感した。

配線もアームに括りつけることができるので本当にスッキリした。実用性も気持ちよさも兼ね備えていて本当に買ってよかった。

ヘッドホンハンガー

一口総評: 「★3 定位置のための投資」

f:id:matsuokah:20171224015132j:plain

物の固定位置を決めることが収納の鍵(うまるちゃんの兄しらべ)らしいので、Amazonで800円くらいのものを購入。 確かに固定の位置に戻す癖がついた。

Nintendo Swiftchと諸々

一口総評: 「★3 安定」

f:id:matsuokah:20171224013839j:plain

購入したソフトは

  • ゼルダ
  • マリオカート8DX
  • ちょきっとスニッパーズ
  • Splatoon2

Splatoon2にハマっていて、Splathon(企業対抗戦)にも参加した。
次の第7回は年が明けてすぐ!楽しみです。

1GBbps対応のネットワーク物理

f:id:matsuokah:20171224014004j:plain
f:id:matsuokah:20171224014007j:plain

スイッチングハブ、LANケーブル、Nintendo Switch用LANコネクタをすべて1GBbps対応にした。
不便は感じてなかったけど、ケーブルは余り物とか付属品でなんとかしていたので、ちゃんと1GBbps対応のものに刷新。

Apple製品たち

f:id:matsuokah:20171224014137j:plain

どんだけ肉の写真とってるんや・・・!!

  • iPad Pro 10.9 inch Cellularモデル 256GB
    • LinksMateのSim 5GBプラン
  • Apple Pencil
  • iPhone X
  • Apple Watch Series 3 Cellularモデル
  • MacBook Pro 15inch BTO(CPU3.1GB, SSD1TB, US Keyboard)

これだけでかなりの出費をしてる気がする…

言い訳をするならばAndroidエンジニアからiOSエンジニアへの転向がきっかけ。

iPad Pro 10.9 inch Cellularモデル 256GB

2017年8月頃に購入

一言総評: 「★3 確かにタブレットの方がカジュアルに調べ物や読書ができる」

デバッグ用の端末+プライベートや旅行でPCを持ち歩かないために購入
Nexus 9も持ってるが、やっぱりiPadProの方がサクサクだったので買い替えてよかった。

CellularモデルということでLinksMateのsimを指している。
AbemaやOPENRECを垂れ流しにしてるが、通信料がほぼディスカウントされるのでかなりいい。Wi-Fiの帯域を食わないのも○
未使用バケットが繰越せて、10GBくらいたまった月もあった。

Apple Pencil

一言総評: 「★3 さすがApple純正のシンクロ率。。。だけど高いような。」

iPadの画面を触りたくなくて購入。やはり純正品同士のシンクロ率はソニー製品くらい感じる。
ちょっとしたメモや物事の整理をiPad + Pencilですることが多くなった。

iPhone X

一言総評: 「★3 安定のiPhone. 特に驚きはない。」

ずっとXperiaとNexusを使っていたがiOSエンジニアに転向したことと、プロダクトのiPhoneX対応をしたこともあって購入。
ホームボタンが無いことにはすぐ慣れた。

7plusから使えるようになったポートレート撮影はかなり重宝していると思う。嫁が夕食の写真を毎度撮っているのだけど、Xを貸してくれと言われる。しかもSNSには投稿しない(笑)

f:id:matsuokah:20171224021131j:plain
Xで撮った写真1

f:id:matsuokah:20171224021332j:plain
Xで撮った写真2
後ろの海老天までぼやけちゃってるけど、ポートレートモード楽しい。

Apple Watch Series 3 Cellularモデル

一言総評: 「★4 あると超便利、なくても困らない」

通知を選りすぐりしたり、頻繁にアクセスする情報(天気やスケジュール)が絞られているので 情報を確認するテンポの良さを感じた。
「時間という気になる情報を一瞬で把握できる」という時計のコンセプト に当てはまるものは意外と存在するなあという感覚。

また、通知から情報の詳細や他の情報に目移りしないようにできているのがいいなと思った。Android Wearのコンセプトもこんな感じだった気がする。
「時間を確認するコンテンツに時間を浪費する」わけにはいかない。

僕は好きです。

また、会社の入館証をApple Watchに登録していて、
入館証のチェッカーがちょうど左胸の位置にあたるので「それでも俺らは抵抗するで、拳で✊」出社を決めています。

MacBook Pro 15inch 2017 late BTO(CPU3.1GB, SSD1TB, US Keyboard)

f:id:matsuokah:20171224024211p:plain

一言総評: 「★3 ただ、ただ、高い」

12月24日に上海から届く。会社で使っているPCと同じスペックなので特にワクワクもない。

現在使っているのは5年前の物(2012年の初代Retina MBPにBTOでメモリを16GBにした)なのでそろそろ潮時。 Apple製品にしては特にトラブルもなく、5年もったことに驚き。(本来はこうあって欲しいw)
さすがにAppCodeを立ち上げると重くてコーディングするのが辛かったため、買い替え。

本当は絶賛キャンペーン中である ヨドバシのApple製品でもポイント10%キャンペーンに乗りたかったけど、US配列がないので断念。

会社にも家にもHHKBを使っているでノートの配列関係ないけど。

JetBrains All Products Pack

f:id:matsuokah:20171224011733p:plain

一言総評: 「★4 AppCodeの完成度が高い。ただし、Xcodeの新機能がワンテンポ遅れる」

Xcode9からAlcatrazが使えなくてVimバインディングが気軽にできなさそう だったので、AppCodeを検討しました。

IntelliJのみを契約していたが、AppCode(swift/objective-c)、たまにWebStormを使うので、All Products Pack を選択。

IntelliJは4年くらい使っているので、
使い慣れたIDEのショートカットがAppCodeでもそのまま使えるのがGood。

まとめ

My Best 3

  1. モニターアーム
  2. 自作PC
  3. iPhoneX

FastlaneがSwiftで書けるようになった〜

これはSwiftアドベントカレンダーの17日目の記事です。

Swiftの方はプラットフォームに依存しないエントリーを書くべきかと思いましたが、
FastlaneのSwift対応がタイムリーだったのでこっちにしました。
元はSwiftでTCPソケット通信を書こうと思ってたので、年末にでも。それでは本題へ

今回は下記のプロジェクトを元に紹介していきます。

github.com

Fastfileの設定ファイルやその周辺がSwiftで書けるようになりました

fastlaneがもともとRubyなのは周知の事実ですが、rubyの実装をSwiftから叩く実装が2.69.0から入りました。

Swift対応の実装方針としてはブリッジ、フック、そしてlane定義に分かれています。

ブリッジ: Rubyのコマンドをコールするラッパーを自動生成
フック: ブリッジファイルをコールしたり、エントリーポイントとなるコード lane定義: コマンド(ブリッジ部分)を叩いたり、ビルドの設定や環境変数を定義している箇所

そしてこれらが、xcodeプロジェクトで管理できるようになっています。
今回の一番のポイントは「慣れたエディタで慣れた言語をつかえる」。これだけで効率化のイメージが湧きますよね

早速使ってみる

fastlane init swiftでfastlane関連のSwiftのファイルが生成されます。

f:id:matsuokah:20171217013103p:plain

To edit your new Fastfile.swif type: open ./fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeprojというメッセージが出ています。

生成されたプロジェクトの開いて、グループの構成をみると下記のようになっていて、基本的にはFastfile.swiftだけを編集すればOK

* Autogenerated API => いわゆるRubyのブリッジ部分
* Fastfile Components
* Networking => 文字通り
* RunnerCode => フック部分
* Appfile.swift, Fastfile.swift => 設定、ビルドフローの定義ファイル
* 各コマンド(Gymfile.swifなど)

このプロジェクトをビルドするとMacでexecutableなバイナリが出来上がります。
生成されたバイナリを介してfastlaneの部分が実行されています。また、fastlaneコマンドをつかって起動した場合、バイナリ自体のビルドもよしなに走るようになっています。

Fastfileを編集してビルドする

FastfileクラスはLaneFileクラスを継承しています。LaneFileには各実装が織り込まれているのでビルドフローを詳しく知りたい人は読んでみるといいと思います。

つけるメソッド名はdebugLaneのように接尾辞を付ける必要があります。理由はlaneを接尾辞にもつメソッドをフィルタしてlaneを見つけ出してフックしているためです

class Fastfile: LaneFile {
    var fastlaneVersion: String { return "2.69.3" }        
    func debugLane() {        
        buildApp()
        crashlytics(apiToken: "TOKEN", buildSecret: "SECRET")
    }
}

最低限の実装はこれだけで済むはずです。

ここからさらに ConfigurationやExport Methodなど、enumを定義してそれらを扱うクラスを用意すれば省コード化が可能になります。

enum Configuration: String {
    case debug
    case release

    var exportMethod: ExportMethod {
        switch self {
        case .release:
            return .appStore
        default:
            return .development
        }
    }
}

上記はConfigurationの列挙ですが、プロジェクトによってはstagingや準本番のような環境もあるかと思います。

それらの環境変数を洗い出したあとは変数のマネージャクラスを用意すればOK
すべて記載すると長いので、Protocolだけ記載しておきます

protocol BuildContextProtocol {
    
    // Xcode
    var workspace: String { get }
    var scheme: String { get }
    var configuration: String { get }

    // Build
    var buildDir: String { get }
    var ipaName: String { get }
    var ipaPath: String { get }
    var dsymName: String { get }
    var dsymPath: String { get }
    var exportMethod: String { get }
}

最終的には下記のようなコードに収まります。

    func debugLane() {
        desc("Submit a new Beta Build to Crashlytics")
    let buildContext = BuildContext()
    buildContext.build()
    crashlytics(apiToken: "TOKEN"
        , buildSecret: "SECRET"
        , ipaPath: buildContext.ipaPath
        , groups: Fabric.testerGroup, notifications: true
    )

    }

各環境変数・コンフィグの切り替えを省コード化して書いてみたサンプルをおいときます。

GitHub - matsuokah/fastlane-swift-sample

今回省コード化のために抜き出した設定ファイルです

fastlane-swift-sample/BuildContext.swift at master · matsuokah/fastlane-swift-sample · GitHub

ハマったこと

  • FastlaneRunnerをgit管理下に置こうとして失敗する
    • gitignoreに追加しました。また、バイナリなので12MBほどの大きさです。毎度FastlaneのSwift部も勝手にビルドが行われて、バイナリが再度生成されるので追跡しなくていいと思います。
  • ルビーでは配列で扱っている部分もすべてStringになっている
    • Crashlyticsのgroupsとかがそうなのですが、rubyだと["tester1", "tester2"]のように配列を指定できますが、SwiftではインターフェースにはString採用されているので使い方に工夫が必要になりそうです。
  • 現状、メソッドにパラメータを渡していない
    • 試してはいないのですが、フック部では_ = fastfileInstance.perform(NSSelectorFromString(laneMethod))と書かれていることから、引数が使えないのでは?と妄想しています。

まとめ

ということで、まだまだexperimantalで機能的にまだ満たされていない部分もありますがそこはPRポイントですね!
若干のワークアラウンドが発生しますが、FastfileがSwiftで定義でできるようになったことでグルーコードが非常に書きやすくなったかと思います!
個人的にはRubyが書けるならRubyでいいなと思います。あくまでRuby主導なリポジトリですので、Rubyで直接実行できるにこしたことはないです。

GitHub - matsuokah/fastlane-swift-sample

CIを前提としたプロジェクトのテンプレートができてた話

これはiOSアドベントカレンダーの10日目の記事です。

私にとって今年は、iOSエンジニアに転向した年でした。 それまではAndroid。 そして、携わっているプロジェクトのSwift化(未完)だったりiPhoneX対応だったりと劇的な半年でした。

ほぼゼロから始めたので、iOS SDKのサンプルアプリをいくつか作って勉強してたのですが、
そのうち、 CIを前提としたプロジェクトのテンプレートのような物が出来上がっていたのでそれを紹介したいと思います。

TL;DR

AnsibleでCIできるまでセットアップするプロジェクトテンプレートができてましたという話です。

大まかな使い方

  1. initialタグをチェックアウト
  2. プロジェクトをつくって、リポジトリと同じパスに配置。( initial-projectタグの状態を作る)
  3. プロビジョニング(Ansible)を実行(実行すると、apply-provisionタグの状態になる)

※ 実際に使う場合はトークンなどの認証情報が設定されている必要があります。

CI

  • fastlane

Deploy

  • Crashlytics Beta

Package Manager

  • CocoaPods

Provisioning

  • Ansible
  • Bundler

テンプレートプロジェクト

下記のテンプレートプロジェクトをもとに詳細を書いていきます

github.com

リポジトリをクローンしてからfastlaneを叩くまで2,3コマンドでできるようにする

Ansibleで環境構築を自動化することで、fastlaneのコマンドを叩くまでの時間を短縮することができました。 なるべく前提を減らし、ビルドに必要なアプリケーションがなくても動くように作りました。

流れを書くと

  1. make setup_toolsコマンドでシェルスクリプトをフック
  2. シェルスクリプトでXcodeのインストール済みかどうかを判定。なければ、Safariを開いてXcodeのインストールを促す
  3. Ansibleがなければインストール、あればアップデート
  4. AnsibleのPlaybookを実行

Ansibleでやったこと

  • brewで必要なアプリのインストール
  • 各テンプレートのデプロイ
  • bundlerのインストール

テンプレート

主に作ったテンプレートは下記の通り

  • .gitignore
  • Podfile
  • fastlane関連
  • Gemfile

.gitignore

.gitignoreは特にプロジェクト作る度にまずこれを作りますが、うっかり忘れたり、排除対象の列挙が漏れると思わぬファイルを追跡してしまいます。
gitignore.ioで作るのもめんどくさい

今回はある程度使うものを予め盛り込んだテンプレートで対応しました。

脱線しますが、CLIではcurl http://gitignore.io/api/swift >> .gitignoreという書き方も可能です。

Podfile

Crashlytics Betaを使いたいので予め盛り込んだ状態にしています。
XXXXX.xcodeprojヒットしたターゲット名で生成しています。これであとはpod installするだけ。

よく使うライブラリは他にも盛り込んでもいいかも。RxSwiftとか。

fastlane関連

テンプレート化したのは下記の3つ。Fastfile 以外は自前の定義ファイルです。

  • Fastfile
    • ビルドシーケンス
  • AppContext
    • Xcodeのターゲット名やConfigurationなど、アプリを作るための環境変数の解決
  • Env

    • ビルドを実行するためのコンスタントな環境変数の定義
      • Fabricのトークンやシークレットキー、SlackのWebhook URLを定義
  • Gemfile

    • Bundlerのインストール

FastlaneEnvの管理について

プライベートリポジトリを使っていたので、気にせず直書きしていました。(セキュリティの意識は甘々ですが別の漏れても被害はほぼないので。) また、サンプルを作るときには、Bitbucketを使っていました。Privateリポジトリの作成が無料かつ無制限につかえます。 余談ではありますがCIにはBitriseを使いました。Bitriseは10分以内ビルドなら月に200回まで無料で使えます。 iOSかつ、プライベートなリポジトリのCIが無料でできるのはBitriseだけ(俺調べ)

テンプレートの使い方まとめ(再掲)

  1. タグinitialをチェックアウト
  2. プロジェクトをつくる(実行すると、タグ: initial-projectの状態になる)
  3. プロビジョニング(Ansible)を実行(実行すると、タグapply-provisionの状態になる)

まとめ

ということで、Xcodeプロジェクトを作ってからfastlaneを走らせるまでの作業を自動化してみました。

殆どのプロジェクトでは初回のみなのであまり機会がなさそうなソースですが、
手順が自動化でき、自分にとってのプロジェクトの作成マニュアルができました。

今回は盛り込んでいませんが、xcodeprojを使えばシェルの埋め込みも可能だと思います。
まだまだプロジェクトのテンプレートは育ちそうです。

それでは次は17日のSwiftのAdvent Calendarで〜。

Macでスクリーンショットの保存先の変更と古いスクショの自動削除

デスクトップがスクショの嵐・・・!

f:id:matsuokah:20171102013640p:plain

こんなデスクトップになった経験はないでしょうか。 スクリーンショットの保存先はデスクトップなので、スクショを撮ってるうちにいつの間にかデスクトップがスクショで埋め尽くされることがあります。

精神衛生上よくない!!!

デスクトップは常にきれいでありたいものです。本当作業中で一時的なファイルを置くだけにしたい。

割れ窓理論があるように、デスクトップが心の余裕の無さや秩序を保とうとしているかを表してるんじゃないかという観念に狩られ るんですよね。もはやこれは健やかな精神なのかしるためのバロメータでもあるといえようッッ!!

ということで、整理される環境を整えたいと思います。

結論

2つの工夫で解決できます

1. ターミナルで保存先の変更

$ mkdir -p ~/Pictures/ScreenShots &&  defaults write com.apple.screencapture location ~/Pictures/ScreenShots

f:id:matsuokah:20171102012746g:plain

2. Automator + Calendarで自動的に古いスクリーンショットをゴミ箱に移動する

ターミナルで保存先の変更はDefaultsで解決!

blog.matsuokah.jp

約2年前の記事でも言及してますがdefaults-write.comでいろんな設定値をコマンドラインから設定することが可能です。

defaultsは、設定アプリで設定不可能な項目にアプローチできるので痒かった設定を自分好みに設定することでよりMacを使いやすくすることができます。

任意のディレクトリを作成し、そこに保存する設定を書き込みましょう。
私の場合は、写真フォルダの下にスクリーンショット用のフォルダを作成して、そこを保存先として扱うようにしました。

ターミナルで下記のコマンド2つを実行するのみです

$ mkdir -p ~/Pictures/ScreenShots
$ defaults write com.apple.screencapture location ~/Pictures/ScreenShots

~/Pictures/ScreenShotsをFinderのサイドバーに登録しておくと便利です。

自動削除にはAutomator!!

次は自動削除です。デスクトップに保存しなくなった分、スクショが棚卸しされなくなりそうです。
いつの間にか凄まじい数のスクショになっているかもしれません。
スクショは往々にしてインスタントなデータなので作成日から1ヶ月過ぎたら消されてもほぼ問題無いと思います。

ということで、1ヶ月以上前のスクショを自動的に削除する仕組みを作っていきます。

Automatorを使います。スクリプトを書かない選択をしてみました。

f:id:matsuokah:20171102024333p:plain

こいつです。癖さえわかれば作業・業務を自動化できるので楽しくなります。
エンジニアなら自動化のスクリプトを組むのは日常茶飯事ですがそれがGUIベースでできるイメージです。
アプリケーションベースのアクションが定義されていてその結果を次の操作に繋いでいくイメージでしょうか。

アプリケーションベースのアクションとは
例えば、Finderだったら

  • フォルダを取得する
  • フォルダの名前を変更する

のような単位です。

立ち上げると

f:id:matsuokah:20171102025033p:plain

こんなウィンドウが立ち上がるので左のカラムに並んでいるアクションを、右のエリアにドラッグしてワークフローを作っていきます。

今回やりたいことは「30日以上前に作成したスクリーンショットの削除」

Automatorで扱えるフローに分解すると

  1. スクリーンショットが保存されているディレクトリのパスを取得する
  2. スクリーンショットをリストする
  3. スクリーンショットが作成された日を30日以上前のものにフィルタする
  4. ゴミ箱に移動する

これをAutomatorのフローに当てはめると

  1. Get Specified Finder Items
  2. Get Folder Contents
  3. Filter Finder Items
  4. Move Finder Items to Trash

f:id:matsuokah:20171102030054p:plain

ということでできました。

これで実行するとスクショフォルダ内にある作成日が31日以上前のファイルが一括でゴミ箱に移動されます。

Calendarアプリで定期実行に組み込む

実はCalendarアプリでアプリケーションの定期実行をすることができます。

手順としては

  1. ワークフロー実行用のカレンダーを作成
  2. 上記で作成したカレンダーにスクリーンショット削除の予定をカレンダーに追加
  3. 全日 or 0時などで 毎日繰り返し予定に
  4. カスタムアラートでその時間になったらファイルを開くを選択

こんな感じです

f:id:matsuokah:20171102031142p:plain
予定の追加

ワークフロー実行用のカレンダーを作成

なぜ、ワークフロー実行用のカレンダーを追加したかというと、毎日定期実行の予定がカレンダーを埋め尽くすことになるからです。 カレンダーは分けておいて、ワークフロー実行用のカレンダーを普段は非表示にしておくことをおすすめします。

まとめ

  • defaultsでスクショの保存先を変えてDesktopにスクショがたまらないようにしよう!
  • Automatorとカレンダーでスクショの棚卸しはPCにやらせよう!

Lottieで再生するアニメーションを作って読み込ませるまで

blog.matsuokah.jp

↑の記事は、Lottieを使ってアニメーションの再生をするところを実装しました。

実はハンバーガーアイコンのアニメーションには大きな余白が含まれていて
このまま使うとアイコン自体が非常に小さい表示になってしまっていました。

ということで、次はAfterEffectsプロジェクトを編集して、JSONを書き出すところ紹介したいと思います。

1. AfterEffectsのプロジェクトでアニメーションを編集する

まずはハンバーガーアイコンのプロジェクトをよみこみます。

f:id:matsuokah:20171014131427p:plain

おわかりかと思いますが、コンポジションのサイズ(いわゆる全体の大きさと思ってください)が800x600になっていて、アイコンに対しての余白が非常に大きい状態です。

そこでComposition Settingsを開き、サイズをアイコンが収まるサイズの正方形にします。

f:id:matsuokah:20171014131922p:plain

設定すると、以下のようになります

f:id:matsuokah:20171014132035p:plain

2. 書き出す

次に書き出しです。
Lottieでは、このオープンソースライブラリでjson形式にExportすることを想定して作られています。

github.com

AdobeCCを使っていれば、↓のページでインストールすればAfterEffectsで使用可能になります。

exchange.adobe.com

それでは、AfterEffectsでプラグインを起動します。

Window > Extensions > Bodymovinで、起動できます

f:id:matsuokah:20171014132345p:plain

起動するとコンポジションを選択する画面がでてきます。

f:id:matsuokah:20171014132431p:plain

Renderで書き出してみます。

特に設定をしていないとこのように、書き出しが許可されてないよというエラーとともに解決方法が提示されます。

f:id:matsuokah:20171014132612p:plain

After Effects CC > Preference > General > Allow Scripts to Write Files and Access Networkにチェックを入れます

f:id:matsuokah:20171014132807p:plain

もう一度書き出すと成功します。

f:id:matsuokah:20171014132841p:plain

あとは、吐き出されたJSONファイルをiOS/Androidに組み込んで使うだけです。

f:id:matsuokah:20171014120502g:plain

これで、自作のアニメーションを組み込めるぞ 🎉🎉🎉🎉🎉🎉🎉

Lottieことはじめ

Lottieとは

Airbnb謹製のアニメーションツールでAfterEffectsでexportしたアニメーションをiOS、Androidで再生できるというすぐれものです。

f:id:matsuokah:20171014111823g:plain

上記のGIFはlottie-iosより転載

僕自身はこういうアニメーションはあまり好きではないのですが、
味気なさがなくなりますね!

ということで、シンプルなハンバーガーアイコンのスイッチを例に使い始めるまでの流れをまとめてみました。

1. Carthageで組み込み

github "airbnb/lottie-ios" "master"

cartfileに上記を記述してcarthage update --platform iOSして、プロジェクトに組み込むだけですね。

2. スイッチを組み込む

LOTAnimatedSwitchというクラスがあるのでそれを使います。

LOTAnimatedSwitch.hをみるとスイッチを作成するクラスメソッドが用意されてるのでこちらを使います。

/// Convenience method to initialize a control from the Main Bundle by name
+ (instancetype _Nonnull)switchNamed:(NSString * _Nonnull)toggleName;

/// Convenience method to initialize a control from the specified bundle by name
+ (instancetype _Nonnull)switchNamed:(NSString * _Nonnull)toggleName inBundle:(NSBundle * _Nonnull)bundle;

toggleNameにはアニメーションを記述したjsonファイルを指定する必要があります。 LottieではLottieFilesといって、
アニメーションをクリエイティブ・コモンズライセンスで公開しているストアがありますのでそこからダウンロードしてきます。

今回はハンバーガーアイコンをダウンロードしてきます。

ダウンロードされたzipを解答するとAfterEffectsのプロジェクトファイルと、Export済みのjsonがはいっているのでjsonをプロジェクトに組み込みます

f:id:matsuokah:20171014114015p:plain

let animatedSwitch = LOTAnimatedSwitch.init(named: "Hamburger")

これで読み込めるようになりました。

3. スイッチのアニメーションの範囲を決める

このままではアニメーションはうまく動きません。なぜならスイッチのON/OFFに対してのアニメーションの対応付をしていないからです。

スイッチはoff -> onのアニメーションとon -> offのアニメーションがあります

f:id:matsuokah:20171014114656p:plain

この間ですね。

LOTAnimatedSwitch.hをみるとアニメーションの範囲を割合で設定するメソッドが用意されています

- (void)setProgressRangeForOnState:(CGFloat)fromProgress
                        toProgress:(CGFloat)toProgress NS_SWIFT_NAME(setProgressRangeForOnState(fromProgress:toProgress:));
- (void)setProgressRangeForOffState:(CGFloat)fromProgress
                         toProgress:(CGFloat)toProgress NS_SWIFT_NAME(setProgressRangeForOffState(fromProgress:toProgress:));

先ほどダウンロードしたHamburger.jsonではoff->on->offということでonに戻るまでのアニメーションが記述されています。

アニメーションの進捗割合に対応付けると

off -> on : 0 -> 0.5
on -> off : 0.5 -> 1.0

と表すことができます。

したがって、setProgressの記述は以下のようになります。

animatedSwitch.setProgressRangeForOnState(fromProgress: 0, toProgress: 0.5)
animatedSwitch.setProgressRangeForOffState(fromProgress: 0.5, toProgress: 1.0)

この進捗割合はアニメーションの元ファイルに依存します。

off -> on : 0 -> 1.0
on -> off : 1.0 -> 0

で表せる場合もあるでしょう。

ということで、アニメーションの対応付が完了し、スイッチの作成ができました。

※レイアウトのコードは本筋から外れるので記載していません

4. 動かしてみる

f:id:matsuokah:20171014120502g:plain

タップしてるのですがわかりづらいですね(汗)

ということで、アニメーションの組み込みができました。

5. InterfaceBuilderで組み込めるようにしてみる

InterfaceBuilderで必要な要素だけを設定したらいい感じに動いてほしいです。
毎度、アニメーションの対応付のコードを書くのは面倒です。
ということで、InterfaceBuilderで設定できるようにします。

LottieSwitchView.swift

@IBDesignable
@IBInspectable

を使って、Interface Builderでアニメーションを定義できるようにします

import UIKit
import Lottie


@IBDesignable
class LottieSwitchView: UIView {
    @IBInspectable
    var filename: String = ""

    @IBInspectable
    var fromProgressToOn: CGFloat {
        set(newValue) {
            _fromProgressToOn = LottieSwitchView.shrinkInZeroToOne(value: newValue)
        }
        get {
            return _fromProgressToOn
        }
    }
    @IBInspectable
    var toProgressToOn: CGFloat {
        set(newValue) {
            _toProgressToOn = LottieSwitchView.shrinkInZeroToOne(value: newValue)
        }
        get {
            return _toProgressToOn
        }
    }

    @IBInspectable
    var fromProgressToOff: CGFloat {
        set(newValue) {
            _fromProgressToOff = LottieSwitchView.shrinkInZeroToOne(value: newValue)
        }
        get {
            return _fromProgressToOff
        }
    }
    @IBInspectable
    var toProgressToOff: CGFloat {
        set(newValue) {
            _toProgressToOff = LottieSwitchView.shrinkInZeroToOne(value: newValue)
        }
        get {
            return _toProgressToOff
        }
    }
    
    //// actual value
    private var _fromProgressToOn: CGFloat = 0
    private var _toProgressToOn: CGFloat = 0.5
    private var _fromProgressToOff: CGFloat = 0.5
    private var _toProgressToOff: CGFloat = 1.0

    override func awakeFromNib() {
        super.awakeFromNib()
        let animatedSwitch = LOTAnimatedSwitch.init(named: filename)
        animatedSwitch.setProgressRangeForOffState(fromProgress: fromProgressToOff, toProgress: toProgressToOff)
        animatedSwitch.setProgressRangeForOnState(fromProgress: fromProgressToOn, toProgress: toProgressToOn)
        self.addSubview(animatedSwitch)
        animatedSwitch.fitToParent()
    }
}

private extension LottieSwitchView {
    static func shrinkInZeroToOne(value: CGFloat) -> CGFloat {
        return min(1.0, max(value, 0))
    }
}

f:id:matsuokah:20171014123342p:plain

これで、InterfaceBuilderでファイル名、アニメーションの範囲を指定できるようになりました

InterfaceBuilder上ではUIViewで枠だけを作っていて
awakeFromNibで内部的にLOTAnimatedSwitchを作ってaddSubviewしています。

fitToParentは親Viewと同じframeになるようにConstraintsを設定しているだけです。

以上、Lottie事始めでした。

リポジトリ

github.com

次は、「AfterEffectsからアニメーションのJSONをExportする」記事を書こうと思います
↓書きました

blog.matsuokah.jp

アクションシートのクロージャをObservable化して処理を一本化する

アクションシートってよくつかわれるんですかね? 私が携わっているプロジェクトではそこそこ使われています。

f:id:matsuokah:20171012231640g:plain

コレです

アクションシートの基本的な使い方

let actionSheet = UIAlertController(title:"Title", message: "Message", preferredStyle: .actionSheet)

let action1 = UIAlertAction(title: "Action 1", style: .default) {
    (action: UIAlertAction!) in
    print("Selected Action!")
}

// キャンセル
let cancel = UIAlertAction(title: "Cancel", style: .cancel) {
    (action: UIAlertAction!) in
    print("Selected Cancel!")
}

actionSheet.addAction(action1)
actionSheet.addAction(cancel)

present(alert, animated: true, completion: nil)
  1. UIAlertControllerをつくる
  2. アクションを追加する
  3. 表示する

これが基本的な使い方です。

また、UIAlertActionには選択されたときのクロージャが用意されていて、都度、処理を定義する必要があります。

パッとみて感じるのが、繰り返しの記述ということです。 こういうのをループにしたくなるのがプログラマの性ですよね。

ここで私は2つ思い浮かびました

  • UIAlertActionの生成を共通化できないか?
  • 「どれが選択されたか」をストリームとして扱えないか?

ということで、Rxをつかってコレを実現したいと思います。

アクションシートの特徴

簡単に述べると下記の3点

  • アクションシート自体はタイトルとメッセージを持つ
  • アクション毎にタイトルと選択された時の処理を持つ
  • キャンセルは例外

選択をObservable化するにあたっての方針

  • アクションの生成元となる情報の配列を引数に取るメソッドを定義し、UIAlertActionの生成をまとめる。
  • どのアクションが選択されたかをenumで表現する
  • アクションシートの起動はUIViewControllerのextensionにまかせてします

実装

早速実装です。 なお、Githubにもソースを上げてます

ActionSheetAction.swift

まずは、アクションシートの各行を表現する構造体を定義します

// MARK: - Action
internal struct ActionSheetAction<Type: Equatable> {
    internal let title: String
    internal let actionType: Type
    internal let style: UIAlertActionStyle
}

ジェネリクス(Type)を用いることによって外からアクションのタイプ(enum)をインジェクトできるようにします。
この型は最終的にObservableに用います。

UIViewController+RxActionSheet.swift

次に、UIViewControllerにUIAlertControllerを表示するextensionを定義します

import UIKit
import RxSwift

internal extension UIViewController {
    internal func showActionSheet<Type>(title: String?, message: String? = nil, cancelMessage: String = "Cancel", actions: [ActionSheetAction<Type>]) -> Observable<Type> {
        let actionSheet = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)       
        return actionSheet.addAction(actions: actions, cancelMessage: cancelMessage, cancelAction: nil)
            .do(onSubscribed: { [weak self] in
                self?.present(actionSheet, animated: true, completion: nil)
            })
    }
}

サブスクライブされた時にアクションシートを表示しています

UIAlertController+RxActionSheet.swift

最後に、UIAlertControllerにアクション追加と同時にObservable化して返すextensionを実装します

import UIKit
import RxSwift

internal extension UIAlertController {
    internal func addAction<Type>(actions: [ActionSheetAction<Type>], cancelMessage: String, cancelAction: ((UIAlertAction) -> Void)? = nil) -> Observable<Type> {
        return Observable.create { [weak self] observer in
            actions.map { action in
                return UIAlertAction(title: action.title, style: action.style) { _ in
                    observer.onNext(action.actionType)
                    observer.onCompleted()
                }
                }.forEach { action in
                    self?.addAction(action)
            }
            
            self?.addAction(UIAlertAction(title: cancelMessage, style: .cancel) {
                cancelAction?($0)
                observer.onCompleted()
            })
            
            return Disposables.create {
                self?.dismiss(animated: true, completion: nil)
            }
        }
    }
}

こんな感じでObservableを作成し、各アクションのクロージャでactionTypeとともにonNextを発行してあげることで
アクションシートの選択をストリーム化することができました。

使い方

AnimalSelectAction.swift

まずアクションの一覧をenumで定義します

enum AnimalSelectAction: String {
    case dog, cat, rabbit, panda

    static var AnimalSelectActions: [AnimalSelectAction] {
        return [.dog, .cat, .rabbit, .panda]
    }
}

ViewController.swift

import UIKit
import RxSwift

final class ViewController: UIViewController {

    private let disposeBag = DisposeBag()
    
    @IBOutlet weak var showActionSheetButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()

        showActionSheetButton.rx
            .tap
            .asDriver()
            .drive(onNext: { [weak self] _ in
                self?.showActionSheet()
            }).disposed(by: disposeBag)
    }
}

private extension ViewController {
    func showActionSheet() {
        let actions = AnimalSelectAction.AnimalSelectActions
            .map {
                return ActionSheetAction(title: $0.rawValue, actionType: $0, style: .default)
        }

        // アクションシートを表示し、返ってくるObservableをSubscribeしておく。
        showActionSheet(title: "Which Do you like?", actions: actions)
            .subscribe { (event: Event<AnimalSelectAction>) in
                NSLog(event.debugDescription)
            }.disposed(by: disposeBag)
    }
}

動作

f:id:matsuokah:20171012225321g:plain

まとめ

ということで、ViewController側ではアクションシートの選択肢の列挙とそれに対して選択された時の記述をするだけで良くなりました。
UIAlertControllerもほぼ意識せずにできてるのはメリデメあるかもですが記述が簡潔になったかと思います。
extension最高\(^o^)/

サンプルリポジトリ

github.com