Cloud Functions for Firebase の環境変数設定後は関数の再デプロイが必要なので注意

ドキュメントにも書いてあることだが、環境変数変更後は関数の再デプロイが必要。

「functions:config:set」を実行した後は、新しい設定を使用可能にするために関数を再デプロイする必要があります。 https://firebase.google.com/docs/functions/config-env?hl=ja#set_environment_configuration_for_your_project

環境変数、動的に変えられるものだと思ってたんだけど勘違いだったっぽい。
そもそもなんで動的に変えられると思ってたのか思い出せない...。

続・イケてない JSON を Swift の Decodable で扱いやすいモデルにデコードする

イケてない JSON を Swift の Decodable で扱いやすいモデルにデコードする の続き。
今度も last.fmuser.getRecentTracks を Decode していく。

尚、文字を打つのが面倒になったためこの記事はあらゆる説明を省略している。

JSON

これを decode していく。

{
    "recenttracks": {
        "track": [
            {
                "artist": {
                    "#text": "TOKIO",
                    "mbid": "d210bd3e-68db-4987-a714-0214449e361d"
                },
                "name": "宙船",
                "streamable": "0",
                "mbid": "",
                "album": {
                    "#text": "Harvest",
                    "mbid": "3824d059-6f1a-4163-ae1a-65d5f87bee38"
                },
                "url": "https://www.last.fm/music/TOKIO/_/%E5%AE%99%E8%88%B9",
                "image": [
                    {
                        "#text": "https://lastfm-img2.akamaized.net/i/u/34s/1a20e578c4ea44c0a8cde03d6c6cc014.png",
                        "size": "small"
                    },
                    {
                        "#text": "https://lastfm-img2.akamaized.net/i/u/64s/1a20e578c4ea44c0a8cde03d6c6cc014.png",
                        "size": "medium"
                    }
                ],
                "@attr": {
                    "nowplaying": "true"
                }
            },
            ...............
        ],
        "@attr": {
            "user": "star__hoshi",
            "page": "1",
            "perPage": "50",
            "totalPages": "807",
            "total": "40316"
        }
    }
}

List 系 API の共通要素

last.fm の List 系 API は、データ本体の配列と attr という API メタデータの 2 つから構成されていて他の API でも同じなので ListResponse という protocol を用意した。
この List に RecentTrack や TopTrack を入れると Generics でいい感じになる。

public protocol ListResponse {
    associatedtype List

    var list: [List] { get }
    var attr: Attr { get }
}

public struct Attr: Decodable {
    public let user: String
    public let page: Int
    public let perPage: Int
    public let totalPages: Int
    public let total: Int
}

protocol を実装するとこんな感じになる。

    public struct RecentTracksResponse: ListResponse, Decodable {
        public typealias List = RecentTrack
        public let list: [List]
        public let attr: Attr

        private enum CodingKeys: String, CodingKey {
            case list
            case attr
        }

        private enum RecentTracksKeys: String, CodingKey {
            case recenttracks
        }

        private enum TrackAttrKeys: String, CodingKey {
            case track
            case attr = "@attr"
        }

        public init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: RecentTracksKeys.self)
            let recentTracks = try values.nestedContainer(keyedBy: TrackAttrKeys.self, forKey: .recenttracks)
            self.attr = try recentTracks.decode(Attr.self, forKey: .attr)
            self.list = try recentTracks.decode([List].self, forKey: .track)
        }
    }

配列の中身を decode

nowplaying がネストしててもしょうもないので、 @attr で decode してそこからさらに nowplaying で decode するようにした。
last.fmAPI は本当にひどいので、 nowplaying が false 場合はそもそも @attr が空っぽになってる。なので if decoder.contains(.nowplaying) してから decode している。

Image とか String -> Int の変換とかは こっち で先にやっていたので使い回せて便利。

public struct RecentTrack: Decodable {
    public let name: String
    public let image: Image
    public let loved: Bool?
    public let streamable: Bool
    public let mbid: String
    public let url: URL
    public let date: Date?
    public let nowplaying: Bool
    public let album: Album
    public let artist: RecentArtist

