[iOS] Decodable でJSONをデコードする時に要素の型を変換する その3 KeyedDecodingContainer のエクステンションと Property Wrapper で一部の要素にのみ独自のデコード処理を適用しつつ狙った型に変換する

はじめに(承前)

本ブログの 以前のポスト で以下のJSONをデコードしようとしました。

{
    "食品名":"肉類",
    "食材": "鶏肉, 豚肉, 牛肉",
    "栄養": ["タンパク質", "ビタミンA", "ビタミンB"]
}

食材 (String) と 栄養 (String の配列) を、どちらも文字列の配列でデコードしたいという話でした。
そのためのコードとして以下を書きました。

struct Food: Decodable {
    let 食品名: String
    let 食材: [Foodstuff]  // 独自の型に変更
    let 栄養: [String]
}

struct Foodstuff: Decodable {
    let value: String
}

extension KeyedDecodingContainer {
    func decode(_: [Foodstuff].Type, forKey key: Key) throws -> [Foodstuff] {
        let valueString = try decodeIfPresent(String.self, forKey: key)
        let valueArray = valueString?.components(separatedBy: ",")
        return valueArray?.map { Foodstuff(value: $0) } ?? []  // 独自の型に変更
    }
}

[Foodstuff] という型に変えてしまえば、KeyedDecodingContainer のエクステンションで
食材 要素のみに独自のデコード処理を適応できます。
で、 Foodstuff.first?.value とかで文字列を取得できます。

いやー[String] で扱いたいですよね!

KeyedDecodingContainer のエクステンションではなくinit(from decoder: Decoder) throws {
自前でデコード処理を書けば[String] でデコードできます。
が、それはそれで手間です。

ということで、「一部の要素にのみ独自のデコード処理を適用しつつ、狙った型に変換する手法」
を書いていきます。

タイトルが長い!

前提条件

Xcode: 12.4
Swift: 5.3

まずは最終的なコード

struct Food: Decodable {
    let 食品名: String
    @StringComponents var 食材: [String]
    let 栄養: [String]
}

extension KeyedDecodingContainer {
    func decode(_: StringComponents.Type, forKey key: Key) throws -> StringComponents {
        if let value = try decodeIfPresent(StringComponents.self, forKey: key) {
            return value
        } else {
            return StringComponents(wrappedValue: [])
        }
    }
}

@propertyWrapper
struct StringComponents: Decodable {
    var wrappedValue: [String] = []

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let valueString = try container.decode(String.self)
        wrappedValue = valueString.components(separatedBy: ",")
    }
    
    init(wrappedValue: [String]) {
        self.wrappedValue = wrappedValue
    }
}

これで狙い通り、食材[String] として扱えます。

説明

食材 に、定義した StringComponentsPropertyWrapper を適応します。
(PropertyWrapper の基本的な挙動については、
以前のポスト[iOS] Property Wrapper の概要について にてまとめています)

