文字っぽいの。

文字を書いています。写真も混ざります。

"carthage-verify" を使って `carthage bootstrap` 忘れを防ぐ

やりたいこと

CocoaPodsがビルド時に「pod install しないとだめだよ」とエラー吐いてくれるのが便利なので、Carthageでも同じことをやりたい。

実現方法

Carthage公式のworkflowsというリポジトリcarthage-verify というスクリプトがあるので、そちらを使います。

github.com

手順

まずは carthage-verify をcloneするなりDownloadするなりして、手元に持ってきます。ディレクトリ構成としては、下記のように scripts ディレクトリを作って、その中に入れてあげます。

.
├── sample.xcodeproj
├── sample.xcworkspace
└── scripts
    └── carthage-verify <- [コレ!]

次にBuild PhasesにScript Phaseを追加していきます。こんな感じです。

f:id:FromAtom:20180104190320p:plain

下記はコピペ用のスクリプトです。

./scripts/carthage-verify
if [ $? != 0 ] ; then
    # print error to STDERR
    echo "error: The sandbox is not in sync with the Cartfile.resolved. Run 'carthage bootstrap --
    platform iOS --use-ssh --no-use-binaries --cache-builds' or update your Carthage installation." >&2
    exit 1
fi

結果

わざと手元の ./Carthage 内を古い状態にして、ビルドしてみると、

f:id:FromAtom:20180104190958p:plain

このようにちゃんとエラーを出してくれます。良かったですね。

参考文献

こちらのスライドを参考にしました。

speakerdeck.com

【esa】社内ドキュメントツールのホッテントリを分かるようにしたら捗った話

この記事はピクシブ株式会社 Advent Calendar 2017の20日目です。

昨日はおしゃれな動画を作るマンであるまつらいの

inside.pixiv.blog

でした。かっこよくてずるいですね。


こんにちは、Atomです。普段はiOS EngineerとしてSwiftを書いたり、9%チューハイを片手に街を散歩したりしています。

さて、弊社ではesaを利用したポエム駆動開発が活発に行われています。esaは、

esaは「情報を育てる」という視点で作られた 自律的なチームのためのドキュメント共有サービスです。

というサービスで、社内での情報共有を気軽に行うことができます。詳しい利用事例やポエム駆動開発についてはこちらの記事をご参照ください。

gihyo.jp

esaはとても優れたサービスで、だれでも気軽にポエム、思考、ノウハウなどを共有することができます。弊社では今年1年で1617件の記事が投稿されており、とても活発に利用されていることが分かります。

一方で、記事が多くあるとすべてを読み尽くすのは厳しくなってきます。Twitterはてなブックマークに加えて、esaの新着記事もすべて読んでいては仕事する時間がなくなってしまいます。

そこで、今熱い記事がわかるyakitoriというWebアプリを作りました。

yakitoriの様子
yakitoriの様子

加えて、社内Slackにも6時間毎に通知するようにしました。

運良く僕の記事が通知されていた

実装

yakitoriはHeroku上に下記3つのアプリケーションを動かすことで構築されています。ちなみに全てRuby製で、無料の範囲でやりくりしています。

  • App1: 定期的にesaの全記事取得しスコアを計算してRedisに保存する君
  • App2: SinatraでRedisから取得したデータを表示するだけ君
  • App3: 定期的にRedisから取得したデータをSlackに投げる君

という構成になっています。マイクロサービシーズっていうやつですね(笑顔)。App2, App3は非常にシンプルな構成なので、キモであるApp1について簡単に解説します。

詳しいコードは https://github.com/FromAtom/Yakitori にあるので、コアの部分だけ解説をします。

time_diff = Time.now - Time.parse(created_at)
duration_hour = time_diff / (60 * 60)
point = COMMENT_GRAVITY * comments_count + STAR_GRAVITY * stars_count + WATCHERS_GRAVITY * watchers_count
score = (point - 1.0) / ((duration_hour + 2.0) ** GRAVITY) # この数値をもとにランキングを作る

このコードで各記事のスコアを計算しています。数式で表すとこのようになります。なお、この計算式はHacker Newsのものを参考にしています。

 
    score =  \frac{(p - 1)} {{(t + 2)}^{G}}


  • t: 投稿からの経過時間[h]
  • G: 重み付け係数(1.8ぐらいがちょうどよい)