    private enum CodingKeys: String, CodingKey {
        case name
        case image
        case loved
        case streamable
        case mbid
        case url
        case date
        case album
        case artist
        case nowplaying = "@attr"
    }

    private enum NowplayingKeys: String, CodingKey {
        case nowplaying
    }

    public init(from decoder: Decoder) throws {
        let decoder = try decoder.container(keyedBy: CodingKeys.self)

        name = try decoder.decode(String.self, forKey: .name)
        image = try decoder.decode(ImageDecodableMap.self, forKey: .image).decoded
        let loved = try decoder.decodeIfPresent(StringCodableMap<Int>.self, forKey: .loved)?.decoded
        self.loved = loved == nil ? nil : loved == 1
        streamable = try decoder.decode(StringCodableMap<Int>.self, forKey: .streamable).decoded == 1
        mbid = try decoder.decode(String.self, forKey: .mbid)
        url = try decoder.decode(URL.self, forKey: .url)
        date = try decoder.decodeIfPresent(DateDecodableMap.self, forKey: .date)?.decoded
        album = try decoder.decode(Album.self, forKey: .album)
        artist = try decoder.decode(RecentArtist.self, forKey: .artist)
        if decoder.contains(.nowplaying) {
            let nowplayingDecoder = try decoder.nestedContainer(keyedBy: NowplayingKeys.self, forKey: .nowplaying)
            nowplaying = try nowplayingDecoder.decodeIfPresent(StringCodableMap<Bool>.self, forKey: .nowplaying)?.decoded ?? false
        } else {
            nowplaying = false
        }
    }
}

最終的には LastfmClient/RecentTrack.swift な感じになった。

本当はもっとややこしくて、 extended=0API 投げると loved や data が帰ってこなかったりするのだが、それは decodeIfPresent でなんとかなる。

おわり

Codable 良いのだが、汚い API は Decode するだけで無限に時間を消費しててつらい。

イケてない JSON を Swift の Decodable で扱いやすいモデルにデコードする

last.fm というサービスがあって、API も公開されているのでそれを Swift で使っているのだが、JSON の構造がイケてなくて苦労している。

こんな感じの JSON があったとする。

{
    "user": {
        "name": "RJ",
        "age": "20",
        "image": [
           {
               "#text": "https://lastfm-img2.akamaized.net/i/u/34s/aaa.png",
               "size": "small"
           },
           {
               "#text": "https://lastfm-img2.akamaized.net/i/u/64s/bbb.png",
               "size": "medium"
           }
       ],
        "url": "https://www.last.fm/user/RJ",
        "registered": {
            "#text": 1037793040,
            "unixtime": "1037793040"
        }
    }
}

image はどんな user でも small, medium しかないので array である必要はないし、age は Int だし、 registered も Date 型で扱いたい。あとトップレベルが user だけど、 user を消して階層も1段上にあげたい。

つまりこんな感じのモデルにマッピングしたい。

public struct Image: Decodable {
    public let small: URL?
    public let medium: URL?
}

public struct User: Decodable {
    public let name: String
    public let age: Int
    public let url: URL
    public let registered: Date
    public let image: Image
}

ネストした構造を 1 段上に上げる

user.name の構造だけど、これを name として decode したい。
一旦 user を decode して、さらに name を decode すれば良さそう。

public struct User: Decodable {
    public let name: String
    ....

    private enum CodingKeys: String, CodingKey {
        case name
        ....
    }

    private enum UserKeys: String, CodingKey {
        case user
    }

    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: UserKeys.self)

        let user = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)
        name = try user.decode(String.self, forKey: .name)
        ....
    }
}

String を Int にする

Swift 4 JSON Decodable simplest way to decode type change - Stack Overflow を参考にした。

  • LosslessStringConvertible
    • 100 と "100", 10.0 と "10.0" みたいに String と数値をいい感じに変換できる時に使える
  • decoder.singleValueContainer()
    • 1つしか値を持っていない場合限定で使う