KeyedDecodingContainer のエクステンションに
func decode(_: StringComponents.Type, forKey key: Key) throws -> StringComponents {
を定義します。

そうすると食材 をデコードする時のみ、その処理を通ります。

その処理の中で if let value = try decodeIfPresent(StringComponents.self, forKey: key) { でデコードを試みます。

実際のデコード処理は StringComponentsinit(from decoder: Decoder) throws { にて行います。
ここで String[String] に変換しています。

あとは StringComponents を返却して、食材 のデコードは完了です。

結果をデバッガを見ると以下のようになります。

デコード結果

左のペインでは _食材 というプロパティ名で表示されますが、
.食材 でアクセスすると [String] で取得できています。


また、KeyedDecodingContainer のエクステンションを変更して、
以下のように返り値などを [String] にしても同様のデコード結果になります。

extension KeyedDecodingContainer {
    func decode(_: StringComponents.Type, forKey key: Key) throws -> [String] {
        if let valueString = try decodeIfPresent(String.self, forKey: key) {
            return valueString.components(separatedBy: ",")
        } else {
            return []
        }
    }
}

プロパティラッパと欲しい型、どちらを返却すると筋がいいかまでは理解が及んでいません🙏

説明は以上となります!

最後に

デコード周りは一区切りとします。
また何か知見が出てきたら書くかもしれません。

以上です。
お疲れ様でした!

[iOS] Property Wrapper の概要について

はじめに

SwiftUI でもよく使われているProperty Wrapper についてさらっとまとめます!

Property Wrapper とは

ある「プロパティ」を「何らかの処理を施したラップ後のプロパティ」に変更する仕組みです。

@HogePropertyWrapper var a = 1

と書いた場合、a というプロパティは
HogePropertyWrapper にて何らかの処理を施された後(= 何らかの処理をラップした後)のプロパティ、
というものに変質します。

プロパティを get / set するときの何らかの処理をメソッド化して分離し、
それを適応したプロパティに対し共通処理として使いまわせる、
というのが Property Wrapper の思想になります。

Property Wrapper の書き方

とっかかりとして、 2速で歩くヒト:【Swift 5.1】Property Wrappersとは?
というブログ記事がとても参考になると思います。

struct, enum, class で定義可能で、wrappedValue というプロパティを定義する必要があります。

以下、Swift.org のコード そのままですが例示します。

@propertyWrapper
struct TwelveOrLess {
    private var number: Int
    init() { self.number = 0 }
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

これは Int の上限を12に制限する Property Wrapper となります。

プロパティにこの TwelveOrLess の処理を適応するには、以下のように @ をつけて宣言します。

class HogeClass {
    @TwelveOrLess var number: Int
    func hogeMethod() {
        number = 100
        print("🐱 mumber: \(number)") // "🐱 mumber: 12" と表示される
    }

Property Wrapper の仕組み

この @TwelveOrLess var number: Int の部分は、コンパイル時に以下のように展開されます。

private var _number = TwelveOrLess()
var number: Int {
    get { return _number.wrappedValue }
    set { _number.wrappedValue = newValue }
}

_number というプロパティができ、 number プロパティへのアクセスは
実態として _number.wrappedValue へアクセスする、という処理に変更されます。

number_number 、つまり TwelveOrLess のインスタンスをラップした値、ということですね。

これが PropertyWrapper の仕組みです!

Property Wrapper の初期化

Property Wrapper を適用したプロパティの初期化は以下のように書きます。

@TwelveOrLess var number: Int = 1

これは number プロパティに初期値1を設定しようとしています。

Swift はこれを以下のコードに変換します。

@TwelveOrLess(wrappedValue: 1) var number: Int

TwelveOrLess 側には以下のコードが必要になります。

@propertyWrapper
struct TwelveOrLess {
    // 略
    init(wrappedValue: Int) { self.number = wrappedValue } // メソッドの中身はお好きに
    // 略
}

このwrappedValue の初期化メソッドを書かないと
Argument passed to call that takes no arguments というエラーが出るかと思います。

複数の引数で初期化する場合

以下のように書きます。

@TwelveOrLess(wrappedValue: 1, hoge: "hoge") var number: Int
@propertyWrapper
struct TwelveOrLess {
    // 略
    private var hoge: String
    init(wrappedValue: Int, hoge: String) {
        self.number = wrappedValue
        self.hoge = hoge
    }
    // 略
}

Property Wrapper の概要については以上となります。

Projected Value という概念もありますが、それはまた別の機会に…。

最後に

以上です。
お疲れ様でした!

[iOS] Decodable でJSONをデコードする時に要素の型を変換する その2 KeyedDecodingContainer のエクステンションで一部の要素にのみ独自のデコード処理を適用する

はじめに(承前)

以前のポスト [iOS] Decodable でJSONをデコードする時に要素の型を変換する
以下のようなデコード処理を書きました。

struct Food: Decodable {
    var 食品名: String
    var 食材: [String]
    
    enum コーディングキー: String, CodingKey {
        case 食品名
        case 食材
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: コーディングキー.self)
        
        食品名 = try container.decode(String.self, forKey: .食品名)
        let 食材String = try container.decode(String.self, forKey: .食材)
        食材 = 食材String.components(separatedBy: ",")
    }
}

この方法のデメリットとして、init(from decoder: Decoder) throws で処理しているので、
全てのデコード処理を自前で書く必要があります。

要素の数が増えれば増えるほど記述するコストが増えてしまいます。

ある程度複雑な構造の場合、自前で全てデコード処理を書くのはつらいです。

そこで本ポストでは、要素の一部のみ独自のデコード処理を適用する方法を書いていきます!

前提条件

Xcode: 12.1
Swift 5.3

やりたいこと

以前のポスト [iOS] Decodable でJSONをデコードする時に要素の型を変換する と同様、
以下のJSONデータをパースします!

{
    "食品名":"肉類",
    "食材": "鶏肉, 豚肉, 牛肉"
}

また、前提としてデコード処理は以下のように呼び出しています。

let food = try! JSONDecoder.init().decode(
    Food.self,
    from: data
)

まずは最終的なコード

struct Food: Decodable {
    var 食品名: String
    var 食材: [String]
}

extension KeyedDecodingContainer {
    func decode(_: [String].Type, forKey key: Key) throws -> [String] {
        let value = try decodeIfPresent(String.self, forKey: key)
        return value?.components(separatedBy: ",") ?? []
    }
}

説明

KeyedDecodingContainer のエクステンションを定義し、
そこに var 食材: [String] のためのデコード処理を書けばOKです。

引数の型の [String].Type ですが、[String] 型でデコードしたい要素は
全てこのエクステンションを通るという処理になります。
(微妙に臭うな…? 🤔 (このあたりは後述))

引数の key には 「食材」 という CodingKeys enum が入ってきます。

let value = try decodeIfPresent(String.self, forKey: key) の部分で、
String 型を持ち且つ「食材」の CodingKey の rawValue (= 今回の場合は「食材」となる。デフォルトのCodingKey なので、rawValue もそのようになっている)
を持つJSON要素のデコードを試みています。

デコードできない可能性を考慮して、オプショナルのStringを返却する
decodeIfPresent(_:forKey:) を利用しています。

これで "鶏肉, 豚肉, 牛肉" という String が取得できるはずなので、
あとは , で文字列を分割して配列に加工し、その配列を返却しています!


var 食品名: String のデコードではこのメソッドに入ってこず、特に記述しなくてもデコードされます。
こちらはおそらく内部的には decode(_:forKey:) のような処理が走っているのではないかと思います。

CodingKeys について

前回のポストでは以下のように CodingKey たちを独自に定義していましたが、
今回は省略して記述できています。
デコード処理で明示的には利用しない(且つ JSON と デコード後 の要素名が同じ)ためです。

enum コーディングキー: String, CodingKey {
    case 食品名
    case 食材
}

もしJSONデータと異なる要素名でデコードしたい場合、
且つ今回のように一部の要素のみ独自のデコード処理を適用したい場合は以下のように
CodingKeys というenum で定義し、rawValue でJSONでの要素名を定義します。

    enum CodingKeys: String, CodingKey {
        case 食品名
        case 食材 = "JSONでの食材要素名"
    }

iOS がデコードする際、要素名のリストとして
CodingKeys という名前の enum を参照しているようです。
参考: Apple の公式ドキュメント: Encoding and Decoding Custom Types


これで必要な箇所のみ独自のデコード処理を施すことができました!🙌

しかしこの方法にもデメリットがあります!

[String] 型にデコードしたい要素がすべてこの処理を通ってしまう

そうです、例えばJSONデータが以下のような構造だとします。

{
    "食品名":"肉類",
    "食材": "鶏肉, 豚肉, 牛肉",
    "栄養": ["タンパク質", "ビタミンA", "ビタミンB"]
}

で、以下のようなモデルデータにデコードしたいとします。

struct Food: Decodable {
    var 食品名: String
    var 食材: [String]
    var 栄養: [String]
}

食材 はJSON要素の String を [String] に変換したいが、栄養 は [String] そのままでデコードしたい。

この場合 栄養 のデコードで失敗します。

なぜなら let value = try decodeIfPresent(String.self, forKey: key) で、
栄養String 型としてデコードしようと試みますが、
栄養[String] 型だからです。

以下のようなエラーが出ると思います。

typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "栄養", intValue: nil)], debugDescription: "Expected to decode String but found an array instead.", underlyingError: nil))

