20191130のiOSに関する記事は7件です。

【2019年版】使ってみてよかったiOSデバッグツール

はじめに

  • アプリ開発の中でデバッグは切っても切り離せない作業です。デバッグ作業の効率を高めるためのツールはたくさんあります。 たくさんのツールの中で使ってみてよかったものについてまとめてみました。 *あくまで個人的な意見です

第5位 SwiftyBeaver

SwiftyBeaver/SwiftyBeaver⭐️4.5k

  • 通信周りなどのログを表示できるツール。browserアプリを使うと便利です

第4位 KZFileWatchers

krzysztofzablocki/KZFileWatchers⭐️954

  • localにあるファイルを参照できるデバッグツールです。mockデータで実際のアプリで取得可能になる

?第3位 LifetimeTracker

krzysztofzablocki/LifetimeTracker⭐️2.1k

  • メモリーリークしているviewController,viewを検知できるツール
  • リークしている事に気が付きやすくなるツールです

?第2位 FLEX

Flipboard/FLEX⭐️10.9k

  • 便利機能が豊富に搭載されているユーティリティツール
  • 特にレイアウトチェックや表示しているクラスの検索に便利

?第1位 iCimulator

YuigaWada/iCimulator⭐️80

  • シミュレータでカメラ機能が使えるツール
  • 是非とも標準機能で対応して欲しいです

まとめ

  • アプリ開発の中でデバッグの効率を上げるツールはたくさんある
  • 様々なツールを見ながら、適切なものを選ぶことが重要である
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

詳解AdMob

広告に関する実装をしてきて、やっと納得のいく実装ができた気がしたので備忘録的にまとめようと思います。
今後モバイルアプリに広告の導入を検討されている方の手助けになれば幸いです。
ユーザー影響大きいのでご利用は計画的に...

下記のような実装は経験できたので関連でもし質問などあれば回答できることもあるかもしれません。

  • バナー広告
    • メディエーションする
    • ヘッダービディングをラップする
    • RecyclerViewの中で使う
    • RemoteConfigと連携する
  • ネイティブ広告
    • メディエーションする
    • RecyclerViewの中で使う
    • Firebase A/B Testingと連携する
  • インタースティシャル広告
    • メディエーションする
    • RemoteConfigと連携する

AdMobについて(3行)

  • Firebase機能群の1つで、アプリ内に広告を表示することができる機能
  • 複数の広告フォーマットに対応しており、他社の広告をAdMobの中で表示するといったことも可能
  • Firebase(Google開発)提供なのでAndroidアプリではAdMobをまずは検討する(はず)

この記事で触れないもの

  • 公式ドキュメントを見てもらえば簡単にわかってしまうこと
  • AdMob以外の広告SDK仕様
  • アドテク用語
    • 全部知っておかないと開発できないわけではないと思います。
    • 本記事の用語集としてリンク先に一読はおすすめします
  • 収益に関わるパフォーマンスチューニングのTips
    • 業種やアプリの規模、採用しているアドネットワークによって全く異なるので言及しません。
  • AdMob管理画面の使い方
    • ちゃんと触ってません!
  • マルチモジュールを使った広告管理
  • CoroutineFlowを使った広告管理
    • こういうのやってみたい....
  • 実装時の開発以外でのつらみ
  • 広告SDKの内部実装について
    • 広告のSDKは大半が実装部分は難読化されていて読み解くことが難しいです。なので以下でつらつら書いていることも動作確認した時の動きだったりおそらくこういうことだろう、という考察で説明しているところも多いので実際は間違っているということもあるかもしれませんのでご理解ください。

基本編

おすすめの実装、ハマりそうなポイントなど

広告の表示

AdMobは機能自体はそこまで多くないので公式ドキュメントを一周してもらえればある程度の使い方を理解するのは簡単だと思います。テスト用の枠IDは公式で用意されているのでアプリを識別するためのAppIDが発行できればすぐ表示まで確認できると思います。

公式ドキュメント
https://developers.google.com/admob?hl=ja