struct StringCodableMap<Decoded: LosslessStringConvertible>: Decodable {
    var decoded: Decoded

    init(_ decoded: Decoded) {
        self.decoded = decoded
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let decodedString = try container.decode(String.self)

        guard let decoded = Decoded(decodedString) else {
            throw DecodingError.dataCorruptedError(
                in: container, debugDescription: """
                The string \(decodedString) is not representable as a \(Decoded.self)
                """
            )
        }

        self.decoded = decoded
    }
}

...

    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: UserKeys.self)

        let user = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)
        name = try user.decode(String.self, forKey: .name)
        age = try user.decode(StringCodableMap<Int>.self, forKey: .age).decoded
    }

ググるInt(try values.decode(String.self, forKey: .age)) ?? 0 などが出て確かに手軽だけど、Optional を 0 で握りつぶしたくないし使いまわせる形にしたかったので StringCodableMap を定義する方法にした。

registered オブジェクトを Date 型にする

       "registered": {
            "#text": 1037793040,
            "unixtime": "1037793040"
        }

これを registered: Date で扱えるようにする。

StringCodableMap のようにカスタム Decodable を定義した。やっていることは今までの応用。
いったん registered を decode して、 #text を取り出して Int に変換して、それを Date にしている。

struct RegisteredDecodableMap: Decodable {
    var decoded: Date

    enum Registered: String, CodingKey {
        case text = "#text"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Registered.self)
        let time = try container.decode(Double.self, forKey: .text)
        let decoded = Date(timeIntervalSince1970: time)
        self.decoded = decoded
    }
}

...

    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: UserKeys.self)

        let user = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)
        name = try user.decode(String.self, forKey: .name)
        age = try user.decode(StringCodableMap<Int>.self, forKey: .age).decoded
        registered = try user.decode(RegisteredDecodableMap.self, forKey: .registered).decoded
    }

Array の image を普通のパラメータにする

       "image": [
            {
                "#text": "https://lastfm-img2.akamaized.net/i/u/34s/aaa.png",
                "size": "small"
            },
            {
                "#text": "https://lastfm-img2.akamaized.net/i/u/64s/bbb.png",
                "size": "medium"
            }
        ],

これを image.small, image.medium でアクセスできるようにする。

これもカスタム Decodable を作る。

  • try decoder.unkeyedContainer()
    • key のないもの、例えば Array などに使う (Dictionary も?)
  • while !unkeyedContainer.isAtEnd
    • Array なのでループで回す。ループの中で一旦 Decode する
    • それを配列で保存しておいて、あとで small, medium にマッピングする
struct ImageDecodableMap: Decodable {
    var decoded: Image

    enum Size: String, Codable {
        case small
        case medium
    }

    struct _Image: Decodable {
        let text: String
        let size: Size

        enum CodingKeys: String, CodingKey {
            case text = "#text"
            case size
        }
    }

    init(from decoder: Decoder) throws {
        var images: [_Image] = []
        var unkeyedContainer = try decoder.unkeyedContainer()
        while !unkeyedContainer.isAtEnd {
            let image = try unkeyedContainer.decode(_Image.self)
            images.append(image)
        }
        var small: URL?
        var medium: URL?
        images.forEach { img in
            let url = URL(string: img.text)
            switch img.size {
            case .small:
                small = url
            case .medium:
                medium = url
            }
        }

        self.decoded = Entity.Image(small: small, medium: medium)
    }
}

...

    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: UserKeys.self)

        let user = try values.nestedContainer(keyedBy: CodingKeys.self, forKey: .user)
        name = try user.decode(String.self, forKey: .name)
        age = try user.decode(StringCodableMap<Int>.self, forKey: .age).decoded
        registered = try user.decode(RegisteredDecodableMap.self, forKey: .registered).decoded
        image = try user.decode(ImageDecodableMap.self, forKey: .image).decoded
    }

おわり

最終的にこんな感じになった。 LastfmClient/Entity.swift at master · starhoshi/LastfmClient · GitHub