ではどのようにするのがよいのか。

一つは、引数のKey によって処理を出し分ける方法が考えられます。

Key によってデコード処理を出し分ける

以下のようなコードに変えれば問題なくデコードできます。

extension KeyedDecodingContainer {
    func decode(_: [String].Type, forKey key: Key) throws -> [String] {
        if key.stringValue == "食材" {
            let value = try decodeIfPresent(String.self, forKey: key)
            return value?.components(separatedBy: ",") ?? []
        } else if key.stringValue == "栄養" {
            let value = try decodeIfPresent([String].self, forKey: key)
            return value ?? []
        } else {
            return []
        }
    }
}

これはやばい香りがしますね… 🤔
今後 [String] 型の要素が増えるたびにここを考慮する必要が出てきます。

独自の型としてデコードする

影響範囲を限定できるので、まだこちらの方がよさそうかなと思います。

struct Food: Decodable {
    let 食品名: String
    let 食材: [Foodstuff]  // 独自の型に変更
    let 栄養: [String]
}

struct Foodstuff: Decodable {
    let value: String
}

extension KeyedDecodingContainer {
    func decode(_: [Foodstuff].Type, forKey key: Key) throws -> [Foodstuff] {
        let valueString = try decodeIfPresent(String.self, forKey: key)
        let valueArray = valueString?.components(separatedBy: ",")
        return valueArray?.map { Foodstuff(value: $0) } ?? []  // 独自の型に変更
    }
}

Foodstuff 構造体を RawRepresentable に準拠させると、より Swifty になるかな? 🤔
場合によってはそうしてもいいかもしれません。

struct Foodstuff: Decodable, RawRepresentable {
    let rawValue: String
}

独自の型を定義する場合のデメリットは、そのまんまですが [String] 型として扱えなくなることですね!

let 食材: [Foodstuff] の部分を [String] で扱いつつ、
KeyedDecodingContainer のエクステンションでキーによる処理の出し分けも避けたい?

PropertyWrapper と組み合わせれば実現できそうです🤔

長くなったのでそちらのパターンについては後ほど書きます!

最後に

Codable は色々できてるので、複雑なことをやろうとすると混乱しがちです😂

以上です。
お疲れ様でした!

[Firebase] 開発環境構築時のTips

はじめに

Cloud Functions for Firebase を試そうとしたらちょいちょいすんなりいかなかったので、メモを残します。

環境

macOS Catalina バージョン10.15.6

firebase init で失敗する

Firebaseプロジェクトが存在する状態で

$ firebase init firestore

とコマンドを打ったら以下のようなエラーが出ました

Error: Failed to get Firebase project YOUR_PROJECT_NAME. Please make sure the project exists and your account has permission to access it.

どうも前回ログインから時間が経っていると発生する場合があるらしいです。

以下のコマンドでログインしなおしたら正常に動作しました。

$ firebase logout
$ firebase login

firebase emulators:start で失敗する

以下のようなエラー文が出ました。

$ firebase emulators:start
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: database, pubsub
✔  functions: Using node@14 from host.
i  firestore: Firestore Emulator logging to firestore-debug.log
⚠  firestore: Fatal error occurred: 
   Firestore Emulator has exited with code: 1, 
   stopping all running emulators
i  hub: Stopping emulator hub
i  functions: Stopping Functions Emulator
i  firestore: Stopping Firestore Emulator
⚠  firestore: Error stopping Firestore Emulator

Having trouble? Try firebase [command] --help

JDKが必要だとというプロンプトが出るし、firestore-debug.log にもそのように書かれています。

Oracle のJDKダウンロードページ から落としてきてインストールしました。

Firebase のバージョンがインストールしたものと異なる

以下のように9.5.0を入れたと思ったのに。

$ npm i -g firebase-tools
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
npm WARN deprecated har-validator@5.1.5: this library is no longer supported
/usr/local/opt/nvm/versions/node/v12.18.3/bin/firebase -> /usr/local/opt/nvm/versions/node/v12.18.3/lib/node_modules/firebase-tools/lib/bin/firebase.js
+ firebase-tools@9.5.0
updated 1 package in 30.88s

バージョン確認をすると8.7.0 が入っている。

$ firebase --version
8.7.0

以下のように解決しました。


参照しているfirebase を確認。

$ which firebase
/usr/local/bin/firebase

npm のグローバルディレクトリの確認。

$ npm bin -g 
/usr/local/opt/nvm/versions/node/v12.18.3/bin
(not in PATH env variable)

想定とは異なるfirebase を参照していたようです。
ので、ローカルのfirebase を削除。

$ rm -rf /usr/local/bin/firebase

その上で以下のようにエイリアスをはる。

