ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

【iOS】Hyperion-iOSもあるんだよ!

かむいです。今月転職しました。

転職活動も忙しかったりして1ヶ月ぶりの投稿となります。
時間過ぎるのホント早い!

Hyperion-Androidが話題に

先週 Android界隈で話題になったコレ。 こりゃすげぇ!てなりましたよね。

qiita.com

てっきりAndroidだけかと思っててんですけどちゃんとiOSもあったんですよ!

github.com

僕はもうかよちんの「さすがです!」LINEスタンプを開発者に送りつけたい気持ちになったのですが、サンプルコードがあったのでまずは動かしてみたわけです。

そしたら実際に動かしらもっと感動しちゃったので、そのことについて書きます。

導入の仕方

CocoaPodsかCarthegeで導入できます。

詳細はREADME.mdがわかりやすいのでそれ見てもらえれば早いのですが、CocoaPodsで言うと:configurations => ['Debug'] と設定することでデバックビルドでだけこれが見れるよという訳。なるほどね!

実際に動かして見た。いや、動かしてみて

Gifや画像を見て納得して終わらず、是非ともサンプルコードを起動するだけで良いのでご自身で触って見て頂きたいです。 zipでソース一式を落としてくればものの2分もせずに体感出来ます。

凄いことその1 : Zeplinとかで見ながら確認してたアレやコレを実行したアプリの中で見れる!

デザイナーに作ってもらったワイヤーフレームをZeplinなどのツールを使ってUIの情報をアプリに落とし込みますよね。

・テキストの配置位置
・テキストのフォントサイズ
・文字色、ボタン色
・セルのサイズ
etc...

それらを実行したアプリ上で確認できるんです。なんなら画面拡大も出来ちゃう
これマジ

https://camo.githubusercontent.com/ec23fbb49d169c85a5d881e35be0c16902c652bf/68747470733a2f2f6d656469612e67697068792e636f6d2f6d656469612f6c34456f4e4f494c72354f6676677973772f67697068792e676966

https://camo.githubusercontent.com/9641f342cee745d6d05c03cc100841be575738be/68747470733a2f2f6d656469612e67697068792e636f6d2f6d656469612f7854315239486639333833576a75636f6d492f67697068792e676966

凄いことその2: アニメーションを細かくチェックすることが出来る

画面遷移やカスタムアニメーション処理とか、実際に想定した動きになっているか、実は早過ぎてよくわかってない動きも実装の中にはあると思います。 それが0.5倍速とか0.25倍速に設定できて、実際の動きをゆっくり確認出来ちゃいます。
これマジ

https://camo.githubusercontent.com/5d18c81486e741052e51c384495b48ddecbfb8da/68747470733a2f2f6d656469612e67697068792e636f6d2f6d656469612f323646655a634e463944627138394d42692f67697068792e676966

凄いことその3: 皆んなでサードパーティプラグイン作ろ!作成ガイド鋭意製作中!

Calling all developers!!! Be one of the first to create a third-party plugin.   
The plugin creation guide is a work in progress, but if you are feeling ambitious  
 you can reference the plugins we have already created along with our documentation.

開発者の方も書いておりますが、新たなプラグインの製作者を募集しております! まだ機能が少ないのでむしろ俺が望むデバッグ機能はコレや!というのがあれば是非すでに書かれているHyperionCore Referenceを読んで見てはいかがでしょうか。

最後に

何か話題になるOSSがあったとして、それが Android界隈だけで盛り上がっていたらiOS側もあるのではと動いてみること、そういう気持ちでAndroid界隈の動向も追うのは大事だなと今更ながら痛感した次第です(・ω・)

Swift 4.2 では boolean の反転(toggle)ができるようになる(SE-0196)

どうも。ルパン三世 Part 5 の続きが楽しみな tobi462 です。

さて前回のenumの新機能に続き、また Swift 4.2 の話題です。

といっても、今回は驚くほど短いです(汗)

Tl;Dr

// before
messageLabel.isHidden = !messageLabel.isHidden

// after
messageLabel.isHidden.toggle()

toggle()

SE-0196 にて Bool の extension として toggle メソッドが用意されました。

冒頭に書いたとおり、いままで foo.bar = !foo.bar というコードを書かなければならなかったところが foo.bar.toggle() と書けるようになるわけです。

まぁ、自前で実装していた方も多いかもしれませんが。

実装

具体的な実装は以下のようになっているようです。

extension Bool {
  @_inlineable
  /// Toggles the value of the Boolean. 
  ///
  /// Calling this method sets the variable to `true` if it was `false`,
  /// and sets it to `false` if it was `true`. For example:
  ///
  ///    var bools = [true, false]
  ///
  ///    bools[0].toggle()
  ///    // bools now contains [false, false]
  public mutating func toggle() {
    self = !self
  }
}

swift/Bool.swift at swift-4.2-branch · apple/swift · GitHub

まぁ・・・まんまですね(笑)

ちなみに2行目の @_inlineable は初めて知ったのですが、SE-0193 で既に実装されていたようです。

どの Swift バージョンで入ったかはすぐに終えなかったのですが、とりあえず Swift 4.2 では利用できました。