複雑な JSON を平坦にしたいだけだったのだが、結構大変だった。
extension KeyedDecodingContainer する方法もあるようだし、どっちがより良いのかはわからないけど、とりあえず汎用的に使いまわせるようにできたので悪くない気がしている。

参考

npm ライブラリの README にバッジをたくさんつける

こんな感じにバッジたくさん出してみた。

f:id:star__hoshi:20180212015753p:plain

starhoshi/rescue-fire

npm version badge

npm version

Version Badge for npm, RubyGems, PyPI, Bower and other packages で取得できる。
自分の package を検索して badge の URL を取得できる。

travis-ci

Build Status

テストがなくても、 lint をするだけに CI してもいいと思う。

セットアップ方法は npm ライブラリを travis-ci でテストして Coveralls でカバレッジを計測する - Qiita に書いた。

Coveralls

Coverage Status

テストカバレッジを出している。テストが書かれてないと出せない。

セットアップ方法は npm ライブラリを travis-ci でテストして Coveralls でカバレッジを計測する - Qiita に書いた。

Cadacy

Codacy Badge

コードの品質を出してくれるサービス。 GitHub 連携して、 Settings から Badge を取得できる。

TypeScript で JavaScript を生成していると JavaScript が汚くて点数が落ちるので、 IgnoreFiles でソースファイルだけ指定すると良い。

f:id:star__hoshi:20180212020909p:plain

License

License: MIT

License Badges for your Project のをコピって貼るだけ。

nodei.co (やってない)

NPM

ダサいのでこれはやらなかった。 npm の install 数とかが見れるっぽい。

Bootcamp で Mac に Windows を無料でインストールする

無料といってもクラックするとかではなく、 Windows Insider Program というのを使って OS をダウンロードできる。

Windows Insider Program

要はベータ版を使うことになる、製品版ではないのでバグを踏むリスクが高くなる。

insider.windows.com

ボタンをぽちぽちして Insider になって OS を DL するだけなので簡単。
今回は Windows10_InsiderPreview_x64_ja-jp_17025.iso というのを DL した。

Bootcamp

インストールは普通に Bootcamp アプリを起動してボタンをぽちぽちする。

インストールには USB メモリなどの外部記憶装置が必要になる、家に USB メモリがなかったのでこれを買ったが小さくて差しっぱにできるので良い。
Prime Now で買ったので2時間後に届いた、Amazon すごい。

Amazon | SanDisk Cruzer Fit USBフラッシュメモリー 32GB [国内正規品] SDCZ33-032G-J57 | サンディスク | パソコン・周辺機器 通販

Bootcamp のインストール方法は下記が参考になる。

これでベータ版の Windows がインストールできた。
2014年の MacBookPro に入れて使っているが、USキーボードだとキーバインドが合わなくて厳しい。
スペックは普通に使うぶんには問題ないけど、ゲームとかやるのは厳しい感じだった。

おまけ

評価版はダメだった

無料で使えるやつっていったら評価版だよな、というのが頭にあったので評価版の Windows を入れてみたんだけど、エラーが出てダメだった。

www.microsoft.com

エラーは下記で、

起動可能なUSBドライブを作成できませんでした
ディスクに十分な空き領域がありません。

USB メモリは 32GB だし問題なさそうなのになんでだ、と思ってググっていたら iso ファイルによってはできなかったりするっぽい。
Insider Program の方は USB メモリの問題は出なかったので結局評価版は使わなかった。

プロモコードを利用したテストに参加する(テスター向け)

プロモコードでのテスト

iOS アプリには「プロモーションコード」を利用することでリリース直前のアプリをテストすることができます。

やり方は簡単で、 App Store からプロモーションコードを入力してアプリを DL するだけです。
プロモコード版を入れたとしても、今後のアップデートなどは通常と変わらず利用できます。

準備するもの

  • iOS 端末
  • バグがあっても許容する気持ち
    • リリース直前のアプリとはいえ人柱なのでバグを踏む可能性がある

手順

1. プロモコードの入手

なんらかの手段で開発者にプロモコードを教えてもらいます。
プロモコードは1度利用したらもう使えなくなってしまうので、コードは流出させないようにしましょう。