$  alias firebase="`npm config get prefix`/bin/firebase"

すると、想定通りのバージョンになりました。

$  firebase --version
9.5.0
$ which firebase
/usr/local/opt/nvm/versions/node/v12.18.3/bin/firebase

参考: stackoverflow

alias コマンドはマシンを再起動すると設定が消えると思うので、
より丁寧にするなら ~/.bash_profile などで以下のようにパスを通します。

export PATH=$PATH:$(npm get prefix)/bin

で、source ~/.bash_profile で変更を反映すればOK。

firebase deploy –only functions で失敗する

以下のようなエラーが出ました。

Error: HTTP Error: 403, Unknown Error

この場合も冒頭と同様、ログアウト・ログインで解消しました

$ firebase logout
$ firebase login

最後に

以上です。

お疲れ様でした!

[GCP] クラウド破産の可能性を抑えるために予算上限の目安と予算アラートを設定する

はじめに

PaaSなどを使ってWebサービスを作るぞ、というとき最初にすることは
開発でも開発環境構築でもなく、クラウド破産を防ぐことかもしれない…🧐

というわけで Google Cloud PlatformCloud Functions を利用しようとしたら有料機能だったので、
有料プランに変更しました。

有料プランでも無料枠 があり、一人で遊ぶ分には十分な枠があります。
しかし従量制課金のため、万が一不具合などで大量に処理をした場合、料金が青天井でやばいことになります😱

参考: クラウド破産の実例と対策

GCP には設定した予算を超えたらお知らせしてくれる機能があります。
公式ドキュメント: 予算と予算アラートの設定

予算と予算アラートの設定

上記公式ドキュメントを見てもらえればと思いますが、
キャプチャがあった方が分かりやすいかなと思ったのでまとめます。(2021/02/27(土) 時点での情報となります。)

Cloud Console にログインします。

で、「お支払い」をクリック。

お支払いをクリック

「予算とアラート」をクリック。

「予算とアラート」をクリック

「予算の作成」をクリック。

「予算の作成」をクリック

ナビゲーションが表示されるので記載されているとおりに進めます。
で、以下の場所で予算の上限を入力します。

予算の上限を入力

こんな画面になります。(試しに予算の上限100円にしてみました)
アラートを飛ばす閾値を複数設定できるんですね。

アラートを飛ばす閾値を複数設定できる

「完了」を押します。

で、「予算とアラート」に予算が追加されているはずです。

予算が追加されている

注意点として、アラートが飛んでも機能が停止する訳ではないようです。

アラートがいきなり飛んできて慌てるのも辛いので、
日頃からCloud Billing コンソールをチェックする癖をつけた方がよいかなと思いました🙂

Google Cloud Console の公式アプリもあったので入れました🙌

最後に

クラウド破産の可能性を低くできたので、Cloud Functions などやっていきたいと思います!

以上です。
お疲れ様でした!

[Mac] CocoaPods をアップデートする

はじめに

CocoaPods 自体のバージョン変更はあまりせず忘れがちなのでメモとして残しておきます。

環境

Mac: Catalina 10.15.6

作業内容

以下のようなエラーが出てライブラリをいい感じに入れられなくなったので、CocoaPods をアップデートします。

Analyzing dependencies
[!] `FirebaseAnalytics` requires CocoaPods version `>= 1.10.0`, which is not satisfied by your current version, `1.9.3`.

という訳で1.10.0 を指定してアップデートを試みる。

$ gem install cocoapods -v 1.10.0
Fetching cocoapods-1.10.0.gem
ERROR:  While executing gem ... (Gem::FilePermissionError)
    You don't have write permissions for the /Library/Ruby/Gems/2.6.0 directory.

パーミッションがない🤔
sudo をつけて再度実行。

$ sudo gem install cocoapods -v 1.10.0
ERROR:  While executing gem ... (Gem::FilePermissionError)
    You don't have write permissions for the /usr/bin directory.

ダメっぽいのでどうしたもんかと思っていたら以下のような記事を発見。
Qiita: MacOSX El Capitanでcocoapodsインストールが出来ない時の対処法

El Capitanからrootlessという概念が登場しました。
簡単に説明をすると、root権限を持つユーザーでも

/usr
/sbin
/System

などへのアクセスが制限されるというものです。

https://qiita.com/trsxxii/items/4bb4708de03e6ee14a4a#%E5%8E%9F%E5%9B%A0

これっぽいなぁ(環境構築したときのことは忘れている🤯)

ということでCocoaPods のインストールフォルダを確認。

$ which pod
/usr/local/bin/pod

/usr/local/bin ね!
同じインストールフォルダを指定してインストールします。

$ sudo gem install -n /usr/local/bin cocoapods -v 1.10.0
Password:
Successfully installed xcodeproj-1.19.0
Successfully installed cocoapods-1.10.0
Parsing documentation for xcodeproj-1.19.0
Installing ri documentation for xcodeproj-1.19.0
Parsing documentation for cocoapods-1.10.0
Installing ri documentation for cocoapods-1.10.0
Done installing documentation for xcodeproj, cocoapods after 4 seconds
2 gems installed

うまくいきました 🙌

最後に

今回当たったアクセス制限は System Integrity Protection 略してSIP というらしいです。
Appleの公式サポートサイト: Mac のシステム整合性保護について

書き記したので忘れることはないでしょう🤧

以上です。
お疲れ様でした!

[iOS] Google Mobile Ads SDK を使ってアプリにネイティブ広告を表示する実用編 Google-Mobile-Ads-SDK 7.64.0

はじめに(承前)

アプリにGoogle 広告を出したい!
というわけで、本ブログの前のポスト
[iOS] Google Mobile Ads SDK を使ってアプリにネイティブ広告を表示する Google-Mobile-Ads-SDK 7.64.0
で、とりあえずタイトルとメイン画像を表示してみました。