実装メモ
  • AdView に使う Context は必ず Activity である必要があります
  • TestMode(広告を読み込んでも収益が発生しない状態)にするには端末ごとに異なるキーを AdRequest.Builder.addTestDevice() に設定して AdView をロードする必要があります
  • AppIDはAndroidManifest.xmlに参照がないとアプリがクラッシュします:construction_worker_tone4:
  • バナー広告のサイズ一覧(https://developers.google.com/admob/android/banner?hl=ja#banner_sizes)
Size in dp (WxH) Description Availability AdSize constant
320x50 Banner Phones and Tablets BANNER
320x100 Large Banner Phones and Tablets LARGE_BANNER
300x250 IAB Medium Rectangle Phones and Tablets MEDIUM_RECTANGLE
468x60 IAB Full-Size Banner Tablets FULL_BANNER
728x90 IAB Leaderboard Tablets LEADERBOARD
Provided width x Adaptive height Adaptive banner Phones and Tablets N/A
Screen width x 32 50 90 Smart banner
  • テスト用の枠IDで広告を読み込んで以下のように表示されたら成功です!
    • Bannerの場合

Screenshot_20191128-204011のコピー.png

  • Rectangleの場合

Screenshot_20191128-204011.png

広告はAACのViewModelで管理しない方が良い?

上でも書いたのですが、広告をインスタンス化する時はActivityContextが必要になります。
AndroidViewModelから取れるApplicationクラスのContextを使うことはできません。
また、外からActivityContextを入れてもViewModelはActivity/Fragmentよりも長生きするのでメモリーリークの原因になり得ます。

ViewModelLifecycleObserver を付けてON_DESTROYのイベントを受け取った時に広告インスタンスを破棄したりすればまあいけるのかもしれないですね。
こういう時ってActivityのインスタンスをDIでViewModelにInjectして使っても問題にならないのでしょうか?(詳しい方教えてください:joy:)

ただ、バナー広告は基本的にキャッシュされることを想定されていないので、Fragmentをまたいで広告インスタンスを使い回すのはあまりオススメしません(広告SDKが計測しているimpの定義ともずれてしまいがち)。なのでViewModelを使うメリットがあまり活かせないのかなと思っています。

広告のライフサイクルを管理する

AdViewクラスが継承しているBaseAdViewクラスにはresume() pause() destroy()が用意されていて、広告を表示しているUIのライフサイクルに応じてそれぞれのメソッドを叩く必要があります。
この3つのメソッドはAdMobに関わらず他社の大半の広告SDKでも同じように設計されている印象です。
各メソッドの役割は例えば以下のようなことがあると言えます(これらは各社SDKの仕様に左右されます。ただ、叩いておいた方がいいことには変わりないという認識は持っておいた方が良いでしょう)。

  • resume()
    • 広告のオートリフレッシュ間隔の計測を再開する
    • Impression(収益に関わる)の計測を再開する
    • メディエーション実装クラスにライフサイクルの変更イベントを通知する
  • pause()
    • 広告のオートリフレッシュ間隔の計測を中断する
    • Impression(収益に関わる)の計測を中断する
    • メディエーション実装クラスにライフサイクルの変更イベントを通知する
  • destroy()
    • 広告のインスタンスを破棄する
    • メディエーション実装クラスにライフサイクルの変更イベントを通知する

これらは記述漏れが発生しやすいので注意が必要です。
Androidでは広告の管理クラスにLifecycleObserverをセットしてライフサイクルの処理も管理クラス内部で処理しておくとイマドキな気がします。

実装メモ
  • 広告で収益を発生させるにはざっくりクリックされる多く見られるの2種類がある
    • クリックされたこと画面に見えているというのは広告のView内で計測しているので外からはいじれない
    • AdMobでは広告が見えていない時にオートリフレッシュのタイミングを迎えるとリフレッシュはスキップされるような動きをする
    • destroy()を呼んだ広告インスタンスはリロードできなくなるので作り直すしかありません

メディエーションはどう動くか

メディエーションというのは、AdMob以外の広告SDKから表示される広告コンテンツをAdMobのAdViewの中で表示できる機能です。例えば、TwitterFacebookAmazonなど広告SDKを提供している企業が抱える広告コンテンツもAdMobの中で表示できるようになります。
もちろんAdMob自身が持っている広告コンテンツも様々な広告配信ネットワークから価値があるものが優先して表示されるのですが、ただこれだとAdMobの配信システムに大きく依存しているので、アドネットワーク内に競合を増やして収益獲得の効率化を目指そうよというものです(ヘッダービディングも思想は同じようなものだと認識してますが違うのかな?)。

具体的なやり方を説明する前に、なぜこういうことができるのかというと、簡単に言ってしまえば、

バナー広告はそれぞれViewGroupを継承しているのでaddView(view: View)みたいなことをすれば中身を上書きできるよ

というだけです。
(ちなみにAdMobのネイティブ広告はViewGroupではなくて、UnifiedNativeAdというデータクラスを使っていて、パーツごとに情報をセットするコンバート処理を挟むことでメディエーションを実現していると言えます)

それでは実装を解説していきましょう。

1. メディエーション実装クラスを作る

CustomEventBannerを継承していることで他社の広告SDKのメディエーションをしたいんだなとAdMob側に認識してもらえる準備になります。

package sample.android.ad

class SampleBannerAdCustomEvent : CustomEventBanner {

    /**
     * @param customEventBannerListener 各社SDKのAdViewを読み込んで必要なタイミングで対象のイベントをコールする
     * @param serverParameter AdMob管理画面で設定できる文字列が返る(枠IDを入れておくことが多いです)
     * @param adSize AdMob側で設定した広告のサイズを受け取り、メディエーション時の読み込む広告のサイズと合わせるために使う
     * @param mediationAdRequest AdRequest.Builder()内で設定したKeywordやBirthday情報を取れる(使ったことないです)
     * @param customEventExtras AdRequest.Builder()内で設定したBundleの値を渡せる(枠IDを入れておくことが多いです)
     */
    override fun requestBannerAd(context: Context, customEventBannerListener: CustomEventBannerListener,
                                 serverParameter: String, adSize: AdSize, mediationAdRequest: MediationAdRequest,
                                 customEventExtras: Bundle?) {
        // 取り扱いたい広告SDKのAdViewを読み込む
    }

    override fun onResume() {
        // 各社SDKの`Resume`時の処理を書く
    }

    override fun onPause() {
        // 各社SDKの`Pause`時の処理を書く
    }

    override fun onDestroy() {
        // 各社SDKの`Destroy`時の処理を書く
    }
}

1つ注意なのですが、Kotlinで実装する場合、customEventExtrasはリクエスト時に設定されていない場合はnullで返ってくるのでNullableで定義しておきましょう:raising_hand_tone1:

ちなみにCustomEventBannerListener は以下のようなメソッドを持っています。

public interface CustomEventBannerListener extends CustomEventListener {
    void onAdLoaded(View adView);
}

public interface CustomEventListener {
    void onAdFailedToLoad(int errorCode);

    void onAdOpened();

    void onAdClicked();

    void onAdClosed();

    void onAdLeftApplication();
}

requestBannerAd内で行なった他社SDKの読み込み処理やイベントに応じていずれかのメソッドを叩く必要があります。
例えば、広告を読み込んで成功した時に広告のインスタンスをonAdLoaded(View adView)に突っ込んであげるとAdMobのAdViewaddView(view: View)されるようなイメージです。
逆に読み込み失敗時にonAdFailedToLoad(int errorCode)が呼ばれるとAdMobが違うメディエーション先へ広告を取りに行くというわけです。

実装メモ
  • Logcatを見る感じ、広告の読み込み時に枠IDごとに設定されている管理画面上のClassNameの文字列とメディエーション実装クラスの絶対パスが一致しているものがあるかをチェックしている挙動も見て取れました。
    • Adsでフィルターすると良い感じです

2. AdMob管理画面でClassNameとParameterを設定する

1で作ったメディエーション実装クラスのrequestBannerAdが呼ばれるためには、枠IDごとにどんなメディエーションが設定されているかを管理画面上で定義しておく必要があります。それを紐付けるのがClassNameParameterという概念です。例えば1のようなクラスを作った場合は対象の枠IDに対して以下のように設定することになります。

設定項目 ClassName Parameter(オプショナル)
Android sample.android.ad.SampleBannerAdCustomEvent 文字列を1つだけ設定可能
iOS SampleBannerAdCustomEvent 文字列を1つだけ設定可能

iOSは実装クラスのクラス名だけでいけるみたいですね:ok_woman:

AdMobの管理画面で設定してみてください!
スクリーンショット 2019-12-02 14.44.09.png

上記2点で最低限のメディエーションの実装は完了です!
あとは管理画面で複数のメディエーションに対してどういう順序でメディエーションするとより収益化が見込めるかという視点で運用しながらチューニングをしていくと良いでしょう。

ただ、最初の頃はこの実装だけでなぜ動くのか不思議なはずです。先ほど作ったSampleBannerAdCustomEventFind Usageしてもどこからも使われていません。
なんで参照されていないクラスのrequestBannerAdが呼ばれるねん!!!
と突っ込みたくなりますが、実装自体は合っています。
内部実装が読み取れないので具体的な解説は難しいのですが、リフレクションとかでうまいことやったりしてるんでしょう(適当)

補足ですが、requestBannerAdのそれぞれの引数と実装者が外から設定した場合の値の紐付き方を考察混じりになりますが書いてみます。

  • context : AdViewをイニシャライズした時に入れたContext
  • customEventBannerListener : SDKの内部実装で勝手に紐付く(これだけが謎紐付き)
  • serverParameter : AdMob管理画面で設定したParameter
  • adSize : AdViewをイニシャライズした時にセットしたAdSize
  • mediationAdRequest : AdViewのload時にセットしたAdRequestの一部のパラメーター(LocationやKeywords)
  • customEventExtras : AdViewのload時にセットしたAdRequestの中のaddCustomEventExtrasBundleで対象のクラスにセットしたBundle

実装メモ
  • カスタムイベントとメディエーションはだいたい同じ意味です
  • Parameterにはメディエーション先で使いたい何かしらの文字列を管理画面から送りたい要件があるときに使うと良いです
    • 画面ごとにAdMobの枠IDを発行して収益性を比較したいのでメディエーション先の枠IDも同様に発行して柔軟に変更させたい。その場合はParameterにそれぞれの枠IDを設定して管理画面側で動的に読み込む枠IDを変更できる
    • バナー広告とレクタングル広告で同じ広告SDKに対してメディエーションしたい。その場合は同じ実装クラスを参照しつつ、指定フォーマットは引数のadSizeから取り、Parameterに対象の枠IDをセットすることでメディエーション実装を使い回しできる
  • メディエーション実装クラスをリリース後にリファクタリングの途中でパッケージやクラス名を変えてしまうと動かなくなる可能性があるので注意
    • JavaからKotlinへのコンバートは問題ないです

3. メディエーション実装クラスに外から値を受け渡ししたい

実際に運用していて困るのが、リリース後に管理画面の設定やメディエーション内の挙動を変えたい場合です。
枠IDやメディエーションは一度リリースしてしまうと古いバージョンのアプリの動きを担保したまま運用していくのが難しくなります。これはアプリのソースコード上でも管理画面上でも同様です。
どれだけ意識して開発していても広告SDK側の破壊的なアップデートや先方との契約の都合など変更せざるを得ないケースはいくつか挙げられるでしょう。
ではメディエーションの実装に関してはどのような柔軟性を持たせておけばいいのか。方法が2つあります。

  • 管理画面で設定したParameterを受け取れるserverParameterを使うようにする
  • RemoteConfigを使って必要なパラメーターをアプリ側でcustomEventExtrasに詰めて渡す

customEventExtrasを使った値の受け渡しは以下のように実装します。

val adRequest = AdRequest.Builder()
            .addCustomEventExtrasBundle(SampleBannerAdCustomEvent::class.java, Bundle().apply {
                // Bundle型にできればなんでも可
                putString("key", value)
            }).build()

adView.loadAd(adRequest)

絶対にやらないほうがいいのは決め打ちで固定の枠IDや特定の文字列をメディエーション実装クラス内で使ってしまうということで、上の2つのやり方に関しては実装しやすい方を選択すると良いでしょう。

ただ、個人的には、serverParameterを使う方法は初めて実装を見た人がどうやって動いているのか理解しづらいと思うので、RemoteConfigを使った処理に寄せてしまった方が良いと考えています。
customEventExtrasに詰める箇所はJavaもしくはKotlinで書くことになるので実装が追いやすいし、SampleBannerAdCustomEventのような実行クラスへの参照を作ることができます。

実装メモ
  • 管理画面もしくはRemoteConfigから渡す文字列はJson形式にしておくと何かと安心です
    • 例えば、古いバージョンに影響を与えずに、新しいバージョンで管理画面もしくはRemoteConfigの値を参照したいような要件に対応できます(特に管理画面では1つしか文字列を設定できないので)
  • 一部のメディエーション設定を消したい場合、古いアプリにメディエーション実装クラスが残っていても、ためらわず消してもらっても大丈夫です
    • メディエーション時に内部でエラーが起きた時の動きと同様に他のメディエーション先にリクエストが優先されます
    • 実装クラスのパスと管理画面のクラスネームが間違っていても同じ動きになります
  • 逆にメディエーション設定を増やしたい場合も新しいバージョンでしか認識されないので古いバージョンを使っているユーザーでクラッシュや意図しない挙動が起きるケースは少ないはずです
  • メディエーションしようとしたのにメディエーション先でエラーとなり、他に表示できる広告がない場合は白板になるのを防ぐためデフォルト設定ではAdMobの広告が表示されるようになります(フィラーの設定で変更可能)
  • メディエーション先の広告SDKのオートリフレッシュ機能はOFFにしておいてAdMobのオートリフレッシュに委ねておくのがいいです
    • ちらつきの原因になり得るのと、無駄なimpが意図せず増えてしまう要因になり得ます

応用編

実務でつまづいたこと、運用してみて良かったこと、分かったことなど

メディエーション実装には大きく2種類あった

どういうことかというと、メディエーションするにあたり必要な実装は1つしかないのですが、自前で実装する場合すでに実装されたjar/aarファイルやGradleで取り込めるライブラリが提供されている場合があります。比較してみると以下のような違いがあります。

実装方式 自前実装 提供品利用
使い方 CustomEventBannerを継承したクラスを作る Gradleで参照する
クラスネーム設定 変動(作成クラスの絶対パス) 常に固定
手軽さ ?‍♂️ ?‍♂️
拡張性 ?‍♂️ ?‍♂️
バグの原因の発見容易性 ?‍♂️ ?‍♂️

提供品は圧倒的に手軽で、ライブラリを入れるだけでアプリ側の作業が完了したりするのですが、内部実装をいじれないので要件によってはうまく適合しないケースもあります。
ですので、すでに提供品がある広告SDKであれば一旦そちらを使ってみて、うまくいかなかった時に自前で実装するというアプローチが良いように思います(最近はもうメディエーションを自前実装する機会は多くないかなと思います)。
最近はGoogleがAdMobに組み込むための各社SDK用のアダプターを公開してくれているので実装がとても楽になってきていると言えます。

AdMobのメディエーションアダプター一覧
https://developers.google.com/admob/android/mediate?hl=ja

これまでに自前実装をしたケース
  • メディエーションしたい広告SDKがアダプターを提供していなかった
  • メディエーション先で特定のキーワードを設定して広告をロードしたかったが、提供されているアダプターが対応していなかった
  • ファーストリクエストの広告がNO_AD(在庫がない)を返してきたらフォーマットの異なる広告を読み込みたい要件があった
    • 広告フォーマットが違うのでメディエーションで対応できないケース

実装メモ
  • アダプターを利用する場合は管理画面に設定するClassNameが他社のパッケージのパスになり違和感がありますが間違ってはないのでご安心ください

広告テストがしづらいので枠IDの管理を考える

これは運用してみると出てくる問題の一部で、例えば以下のような問題がありました。対策と合わせて書いてみます。

1. TestModeを使用せずにDev環境やQAで広告をテストしすぎると垢BAN食らって2週間ほどアカウント停止されがち問題

AdMobでTestModeを使うには、一度ビルドしてログから一意なKeyを取得してきて都度設定する必要があります。
QAのために社内全ての端末のIDを管理するのは大規模なプロジェクトほど骨が折れます。
また、判定の仕組みは全く不明ですが、StoreにリリースされているApplicationIdと異なるビルドやDebugビルドからTestModeを設定せずに広告を読み込みすぎる(おそらくクリックすると更に良くないです)とAdMobのアカウントが停止されて問答無用で広告も読み込めなくなるし管理画面もいじれなくなります。
本番にリリースされている状態で垢BANされると相当クリティカルな問題ですので、広告の挙動テストではせめてReleaseビルドを使う不正に収益を得ることがまずいのでクリックするテストは避けるは心がけておいたほうが良いでしょう。Debugビルドでは広告を表示しない仕組みがあると安心ですね。

実装メモ
  • 他社のSDKではTestModeはTrue/Falseだけで切り替えられるSDKも多い
  • AdMobに理由を添えて問い合わせしても例外なく2週間程度停止されるようです
  • 今はもう起きてないかもですが、TestModeをONにしているとメディエーションのテストはできなかったです

2. 広告機能のリリース後に広告に関する機能追加をしようとした時、本番の枠IDの設定をテストのためにいじりづらい問題

これはもう解決策は明確で、枠IDにもProduction、Staging、Developの概念を取り入れると良いです。
例えば自社では下記のような思想で運用していました。

  • Production枠ID : 基本ProductionReleaseのようなVariantでしか参照しないようにする
  • Staging枠ID : ここで機能開発を行う。eCPMも自由に調整してメディエーション配信の優先順位も操作できるようにする
  • Development枠ID : 公式のテスト用IDを使うか、抽象化した広告インターフェースにテスト用のダミーViewを実装したようなクラスを差し替えれるようにしてAdMobを実行クラスで使わないようにする(AdMobのAdViewの代わりにImageViewを表示しておく的な)

3. メディエーション先の広告が表示されているのかわかりづらい問題

2の案でStaging枠でメディエーション設定は自由に弄れるようになるのですが、実際にメディエーションが成功しているかを目視で判断するのは難しいケースもあります。広告出稿側がAdMob以外にも同じ広告を提供しているケースがあるためです。そんな時は以下のような判断方法を検討してみてください。

  • AdView.getMediationAdapterClassName()を使う
    • メディエーション先にリクエストが渡る時はLogcatでも確認はできます
  • クリックした時にブラウザが開く時のURLをチェックする
    • 広告のスクショと一緒に広告担当者に確認すれば出稿している広告かどうか判断してもらえるケースが多いです
    • Production以外は垢BANにドキドキしながらクリックすることになりますが、、、

実装メモ
  • TwitterやAmazonは見た目に特徴があるので判断しやすい
  • TwitterやAmazonなどは公式アプリが入っていないとメディエーションが反応しないケースもあります

4. 意図したメディエーションを試すのに手間がかかる問題

2で解説したStaging枠を更に拡張させるやり方の提案です。
管理画面で設定を都度変更するのは反映ラグ(1~2時間程度)もあり、複数人からの一斉の要望に答えづらい問題があります。
QAを置いている企業だと連携コストがかかってしまうので、弊社ではStaging枠IDをメディエーション分だけ複数発行するというやり方を取っています。
1つの枠IDに対して例えば3つのメディエーションが紐づいていると目視での確認がしんどくなってくるので、
1つの枠IDに対して1つのメディエーションというセットを複数作って参照する枠IDに応じて表示される広告が切り替わるような仕組みにしています。
アプリのデバッグ設定などで動的に枠IDを切り替えられるようにしておくと即反映できるし便利ですね!

実装メモ
  • 設定してすぐの時はある程度広告リクエストを送らないとメディエーションが反応しないので辛抱強くリクエスト送ってみましょう

白板を防ぐ

広告コンテンツが読み込めない、在庫がない、広告の読み込み中などの状態でユーザーに広告のViewが見えてしまうと真っ白に見えるようになっています。これは白板(しろいた/はくばん)と呼ばれていて、これが見えている状態というのは、ユーザー側にも収益的に見てもベネフィットが限りなく小さいです。
SDK側の仕様に大きく左右されるということもあり、この白板をゼロにすることは難しいので、なるだけ起きない方法を考えていきたいと思います。

1. 広告の読み込み中もしくはエラーになった場合は広告表示領域を隠す

読み込み中かどうかはAdMobでは、AdView.isLoadingというメソッドがあり、状態を取得することが可能です。またエラー状態もAdListenerのonAdFailedToLoadで受け取れるので状態管理の仕組みを作れば簡単に判定できます。広告表示領域はデフォルトGoneにしておいて、読み込みに成功したらVisibleに切り替えるのが良さそうです。

private fun loadAd() {
    binding.adContainer.isGone = true
    ad?.also {
        it.setCallback(object : Ad.Callback {
            override fun onSuccess(view: View) {
                bindAdView(binding.adContainer, view)
                binding.adContainer.isVisible = true
            }

            override fun onFailure(errorCode: Int) {
                binding.adContainer.isGone = true
            }
        })
        if (it.isLoading()) {
            return
        }
        it.load()
    }
}

ちなみにこの辺の実装は各広告SDKの仕様によって書き方が異なるので、状態管理を内部で行うことも含めて下記のようなInterfaceを切っておくと便利です。

interface Ad {

    fun isLoading(): Boolean

    fun isOnError(): Boolean

    fun getView(): LiveData<View>?

    // 必要に応じて
    fun getNativeAd(): LiveData<UnifiedNativeAd>?

    fun resume()

    fun pause()

    fun destroy()

    fun setCallback(callback: Callback?)

    fun load()

    interface Callback {

        fun onSuccess(view: View?)

        fun onFailure(errorCode: Int)
    }
}

実装メモ
  • オートリフレッシュが有効な場合、AdListenerをセットしていた場合はオートリフレッシュ間隔でコールバックが返ってきます。成功->失敗のようなケースに広告が白板になることもあるので考慮できると良いです。
  • Adの実行クラスはDaggerなどでManagerクラスに注入できるようにすればテストが簡単にできそうです。

2. プリフェッチしておく

バナー広告でキャッシュがNGと書いてあるものの、見えることがほぼ確実な広告を先に読み込んでおいてもバチは当たらないと思います。プリフェッチを実装した公式サンプルもあります。
ただこのサンプルおそらく問題があるケースがあって、オートリフレッシュが有効な広告の場合だとプリフェッチがループしてしまいます。

private fun loadAdForInitialize(adLoadCount: Int) {
    if (adLoadCount >= adList.size) {
        return
    }
    adList?.also {
        if (it.isEmpty() || !isInitialized) {
            // 読み込み中に画面が破棄されていたら止めたい
            return
        }
        val ad = it[adLoadCount]
        val callback = object : Ad.Callback {
            override fun onSuccess(view: View) {
                ad.setCallback(null)
                loadAdForInitialize(adLoadCount + 1)
            }

            override fun onFailure(errorCode: Int) {
                ad.setCallback(null)
                loadAdForInitialize(adLoadCount + 1)
            }
        }
        ad.setCallback(callback)
        ad.load()
    }
}

サンプルコードのように広告実装を抽象化している場合はCallbackを、AdMobを使っている場合はAdListenerをnullにしてあげましょう。また、非同期処理がループしているので画面のライフサイクルに応じて処理を止めてあげる仕組みがあるとさらに良いかなと思います。

3. (任意)リスト内で広告を取り扱う場合の白板防止策

リスト内で広告を扱う実装方法はまだ確立したベストプラクティスがないので実装方法によりますが、プリフェッチを採用していて、かつList型で広告を管理している場合、以下のようなUtilメソッドを作っておくと便利です。

fun getIncrementedIndex(targetIndex: Int, adListSize: Int): Int {
    var nextIndex = targetIndex + 1
    if (nextIndex >= adListSize) {
        // 配列のサイズを超えたら0に戻す
        nextIndex = 0
    }
    return nextIndex
}

fun getValidAd(adList: List<Ad>, targetIndex: Int): Ad? {
    val targetAd = adList[targetIndex]
    if (!targetAd.isOnError()) {
        // 白板でなければそのまま利用する
        return targetAd
    }
    var nextAd: Ad? = null
    var incrementedIndex = getIncrementedIndex(targetIndex, adList.size)
    // adListを一周して有効な広告がないか探してくる
    while (incrementedIndex != targetIndex) {
        val incrementedAd = adList[incrementedIndex]
        val isLoadedAd = !incrementedAd.isLoading() && !incrementedAd.isOnError()
        if (isLoadedAd) {
            nextAd = incrementedAd
            break
        }
        incrementedIndex = getIncrementedIndex(incrementedIndex, adList.size)
    }
    return nextAd
}

これで白板が見えてしまう可能性は大きく下がるはずです!
これでもAdnullで返ってきてしまう場合は広告表示領域をGONEにすると良いでしょう。

開発Tips/小ネタなど

  • Firebase A/B Testingを使えば広告のパフォーマンス比較ができる
    • 例えばバナー広告とネイティブ広告
    • AdMobを使っていると収益性やインプレッション数もFirebase上でわかるので比較しやすい
    • TargetingにPredictionsを活用できて非常にイケている
  • 広告の詳細な実装はInterfaceで隠してテスタブル設計にしておく
    • 最近のdex.fmでも話題に上がってたので少しテンション上がりました:relaxed:
  • AdViewはリロード時に再利用できるか、作り直すべきか
    • AdViewはdestroy()を呼ばない限りはリロードが可能みたいです
    • リングバッファ管理できますね!
    • 広告SDKによっては作り直さないとダメなやつもあるっぽいです
  • 広告は比較的プレミアム機能と絡むことが多いので、destroy時とcreate時の処理を外から叩けるようにしておくと良いと思います
    • たまーーーーーに行儀の悪い広告コンテンツだと、画面に見えていなくても、広告の読み込みが成功していると画面を奪ったり、動画広告であれば裏で音声を流し始めたりするものもあるので、プリフェッチの取り扱いには気をつけるのと、例えばプレミアム機能が有効なユーザーはそもそも広告を読み込まない、という設計にするべきです。

まとめ

  • 実装設計するにあたり、LifecyclesとLiveDataはAdMobと相性がいい気がした
  • ViewModelで広告インスタンスを管理するのは玄人でなければやめておく
  • RemoteConfigどんどん使っていきましょう
  • A/B TestingとPredictionsの連携がアツい
  • できるだけ公式の実装から外れないようにする
  • メディエーションは各社SDKがアダプターを提供してくれているかをまず探す
  • バナー広告はキャッシュしすぎないようにする
  • 自前で実装するメディエーション用のクラスはパッケージを安易に移動させないように気をつける
  • メディエーションをたくさん付けるよりもパフォーマンスチューニングをうまくやるほうが有益
  • RequestクラスとViewクラスをそれぞれ使いまわしてもいいかはSDKごとに確認する
  • ユーザーに悪影響がある分、良い実装をして出来る限りストレスは軽減してあげる努力をしていきましょう

最後に

長らく読んでいただきありがとうございました!
最初は取っつきづらい領域なんですが、実装方法を理解してしまえば拡張もしやすく、色々な要件にも柔軟に対応できて収益の最大化に貢献できるはずなのでネット上にもっと情報が増えてくれると良いなぁと思っています。

広告のSDKはAndroidXに対応されてなかったりJavaのままだったりSDKによって挙動が大きく違ったりと最新のトレンドと比べると遅れているなぁと感じますが、技術としてはとても面白いと思うのでどんどん盛り上がってくれるといいなぁと切に願っています!

MoPubはAndroidX対応をすでに諦めているっぽい:ghost:
https://github.com/mopub/mopub-android-sdk/issues/311

そして、リストの中で広告を表示させたい要件ってあると思うんですが、公式のBannerRecyclerViewSampleはリストのデータをObject型で管理してたりと苦しい感じだし、
ViewHolderにAdViewを設定してonBindのタイミングで読み込みを開始するだけだと読み込みのレイテンシで白板の状態が見えてしまって見栄えが良くないです。

ということで良い感じにリスト内でも動くサンプルを作ってみました。

https://github.com/FujiKinaga/AdMobSample

まだこれがベストプラクティスな実装ではないと思っているのでアドバイスなども頂けると大変嬉しく思います!
個人的にはRecyclerViewPoolをうまく扱えるともっと良い感じになるのかなと思ったりします。

Qiita楽しい:santa:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Facebook、Messangerで端末に保存されている画像をシェアする時に使用するattachment idの取得方法

この記事を書いたわけ

FacebookやMessangerに画像をシェアする時には基本的にはFacebook URLを使用して画像を選択します。ですが、この場合だとすでにFacebookに投稿されている画像しか選べないのです。なので端末に保存されている画像や、一般的な画像URLを指定してシェアしたい時は一度画像アップロードAPIを使用してからattachment idを取得し、その値を設定してやる必要があります。ですがこの画像アップロードAPIはSDKが用意されておらず、実装に大変苦労したのでこの記事を書いてみました。

Alamofire のインストール

今回APIの送信にはAlamofireを使います。cocoaPodsを使ってインストールしていきます。

cocoa Pods を利用して Alamofire をインストール

PodFileに以下を追加して、Alamofireをインストールしていきます。

# Alamofire
 pod 'Alamofire';

ターミナルで以下のコマンドを打って完了です。

pod install
pod update

Attachment IDの取得

ここらは実際に画像アップロードAPIを使ってattachment idを取得していきます。

Attachment upload api を使って Attachment IDを取得する

https://developers.facebook.com/docs/messenger-platform/reference/send-apiによるとFacebook URLを使用せずに画像をアップロードするためには、

curl  \
  -F 'message={"attachment":{"type":"image", "payload":{"is_reusable":true}}}' \
  -F 'filedata=@/tmp/shirt.png;type=image/png' \
  "https://graph.facebook.com/v5.0/me/message_attachments?access_token=<PAGE_ACCESS_TOKEN>"

のようにmultipart/form-data形式ででPOSTする必要があります。
そこでAlamofireを使って以下のようにAPI通信を行なっていきます。

    // multipart/form-data形式 で post request を実行する
    func postByMultipartFormData (
        requestUrl: String,
        params: [String : Any],
        image: UIImage,
        completion: @escaping (Result<DataResponse<Any>, APIManegerError>) -> ()) {

        // paramをJSONArray -> Dataに変換する
        let jsonParams: Data?
        do {
            jsonParams = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
        } catch {
            return completion(.failure(.parameterError(message: "有効なパラメータではありません")))
        }

        // imageをDataに変換する
        guard let dataImage : Data = image.jpegData(compressionQuality:1.0) else {
            return completion(.failure(.imageError(message: "有効な画像ではありません")))
        }

        // API送信
        Alamofire.upload(
            multipartFormData: { multipartFormData in
                // 送信する値の指定
                multipartFormData.append(dataImage,
                                         withName: "image",
                                         fileName: "hoge.jpeg",
                                         mimeType: "image/jpeg")
                multipartFormData.append(jsonParams!,
                                         withName: "message")
        },
            to: requestUrl, // 送信先URL
            // API送信の実行
            encodingCompletion: { encodingResult in
                switch encodingResult {
                case .success(let upload, _, _):
                    upload.responseJSON { response in
                        completion(.success(response))
                    }
                case .failure(let encodingError):
                    // 失敗
                    completion(.failure(.uploadError(error: encodingError)))
                }
        }
        )
    }

シェアする画像、リクエストURL、リクエストパラメータは以下のようにで指定します。

        // アクセストークン
        let accessToken = <自分のfacebookページのアクセストークン>
        // リクエストURL(attachment upload API)
        let requestUrl = "https://graph.facebook.com/v2.6/me/message_attachments?access_token=\(accessToken)"
        // リクエストパラメータ
        let params:[String:Any] = [
            "attachment":[
                "type":"image",
                "payload":[
                    "is_reusable": true
                ]
            ]
        ]     
        // シェアしたい画像を設定
        let image: UIImage = <投稿したい画像>

これらを使ってAttachment upload APIの実行、Attachment IDの取得をしていきます。

        // attachment upload APIのレスポンスを変換するためにCodableを定義
        struct AttachmentId: Codable {
           let attachment_id: String
        }

        // attachment upload API の実行
        postByMultipartFormData(requestUrl: requestUrl,
                                params: params,
                                image: image){ result in
                                                    switch result {

                                                    // Upload API成功時
                                                    case .success(let responce):
                                                        // attachment IDを取得する
                                                        if let data = responce.data {

                                                            // responceをAttachmentIdに変換する。
                                                            let responceDataJson: AttachmentId
                                                            do {
                                                                responceDataJson = try JSONDecoder().decode(AttachmentId.self, from: data)
                                                            } catch {
                                                                print("Json Decode error")
                                                                return
                                                            }

                                                            // attachmentIdを取得する
                                                            let attachmentID: String = responceDataJson.attachment_id
                                                            print(attachmentID) // 取得したattachment IDを表示してみる
        }
                                                    // Upload API失敗時
                                                    case .failure(let error):
                                                        print(error)
                                                    }
        }

これでやっとattachment IDを取得することができました。

491466764797083

attachment idを使って画像のシェアをしてみる

実際に作ったアプリではattachment idを使用してMessengerに画像シェアするところまで作成していたのですが、Messanger Platform Changelogによると「Messanger SDK は廃止するからネイティブ機能使ってね。」とのことです。チクショー!!
いまいちattachment idの使い道はないかもしれませんが、、、もし何かで必要な方がいらっしゃいましたら参考にしてください。

ちなみにソースは以下においてます。
https://github.com/mterada1228/FacebookCommon

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftにおける構造体(struct)vsクラス(class)問題に向き合ってみた

項目 構造体 クラス
タイプ 値型 参照型
格納型プロパティ
計算型プロパティ
スタティックプロパティ
クラスプロパティ ×
メソッド
スタティックメソッド
クラスメソッド ×
継承 ×
プロトコル準拠
拡張
メモリリーク回避
スレッドセーフ

Swiftにおける構造体とクラスの性質の違い、できる・できないことをまとめるとこんな感じになると思います。これを踏まえつつ、この記事ではアップルさんの見解をもとに構造体とクラスの使い分けの方針についてつづります。(結論を先に知りたい方はここ)

構造体とクラスの選択についてのアップルさんの見解

原文はこちらのリンクにあります。
https://developer.apple.com/documentation/swift/choosing_between_structures_and_classes

原文のみorその日本語直訳とかは眠すぎる内容になると思ったので、原文と筆者による日本語要約(要約でも眠さは残る)を載せます。大筋of大筋としてはSwiftでは構造体推し!ってことを言いたいのだと思います。

Overview

Structures and classes are good choices for storing data and modeling behavior in your apps, but their similarities can make it difficult to choose one over the other.

Consider the following recommendations to help choose which option makes sense when adding a new data type to your app.

  • Use structures by default.
  • Use classes when you need Objective-C interoperability.
  • Use classes when you need to control the identity of the data you're modeling.
  • Use structures along with protocols to adopt behavior by sharing implementations.

概要

アプリのデータや処理をまとめておくのに構造体やクラスは便利ですが、両者は似ているためどちらを使えばいいのか迷います。そんな時は判断基準として以下がおすすめです。

  • まずは構造体を使ってみる
  • Objective-Cやモデル化したデータの操作が必要ならクラスを使う
  • 実装を共有したいなら構造体はプロトコルと一緒に使う

Choose Structures by Default

Use structures to represent common kinds of data. Structures in Swift include many features that are limited to classes in other languages: They can include stored properties, computed properties, and methods. Moreover, Swift structures can adopt protocols to gain behavior through default implementations. The Swift standard library and Foundation use structures for types you use frequently, such as numbers, strings, arrays, and dictionaries.

Using structures makes it easier to reason about a portion of your code without needing to consider the whole state of your app. Because structures are value types—unlike classes—local changes to a structure aren't visible to the rest of your app unless you intentionally communicate those changes as part of the flow of your app. As a result, you can look at a section of code and be more confident that changes to instances in that section will be made explicitly, rather than being made invisibly from a tangentially related function call.

まずは構造体を使ってみる

Swiftの構造体は、他の言語ではクラスにしかできないような性質(プロパティとかメソッドを持ったりなど)を持っており、プロトコルを使って既定の実装に応じた振る舞いも可能です。

また、構造体は値型なので、ローカルで変更を加えても変更した部分以外には影響しません。アプリ全体の状態を気にすることなく、一部のコードを扱うことができるわけです。

Use Classes When You Need Objective-C Interoperability

If you use an Objective-C API that needs to process your data, or you need to fit your data model into an existing class hierarchy defined in an Objective-C framework, you might need to use classes and class inheritance to model your data. For example, many Objective-C frameworks expose classes that you are expected to subclass.

Objective-Cが必要ならクラスを使う

データ処理にObjective-CのAPIが必要だったり、Objective-Cのフレームワークで書かれたクラスにモデルを当てはめたいのであれば、クラスやクラス継承を使う必要があるかもしれません。

Use Classes When You Need to Control Identity

Classes in Swift come with a built-in notion of identity because they're reference types. This means that when two different class instances have the same value for each of their stored properties, they're still considered to be different by the identity operator (===). It also means that when you share a class instance across your app, changes you make to that instance are visible to every part of your code that holds a reference to that instance. Use classes when you need your instances to have this kind of identity. Common use cases are file handles, network connections, and shared hardware intermediaries like CBCentralManager.

For example, if you have a type that represents a local database connection, the code that manages access to that database needs full control over the state of the database as viewed from your app. It's appropriate to use a class in this case, but be sure to limit which parts of your app get access to the shared database object.

Important

Treat identity with care. Sharing class instances pervasively throughout an app makes logic errors more likely. You might not anticipate the consequences of changing a heavily shared instance, so it's more work to write such code correctly.

モデル化したデータを操作したいならクラスを使う

Swiftのクラスは参照型なので、クラスインスタンスに変更を加えれば、アプリ内のそのインスタンスの参照部分すべてに影響します。このような性質を求めるならクラスを使いましょう。

ただ、クラスインスタンスを共有することで、ロジックエラーなどの予期せぬ結果を招くことがあるのでクラスは慎重に扱いましょう。

Use Structures When You Don't Control Identity

Use structures when you're modeling data that contains information about an entity with an identity that you don't control.

In an app that consults a remote database, for example, an instance's identity may be fully owned by an external entity and communicated by an identifier. If the consistency of an app's models is stored on a server, you can model records as structures with identifiers. In the example below, jsonResponse contains an encoded PenPalRecord instance from a server:

struct PenPalRecord {
    let myID: Int
    var myNickname: String
    var recommendedPenPalID: Int
}

var myRecord = try JSONDecoder().decode(PenPalRecord.self, from: jsonResponse)

Local changes to model types like PenPalRecord are useful. For example, an app might recommend multiple different penpals in response to user feedback. Because the PenPalRecord structure doesn't control the identity of the underlying database records, there's no risk that the changes made to local PenPalRecord instances accidentally change values in the database.

If another part of the app changes myNickname and submits a change request back to the server, the most recently rejected penpal recommendation won't be mistakenly picked up by the change. Because the myID property is declared as a constant, it can't change locally. As a result, requests to the database won't accidentally change the wrong record.

モデル化したデータを操作したくないなら構造体を使う

変更したくないデータをモデル化するなら構造体を使いましょう。

リモートDBと接続するアプリでは、サーバー上でモデルの一貫性を保つなら、識別情報を持たせた構造体としてレコードをモデル化することができます。

コード例にあるPenPalRecordは構造体なので、ローカルで変更を加えてもDB内のレコードを操作することがなく、不意にDB内の値が変わってしまうといったリスクがありません。

Use Structures and Protocols to Model Inheritance and Share Behavior

Structures and classes both support a form of inheritance. Structures and protocols can only adopt protocols; they can't inherit from classes. However, the kinds of inheritance hierarchies you can build with class inheritance can be also modeled using protocol inheritance and structures.

If you're building an inheritance relationship from scratch, prefer protocol inheritance. Protocols permit classes, structures, and enumerations to participate in inheritance, while class inheritance is only compatible with other classes. When you're choosing how to model your data, try building the hierarchy of data types using protocol inheritance first, then adopt those protocols in your structures.

モデルを継承したり振る舞いを共有したいなら構造体とプロトコルを使う

構造体はクラスを継承することができませんが、プロトコル継承と構造体を使えばクラス継承のようなことが可能です。

データをモデル化する際は、プロトコル継承を使って型の階層を作り、それからそのプロトコルを構造体に準拠させるようにしましょう。

つまり、

筆者の知識不足でたどたどしい要約もありますが、以上がアップルさんの見解です。これを踏まえてまとめると、以下の2つの場合はクラスを使ってそれ以外は構造体を使いましょう、ということだと思います。

  • Objective-CのAPIを使ったり、そのAPIを含む標準フレームワークのクラス(UIViewControllerなど)を継承したりしたい
  • モデル化したデータをオブジェクト間で統一的に変更(参照渡しが適切)しやすくしたい

思えばSwiftを勉強したての頃もこの構造体vsクラス問題に悩み、実務経験を5ヶ月ほど積んだ今も悩んでます。多分これからも悩むんでしょうね。ただ、Swiftには構造体を扱いやすくするプロトコルなどの仕組みが多数用意されているので、構造体を使える場面がないか積極的に探していきたいなあと思った次第です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初学者向け】ややこしいSwiftの文字列切り出しの処理を理解する

Swiftで文字列切り出しを「何文字目から何文字目まで〜」みたいにやろうとした時、切り出し範囲の指定がめんどくさくて、ん????ってなりました。
Javaの文字列型のsubstringメソッドみたいにIntで指定できればいいのに。。。

今回は混乱したSwiftの文字列切り出しについて整理します。

目次

  1. 文字列切り出しの表記法
  2. 内部でなにがどうなってるのか
    1. Stringからサブスクリプトで文字にアクセスする
    2. 引数:範囲型(Range<String.Index>)
    3. 返り値:Substring
  3. まとめ

1.文字列切り出しの表記法

先頭から任意の文字数の切り出し

let str = "0123456789"
// 先頭から3文字切り出し
str.prefix(3) // -> "012"

ワカル。シンプル。

先頭から任意の文字数の切り出し

let str = "0123456789"
// 末尾から3文字切り出し
str.suffix(3) // -> "789"

ワカル。シンプル。

任意の位置から任意の文字数の切り出し

let str = "0123456789"
// 4文字目(先頭から前に3文字目ずらした文字)の位置を指定
let startIndex = str.index(str.startIndex, offsetBy: 3)
// 7文字目(末尾から後に4文字ずらした文字)の位置を指定
// endIndexは末尾+1文字目なので4文字ずらす
let endIndex = str.index(str.endIndex,offsetBy: -4)

str[startIndex]             // -> "3"
str[endIndex]               // -> "6"
str[startIndex...endIndex]  // -> "3456"

チョットマッテ。キュウニムズイ。

は?なんか最初の文字位置と最後の文字位置指定して最終的に添字みたいなやつでアクセスしてる?は?急になに?は?
雰囲気はふんわりわかる気がしますがこれ毎回ググるはめになるやつですね。。。
これを頑張ってしっかり理解しましょう。

※ここ以降は備忘録的な位置付けが強いので、書き方がわかればいい方は上記までを参照していただければ良いかと思います。

2. 内部でなにがどうなってるのか

最後の「任意の位置から任意の文字数の切り出し」で実際に文字列切り出しを行なっているのはこの部分。
str[startIndex...endIndex] // -> "3456"
この処理を紐解き、なにが起こっているのかを見ていきましょう。

2-1. Stringからサブスクリプトで文字にアクセスする

str[startIndex...endIndex] // -> "3456"
↓補足込みでかくと

image.png

これは文字列型に添字でアクセスして文字列を切り出しているように見えます。
コレを理解するために必要知識として、サブスクリプトという概念があります。
(参考:サブスクリプト | Swift言語を学ぶ - Tea Leaves

Swiftでは構造体やクラス、列挙型に添字式でアクセスするときにはsubscriptキーワードを使って定義することが決まっており、[]で定義されている処理はsubscriptメソッドの部分に書かれています。
ですので、String構造体についてsubscriptキーワードで記述されている処理を見れば、文字列切り出しで何をやっているのか理解できそうです。実際に定義が書かれている部分が以下です。

String構造体でのsubscript処理
@inlinable public subscript(r: Range<String.Index>) -> Substring { get }

これを見るに、str[startIndex...endIndex]で行なっている処理は、実のところsubscriptキーワードを使って定義されており、str文字列に対して、String.Indexの範囲型を引数にして、Substring型の返り値をgetする」という処理をしているようです。
おお。なんか理解した感が出てきました。

あとは、引数であるRange<String.Index>と、返り値であるSubstring型について何者なのか理解できれば、今後はいちいち理解が曖昧なままググらずに済みそうです。

2-2. 引数:範囲型(Range<String.Index>)

引数であるRange型は範囲型いう、その名の通り値のとりうる範囲を示す型となります。

そのRange型の<>内で示されている、String.Indexは文字列型の位置を示す型であり、String型から指定の位置のCharacter型の文字を取り出すときに使われます。

また、String.Indexを指定するときは、文字列の開始位置を示すstartIndexプロパティや、終了位置を示すendIndexプロパティ、特定の文字位置からのズレを指定することで任意の文字列の場所を取得するindex(_:,offsetBy:)プロパティで取得を行います。例えば、
let startIndex = str.index(str.startIndex, offsetBy: 3)
という処理では、「str文字列の最初の文字"0"を基準に3文字分ずらした文字位置を取ってきてくださいね」という処理をしているわけです。

つまり、Rangeは「文字列の位置(String.Index)を範囲で教えてください」ということで、最終的にstr[startIndex...endIndex]の引数として与えているstartIndex...endIndexの部分は切り出したい最初の文字列位置から最後の文字列位置までの範囲を示す必要があるというわけですね。

2-3. 返り値:Substring

よし、ここまでで文字列切り出し処理は、文字列型に対して文字位置範囲を引数に指定してあげることで取得してるのね、理解理解。ってなったと思ったらきました、(初学者に取っては)未知の型Substring。大人しくString型で返しとけや!!って感じですよね。

このSubstring型は部分文字列といい、切り出した部分だけでなく、元の文字列の全体への参照を保持します。そのため部分文字列を保存すると、文字列データの寿命が長くなり、メモリリークのように見える場合があるとのこと。長い文字列から切り出す場合は、String型にキャストしておいた方が無難なようです。

3. まとめ

subscript(r: Range<String.Index>) -> Substring { get }
で定義される文字列切り出しで行なっている処理は、
「文字列に対して、String.Indexで示された文字位置の範囲型を引数にして、Substring型の返り値をgetする」
ということをしてるんですね。

文字列切り出し完全に理解しました。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C の配列を Swift が Tuple として扱うのが面倒なのを倒す

課題

Swift から C のライブラリを利用している際に、C の固定長配列を扱うのは少し煩わしいです。
なぜなら、Swift では Tuple として扱われるからです。
Tuple は Iteratable でないため、要素の参照に困ります。これをどうするか?

対応

方法1: reflection で倒す

https://qiita.com/codelynx/items/83f4b3829267d8d25b07 で述べられているように、reflection で Tuple を Array に変換する方法。
問題なくいけます。ただし、reflection は Compiler にとって負荷が高いため、要素が複雑だったりすると避けたくなります。

方法2: ポインタで倒す

var foo_list = func_with_c()
let ptr = UnsafeMutablePointer<Foo>(&foo_list.foo.0)

// "固定長"のサイズをベタに書く。例えば長さが 34 だと知っているとする。
for i in 0..<34 {
    let foo = ptr[i]
    print(foo)
}

var を使うことになるのが嬉しくないのですが、reflection を行わないので Compiler に優しいです。
特段の理由が無ければこう書くことが多いのかな..?と思っています。

参考: Swift Foundation UUID implement

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHub Actions で 自作Cocoapodsライブラリを自動デプロイする

Treasure Advent Calendar 2019 7日目の記事です。

iOS向けライブラリを開発していたところ、バージョンを更新する際に毎回ローカルの環境で
pod trunk push Hoge.podspec をやっていたのですが、毎回やるのは面倒だということで自動でデプロイされるようにしました。
仕組みを作ろうとした際にハマったりしたので今後やろうとしている方の参考になると幸いです。

トークンを取得する

自作のCocoapodsライブラリをデプロイするためには環境変数にトークンを設定する必要があります。

ユーザー情報を登録する

pod trunk register コマンドを使用してユーザー情報を登録します。
コマンドを打つとメールが届くので、メール内にあるリンクに飛び、アクティベーションします。

pod trunk register ry-itto@example.com

トークンをファイルから取得する

ユーザー情報を登録したらローカル環境の ~/.netrc に以下のような情報が追加されていると思います。この情報の中の password がトークンに当たります。

machine trunk.cocoapods.org
  login ry-itto@example.com
  password jaeiwghwe83hlagw8

リポジトリに登録する

GitHubのリポジトリにアクセスし、設定画面を開きます。
スクリーンショット 2019-11-25 0.35.50.png

設定画面を開いたら、その中の"Secrets" を開きます。
スクリーンショット 2019-11-25 0.36.33.png

そしてその画面の中の"Add a new secret"を押し、
NameにCOCOAPODS_TRUNK_TOKEN, Valueに先ほど取得したトークンを入れます。

これでリポジトリへのトークンの登録は完了です。
スクリーンショット 2019-11-25 0.40.29.png
このようになっていればOKです。

ワークフローの設定ファイルを作成する

完成形は以下です。それぞれ部分ごとに説明をしていきます。
ワークフローの構文

name: pod-deploy

on:
  push:
    tags: v*

jobs:
  build:
    runs-on: macOS-latest
    steps:
    - uses: actions/checkout@v1
    - name: Lint
      run: pod spec lint
    - name: Deploy
      env:
        COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
      run: pod trunk push Hoge.podspec

処理を行うタイミングを設定する

v から始まるタグが作成された際に処理が行われるように設定。

on:
  push:
    tags: v*

行う処理を設定する

jobs:
  build: # 'build' という名前でジョブを作成
    runs-on: macOS-latest # GitHub Actionsで提供されているmacOSの最新版を使うように指定
    steps:
    - uses: actions/checkout@v1 # リポジトリの情報にアクセス
    - name: Lint
      run: pod spec lint # 作成したコードが正しいかチェック
    - name: Deploy
      env: # 環境変数に `COCOAPODS_TRUNK_TOKEN` を設定
        COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
      run: pod trunk push Hoge.podspec

自作したライブラリを公開するために COCOAPODS_TRUNK_TOKEN を環境変数に設定する必要があります。ここには序盤でリポジトリのSecretsに設定したトークンを設定します。
なお、リポジトリのSecretsに設定した値は secrets.設定したName で取得できます。

終わりに

思いの外簡単に自動デプロイの仕組みを作成することができました。
GitHub ActionsはPublicなリポジトリなら無料で利用できるため、ぜひ試してみてください。

おまけ

https://github.com/ry-itto/QiitaAPIKit
今回のワークフローを作成するにあたって自動化したライブラリです。フィードバックなどあればいただけるとありがたいです!

参考

Automated CocoaPod releases with CI

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む