2. アプリのダウンロード

  1. App Store アプリを起動
  2. 右上のアカウントをタップ
  3. ギフトカードまたはコードを使うをタップ
  4. コードを入力

これだけです。

もしくは Today タブの一番下にも コードを使う があります

テストでバグや不明点が見つかったら

ちゃんと開発者に報告しましょう。
「一通り触ってみたけど問題なかったよ!」という報告があるのも開発者としてはありがたいです。

iOS10 以下のサポートを切ったら ERROR ITMS-90502 が出た

iOS9, iOS10, iOS11 をサポートしていたのを、iOS11 のみサポートするように変えた。

f:id:star__hoshi:20180109011357p:plain

そして申請しようとしたらエラーがでた。

ERROR ITMS-90502: "Invalid Bundle. Apps that only contain the arm64 slice must also have 'arm64' in the list of UIRequiredDeviceCapabilities in Info.plist."

UIRequiredDeviceCapabilities で arm64 だけをサポートしろってことらしい。

Info.plist

Info.plist の UIRequiredDeviceCapabilities を見てみると、 armv7 になっている。

f:id:star__hoshi:20180109012655p:plain

これを arm64 にする。

f:id:star__hoshi:20180109012834p:plain

これで無事バイナリ提出できた。

なぜエラーが出たのか

iPhone 5c 以下は armv7s であり、 iOS11 にはアップグレードできない。
iOS11 以上のサポートになるということは arm64 が RequiredDevice になってくれればいいので、そこの食い違いでエラーが出てたっぽい。

Youtube Live 放送終了後の動画でチャットを見たかったが Youtube API を使っても見れない

ニコニコ生放送だとタイムシフトで見るときもコメントが見れるけど、 Youtube Live で放送終了後の動画はコメントが見れない。
API からコメントの取得くらいできないかと思って実際に API を叩いてみたがダメだった。

Youtube API

Youtube Live のコメントにアクセスするには、 liveChatId を取得する必要がある。
2段階のデータアクセスが必要。

  1. videoId を使って Videos: list API から liveChatId を取得
  2. liveChatId を使って LiveChatMessages API からチャットデータにアクセス

Youtube Live 放送中であればこれでチャットデータの取得が可能。

Youtube Live 放送後

Youtube Live が終わった後だと、まず liveChatId が取得できなくなる。

URL: https://www.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=iEO1rjU16SY&key=YourApiKey

Live 中

Live 中はこのような Response。

{
 "kind": "youtube#videoListResponse",
 "etag": "\"aaaaaaa-jxt9MnFhfJsoACygTo\"",
 "pageInfo": {
  "totalResults": 1,
  "resultsPerPage": 1
 },
 "items": [
  {
   "kind": "youtube#video",
   "etag": "\"S8kisgyDEblalhHF9ooXPiFFrkc/0-Viv-YeQmW8wgR2wZnDPCBPXqY\"",
   "id": "iEO1rjU16SY",
   "liveStreamingDetails": {
    "scheduledStartTime": "2018-01-06T10:17:40.000Z",
    "scheduledEndTime": "2018-01-06T14:39:46.000Z",
    "activeLiveChatId": "Cg0KC2lFTzFyalUxNlNZ"
   }
  }
 ]
}

Live 終了後

actualStartTime, actualEndTime が増えて activeLiveChatId が消えているのがわかる。

{
 "kind": "youtube#videoListResponse",
 "etag": "\"aaaaaaa/KYex8I1p-jxt9MnFhfJsoACygTo\"",
 "pageInfo": {
  "totalResults": 1,
  "resultsPerPage": 1
 },
 "items": [
  {
   "kind": "youtube#video",
   "etag": "\"S8kisgyDEblalhHF9ooXPiFFrkc/0-Viv-YeQmW8wgR2wZnDPCBPXqY\"",
   "id": "iEO1rjU16SY",
   "liveStreamingDetails": {
    "actualStartTime": "2018-01-06T10:17:56.000Z",
    "actualEndTime": "2018-01-06T14:22:59.000Z",
    "scheduledStartTime": "2018-01-06T10:17:40.000Z",
    "scheduledEndTime": "2018-01-06T14:39:46.000Z"
   }
  }
 ]
}