となっていて、この時のp

 
    p = star\_count \times 1.0 + comment\_count \times 1.2 + watch\_count \times 0.2

と設定しています。

esaでは記事に星をつけるスター、記事の更新を通知で受け取るウォッチ、そしてコメントがあり、これらを利用して記事を書いた人にフィードバックを送ることができます。なので、これら3つの数値を重み付け(1.0, 1.2, 0.2の定数がそれ)をしながら記事のスコアとして利用しました。ちなみに、コメントをするとウォッチも自動的にされるので、ウォッチは弱めにしてあります。

そして、投稿からの経過時間(t)が増えていくことで、古い記事のスコアは低くなるようになっています。

やってみて

今までであれば見ることがなかった記事も見るようになり、新着記事を全部みる必要もなくなったのでとても便利になりました。また、普段Swiftばかり書いているので、こういった小さなサービスやツールをRubyでササッと書くのは楽しかったです。(本当はSwiftで書こうと思ったけどRedis周りが面倒でやめた……)

esaAPIも用意されているので、記事を簡単に取得することができてとても助かります。

最後に

明日はverno3632がAndroidの話をしてくれるようです。楽しみですね!

そしてピクシブ株式会社では、仕事の合間に小さなツールを作るのが好きな人や、焼き鳥が好きなスマートフォンアプリエンジニアを募集しています。焼き鳥が好きではないスマートフォンエンジニアも応募しています。

recruit.jobcan.jp recruit.jobcan.jp www.wantedly.com

iOS9以降では "NotificationCenter.default.removeObserver(_:name:object:)" をdeinitに書かなくても良いですよ。

表題通り。間違ってたら教えて欲しい。

iOS9以降をDeployment Targetにしている場合のみの話ですので、iOS8系をサポートしている場合は今まで通り明示的にremoveしましょう。

今までは NotificationCenter.default.addObserver(_:selector:name:object:) して追加されたNotificationを、明示的にremoveする必要がありました。用途によりますが、 viewWillDisappeardeinit の中で NotificationCenter.default.removeObserver(self) とか書いていることと存じます。

この処理がiOS9以降を対象としたビルドでは不要になりました。

ほんまかいなという話ですが、addObserver(_:selector:name:object:) - NotificationCenter | Apple Developer Documentation を見ると、

If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method. Otherwise, you should call removeObserver(_:name:object:) before observer or any object passed to this method is deallocated.

と書いてあります。便利ですね。

9%の世界

f:id:FromAtom:20171210153847j:plain

ストロングゼロがTLでにわかに流行っている。

ちょうど休日だったのと、洗濯機が焼肉屋でこびりついた匂いを消してくれるまで暇だったので、缶チューハイを片手に商店街を散歩してみた。 昼下がりにこんな酒を片手にふらふらとしていると、定職についていない気持ちになってなんだか楽しい。

カップルの男性が「今日何も食べてない」と苛立っているのを、おそらく彼女であろう女性が夕飯の提案をしながらたしなめていたり、 路上の看板に歩きスマホの男性がぶつかりかけているのを見ていた。

商店街には何かしらの目的がないと訪れない。 ましてや9%の酒を片手に散策するために来たことは一度もないし、そんな目的で来る人はほとんど居ないだろう。 ただ、いったん酒を飲んでしまえば、あとはもうカッコつけることができない世界になる。 僕は昼間から高アルコールな缶チューハイを片手に徘徊するおっさんになって、その缶を飲み尽くすまではどんなお店にも入れなくなる。 スタバなどのカフェはおろか、100均ですら気後れする。

そんな状態だから、街を歩き回るしかやることがない。 強制的な散歩状態になるから、いろいろ発見もある。 アルコールの効果で体は温まるし、普段見ない角度で街を見ることができる。 なんだか高尚なことをしている気分にすらなる。

一通り歩き回って、缶も空になったので、丁寧にチューハイを買ったコンビニまで戻って捨てた。 酔いも回っていたので、そのままコンビニでコーヒーを買って近所の公園で飲みながら、 夕飯のこと、明日の仕事のこと、家で脱水が終わっているであろう洗濯機の中身のことを考え、 コーヒーを飲み終わるとまたコンビニに戻って捨て、家に帰った。