関数のインライン化はパフォーマンス的に有利な面もあると思いますが、多くのケースでは過度な最適化にあたると思うので、普段のiOSアプリ開発では利用しないほうが良いのでしょう。(きっと)

閑話休題。

おわり

foo.bar = !foo.bar はある意味、DRY原則にも反していると言えます。

今後は foo.bar.toggle() というスッキリした書き方ができて良いですね。

Swift 4.2 では enum の件数がとれるようになる(SE-0194)

どうも。赤ワインが一番美味しい季節は秋だと思うのですが、この初春という時期もなかなか乙だと感じる tobi462 です。

今日は Swift 4.2 で追加される SE-0194 について軽くメモです。

ちなみに執筆時点で Xcode 9.4 beta 1 がリリースされていますが、こちらには Swift 4.2 は含まれていないので、現時点では自前でビルドする必要があります。

追記(2018/06/03):
現在は Swift 公式サイトのDownloads の「Swift 4.2 Development」から Xcode のツールチェーンをダウンロードすることができるので、そちらを利用するのが楽でしょう。 f:id:yu_dotnet2004:20180603082228p:plain 追記(ここまで)

今回、Swiftのビルドにあたっては以下の記事を参考にさせていただきました。 https://qiita.com/rintaro/items/2047a9b88d9249459d9aqiita.com

Tl;Dr

enum にCaseIterableを適合することで.allCasesでcase値の一覧が取得できます。

enum Fruits: CaseIterable {
    case apple, orange, banana
}

Fruits.allCases.count // => 3
Fruits.allCases // => [Fruits.apple, Fruits.orange, Fruits.banana]

これまで

これまでは enum の件数を取得したり、すべての値を列挙する方法はありませんでした。

そのため以下のように自前で実装する必要がありました。

enum Fruits {
    case apple, orange, banana
}

extension Fruits {
    static var count: Int {
        return all.count
    }
    static var all: [Fruits] {
        return [.apple, .orange, banana]
    }
}

Fruits.count // => 3
Fruits.all   // => [.apple, .orange, .banana]

これは実装が面倒であるという以上の問題があり、 enum に値が追加された場合に修正を忘れる可能性があります。

以下ではgrapeを追加していますが、allプロパティの修正を忘れています。しかし、Swiftコンパイラはこのミスを検出できません。

enum Fruits {
    case apple, orange, banana, grape
}

extension Fruits {
    static var count: Int {
        return all.count
    }
    static var all: [Fruits] {
        return [.apple, .orange, .banana] // .grape を追加し忘れている
    }
}

Swift 4.2 から

冒頭でも書きましたがCaseIterableに適合することで、自動的に.allCasesが使えるようになります。

enum Fruits: CaseIterable {
    case apple, orange, banana
}

Fruits.allCases.count // => 3

for fruit in Fruits.allCases {
    print("I like \(fruit).")
}
// => I like apple.
// => I like orange.
// => I like banana.

これはCodableと同じようにコンパイル時に自動的にコードを生成する仕組みのようです。

ちなみに私自身は利用したことが無いのですが、Sourceryでも同等のことが出来るようです。

ただし、Sourceryの場合はSwiftソースが生成されるのに対し、CaseIterableではコンパイラによって自動的に生成されるので、生成後のコード管理などについて意識する必要がありません。(これもCodableと同様ですね)

自前で実装する

さて、enumに関連値(Associated-Value)があった場合はどうなるのでしょうか?

結論から言うとコンパイルエラーとなります。

enum Maybe: CaseIterable {
    case nothing
    case some(Bool)
}
// => error: type 'Maybe' does not conform to protocol 'CaseIterable'

こういった自動生成されないケースでは、static var allCasesを自前で実装する必要があります。

enum Maybe: CaseIterable {
    static var allCases: [Maybe] {
        return [.nothing, .some(true), .some(false)] // 網羅的でなくてもOK
    }
    case nothing
    case some(Bool)
}

ちなみにenum in enumといったケースでは、(理論的には)コンパイラが網羅的に列挙できますが、関連値がある場合は自前で実装が必要になります。

enum Food: CaseIterable {
    static var allCases: [Food] {
        return [.drink] + Fruits.allCases.map(Food.fruit)
    }
    case drink
    case fruit(Fruits)
}

Food.allCases.forEach { print("\($0)") }
// => drink
// => fruit(Fruits.apple)
// => fruit(Fruits.orange)
// => fruit(Fruits.banana)

個人的にはこれは妥当な仕様だと感じます。

CaseIterable vs Sourcery

実行時に動的なアプローチが行えないSwiftについて、Sourceryなどは静的メタプログラミングという手段を使って冗長さを防ぐのは良いアプローチだと感じます。

しかし、静的メタプログラミングと言えば聞こえはいいですが、古来からあるコード自動生成のアプローチなので、テンプレート言語を覚えたりコード生成・管理というワンクッションの手間はかかります。

そうしたちょっとした手間を防ぐという意味で、言語自体にそうしたコード生成の仕組みが用意されるのは良い方針だと個人的には感じます。(そのブラックボックスさに微妙さを感じる人もいるかもしれませんが、言語仕様として明確化されていれば問題ないかなと思います)

