RabbitBox v1.5 の配信を開始しました
RabbitBox v1.5の審査が終了して、無事配信開始していました。
apps.apple.com本バージョンではイメージ一覧表示時に列レイアウト を利用できるようになりました。 今までは行(横)方向にイメージを並べていましたが、iPhoneなど縦長画面だったり管理しているイメージに縦長画像が多い場合はサムネイルが小さすぎたり大きすぎたりと見やすい状態とは言えませんでした。
列レイアウト では縦方向の列数を固定し、上から下に向かってサムネイルを並べて表示するモードです。 次のように縦長画面でもバランス良く表示できるようになっています。
この設定はイメージ一覧ビューの歯車アイコンから変更できます。
AdMobの広告が表示されない件
RabbitBoxの広告配信にはAdMobを利用しています。
apps.apple.comリリース後一週間ほどはちゃんと表示されていたのですが、それ以降はずっと表示されていません😇
とりあえず、よくある原因を調べてみたのですが、よくわかりませんでした……
- 新しく作成したアプリや広告ユニットかどうか?
- 反映まで数日かかることもあるらしい
- → 2週間くらい前に作成済み
- 実装コードは適切に機能しているか?
- → テスト広告は表示されていますし、リリース後もしばらくは表示されている
- ポリシー違反は?
- → ポリシーセンターで確認するも特になし
- エラーコードは?
- → ERROR_CODE_NO_FILLっぽい
- → ヘルプを見た感じ、実装は正しいが配信可能な広告が不足?
ググったりした結果、広告のリクエストが安定していない(≒アクティブユーザが少ない?)っぽいので様子を見ています。
AdMobの本番広告が表示されない場合に確認すべきこと | デロイト トーマツ ウェブサービス株式会社(DWS)公式ブログ
参考までにここ30日間のリクエスト数は合計約700回で1日20回くらいです。おそらくアクティブユーザ数は2,3人とかじゃないでしょうか…… これだとまだダメみたいですね🥲
無事配信されるまで定期的に様子を記事にしようと思います。目指せ広告表示!
突然の広告配信制限
……な感じで記事を書きつついつか表示されるだろうと楽観的に待っていたのですが、Googleさんからステキなメールをいただきました。
いやー、「そもそも広告表示されてないのに不正もクソもないだろ😡」「とりあえず広告表示して不正タップできるようにしてから言えや💢💢💢💢」とか思わないでもないですが、来てしまいました。
ちなみにそもそも広告が表示されていないので、配信制限されてもノーダメージです✌️
現在、アプリ内で広告が表示されるはずの領域には謎のスペースがあるのですが、そこをユーザがタップすると不正タップ判定になるんでしょうか🤔
制限に関しても何か状況が変わればまた記事にしようと思います。
偏見にまみれたSwift言語入門
今回RabbitBoxの開発ではじめてSwiftを使用しました。個人的な感想としては「他のモダンな言語から節操なく機能を取り込んだ言語」という印象でした。
最初は若干面食らいますが、他の言語のこの機能というのがわかればスムーズに理解できると思いましたので、これらのポイントを個人的な復習も兼ねて書こうと思います。
apps.apple.comSwift全体
基本的にはよくあるC/C++系統の書式ですが、Go言語に近い雰囲気だと思います。文末のセミコロンや条件式に括弧が不要だったりします。個人的に好きなタイプです。
また、型推論がとても強力で静的型付け言語であることを忘れそうになります。
この書式を基本に古今東西いろんな言語のプログラミングパラダイムを取り込みまくっているのが、Swiftという言語です。個人的には (C++ + Go + Python + javascript)÷3 みたいな印象です。
変数と定数
変数は当然のように型推論です。後付けされた言語と違い最初から型推論前提で設計されたためか、PythonやRubyなどの動的型付け言語に近い使い勝手になっています。Python3のType Hintingにそっくりかもしれません。
var v1 = 1.23 var v2 = funcFoo() // 型を明示したい場合は後ろにつける var v3: CGFloat = 1.23
書式が洗練されすぎていて一見するとVariant型のように感じますが、あくまで型推論による 静的型付け 言語です。なので次のようなことはできません。
var v1 = 1.23 v1 = "foo" // 変数型エラー
また、宣言と初期化を同時に行う必要はありませんが、初期化していない変数にはアクセスできません。
var v1: Int print(v1) // エラー
変数宣言のvarとは別にletがあります。letで宣言した定数は初期化後変更できません。
let v1 = 1.23 v1 = 2.00 // ERROR
コレクション
配列
当然基本的なデータ構造は用意されています。配列は次のように宣言します。
let intArray: [Int] let stringArray: [String] = [] // 型宣言&空で初期化 let array = [1, 2, 3] // 型推論&初期化
コレクションに関しても未初期化の変数にはアクセスできません。このため、使用前に明示的な初期化が必要です。
var intArray: [Int] intArray.append(1) // 未初期化エラー var intArray2: [Int] = [] // または var intArray2: [Int] = Array() intArray2.append(1) // OK
基本的な操作はPythonをはじめとしたモダンな言語と同様です。
array = ["First", "Second"] array.append('Third') array[1] # "Second" len(array) # 3 array[1:] # ["Second", "Third"]
Swift
var array: [String] = ["First", "Second"] array.append("Third") array[1] // "Second" array.count // 3 array[1...] // ["Second", "Third"]
スライス操作に関してはPythonとやや異なっているため注意が必要です。Pythonの場合、スライス操作では部分列のlistを得られますが、Swiftの場合はArraySliceという特殊なオブジェクトが返ってきます。詳細は別記事にでもしようと思いますが、新規のArrayではなく部分列を参照するために専用のビューオブジェクトが使用されると覚えておいてください。(基本的にはそれほど意識する必要はないと思いますが)
あともう一点、Pythonのリストと非常に似ているのでハマってしまったのですが、SwiftのArrayは参照型ではなく値型です。
例えば、Pythonで次のようなコードを実行するとaとbは同じオブジェクトを参照しているため、bの内容を更新するとaの内容も変更されます。
a = [1,2] b = a b.append(4) print(a) # [1,2,4]
Swiftの配列は値型であるため別の変数に代入した時点でコピーされます。このため片方の配列を更新してももう片方には影響ありません。
let a = [1,2] var b = a b.append(4) print(a) // [1,2]
ディクショナリ
ディクショナリ型も用意されています。 静的型付け言語なので格納する型の宣言が必要です。PythonのdictやJavascriptの連想配列ほどの自由さはなく、C++やJavaのハッシュマップに近いです。
HashMap<String,Integer> map = new HashMap<>();
Swift
let map: [String:Int] // 型宣言 let map: [String:Int] = [:] // 型宣言&空で初期化 let map = ["key1": 1, "key2": 2] // 型推論&初期値
配列と同様に値型です。また、アクセス前には初期化が必須です。
制御構文
Pythonとそっくり。以上
If
a = 2 if a == 0: print('0') elif a > 0: print('a>0') else: print('a<0')
Swift
let a = 2 if a == 0 { print("0") } else if a > 0 { print("a>0") } else { print("a<0") }
For-in
items = [1, 2, 3, 4, 5] for item in items: print(item) for i in range(len(items)): print(i) for i, item in enumerate(items): print(i, item)
Swift
let items = [1, 2, 3, 4, 5] for item in items { print(item) } for i in 0..<items.count { print(i) } for (i, item) in items.enumerated() { print(i, item) }
While
想像通りです。
Switch
一度は誰もが思ったであろう「Python君、アタマ固すぎ!」
個人的には一貫した思想のもとにswitch文を拒み続けるPython君も好きですが、Swiftちゃんはそんなこだわり関係なく当然のようにswitch文が使えます。(冗談おいとくと、たぶんパフォーマンス最適化のため必要)
enum Choice { case a case b } var v = Choice.a switch v { case .a: print("A") case .b: print("B") }
特徴的なのが、breakしなくても次のcaseが以降は実行されません。 また、取りうる値を網羅したコードを書かなければビルドエラーとなります。この仕様は非常に便利で、実装のパターン漏れをビルド時点で検出することができバグやロジックの不備を早い段階で検出できます。 パターンを網羅しない場合はdefaultが必須になります。
let v = 2 switch v { case 1: print("1") case 2: print("2") default: print("other")
関数
ここらへんからややクセがでてきますが、定義方法がやや異なるだけで基本的にはよくある関数の仕様と同じです。
func add(x: Int, y: Int) -> Int { return x + y } let z add(x: 1, y: 2) // z: 3
関数呼出時に引数名がデフォルト必須になっています。これは好み分かれそうですが、 例えば次のようなC++のコードがあった場合、
int findIdByName(const char* name); int findIdByIdentifier(const char* ident); int findIdByPath(const char* path); findIdByName("hogehoge");
というのがSwiftであれば
func findId(name: String) -> Int func findId(ident: String) -> Int func findId(path: String) -> Int findId(name: "hogehoge")
のように書けます。どちらも同じと言えば同じですが、個人的にはSwiftの方が美しく感じます。
また、次のようにアンダースコアによって引数名が不要な関数も定義できます。
func findId(_ name: String) -> Int findId("hogehoge")
クラス
基本的にはC++系と同様です。他の言語を触っていれば戸惑うところはあまりないと思います。ただ、継承まわりの機能はC++などと比べ機能が少ないです。(シンプルと捉えるか貧弱と捉えるかは人によると思います。)
class ClassA { // 変数 var num = 10 var name: String init(name: String) { // 初期化 self.name = name } // 関数 func increase(v: Int) { num += v } } let a = ClassA(name: "hoge")
構造体
Swiftでは構造体も定義できます。構造体はクラスとほぼ同じ文法で定義できます。
struct StructA { // 変数 var num = 10 var name: String } let a = StructA(name: "hogehoge")
構造体も関数を持てますし、Initializer(init)も定義できます。ほぼクラスと同じ機能なのですが、大きな差としてクラスは参照型、構造体は値型データです。 このため変数への代入時の挙動が異なるほか、構造体は継承が行えないなどの制限があります。
Protocol
JavaやC#で言うところのInterface、C++の純粋仮想クラスが非常に近いです。
protocol SimpleProtocol { func hoge() } class SimpleClass: SimpleProtocol { func hoge() { print("hoge!") } } let proto: SimpleProtocol = SimpleClass() proto.hoge()
Dictionaryのキーに格納すためのHashableやJSONシリアライズに使用されるCodableプロトコルなどSwiftの標準フレームワーク内でも多用されています。
クロージャ
簡単にいうと無名関数です。 キモ可愛いで有名な(?)Javascriptのあいつです。
let add = (x, y) => { return x+y; }
Swift
let add = { (x: Int, y: Int) -> Int in return x + y }
実際に使用する際は、引数と返り値の型に関する情報は型推論によって省略できるため、次のような使い方になります。
let add: (Int, Int) -> Int // 関数型変数の宣言 add = { x, y in // クロージャの定義側では型に関する宣言を省略 return x + y }
サンプルコードでこの構文を初めて見た時、まさかクロージャだとは思わず、調べるのに苦労しました……。「swift in」とかで調べても全然ヒットしないですし。(さらにTrailing Closureという初見殺しの構文があるんですよ)
実はさらにここから引数名とreturnも省略することができ、
let add: (Int, Int) -> Int // 関数型変数の宣言 add = { $0 + $1 }
ここまで省略しなくても……と最初は思っていましたが、慣れてくるとかなり使います。 例えば、次のように汎用アルゴリズムに関数を渡す場合です。Pythonでいうlamda式の感覚で使用するといい感じなんじゃないでしょうか。
let array = [1,2,3] let array2 = array.map({ $0 * 2 })
Trailing Closure
日本語だと「後ろに続くクロージャ」といった感じでしょうか。個人的にはSwift内でトップレベルにわかりづらい構文でした。
簡単にいうと、関数の最後のクロージャ型引数の定義を関数呼び出し文の後に続けて記述できるという構文です。意味わからんと思います。
例えば、非同期実行処理は次のように async(execute: ()->Void) という関数に非同期実行したいクロージャを指定します。なので素直に記述すると次のようなコードになります。
DispatchQueue.main.async(execute: { print("hogehoge") })
ただ、Swift開発者はこれに満足できなかったようで、次のように最後の引数がクロージャであった場合、関数呼び出し文の後に記述できるようにしてしまいました。
DispatchQueue.main.async() {
print("hogehoge")
}
さらに引数がクロージャだけの場合はカッコも省略できます。こうして一見関数呼び出しとは思えないようなコードが誕生します。
DispatchQueue.main.async {
print("hogehoge")
}
順を追ってみていくと比較的わかりやすいですが、初見で次のようなコードをみるとイミフです。 いつか書こうと思いますが、UIフレームワークのSwiftUIでは多用されているため、そちらを使う場合は理解必須です。
let array = [1,2,3] let array2 = array.map { $0 * 2 }
Optional型
無効値状態を保持できる変数型です。無効値とはNULL, None, nil, nullptr的なやつらです。Swiftではnilで表現します。
Swiftの変数はクラス型のような参照型変数であっても無効値は代入できません。
var obj = ClassA() obj = nil // 一見C++のnullptrのように扱えそうだがエラー
Swiftではデータ型に?をつけることでOptional型を表現できます。例えばIntのOptional型はInt?で次のように宣言します。
var obj: ClassA? = ClassA() obj = nil // 無効値を設定
大きな特徴ですが、Optional型は値型に対しても指定できます。Pythonのような動的型付け言語を使用していると忘れそうになりますが、静的型付け言語では通常値型の変数に無効値を指定することはできません。
int v = NULL; // おそらく0が代入されるだけ。0と無効値の区別はつけられない string str = ""; // 無効値なのか空文字なのか謎
Swiftでは全てのデータ型に対してOptional型が使用できます。
var v: Int? = nil var str: String? = nil
例えば何かを検索するような関数があったとき、Pythonでは見つからなかったら当たり前のようにNoneで返すと思います。
def findValue(name): for i in items: if i.name == name: return i.value return None value = findValue('hogehoge') if value is not None: // hogehoge
ただ、通常の静的型付け言語では返値の型を制限する必要があるため、関数ごとに色々工夫する必要があります。
int findValue(const char* name, int& outValue) -> Int { for (const auto& i : items) { if (i.name == name) { outValue = i.value return 0 } } return -1 } int value; if (findValue("hogehoge", value) == 0) { // hogehoge }
SwiftではOptional型により無効値を扱えるため、Pythonのようにシンプルに実装できます。
func findValue(_ name: String) -> Int? { for i in items { if i.name == name { return i.value } } return nil } if let value = findValue("hogehoge") { // Optional型→非Optional型変換の簡易記法(後述) // hogehoge }
Optional型と非Optional型の相互変換
Optional型と非Optional型は別の型です。このため、相互に変換する必要があります。
非Optional型からOptional型への変換は暗黙で行われます。ただし、逆は行われません。
var value: Int = 3 var optValue: Int? = nil optValue = value // OK value = optValue // エラー
このため、unwrapという操作でOptional型から非Optional型へ明示的に変換する必要があります。
強制unwrap
変数の後ろに!をつけることにより強制的にOptional型を非Optional型へ変換できます。nilが格納されている変数をunwrapするとクラッシュします。 1番最初に紹介しといてなんだけど使わない方がいいと思う。
var value: Int = 3 var optValue: Int? = nil value = optValue! // OK optValue = nil value = optValue! // ✨DEATH✨
if-let
前述のunwrapでクラッシュさせないためには、変数がnilでないことを確認すれば良いですよね。ということは次のように書けばいいじゃんというわけです。
if optValue != nil { value = optValue! }
ただ、毎回これ書くのは美しくないと思ったのか、よりスマートな構文が用意されていて次のように書けます。
if let optValue = optValue { // 変数名はなんでもいい value = optValue // ここのoptValueは非Optional型 } else { // 必要であればnilだった時の処理 }
guard-let
「if-letのunwrapもいいけどネストが深くなるのがなぁ……nilが入っている場合はエラーだから処理スキップしたいだけなんだよ。わかってないなぁ……」って思っているなら先ほどと条件を逆にしてこんな書き方もできますよね。
if optValue == nil { return // continue, breakなどで中断 } value = optValue!
まぁこれも当然のごとくスマートな記法が用意されています。
guard let unwrappedValue = optValue else { return } value = unwrappedValue
さいごに
Swift触りはじめた時に理解しづらかったことを思い出しながら書いてみました。 間違っていることや不正確なこともたくさんあると思いますが、見つけたら優しく教えてください。
App Storeの審査
App Storeの審査といえば厳しいことで有名です。RabbitBox初版リリース時は戦々恐々としていたのですが、意外とスムーズに行えました。
ただ、分かりづらい要件などがありましたので、ハマった点などについて書きたいと思います。
初提出(アプリ機能の不備)
12月初旬、RabbitBoxがある程度アプリとして形になってきたので審査に出すことにしました。
そわそわしながら待つこと2日……審査結果は「Guideline 2.1 - Performance - App Completeness」でした。
これは操作ミスが原因でした。無謀にも初版からアプリ内課金を実装したのですが、アプリ自体とは別に「アプリ内課金」の審査があることを知らず、「アプリ内課金」を審査に提出していなかったためリジェクトされたようです。
特にアプリに問題がある訳ではないようだったので、指示通り「アプリ内課金」とビルドを再アップロードし提出しました。
リジェクト2回目(アプリ内リンクの不備)
2回目の提出でアプリ内課金に関する不備は解消されたのですが、次に「Guideline 3.1.2 - Business - Payments - Subscriptions」でリジェクトされてしまいました。
iOSアプリを使用していて気にしたことなかったですが、アプリ内にEULAとプライバシーポリシーへのリンクが必要なようでした。これに関しては次のように「RabbitBoxについて」のビュー下部にリンクを追加、審査のメモ欄に追加位置を記載して再提出しました。
3回目(Appメタデータの不備)
これでApp Storeに登録されるのを待つだけだなとワクワクしながら待っていたんですが、返ってきたのは再び「Guideline 3.1.2 - Business - Payments - Subscriptions」
Appのメタデータに EULAへのリンクがないとお叱りを受けているようのですが、Appleの標準EULAを設定している場合、リンクを指定するとかできないんですよね……
ググってみたりしてみましたが、しっくりくる解決策が見つからず……
ということで若干ヤケクソ気味でしたが、アプリ説明内にリンクを書いて再提出しました。
🍎の怒りを買ってしまうかも……と若干ビビってましたが、結果は無事通過!めでたくリリースとなりました🎉
メタデータと指摘されていたので、App情報フォームの内容ばかり見直していたのですが、アプリの説明文中に書けばよかったんですね。
そんなこんなでリリースできたRabbitBoxよろしくお願いします🙏
iPad向けの画像管理アプリRabbitBoxの紹介
昨年、M1 MacBook Airの購入をきっかけにiOSアプリの開発に挑戦してみようと思い、全くiOSアプリの開発が経験ない状態からコツコツと7ヶ月ほど開発をしていました。
先月12月、ようやくリリースできたのですが、まぁ全くダウンロードされません(笑)
ちょっとした宣伝も兼ねてこちらで紹介しようと思います。
アプリ概要
RabbitBoxは資料や素材画像などをWebから集めてストックし、タグや説明文など様々なメタデータを設定して整理や検索を行えるアプリです。
似たようなアプリはいくらでもありそうな気はしますが、いざ探してみると理想のアプリは見つかりませんでした。それなら僕の考えた最強のアプリを開発してやろうじゃないか……で出来上がったのがRabbitBoxです。
超強力なタグ機能
タグを画像に付与して管理するアプリは探せばいくらでも見つかると思います。ただ、RabbitBoxのタグ機能は一味違います。いろんな機能をぶっ込んでいます。
親子関係を持ったタグ
例えば、風景の画像を管理してるときに、山、川、ビル、道路、信号機……のようにタグをつけていきます。こんなとき、「山と川」を自然、「ビル、道路、信号機」を街並みというタグでグルーピングしたいと思いませんか?さらに「自然と街並み」を風景でグルーピングできたら最高ですよね?当然、RabbitBoxではできます!
タグの表記揺れもサポート
タグが増えてくると、どのようなタグを作ったかわからなくなってきます。特に日本語は表記揺れ多いです。例えば、「カラス、からす、烏」のように「平仮名だっけ?漢字だっけ?あれカタカナ?ああ、すでに2つある……」なんてことが頻繁に発生します。RabbitBoxでは代表となるタグ名の他にいくつでも別名をつけることができます。つけた別名はタグの付与や検索で利用できます。
その他にも便利な機能が多数
タグに説明文を設定できたり、URLを紐づけたり……。
重複、類似画像の検知機能
画像ファイルをたくさん収集していると、同じ画像や似たような画像を何枚も保存していることありますよね?RabbitBoxはそんな画像を検知する機能があります。(一部サブスクリプションにしてみてます)
同じファイルがすでにRabbitBoxに保存されている場合、ワンタップであとから追加したファイルを削除できます。
これだけでも十分便利なのですが、Webで画像を収集していると同じ画像なのにリサイズされていたりわずかに編集されて別ファイルになってしまっていることも多いと思います。RabbitBoxでは同じファイルの検出から一歩先に進んで見た目の似ている画像の検出機能が搭載されています。これはリサイズや明るさが編集されている画像でも完璧ではありませんが検出できます。
ダウンロード機能
RabbitBoxはWebページからの画像ダウンロード機能も搭載しています。元々はおまけ程度の機能だったのですが、機能追加していくうちになかなか強力な機能になりました。
- 画像のダウンロード元URLを自動格納
- ダウンロードするたびに専用のタグを自動作成しグルーピング
- Twitterやpixivなど一部のサイトではユーザ名などをタグとして自動グルーピング
さいごに
他にも色々な機能が搭載されていますが、兎にも角にもぜひダウンロードして一度お試しください。気に入ってもらえたら他の人にも是非お勧めしてもらえると嬉しいです。
マジで全然ダウンロードすらされないです😭
RabbitBox - プライバシーポリシー
RabbitBox - プライバシーポリシー
RabbitBoxの利用規約および利用者情報の取り扱いを定めたものです。
1 収集する情報
本アプリの利用に際して、以下の利用者情報を収集いたします。
1.1 広告配信
本アプリではGoogle AdMobを使用し広告配信を行なっております。広告配信にあたり AdMobが利用者情報を自動収集する場合があります。取得する情報、利用目的、第三者への提供等の詳細につきましては、以下のプライバシーポリシーのリンクよりご確認ください。
1.2 アナリティクス
本アプリでは広告配信、アプリ効果の計測を目的として、Google Firebase を利用しております。これらの計測にあたりFirebaseが利用者情報を自動収集する場合があります。取得する情報、利用目的、第三者への提供等の詳細につきましては、以下のプアイバシーポリシーのリンクよりご確認ください。
2. 連絡先
下記お問い合わせフォームよりご連絡をお願いいたします。