ちなみに、9%のチューハイは結構あって、ストロングゼロ以外にはキリンの氷結ストロングやビターズがある。 チューハイではないけども、角ハイボール缶の濃いめも9%だ。 ストロングゼロは結構甘いけど、ビターズはそんなに甘くないという特徴がある。 個人的にはビターズが好きで、ハイボールまで行くと味がなさすぎてさみしい。 単体でも飲めるけども、おつまみとも合うのがよい。

こういった飲み物を愛飲すると次の日(つまりそれは将来)が破滅してしまうことはわかっているので普段は飲まないけれども、 深夜のセブンイレブンで適当なパウチ(ごぼうサラダや豆のサラダがあるやつ)とホットスナックと9%チューハイのトール缶を買って帰るときの、 あの幸福なのか不幸なのかわからない感覚は結構好きではある。

Xcodeでプロジェクトごとにインデントスタイルを指定する

環境

やりたいこと

EditorConfigのようにインデントのスタイルを指定しておくことで、新しい協力者が増えた時に「あ、インデントはハードタブなんで。」などという煩わしい対話をしないようにしたい。

手順

Xcodeを開いて、プロジェクトファイルを選択します。

f:id:FromAtom:20171109195550p:plain

そしてこの"Text Settings"にある"Indent Using"を選択します。

f:id:FromAtom:20171109195840p:plain

ここで重要なのは、すでにTabsが選択されていても、もう一度Tabsを選択しないとプロジェクトのインデント設定がされない という点です。

なお、ちゃんと反映されたか確認したい場合は、git diff などで差分を確認して

f:id:FromAtom:20171109200825p:plain

usesTabs = 0; もしくは usesTabs = 1; が設定されているかを見ると良いです。

この設定が済めば、設定画面で

f:id:FromAtom:20171109201515p:plain

このようにSpacesが設定されていても、プロジェクト内のファイルではTabsでインデントがされます。また新しく生成されるファイルでも、もちろんTabsでインデントされます。

おまけ

プロジェクトファイルを選択すると全体のインデント設定ができますが、個々のファイル毎でもインデントの設定ができます。なので *.swift ファイルではTabsで、*.mm ファイルはSpacesでインデント指定をするということも可能です。

感想

EditorConfigが使えなくなって、Xcodeではプロジェクトごとのインデント設定はできないと思い込んでいましたが、実はできました。

謝辞

教えて頂いた @kishikawakatsumi さん、ありがとうございます!

iOSのTwitterKitで投稿しようとすると、401が返ってきて投稿できない問題。

環境

  • Xcode9.1
  • Swift 4.0.2
  • TwitterKit 3.2.1

問題

上記環境でTwitterに投稿をするコードを書いた。雑にコードの様子を書くと、Twitter.sharedInstance().logInをしてログインが成功した後で、

guard let session = Twitter.sharedInstance().sessionStore.session() else {
    return
}

let apiClient = TWTRAPIClient(userID: session.userID)
let request = apiClient.urlRequest(
    withMethod: "POST",
    url: "https://api.twitter.com/1.1/statuses/update.json",
    parameters: [
        "status": status
    ],
    error: nil
)
apiClient.sendTwitterRequest(request) { response, _, error in
    if let error = error {
        print(error)
    }
}

とする感じ。特に難しいことはしていないのでシミュレータでは難なく動いたが、なぜか実機だと "Request failed: unauthorized (401)" と error が返されて投稿できない。なんでやねん。

解決方法

Twitterのアプリ管理画面( Twitter Application Management ) で対象のアプリを開いて、

  • Access Levelを "Read and write" から "Read only" に変えて "Update Settings"ボタンを押す
  • Access Levelを "Read only" から "Read and write" に変えて "Update Settings"ボタンを押す

とすると直る。

どうやらアプリを登録してすぐ表示されている"Read and write"というAccess Levelは正しくないらしい。実際にこの解決方法を試す前に、自分のアカウントで連携したアプリ一覧を見てみると、

f:id:FromAtom:20171109125450p:plain

となっていて、『読取専用』と書いてある。つらい。