あらかじめ liveChatId を取得して放送後に Chat API にアクセスしてみる

では放送中に liveChatId を取得しておいて、放送終了後にアクセスしてみるとどうなるか。

URL: https://www.googleapis.com/youtube/v3/liveChat/messages?part=snippet,authorDetails&liveChatId=Cg0KC2lFTzFyalUxNlNZ&key=YourApiKey

{
 "error": {
  "errors": [
   {
    "domain": "youtube.liveChat",
    "reason": "liveChatEnded",
    "message": "The live chat is no longer live."
   }
  ],
  "code": 403,
  "message": "The live chat is no longer live."
 }
}

403 が返ってきてチャットデータにアクセスはできなかった。

おわり

ということで、Youtube Live の放送終了後はチャットデータにアクセスができない。( activeLiveChatId って Response だし放送終了したら active じゃないなってしまう... )

もしかしたら Youtube Live のチャットデータは DB に残っておらず放送終了後に消えてしまっているのかもしれない。もし DB に残っているのであれば API でだけでもチャットデータにアクセスできると嬉しいのだが。

もし放送後にもチャットデータを参照したいのであれば、放送中にデータを保存しておくしかなさそう。

ssh 経由で fastlane match できるようにする

$ ssh my@mac.com
$ bundle exec fastlane match
...

[12:19:58]: Enter the passphrase that should be used to encrypt/decrypt your certificates
[12:19:58]: This passphrase is specific per repository and will be stored in your local keychain
[12:19:58]: Make sure to remember the password, as you'll need it when you run match on a different machine
[12:19:58]: Passphrase for Git Repo: *********
[12:20:12]: Type passphrase again: *********
security: SecKeychainAddInternetPassword <NULL>: User interaction is not allowed.
[12:20:15]: Couldn't decrypt the repo, please make sure you enter the right password!

match のパスワードは正しく入れているのになにやらエラーが出ている。ssh じゃなくて直接ログインして match は成功するので ssh 経由なのが問題っぽい。

security unlock-keychain

どうやら keychain を unlock しないといけないっぽい、 ssh 経由だと keychain にアクセスできなくなっているようだ。

下記のコマンドを実行後に fastlane match した Passphrase のところはうまくいった。

$ ssh my@mac.com
$ security unlock-keychain -p <YourPassword> ~/Library/Keychains/login.keychain

superuser.com

WARNING: fastlane requires your locale to be set to UTF-8.

しかし次のようなエラーが出た

[12:20:32]: WARNING: fastlane requires your locale to be set to UTF-8. To learn more go to https://docs.fastlane.tools/getting-started/ios/setup/#set-up-environment-variables

どうやら sshUTF-8 になってないっぽい。
ssh 接続元マシンの ssh config に UTF-8 で繋ぐように設定した。

Host *.hoge.com
  SendEnv LANG LC_ALL=en_US.UTF-8

おわり

これで万事解決。

jenkins などでビルド環境作るときはおきまりの手順っぽいけど、気軽に unlock-keychain していいものか不安である。

参考

2017 年振り返り

2017 年色々あった。

日報

1月から wikihub で日報を書き始めて、ずっと続けている。
nippo つながりで友達もできたし、ざっくりとだけど毎日アウトプットにもなったし良かった。

nippo.wikihub.io

GitHub

f:id:star__hoshi:20180101013110p:plain

約 4000 Commit だったのでまあまあやった方だと思う。
8月以降は基本的に毎日やっていて、会社より家の方がコード書いていた気がする。

OSS

VIPER 勉強の為に作ったアプリを公開したり、ライブラリを作ったり、fastlane の設定を公開するなどした。

登壇

6回勉強会で登壇した。去年まで1回もしたことなかったので頑張ったと思う。
Presentations by star__hoshi // Speaker Deck