大体の骨組みはできたので、本ポストでは表示要素の追加や複数同時リクエスト機能などを入れて
もう少し進んだ広告表示まで行って完了とします!

注意: 2021/02/23 時点の最新版は Google-Mobile-Ads-SDK 8.1.0 ですが、公式ドキュメントがそれに追いついていません。
(最新版では GADUnifiedNativeAd などが消えているっぽい)

なので今回は簡単のために、手順書通りに進められる 7.64.0 でいきます。
8.1.0 バージョンはできたらまたまとめます!

環境

  1. Xcode12.1
  2. Swift 5.3
  3. シミュレータ: iOS 14.1

色々な要素を表示する

このあたりの公式ドキュメントはこちら

タイトルやメイン画像以外にも表示要素はあります。
が、それらは要素があることは保証されていないので、
存在しないパターンも考慮しておく必要があります。

というわけで色々な要素を表示してみます。
要素をIBOutlet で接続していきます。

色々な要素をIBOutlet で接続していく

コード最下部の adChoiceView には接続していませんが、それについては後述します。

要素に値を代入していきます。

extension SampleGoogleAdView {
    func setNativeAd(_ nativeAd: GADUnifiedNativeAd) {
        (nativeAdView.headlineView as? UILabel)?.text = "HeadlineView: \(nativeAd.headline ?? "")"
        nativeAdView.mediaView?.mediaContent = nativeAd.mediaContent

        (nativeAdView.callToActionView as? UIButton)?
            .setTitle("Call To Action View: \(nativeAd.callToAction ?? "")", for: .normal)
        (nativeAdView.iconView as? UIImageView)?.image = nativeAd.icon?.image
        (nativeAdView.bodyView as? UILabel)?.text = "Body View: \(nativeAd.body ?? "")"
        (nativeAdView.storeView as? UILabel)?.text = "Store View: \(nativeAd.store ?? "")"
        (nativeAdView.priceView as? UILabel)?.text = "Price View: \(nativeAd.price ?? "")"
        (nativeAdView.imageView as? UIImageView)?.image = nativeAd.images?.first?.image
        (nativeAdView.starRatingView as? UILabel)?.text = "Star Rating View: \(nativeAd.starRating?.stringValue ?? "")"
        (nativeAdView.advertiserView as? UILabel)?.text = "Advertiser View: \(nativeAd.advertiser ?? "")"
        
        nativeAdView.callToActionView?.isUserInteractionEnabled = false
        nativeAdView.nativeAd = nativeAd
    }
}

下の方でcallToActionView のユーザ操作を禁止していますが、
これをしないとSDK側でタップイベントを正しく処理できないとのことです

アプリを実行するとこんな感じになります!

実行時のキャプチャ

割と値が入っていないものがありますね!

iマークボタンの位置を変える

このあたりの公式ドキュメントはこちら

SDKが自動的に、広告の右上にiマークボタンを挿入していますが、これは位置を指定することができます。
SDKで設定可能な位置が4箇所定義されています。

  1. 右上 (デフォルト)
  2. 左上
  3. 右下
  4. 左下

位置の指定は GADAdLoader の初期化時 にオプションとして指定します。
こんな感じ。

        let adViewOptions = GADNativeAdViewAdOptions()
        adViewOptions.preferredAdChoicesPosition = .topRightCorner
        
        adLoader = GADAdLoader(
            adUnitID: type(of: self).adUnitId,
            rootViewController: self,
            adTypes: [GADAdLoaderAdType.unifiedNative],
            options: [adViewOptions]
        )
        adLoader.delegate = self
        adLoader.load(GADRequest())

上でIBOutlet 接続しなかった adChoiceView はこのiマークボタンを表示するView となります。
iマークボタンの調整ははコードで位置指定するだけと思うので、基本的にはIBOutlet 接続は不要なのかなと思います。

複数の広告を同時にリクエストする

このあたりの公式ドキュメントはこちら

こちらも GADAdLoader の初期化時 にオプションとして指定します。
こんな感じ。

        let multipleAdsOptions = GADMultipleAdsAdLoaderOptions()
        multipleAdsOptions.numberOfAds = 5
        
        adLoader = GADAdLoader(
            adUnitID: type(of: self).adUnitId,
            rootViewController: self,
            adTypes: [GADAdLoaderAdType.unifiedNative],
            options: [multipleAdsOptions]
        )
        adLoader.delegate = self
        adLoader.load(GADRequest())

複数の広告が配列として一発で返ってくる、というわけではなく、
指定した数分のデリゲートメソッドが何回も呼ばれます!

メディエーション を指定している場合は利用しないよう公式ドキュメントに記載されているので、
確認してから使った方がよさそうです。

その他のオプションなど

他にも、画像や動画のアスペクト比などを指定できるオプション や、動画広告 などもあります。

動画広告については、動画をリクエストするオプション指定が必要そうです。

動画再生は画像のときから何も変更を加えずとも GADMediaView にて自動的に再生されます。
ただし制御をするためには動画のデリゲートメソッドの実装が必要になるかと思います。

最後に

これで稼げるようになれると嬉しいですね!💰

Google のネイティブ広告の実装まとめは以上です。
お疲れ様でした!

[iOS] Google Mobile Ads SDK を使ってアプリにネイティブ広告を表示する Google-Mobile-Ads-SDK 7.64.0

はじめに

アプリにGoogle 広告を出したい!
というわけでその手順を書いていきます。

本ポストは、細かいことは置いておいてとりあえず
ネイティブ広告を表示して実装の流れを掴みたい人向けです。