おわり

Codableもそうですが、最近のSwiftではコンパイラによるコード自動生成というアプローチが増えてきたなという印象があります。

こうしたSwiftコンパイラによる自動生成で便利にする仕様は今後も増えていくのではないでしょうか?(と、Swift Evolutionも追ってないのに勝手に予測してみます)

P.S.
気が向いたら他の Swift 4.2 の機能も記事にしてみたいと思います。

「がんばるぞいbot」を作った話、そしてAWS Lambdaに移行しようという話

どうも、最近週末に映画を観るのが楽しみなtobi462です。ちなみにAmazon Prime Videoです。

今日はプライベートで作ったシステムの話、というか物語を書いてみたいと思います。

がんばるぞいbotとは

Slackで「がんばるぞい!」と書き込んだら、キャラクターが応援してくれるBotです。

ちょうど以下のような感じですね。 f:id:yu_dotnet2004:20180409230924p:plain

NEWGAME!という漫画の第一巻で有名になった「がんばるぞい」を活用したBotですね。

開発のキッカケ

開発のキッカケはシンプルで、 仕事前とか勉強前にがんばるって書き込んだら応援して欲しいな というものでした。

ペンギン村の住人でSlackのワークスペースを持っているのですが、そこの「newgame」チャンネルで活躍しています。

残念ながらSlackAppとして公開はしていませんが悪しからず。

歴史を振り返る

そんな(最初は)シンプルなBotだったのですが、システム構成やら機能やら様々な変化がありました。

実は今度「AWS Lambda」に移行しようと思っているのですが、その前に歴史を振り返ってみよう、というのが今回の記事です。

0. はじまり - Android / Kotlin アプリ時代

Slack用のBotのはずなのに、最初はなぜかAndroidアプリからのスタートでした。

当初、私はAndroid/Kotlinを勉強しようという意気込みにあふれており、せっかくなのでAndroidアプリからSlackにPostしてみようというノリでした。

ユーザ名などを設定画面で入力して「がんばるぞい」ボタンを押したらSlackにポストするという単純なものでしたが、記念すべき「がんばるぞいbot」の開発の始まりでもありました。

f:id:yu_dotnet2004:20180409234436p:plain

記念すべき最初の実装なので個人的にもとても懐かしいものです。

開発したアプリは alpha 版として公開することで、ペンギン村の住人に利用してもらえる状態にしました。

意外な問題

さて、初期バージョンを開発したのは良かったのですがすぐに課題がみつかりました。 それはペンギン村の住人でこのBotを使いたいユーザは全員iPhoneユーザということでした。

というわけで、初期のAndroid/Kotlinバージョンはあっという間に闇に消えました。

悲しいぞい・・・

1. 始動 - Swift / Vapor / Heroku 時代

そんなわけで、すぐに次の開発をスタートしました。 もはやモバイルを捨て、サーバサイドへ旅立つ必要がありました。

仕事でSwiftを多く触っていた私は、サーバサイドSwiftでやろうと決めました。

Let's Server Side Swift

構成は以下のとおりでした。

  • Swift 4.x
  • Vapor
  • Heroku

サーバを自分で用意・管理するのが面倒だったので、PaaSであるHerokuを利用しました。

Vaporを選んだ強い理由はありませんが、過去にVaporとKituraを軽く触った時に、Vaporの方がシンプルだなと感じたのが大きいかもしれません。

分かっていた問題

さてHerokuは無料プランで使いました。

Herokuをご存知の方は、何が問題になるかよくご存知かと思います。 そう「スピンダウン」です。

Herokuは一定時間アクセスがないとインスタンスが止まってしまう(スピンダウン)ため、次回アクセス時に起動するまでに時間が掛かる(スピンアップ)という問題でした。

以前、Herokuを使ったことがあったので、この問題は事前によく分かっていました。

そこでクラウドのIaaSである「Digital Ocean」にサーバを立て、そこから定期的にアクセスするようにしました。Digital Oceanは、全てSSDで値段も安いと聞いたことがあったので、AWSやGCPではなくDigital Oceanを使ってみることにしてみました。

  • DigitalOcean
  • Ubuntu
  • Digdag

定期アクセスであればcronで十分かと思ったのですが、仕事でDigdagを利用していたこともあり、せっかくなので使ってしまおうというノリでした。

Herokuを無料で利用するために$5/moなインスタンスを契約するというなんだかおかしな状況 になりましたが、問題は完璧に解決しました。

2. 激動 - Kotlin / Spark / Docker 時代

さて、こうして出来上がったがんばるぞいBotはなかなか人気で、毎朝「がんばるぞい」と書き込まれる毎日が続きました。

私自身もアクティブユーザとして、今日はどんなキャラが応援してくれるか楽しみにしていました。

がんばったぞいBot

さて、そうこうするうちに新しい機能が欲しくなりました。

「がんばるぞい」も良いけど、がんばったら褒めてほしくない? という単純な動機により「がんばったぞいbot」を作ることにしました。

f:id:yu_dotnet2004:20180410000832p:plain

いやはや我ながら素晴らしい機能だと思いました。