特にこれは950ブクマくらいついてバズって嬉しい。
この登壇資料作っている時はマジで辛くて、こんなくそみたいな発表誰が喜ぶんだ発表したくないと思っていた。でもいざ発表したらバズって、あまり考えすぎずにもっとアウトプットすべきなのだろうな、と気が楽になったのを覚えている。

blog / Qiita

この blog に 75 記事、 Qiita に 50 記事書いたので結構アウトプットした気がする。
しょうもない記事も多いけど 2018 年も気楽に書いていこうと思う。

既存のサービスを組み合わせて本質的な開発に集中する という記事がホッテントリ入って良かったけど、これは表面を撫でるだけの浅い記事なのでもっと深い記事をかけるようになりたい。

また、 モバイルアプリアーキテクチャ勉強会 - Qiita を書くのにあたって色々調べて DI やテスト、疎結合など色々詳しくなれてとても勉強になった。

個人的には人間だものという記事が一番好きで、これは今でも毎日思っている。
布団に入って色々思い詰めて最終的には「人間だもの、気楽に生きよう」と思って就寝する日々、気楽に生きたい。

starhoshi.hatenablog.com

Twitter

star__hoshi_dev というアカウントを作った。star__hoshi はフォロワー多いしあんま雑なツイートしたくないという気持ちがあって、逆に star__hoshi_dev は雑になんでもツイートできるアカウントになっている。

star__hoshi はタイムライン早くて追えないけど dev の方は追えるし人数少ないからコミュニケーション増えたり、昔のツイッターこんなだったなって懐かしさがあってこのアカウントばかり使っている。アイコンや名前を毎日変えたり好き勝手やっている。

twitter.com

転職

前の会社を10ヶ月くらいでやめて8月からクックパッドという会社で働いている。

starhoshi.hatenablog.com

内定出た時はモバイル基盤という部署に行く予定だったのだが、やっぱサービス作りたいという気持ちになりいまは新規事業をやっている。 Firebase を使っており iOS エンジニアだけでサービスを作っている。

転職して給料は上がり労働時間は減り勤務体系がフルフレックスになり自宅勤務可になり周りのレベルも高くとても良い。とても良いのだが周りに引けを取らないように頑張らないと... という感じで気を張り詰めてしまいなんか疲れている。

個人開発

今年はアプリを 3 つリリースした。
新しいアプリを作るというより ダメージ計算Z の開発と機能追加をやるのが主な一年だった。
アプリの評価もそこそこいいし、頑張って作った甲斐があった。リリースした時に App Store のトレンドに載ったのが嬉しかった。

ライバロリ氏にツイートしてもらえたのも嬉しかった。

ダメージ計算Z for ポケモン ウルトラサンムーン

ダメージ計算Z for ポケモン ウルトラサンムーン

  • Kensuke Hoshikawa
  • ユーティリティ
  • 無料

個人チーム開発

2016年振り返り でも書いているが、チームでサービスを作るというのをやっていた。

しかし権利的にまずそうだったり仕事が忙しかったり気持ちがついてこなかったり、なんやかんやあって結局リリースされずになってしまった。

チーム開発、楽しいし勉強にもなるけど熱量の差や考え方の違いなどやはり人間的なところで難しさも出てくる。仕事と違って任意での開発になるし余暇の過ごし方は人それぞれだし難しい。難しいけどチームでやることの可能性みたいなのもあるしまた何かやれたらなとは思っている。

2018年

仕事はとりあえず 4 月にリリースが迫っているのでそれに向けて頑張る。
また、チームの方針としてアウトプットどんどんしていきましょう、というのがあるのでやっていきたい。特に自分はライブラリを作って公開などあまりしていないのでそこをもっとやりたい。

やりたいということもあるのだが、この記事でも何回も出てきている 気楽に生きる というのをやりたい。もっと気軽に何かをやったり何かをやめたりしたい。
別に仕事をサボりたいとか勉強をやらないということじゃなくて、圧倒的成長とか一発当てるとかじゃなくてゆるく生きたい。

今年もよろしくお願いします。