基本的なことはGoogle公式サイトのドキュメント やGitHub に上がっている公式のサンプルアプリ があるので、
それらを補う感じで書いていきます。

注意: 2021/02/23 時点の最新版は Google-Mobile-Ads-SDK 8.1.0 ですが、公式ドキュメントがそれに追いついていません。
(最新版では GADUnifiedNativeAd などが消えているっぽい)

なので今回は簡単のために、手順書通りに進められる 7.64.0 でいきます。
8.1.0 バージョンはできたらまたまとめます!

iOSアプリでのGoogle 広告の種類

公式ドキュメント に4種類載っています。

  1. バナー
    1. 画面上部か下部に表示され続ける横長広告
  2. インタースティシャル
    1. 全画面表示される広告
  3. ネイティブ
    1. UIView を自由に配置してデザインをカスタマイズできる広告
  4. リワード広告
    1. フルスクリーンの動画広告で最後まで見るなどするとアプリ内で報酬を獲得できる

本ポストではデザインをカスタマイズできるネイティブ広告を実装します!
が、ネイティブ広告の説明に気になる一文が。

これは現在、限定ベータ版で提供されており、利用できるのは一部のパブリッシャーに限られます。

https://firebase.google.com/docs/admob/ios/quick-start?hl=ja#choose_an_ad_format

んんん、大丈夫なのかなこれはw
まぁ参考になれば。。!

環境

  1. Xcode12.1
  2. Swift 5.3
  3. シミュレータ: iOS 14.1

環境構築

とりあえず公式ドキュメント通りに進めます。

Firebase を利用する・しないで、手順が異なるようです。

流れは以下のようになっており、粛々とやっていけば大丈夫です。

  1. CocoaPods でインストール
  2. Info.plist の編集
  3. AppDelegate に1, 2行追加する

今回はテスト的に表示するだけなのでAdMobアカウント周りの設定は飛ばして大丈夫です。

ちなみにSwiftPM 版のFirebase を見つけました。
Swift Package Manager for Firebase Beta
が、広告系のパッケージがどうもなさそうだったので(見逃した?)、今回は手順通りCocoaPods で入れます。

Firebase を利用しない場合の手順に沿って進めます。
(利用する場合の手順だと後述のIBOutlet の接続がなぜかうまくいかず詰みました😭)

日本語版と英語版で公式ドキュメントの記載内容が異なっていたり、
そもそも最新版のSDKに対応しておらず混乱するので、本ポストで使うパッケージとバージョンを書いておきます。
Podfile に以下のように設定しました。

  pod 'Firebase/Analytics'
  pod 'Google-Mobile-Ads-SDK', '7.64.0'

ここで重要なのが Google-Mobile-Ads-SDK のバージョンです。

2021/02/23 時点の最新版は 8.1.0 ですが、公式ドキュメントがそれに追いついていません。

なので今回は簡単のために、手順書通りに進められる 7.64.0 でいきます。
8.1.0 バージョンはまたまとめます!

さて、上記の設定で pod install --repo-update を実行すると、以下のようなパッケージが入りました。

Analyzing dependencies
Downloading dependencies
Installing Firebase (6.34.0)
Installing FirebaseAnalytics (6.9.0)
Installing FirebaseCore (6.10.4)
Installing FirebaseCoreDiagnostics (1.7.0)
Installing FirebaseInstallations (1.7.0)
Installing Google-Mobile-Ads-SDK (7.64.0)
Installing GoogleAppMeasurement (6.9.0)
Installing GoogleDataTransport (7.5.1)
Installing GoogleUserMessagingPlatform (1.4.0)
Installing GoogleUtilities (6.7.2)
Installing PromisesObjC (1.2.12)
Installing Succinct (0.4.31)
Installing SwiftGen (6.4.0)
Installing nanopb (1.30906.0)
Generating Pods project
Integrating client project
Pod installation complete! There are 4 dependencies from the Podfile and 14 total pods installed.

では以降実装していきます!
まずは最低限の実装で、広告リクエストから表示するまでに焦点を絞ります。

  1. View 周りを準備する
  2. 広告リクエストする
  3. レスポンスを受け取り、要素を表示する

の順で書いていきます。

View 周りを準備する

このあたりの公式ドキュメントはこちら

本ポストでは4つのファイルを準備します。

  1. 広告用のViewクラス.xib
  2. 広告用のViewクラス
  3. ViewController
  4. Main.storyboard (ViewController 用)

広告用のViewクラス.xib

SampleGoogleAdView.xib とします。

以下のように適当にUIView を配置し、Custom Class に GADUnifiedNativeAdView を指定します。

UIView を配置する

GADUnifiedNativeAdView はGoogle 広告SDKのView 用クラスで、
これの上にタイトルなどの要素を配置していきます。

ということでタイトルとメイン画像を配置します。

タイトルは通常のUILabel で配置します。
配置した後、以下のようにGADUnifiedNativeAdView の headlineView プロパティに IBOutlet で接続します。

タイトル用ラベルをheadlineView プロパティにIBOutlet 接続

続いてメイン画像を配置します。

まずUIView を配置し(分かりやすいように赤色にしています)、
その後以下のようにCustom Class を GADMediaView にします。

メイン画像用View の Custom Class を GADMediaView にする

GADMediaView にすると、メインの広告画像や動画を表示することができます。
逆に言うと、GADMediaView にしないと表示されません

で、タイトルと同様にmediaView にIBOutlet を接続します。

メイン画像用View をmediaView プロパティにIBOutlet 接続

xib の設定は以上です。

以降はさくっと残りのView の準備をしていきます。

広告用のViewクラス

SampleGoogleAdView クラスとします。

import UIKit
import GoogleMobileAds