既存のサーバサイドSwiftに機能追加しても良かったのですが、その頃Kotlinに興味を持っており(ry

ということで以下のような構成でシステムを立ち上げました。

  • Kotlin
  • Spark
  • MongoDB
  • MongoExpress
  • Docker (DockerCompose)
  • Ubuntu
  • DigitalOcean

ここで流行りのDockerも触ってみることにしました。勉強がてらというところです。

MongoDBにすべき強い理由はなかったのですが、なんとなくKVSやってみようくらいのノリでした。

CI/CD・さらなる機能追加

さて、ここまできたら自動ビルド・デプロイもしたくなりました。

ここまできたら全部マニュアルな感じてやってみよう、ということでJenkinsを使った自動ビルド・自動デプロイを実現しました。

自動デプロイはmasterブランチにマージされたら、それをトリガーにデプロイされる感じです。(Blue/Greenデプロイまではやりませんでした) f:id:yu_dotnet2004:20180410001558p:plain

と、書くと簡単にできたようにも見えるのですが、まぁそのいろいろ苦労しました・・・あまり慣れていなかったこともあり。

そして「ゆんのティータイム」(15:00になったらティータイムに誘ってくれる)という機能を追加するため、こちらにもDigdagが導入されました。

追加されたコンポーネント:

  • Jenkins
  • Digdag

システム統合

さてこのあたりでペンギン村の住人の一人が開発メンバーに加わることになりました。

そして紆余曲折あり 「がんばるぞいbot」と「がんばったぞいbot」を統合する ことになりました。

まぁ元々別システムにしている理由がなかったので、統合できてよかったかと思います。

これにより2号である「Swift / Vapor / Heroku」によるBotは幕を閉じました。お疲れ様でした。

S3連携、さらなる機能追加

さて、もうここまできたらイケイケな感じです。

今時、画像データはS3に置くべきでしょ ということでAWSも使うことになりました。

そして、機能も思うがままに開発・デプロイし、利用者のフィードバックを楽しみにしていました。

そう・・・あの事件が起こるまでは・・・

AYBABTU

ある日、MongoExpressにアクセスしてみると奇妙な状況になっていました。

DBのデータは消えており、何やら怪しいデータが作成されています。 そして、データに残された英文を見ると、「お前たちのデータは頂いた。返してほしくば xx BTCを」と書かれています。

・・・ ランサム被害やんけ!

そしてデータはローカルに残っていたものを除いて失われてしまいました。 幸い、そこまで失われたデータは多くなかったのですが、個人的にショックでした。

原因

原因を調べたところ、犯人は意外なものでした。

Firewall(UFW)は設定しており、きちんとブロックされるはずのアクセスがなぜ通ってしまっていたのか。

端的にいうと以下の問題でした。 ngzm.hateblo.jp

個人的に手痛い失敗でしたが、多くを学ぶことが出来ました。

  • 稼働後に外からアクセス出来ないことを確認すべき
  • クラウドのFirewallも活用すべき
  • 大事なデータはバックアップすること

まぁ当たり前の話かもしれません。 おそらくこれが業務であればいずれも実行したと思います。

しかし、プライベートでこういった事態に遭遇することが出来て、かえって良い経験ができた。そんな風に今は思っています。

自動バックアップ、管理画面追加、最終的なシステム構成

ここから先はそこまで大きな話はありません。

まずJenkinsのJobでMongoDBのデータを自動バックアップし、S3にアップするようにしました。

データの追加が面倒なので、Railsを使った管理画面も追加しました。なぜかこれはHerokuにデプロイしたのですが。

そして最終的なシステム構成は以下のようになりました。

  • AWS
    • S3
  • DigitalOcean
    • Kotlin / Spark
    • MongoDB / MongoExpress
    • Digdag
    • Docker / DockerCompose
    • Jenkins
    • Ubuntu
  • Heroku
    • Ruby / Rails

メモリが不足する度にDigitalOceanのインスタンスをスケールアップした為、 $20/mo のインスタンスになっていました。

そこで気づきます。 これただのSlackのBotだよね? と。

3. 未来へ - AWS Lambda 時代?

自分で一からインフラも含めてやってみようということで、いろいろ試したシステムでしたが、 さすがにお金をかけすぎている とあらためて思いました。 もちろんそれが楽しかったので、良かったのですが。

さてBotという要件を考えると、リクエストが要求された時などごく短時間だけ計算リソースがあれば十分です。

そもそもBotの利用者も少なく、一日のうち利用している計算リソースの時間はおそらく30秒程度でしょう。(デプロイまわりはさておき)

そのために$20/moなインスタンスを24h稼働しているのはあまりにも無駄です。

なんだか結論ありきな書き方をしてしまいましたが、 そこで「AWS Lambda」ですよ というお話です。

新規に開発メンバーも一人加わって、今はAWS Lambda移行を進めているというか、これから始まるというか、まぁそんな状況です。

おわりに

なんだか無駄なことばっかりしているプライベート開発だったような気もしますが、こうして自分でいろいろ試してみるのは面白く、勉強になるとあらためて思いました。

AWS Lambda への移行でも、いろいろと学べる部分があると思うので今から楽しみです。

そんなわけで「がんばるぞいBot」の始まりから今後の未来まで書いてみました。 物語はこれからも続きます。

「今日も1日がんばるぞい!」

自作アプリにキャラクターを登場させてみた

f:id:kamui_project_tony:20180310104210p:plain

はじめに

そだねー。カムイです。
平昌で日本代表が頑張っていた2/19、クラスメソッド株式会社で主催されたSwift勉強会『AKIBA.swift』にて登壇をして参りました。

classmethod.connpass.com

注) アニメ版権絵多用したため公開できる資料はありません。🙇‍♂️