感想

どうやらこの挙動はTwitterアプリや連携を開発したことがある人の間では、よく知られているらしい。知らんがな。

iOS11からSocialKitが使えなくなり、TwitterKitを使うことになったアプリが多くあると思うが、この罠に引っかからないことを切に願う。つらい。

Swift4のCodableをUIImageに対応させる

環境

  • Xcode9 GM
  • Swift4

前提

Swift4からCodableという便利Protocolが追加されました。

public protocol Encodable {
    public func encode(to encoder: Encoder) throws
}

public protocol Decodable {
    public init(from decoder: Decoder) throws
}

public typealias Codable = Decodable & Encodable

やりたいこと

UIImageをプロパティに持つstructをCodable protocolに準拠させたい場合があります。

struct Photo {
    let id: Int
    let title: String
    let image: UIImage?
}

前述した通りCodableは、DecodableEncodableの2つのprotocolを要求するprotocolであるため、DecodableEncodableのそれぞれに準拠すれば良いです。

ここで、UIImageが邪魔になってきます。IntStringDecodable / Encodableに対応していますが、UIImageはしていません。なので、自分で処理を書く必要があります。

ちなみに、Custom TypeをCodableに対応させる方法はAppleのドキュメントにあるので、そちらを見ると良いです。

structで対応させる場合

こんな感じで、UIImageはbase64で文字列にエンコードしてあげます。簡単ですね。

extension Photo: Decodable {
    enum CodingKeys: String, CodingKey {
        case id
        case title
        case image
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        title = try values.decode(String.self, forKey: .title)

        let imageDataBase64String = try values.decode(String.self, forKey: .image)
        if let data = Data(base64Encoded: imageDataBase64String) {
            image = UIImage(data: data)
        } else {
            image = nil
        }
    }
}

extension Photo: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(title, forKey: .title)

        if let image = image, let imageData = UIImagePNGRepresentation(image) {
            let imageDataBase64String = imageData.base64EncodedString()
            try container.encode(imageDataBase64String, forKey: .image)
        }
    }
}

このサンプルコードでは、DecodableEncodableをわざわざ分けて書いていますが、

extension Photo: Codable {
}

とすれば、分けて書かなくて良くなります。ちなみに、UIImageそのものをCodableに対応させようとあれこれ試したのですが、うまくいきませんでした。強い人教えてくれ!

余談その1

structではなくclassをCodableに対応させる場合、すこしだけ記法が変わります。

final class Photo {
    let id: Int
    let title: String
    let image: UIImage?

    init(id: Int, title: String, image: UIImage?) {
        self.id = id
        self.title = title
        self.image = image
    }
}

extension Photo: Codable {

    // Decodable
    enum CodingKeys: String, CodingKey {
        case id
        case title
        case image
    }

    convenience init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let id = try values.decode(Int.self, forKey: .id)
        let title = try values.decode(String.self, forKey: .title)

        let imageDataBase64String = try values.decode(String.self, forKey: .image)
        let image: UIImage?
        if let data = Data(base64Encoded: imageDataBase64String) {
            image = UIImage(data: data)
        } else {
            image = nil
        }
        self.init(id: id, title: title, image: image)
    }

    // Encodable
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(title, forKey: .title)

        if let image = image, let imageData = UIImagePNGRepresentation(image) {
            let imageDataBase64String = imageData.base64EncodedString()
            try container.encode(imageDataBase64String, forKey: .image)
        }
    }

}

余談その2

上記サンプルコードではUIImagePNGRepresentation を使って決め打ちで画像を変換していますが、

extension UIImage {

    private var hasAlpha: Bool {
        guard let alphaInfo = cgImage?.alphaInfo else {
            return false
        }

        switch alphaInfo {
        case .first, .last, .premultipliedFirst, .premultipliedLast, .alphaOnly:
            return true
        case .none, .noneSkipFirst, .noneSkipLast:
            return false
        }
    }

    var data: Data? {
        if hasAlpha {
            return UIImagePNGRepresentation(self)
        } else {
            return UIImageJPEGRepresentation(self, 1.0)
        }
    }

}

のように UIImage を拡張し、画像フォーマットを切り替えてあげると便利です。もちろん用途によりますが。