final class SampleGoogleAdView: UIView {
    private var nativeAdView: GADUnifiedNativeAdView!
    
    override init(frame: CGRect){
        super.init(frame: frame)
        loadNib()
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }

    func loadNib(){
        nativeAdView = Bundle.main.loadNibNamed("SampleGoogleAdView", owner: self, options: nil)?.first as? GADUnifiedNativeAdView

        nativeAdView.frame = self.bounds
        self.addSubview(nativeAdView)
    }
}

以上です。
特に言うことはありません!

ViewController

SampleGoogleAdVC とします。

import UIKit
import GoogleMobileAds

final class SampleGoogleAdVC: UIViewController {
    private var sampleGoogleAdView: SampleGoogleAdView!
    
    var adLoader: GADAdLoader!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        sampleGoogleAdView = SampleGoogleAdView(
            frame: CGRect(x: 20, y: 20, width: 300, height: 300)
        )
        view.addSubview(sampleGoogleAdView)
        
        loadAd()
    }
}

上で作ったSampleGoogleAdView を貼り付けています。

また、adLoader という広告リクエスト用のプロパティもついでに用意しています。
リクエストのデリゲートメソッド呼び出しのために参照を持っておく必要があります。

loadAd は広告リクエスト用に準備したメソッドで、後述します。

以上です!

Main.storyboard (ViewController 用)

ViewController を配置し、Custom Class に上で作成した SampleGoogleAdVC を指定します。

VCを配置

is Initial View Controller にチェックを入れるのを忘れずに。

以上です。
特に言うことはありません!

これでView周りの準備が整ったので、
あとはリクエストして表示するロジックを入れていくだけです。

広告リクエストする

このあたりの公式ドキュメントはこちら

広告リクエスト用にSampleGoogleAdVC に以下のロジックを追加します。

extension SampleGoogleAdVC {
    private static let adUnitId = "ca-app-pub-3940256099942544/3986624511"
    private func loadAd() {
        adLoader = GADAdLoader(
            adUnitID: type(of: self).adUnitId,
            rootViewController: self,
            adTypes: [GADAdLoaderAdType.unifiedNative],
            options: []
        )
        adLoader.delegate = self
        adLoader.load(GADRequest())
    }
}

loadAd メソッドとして切り出しました。
先に書いた通り、今回はviewDidLoad で呼び出しています。

adUnitId はサンプル用のIdを指定しています。

表示までの流れに焦点を当てるので、リクエスト時のオプションなどの説明はまたの機会に。

レスポンスを受け取り、要素を表示する

広告リクエストの受け取りは、リクエスト時に指定したデリゲートのクラスで行います。
このあたりの公式ドキュメントはこちら

extension SampleGoogleAdVC: GADUnifiedNativeAdLoaderDelegate {
    func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADUnifiedNativeAd) {
        sampleGoogleAdView.setNativeAd(nativeAd)
    }
    
    func adLoader(_ adLoader: GADAdLoader, didFailToReceiveAdWithError error: GADRequestError) {
    }
}

書いているまんまですが、上が成功時、下がエラー時に呼ばれるデリゲートメソッドです。

成功時に GADUnifiedNativeAd クラスのオブジェクトが返ってきます。
これにタイトルや画像などのプロパティが含まれているので、
それらをView の各要素に格納すれば表示できます。

表示周りの公式ドキュメントはこちら

上記デリゲートメソッド内で SampleGoogleAdView.setNativeAd メソッドを呼ぶようにしているので、
SampleGoogleAdView にその中身を書いていきます。

extension SampleGoogleAdView {
    func setNativeAd(_ nativeAd: GADUnifiedNativeAd) {
        (nativeAdView.headlineView as? UILabel)?.text = nativeAd.headline
        nativeAdView.mediaView?.mediaContent = nativeAd.mediaContent
        
        nativeAdView.nativeAd = nativeAd
    }
}

ここは公式ドキュメント通りなので特筆することはありません。

ということで実行してみます!

実行してみる

広告が表示された

表示されました!👏

右上のiマークはSDK側で自動的に挿入してくれています。
また、各要素をタップするとページがブラウザで開くのでお試しあれ。

とりあえず表示までの大まかな流れでした!

最後に

少し長くなったので、一旦切ります。
今回の続き(他の要素やオプションなどについてのまとめ)は以下ポストになります。

[iOS] Google Mobile Ads SDK を使ってアプリにネイティブ広告を表示する実用編 Google-Mobile-Ads-SDK 7.64.0

以上です。
お疲れ様でした!

Amazon アソシエイトに登録する手順

はじめに

結論から言うと、登録申込後から180日間の購買数が2件以下の場合は審査すらされないというお話です!
合格後から180日以内ではありません、登録申込後からです。

ですので、ある程度集客力を高めてから登録申込した方がよさそうです!

また、申込だけではなく「アソシエイトであることの表示」も必要となります。


とりあえず以下さっくり登録申込手順を紹介します。
申込自体は10分くらいでできそうです。

登録申込をする

Amazonアソシエイト・プログラムにてアカウントを登録申込をします。

「登録」をクリック。

「登録」をクリック

ログインしていなければログインをします。

そのあと以下の画面にとびます。

アカウント情報を入力

この「アカウント情報」では氏名・住所・電話番号など、また税務上の米国人かどうかや、法人かどうかを入力します。

「次へ」を押すと以下のようになります。

URLを入力

サイトのURLを入力します。
このサイトで言えば https://web-y.dev と入力します。

「次へ」をクリック。
最後の入力ページが表示されます。

ウェブサイトとアプリの情報

ウェブサイトについていくつか質問されます。
どういう商品を紹介したいか、どういう内容のウェブサイトなのか、どうやって集客しているか、月間ユーザー数、などなど。
まぁ正直に埋めていきます。