話したことについて

日課メーターのver2.0.0でキャラクターを登場させたので、その機能紹介と合わせて外注でイラストや声を依頼した時のことについて発表してきました。

Q: Swift関係なく無い?
A: モルゲッソヨ

ではなくて、今回発表テーマが「フリートーーーク!」だったので、AKIBA.swiftにはAKIBA枠というアニメ好きな人のための枠もあるくらいだし、みんな2次元好きやろ?という思惑のもとで発表をさせて頂いた次第です。


主催者のお二方からも良いリアクション頂きましたb

内容

アジェンダは以下の通り
1. どこで頼めるの?
2. どのようにお願いするの?
3. いくらかかるの?
4. 何日くらいで出来るの?
5. ここに詰まったからおまいらも気をつけろ!
6. まとめ(登場させてみた結果www)

どこで頼めるの?

アウトソーシングサービスは色々ありますが、上げさせて頂いたのは以下の通りです。 f:id:kamui_project_tony:20180310095455p:plain

下の3つは声優さん専用のサービスです。
ボイスサンプルが用意されており、声を直接聴いて気になった人を選定できます。

僕は今回ランサーズと萌えボイスを利用しました。

www.lancers.jp
www.moe-v.net

どのようにお願いするの?

各サービスによって変わる点がありますが、主に2タイプあります。

コンペ方式

算と仕事内容を提出し公募。不特定多数のランサー(作れる人)の提案の中から、その中から気になったランサーをこちら側が選定出来るスタイル。

プロジェクト方式

ランサー・予算を先に決めて提出。ランサーが承諾してくれたら作業を開始するスタイル

プロジェクト方式が一般的なイメージかと思いますが、どんな人がいるのかわからない人にはコンペ方式がオススメ。 僕は依頼したいランサーが予め決まっていたので、プロジェクト方式のスタイルでやりました。

いくらかかるの?

ピンキリです。。

イラストで言えばデザイナーの知名度や熟練度, 依頼する枚数, 依頼する内容により差が出ます。
1枚XX円からという説明をして頂ける方もいますが、内容によりけりです。
もちろん仕事開始前に依頼内容を打ち合わせし、そこで前もって見積もって頂けます。

声では文字数*X円 + 声優さんの知名度に紐づくチップ みたいな計算方式になっているらしく、台本ベースになると結構な額になると思います。日課メーターでは一言ボイスを10パタンほどだったのでそこまででは無かったのですが、あまりに文字数が少ないとギャラも少額になるため、仕事を受け付けられないといったケースもあるようです。

何日くらいで出来るの?

これも依頼するボリュームによりけりなのですが、日課メーターを参考にまとめます。

イラスト: 日課メーターでは200x200サイズで上半身のみのデフォルメキャラクターデザインを5パタン依頼。キャラクターのイメージやポージングもデザイナーと打ち合わせしながら進めたため、割と時間がかかりました。最終的に納品頂けたのは1ヶ月ほどかかりました。

声: 一言ボイスを10パタンほどだったので、1週間ほどでご用意頂けました。ただ人気な方ですと仕事が立て込んでいるため、事前に遅れる旨を報告頂けたりします(これはデザイナーの方にも言えるのですが)。

ここに詰まったからおまいらも気をつけろ!

色々あったのですが、発表では2つお話をしました。

絵・声を並行で進めるとどちらかで「あれ?」が出たら調整が難しいゾ!

僕の場合、絵の仕様がざっくりで打ち合わせしながらだったせいもあるのですが、いざ形になったものを見て、それと声とを当て込んで見ると「あ、合わない…」てケースが出てしまいました。
デザイナーの方に調整をお願い事なきを得ましたが、最悪時間とお金をかけて解決しないといけなくなるところでした。

出来る事なら事前に仕様を固めて依頼を出す、またはどちらかを先に納品してから、それに合わせて依頼を修正するみたいなやり方が出来ると良いかもしれません。

キャラクターデザインはユーザー指向で!

僕が単にアニメ好きだったので実装したい機能、実装したかったキャラデザや声で進めた話だったのですが、主に日課メーターのユーザーって20〜30代のデキる女性が多いんですよ…ダイエットなど美容に努める女性の方々からご好評頂けております。

そのためこんな萌え萌えしてるものが果たしてウケるかというと微妙でした。
ユーザーのことを思えば、むしろイケメンにすれば良かったですね笑
幸いレビューで厳しいお言葉は頂けて降りませんが、例え表示をOFFにできる機能でも星1をつけられる可能性はあるので、キャラクターデザインは慎重に進めるべきだと思いました。

まとめ(登場させてみた結果www)

f:id:kamui_project_tony:20180310103535p:plain

さいごに

かなりマニアックな発表内容となってしまいましたが、良い評価も多く頂けたので良かったです。 もしキャラクターコンテンツを自作アプリに入れて見たいという方がいらっしゃれば、参考にしてみてはいかがでしょうか。

最後に、今回日課メーターの褒めキャラにご協力頂いた珠川まほろさん、寿松木明日花さん、本当にありがとうございました!

twitter.com
twitter.com

【iOS】APIKitを使ったXML取得

まえがき

初めまして。ペンギン村の通行人k_miyasakaです。

APIKitを使用してGoogle Suggest APIからXMLを取得する方法をまとめました。

結果

"hello"という文字に対するサジェスト結果をパースしてテーブルに表示

xml取得部分はこんな感じのコードになりました。

import APIKit
import SwiftyXMLParser

/* リクエスト */
struct GoogleSuggestionRequest: Request {
    typealias Response = [String]
    var baseURL: URL = URL(string: "https://www.google.com")!
    
    var path: String = "/complete/search"
    
    var method: HTTPMethod = .get
    
    var parameters: Any? = ["q": "hello", "hl": "ja", "output": "toolbar"]
    
    var dataParser: DataParser = MyXMLParser()
    
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> [String] {
        return object as! [String] // 強気😼
    }
}

/* DataParser実装クラス */
class MyXMLParser: DataParser {
    public var contentType: String? {
        return "application/xml"
    }
    
    public func parse(data: Data) throws -> Any {
        guard let xmlString = String(data: data, encoding: .shiftJIS) else {throw ResponseError.unexpectedObject("文字列にキャストできなかった。")}
        let xml = try SwiftyXMLParser.XML.parse(xmlString)
        let suggestion: [String] = xml["toplevel", "CompleteSuggestion"].map{ $0["suggestion"]}.flatMap{$0.attributes["data"]}
        if suggestion.isEmpty {
            throw ResponseError.unexpectedObject("候補なし")
        }
        return suggestion
    }
}



/* 適当な通信するクラス */
class Connector {
    static func getSuggestion() {
        let request =  GoogleSuggestionRequest()
        Session.send(request){result in
            switch result {
            case .success(let suggestion):
                print(suggestion) // 成功した時の結果
            case .failure(let error):
                print(error) // 通信、パースのエラーはここでハンドリング
            }
        }
    }
}

つまづいたポイント

"https"から始まるURLじゃないと通信できない。

AppleによるATS(App Transport Security)のため、
”http“で始まるURLには原則アクセスできません。
以下のようなエラーログが出力されます。

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.String(data: data, encoding: .shiftJIS)

独自のXMLパーサーを実装する必要がある。

APIKitのRequestプロトコルはdataParserというプロパティを持っています。
(APIKitのDocumentにはたぶん記載がなく、コードを見て知りました。)
明示的に指定しない場合はデフォルトでJSONのパースを実行するようになっています。
(パースに失敗するとRequestクラスのresponseメソッドは実行されません。)

デフォルト実装のままxmlを取得するとパースに失敗し以下のようなエラーとなります。