面接されてる感😇

ちなみにこのページの最後で、ロボット防止のために画像に書かれた文字を入力するんですが、
正しいものを入力しても「正しくない」と引っかかりました。

そんなときはブラウザのリロードをすれば、画像も別のものになり正常に入力を受け付けてくれました。
ここで入力した内容は白紙になったけど、大した量じゃないので仕方ない。。!

「次へ」を押すと以下のような画面になって、一旦完了です!

登録申請完了画面

登録申込後メールが来る

すぐに以下のようなメールがきます。

申込完了のメール

要約すると、登録申込後から180日間の購買数が2件以下の場合は審査すらされないよ、ということのようです。

以前はサイトの審査がすぐ始まっていたはずですが、最近規約が変わったようで、
いまは最低限の販売力がないと審査すらしてくれないようです😇 (知らなくて誤算だった)

ですので、ある程度集客力を高めてから登録申込した方がよさそうです!
Amazon がビッグテック企業となったが故に、
審査するサイトをまず選別しないと回らなくなったんでしょうかね🤔

審査合格を目指すべく、早速商品の画像リンクを載せておきます!

忘れてはいけない「アソシエイトであることの表示」

Amazonアソシエイト・プログラム運営規約 に以下の記述があります。

乙は、乙のサイト上または甲がアソシエイト・プログラム・コンテンツの表示を許可した他の場所のどこかに 「Amazon.co.jpアソシエイト」または「[乙の名称を挿入]は、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイトプログラムである、Amazonアソシエイト・プログラムの参加者です。」の文言を表示しなければなりません。甲は、この文言を適時変更することがあります。

Amazonアソシエイト・プログラム運営規約 10.乙がアソシエイトであることの表示

この「アソシエイトであることの表示」を実施しないと、審査に落ちる可能性がありそうです。

参考:
クマノタスケ: 【2020】Amazon アソシエイトの審査に落ちた後合格するまでにやったことまとめ
https://kuma-tasuke.com/summary-of-what-done-to-pass-amazon-associate/#index_id1

最後に

色々なブログを見てみると、合格するのはすごい高難易度に思えてきました😖

以上です。
お疲れ様でした!

[iOS] Swift で端末の使用言語を取得する

環境

  1. Xcode12.1
  2. Swift 5.3
  3. シミュレータ: iOS 14.1

取得するコード

let language = NSLocale.preferredLanguages.first?.components(separatedBy: "-").first

print("🐱: \(String(describing: language))") // 🐱: Optional("ja")

取得できるのは jaen などのアルファベット2文字です。
これは言語の国際規格 ISO 639-1 で定義されている言語コードです。

メジャーではない言語に関しては ISO 639-2 で返却される可能性もありますがそれは調べていません😣
が、メジャーな言語は ISO 639-1 でカバーされているので、言語の専門アプリでもない限りそれで事足りるかと思います。

ISO 639 については、本ブログで以前書いた 言語の国際規格: ISO 639 について も参考になるかもしれません🙌

NSLocale.preferredLanguages の返り値についてもう少し詳しく

記事タイトルは上の項目でさくっと回収できましたが、返り値についてもう少しまとめておきます。

設定の優先順に複数の値を返す

NSLocale.preferredLanguages を呼び出すと以下のような String の配列が返ってきます。

["ja-JP", "en-JP", "en-GB", "es-ES"]

このときの端末の設定は以下のような状態です。

言語と地域の設定画面

言語を4つ設定しており、それが優先度順に返ってきている形です。

一つ一つの値について

配列の1つ1つは 言語コード - 地域コード という形式になっています。
参考: Apple の公式ドキュメント Language and Locale IDs

言語コードは先述の通り ISO 639 に、地域コードは国名の国際規格 ISO 3166-1 alpha-2 に沿っているようです。
(ISO 3166 に関しても本ブログの 国名の国際規格: ISO 3166 について に概要をまとめたので、よろしければ)

以下の例で言うと、

["ja-JP", "en-JP", "en-GB", "es-ES"]

ja-JP は言語が日本語地域が日本となります。
en-JP は言語が英語地域が日本となります。
これは上図の通り、「地域」を日本に設定しているからです。

en-GB は言語が英語地域がイギリスとなります。
es-ES は言語がスペイン語地域がスペインとなります。
地域が日本になっていないのはなぜなのか🤔

それは↓

端末の設定との関係について

以下のように末尾に (国名) と表示している言語は、地域コードをその国名で返すという仕様のようです。

国も含んだ言語設定

つまり、英語のイギリス方言は地域コードがイギリス(GB)になり、
スペイン語のスペイン方言は地域コードがスペイン(ES)になります。

言語と地域が被っていてやや分かりづらいので更に言うと、英語のオーストラリア方言は「en-AU」のようになります。

一方、以下のように国名が表示されていない言語は、端末設定の「地域」を地域コードとして採用するようです。

国を含めない言語設定

「地域」設定の存在感は実はやや薄めかも。。🤔

端末の「地域」設定について

ちなみにこの「地域」の設定は、日付表記などをその地域の慣習に合わせた形式で表現するために使用されるようです。
例えば日本だと日付は「年月日」ですが、アメリカだと「月日年」の順になる、というようなことです。
参考: Apple の公式ドキュメント Reviewing Language and Region Settings

しかし、シミュレータのカレンダーアプリで確認したところ、
en-JP の場合でも日付は「月日年」形式になっていて、日本の「年月日」形式ではありませんでした。

このあたりを実装する際は注意が必要かもしれません😔

最後に

NSLocale.preferredLanguages の理解が深まった感があります。

以上です。お疲れ様でした!