responseError(Error Domain=NSCocoaErrorDomain Code=3840 "JSON text did not start with array or object and option to allow fragments not set." UserInfo={NSDebugDescription=JSON text

XMLを取得する場合はdataParserプロパティにDataParserプロトコルに準拠した独自のXMLParserを代入する必要があります。

var dataParser: DataParser = MyXMLParser()

また、今回はSwiftyXMLParserというライブラリを使用しXMLをパースしました。

文字エンコーディングに気をつけよう

取得したData型をString型に直す時は文字エンコーディングの形式を指定する必要があります。

String(data: data, encoding: .shiftJIS)

指定するエンコーディングはresponseメソッドの引数のurlResponseに格納されており、 print(urlResponse.textEncodingName)のようにして確認しました。

【iOS】【Swift】太り過ぎなUIViewController.viewDidLoadをスリムにして可読性を爆上げする

前書き

本命/義理チョコを今か今かと待ちわびています、ナガクラ(@nagakuta)です!一ヶ月ぶりの投稿になります!
今回は上級者が開発時に使うといわれる、ちょこっと役立つTipsを紹介します!(バレンタインだけに)

TL;DR

viewDidLoadをスリムにするには

  1. XIBでViewを実装する場合はdidSetを使う
  2. コードでViewを実装する場合はClosureを使う

太り過ぎなviewDidLoad

皆さんは、Viewを実装する際にどの方法でUIパーツの設定を行いますか?(´ε`;)ウーン…

自分は、今回の方法を知るまでは以下の方法を採っていました。

  • XIBでCustom Viewのレイアウトを作成する場合
    • Interface Builderにて設定 or viewDidLoad内でコードによる設定
  • コードでCustom Viewを実装する場合
    • loadViewでCustom Viewを実装、viewDidLoadで細かい設定を実装

しかし、上記の方法では、UIパーツの数が多ければ多いほどviewDidLoadが肥大化してしまい、最悪の場合は何十行もあるviewDidLoadになってしまうこともありましたorz

final class CustomViewController: UIViewController {

    @IBOutlet private weak var firstLabel: UILabel!
    @IBOutlet private weak var secondLabel: UILabel!
    @IBOutlet private weak var thirdLabel: UILabel!
    @IBOutlet private weak var fourthLabel: UILabel!
    @IBOutlet private weak var firstButton: UIButton!
    @IBOutlet private weak var secondButton: UIButton!
    @IBOutlet private weak var thirdButton: UIButton!

    // MARK: - Life Cycle Methods

    override func viewDidLoad() {
        super.viewDidLoad()

        // FirstLabelの設定
        self.firstLabel.text = /* 省略 */
        self.firstLabel.font = /* 省略 */
        self.firstLabel.textColor = /* 省略 */
        self.firstLabel.textAlignment = /* 省略 */
        self.firstLabel.lineBreakMode = /* 省略 */

        // SecondLabelの設定
        self.secondLabel.text = /* 省略 */
        self.secondLabel.font = /* 省略 */
        self.secondLabel.textColor = /* 省略 */
        self.secondLabel.textAlignment = /* 省略 */
        self.secondLabel.lineBreakMode = /* 省略 */

        (中略)

        // FirstButtonの設定
        self.firstButton.setTitle("ボタン1", for: .normal)
        self.firstButton.setTitleColor(UIColor.red, for: .normal)
        self.firstButton.tintColor = /* 省略 */

        // SecondButtonの設定
        self.secondButton.setTitle("ボタン2", for: .normal)
        self.secondButton.setTitleColor(UIColor.blue, for: .normal)
        self.secondButton.tintColor = /* 省略 */

        (省略)
    }

}

(可読性が低い…低すぎるぞ…!😨😨😨)

それでは可読性が低いので、各UIパーツごとに設定用のメソッド(e.g. setupLabel)を実装し、それをviewDidLoadで呼び出す、という方法で回避を試みましたが…

final class CustomViewController: UIViewController {

    (省略)

    // MARK: - Life Cycle Methods

    override func viewDidLoad() {
        super.viewDidLoad()

        self.setupFirstLabel()
        self.setupSecondLabel()

        (中略)

        self.setupFirstButton()
        self.setupSecondButton()

        (省略)
    }

}

// MARK: - Private Methods
extension CustomViewController {

    /// FirstLabelの設定
    private func setupFirstLabel() {
        self.firstLabel.text = /* 省略 */
        self.firstLabel.font = /* 省略 */
        self.firstLabel.textColor = /* 省略 */
        self.firstLabel.textAlignment = /* 省略 */
        self.firstLabel.lineBreakMode = /* 省略 */
    }

    /// SecondLabelの設定
    private func setupSecondLabel() {
        self.secondLabel.text = /* 省略 */
        self.secondLabel.font = /* 省略 */
        self.secondLabel.textColor = /* 省略 */
        self.secondLabel.textAlignment = /* 省略 */
        self.secondLabel.lineBreakMode = /* 省略 */
    }

    (中略)

    /// FirstButtonの設定
    private func setupFirstButton() {
        self.firstButton.setTitle("ボタン1", for: .normal)
        self.firstButton.setTitleColor(UIColor.red, for: .normal)
        self.firstButton.tintColor = /* 省略 */
    }

    /// SecondButtonの設定
    private func setupSecondButton() {
        self.secondButton.setTitle("ボタン2", for: .normal)
        self.secondButton.setTitleColor(UIColor.blue, for: .normal)
        self.secondButton.tintColor = /* 省略 */
    }

    (省略)

}

(何だこれは…やたらメソッドが増えるじゃないか…😥😥😥)

結局のところ、この方法もベストとは言えない方法だと思っていました。

viewDidLoadをシェイプアップする冴えた方法

なんとかviewDidLoadを膨らませず、かつ設定用メソッドの実装以外の方法でUIパーツの設定を行えないか…
その方法を探るべく、インターネットを彷徨っていたところ、一件のページを見つけました。

thatthinginswift.com

その記事中には、

  • viewDidLoadでUIパーツの設定やプロパティの初期化をするのは古臭いし、いい方法じゃないよね
  • Swiftでは、didSetClosureでプロパティの初期化をする方法があるって知ってた?
  • じゃあ、これらを使ってUIパーツの設定を行えばいいんじゃないかな

といったことが書かれていました。

「…え、Closureでプロパティの初期化ができるの!?」と驚いた私がApple公式のSwiftガイドを見ると、たしかにClosureでのプロパティ初期化の方法が載っているではありませんか。

developer.apple.com

なるほど、確かにこの方法をUIパーツに適用すれば、わざわざviewDidLoadでゴリゴリと設定を実装しなくても良くなりそうです。

実際にやってみた

XIBでCustom Viewのレイアウトを実装している場合

XIBでCustom Viewのレイアウトを実装する場合、IBOutletでコードと結びつけてから設定を行います。

その場合はClosureによる初期化ができないので、didSetを利用して設定します。

final class CustomViewController: UIViewController {

    @IBOutlet private weak var firstLabel: UILabel! {
        didSet {
            self.firstLabel.text = /* 省略 */
            self.firstLabel.font = /* 省略 */
            self.firstLabel.textColor = /* 省略 */
            self.firstLabel.textAlignment = /* 省略 */
            self.firstLabel.lineBreakMode = /* 省略 */
        }
    }

    @IBOutlet private weak var secondLabel: UILabel! {
        didSet {
            self.secondLabel.text = /* 省略 */
            self.secondLabel.font = /* 省略 */
            self.secondLabel.textColor = /* 省略 */
            self.secondLabel.textAlignment = /* 省略 */
            self.secondLabel.lineBreakMode = /* 省略 */
        }
    }

    @IBOutlet private weak var thirdLabel: UILabel! {
        didSet {
            (省略)
        }
    }

    @IBOutlet private weak var fourthLabel: UILabel! {
        didSet {
            (省略)
        }
    }

    @IBOutlet private weak var firstButton: UIButton! {
        didSet {
            self.firstButton.setTitle("ボタン1", for: .normal)
            self.firstButton.setTitleColor(UIColor.red, for: .normal)
            self.firstButton.tintColor = /* 省略 */
        }
    }

    @IBOutlet private weak var secondButton: UIButton! {
        didSet {
            self.secondButton.setTitle("ボタン2", for: .normal)
            self.secondButton.setTitleColor(UIColor.blue, for: .normal)
            self.secondButton.tintColor = /* 省略 */
        }
    }

    @IBOutlet private weak var thirdButton: UIButton! {
        didSet {
            (省略)
        }
    }

    // MARK: - Life Cycle Methods

    override func viewDidLoad() {
        super.viewDidLoad()

        (ここで何もしなくていい😄)
    }

}

おお…!これならviewDidLoadが初期化で膨れ上がることもないし、設定用のメソッドを書かなくてもいいし、どのUIパーツにどの設定がされているかが一目瞭然ですね😊😊😊

ちなみに、このdidSetが呼ばれるタイミングはviewDidLoadの前になります。IBOutletプロパティに値がセットされるタイミング(loadViewと同じタイミング…?勉強不足ですみません🙇)で各々のUIパーツの設定がなされます。

コードでCustom Viewを実装する場合

そして、Custom Viewをコードで実装する場合はClosureで初期化を行います。

final class CustomViewController: UIViewController {

    private let firstLabel: UILabel! = {
        let label: UILabel = UILabel()
        label.text = /* 省略 */
        label.font = /* 省略 */
        label.textColor = /* 省略 */
        label.textAlignment = /* 省略 */
        label.lineBreakMode = /* 省略 */
        return label
    }()

    private let secondLabel: UILabel! = {
        let label: UILabel = UILabel()
        label.text = /* 省略 */
        label.font = /* 省略 */
        label.textColor = /* 省略 */
        label.textAlignment = /* 省略 */
        label.lineBreakMode = /* 省略 */
        return label
    }()

    private let thirdLabel: UILabel! = {
        let label: UILabel = UILabel()
        (中略)
        return label
    }()

    private let fourthLabel: UILabel! = {
        let label: UILabel = UILabel()
        (中略)
        return label
    }()

    private let firstButton: UIButton! = {
        let button: UIButton = UIButton()
        button.setTitle("ボタン1", for: .normal)
        button.setTitleColor(UIColor.red, for: .normal)
        button.tintColor = /* 省略 */
        return button
    }()

    private let secondButton: UIButton! = {
        let button: UIButton = UIButton()
        button.setTitle("ボタン2", for: .normal)
        button.setTitleColor(UIColor.blue, for: .normal)
        button.tintColor = /* 省略 */
        return button
    }()

    private let thirdButton: UIButton! = {
        let button: UIButton = UIButton()
        (中略)
        return button
    }()

    private lazy var customView: UIView = { [weak self] in
        let view: UIView = UIView(frame: UIScreen.main.bounds)

        // ここでViewを追加する
        view.addSubview(self?.firstLabel)
        view.addSubview(self?.secondLabel)

        (中略)

        view.addSubview(self?.firstButton)
        view.addSubview(self?.secondButton)

        (中略)

        return view
    }()

    // MARK: - Life Cycle Methods

    override func loadView() {
        self.view = self.customView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        (ここで何もしなくていい😄)
    }

}

こちらもdidSetの場合と同じく、どのUIパーツでどの設定がされているかが分かりやすいと思います👍

letlazy varで宣言しているプロパティがありますが、その違いは「初期化時にプロパティの値を利用したいか」です。

letの場合はプロパティを利用しなくてもいい場合です。lazy varの場合は、weak selfでプロパティを弱参照にて利用することができます👍

あとがき

いかがでしょうか。はじめに載せたソースと最後に載せたソース、どちらが可読性の高いものだったでしょうか。

自分は、この方法をデファクトスタンダードにしたいと思っています(๑•̀ㅂ•́)و✧
なので、この記事を見て「良さそう!」と思った方、今すぐリファクタリングだ!!

追記

Qiitaのこちらの記事でも今回の記事と同様の内容が紹介されていました!こちらもぜひ参考にしてみてください!

qiita.com