20210111のiOSに関する記事は19件です。

iOS Safariでも下までスクロールするモーダルの作り方

iOS Safariでモーダルを表示した際、作り方によっては画面下部のツールバーが表示されているとモーダルを下までスクロールすることができないことがあります。

おそらくheightのとり方に問題がありそうで、width: 100vw; height: 100vh;で設定しているときに発生しました。
それをtop,right,bottom,leftにすることで解決することができました。

コードとしては下記のようになります。

<div class="modal">
  <div class="modal_background"></div>
  <div class="modal_content_wrap">
    <div class="modal_content">モーダル</div>
  </div>
</div>
<style>
.modal {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 1000;
  overflow: scroll;
}
.modal_background {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}
/* モーダルをセンタリングするため */
.modal_content_wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}
.modal_content {
  height: 1000px;
}
</style>

ちなみに元の作りは下記のようにしていました。

<style>
.modal {
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1000;
  overflow: scroll;
}
</style>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter】iOS14実機で起動するとクラッシュする時の対処法

Flutterでの開発中、iOS14の実機でアプリをビルドすると、画面が真っ暗になり起動しない症状が起こりました。iOS14のシミュレーターでは問題なく起動されます。

環境(バージョン)

Flutter: 1.22.5
Xcode: 12.2
iOS: 14.2

対処法

XcodeのEditSchemeから、Debug -> Releaseに変更する。

試しに再度Debugに戻してビルドしてみたところ、クラッシュせず起動できたので、一回だけReleaseでビルドすればよいということなんでしょうか。そこのところはいまいちわかっていません。

Flutter公式に、今回の不具合?の記事が載っていました。
https://flutter.dev/docs/development/ios-14#launching-debug-flutter-without-a-host-computer

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

【初心者】~私、「Swiftを触ったことがあります。」と言えるまで〜

みなさんiOSアプリ開発わくわくしていますかー??
iOSアプリを作っていくうえでは、Swiftは避けて通れないものです。
※Objective-Cもあるけど、理由がない限りわざわざお勉強する必要はないと思っています。
Swiftを一つずつ紹介している記事が見当たらなかったので、Swiftのお作法をこちらの記事でご紹介していきます。

対象読者

  • 他のプログラミング言語に多少触れたことがある方
  • Swift言語に触れたことがない方
  • モバイルアプリ開発が初めての方
  • これからスマホアプリを開発していきたい方
  • iOS案件のプロジェクトに携わる予定の方

注意

  • ご紹介する内容は、こちらの書籍を参考にしております。
    記載内容には十分に配慮しておりますが、万が一抵触する部分がありましたらお申し付けください。
  • Playgroundsで動作済みのソースコードを記載しています。
  • バージョン5.3.2での動作を確認しています。
  • Swiftを実行するには、Xcodeが必須です。
    AppStoreでダウンロードできますので事前に行なっておいてください。
    ※この記事ではダウンロード&インストール、設定等は触れません。

Swiftリファレンス

Appleが公式にSwiftに関するリファレンスを公開しています。一度は目を通しておくといいと思います。私には理解できませんでしたが...
Swiftコミュニティーが分かりやすくまとめたサイトがありますので、こちらもオススメです。日本語への翻訳をすると、ちょっとした参考書レベルの内容が得られます。
(こっちを見たほうがいいじゃんとか言わないの。)

定数と変数

変数を定義する場合は、varを使用する。
定数を定義する場合には、letを使用する。 
※定数は定義時の値から値を変更できない

var str_var = "Hello"
let str_let = "world"
str_let = "World"     // エラー発生
print(str_var)        // Hello
print(str_let)        // world

演算

他にも様々な演算子がありますが、ここでは割愛します。
気になる方は調べてみてください。

var x = 5
var y = 3
print(x + y)    //加算
print(x - y)    //減算
print(x * y)    //乗算
print(x / y)    //除算
print(x % y)    //剰余
print(x > y)    //より上
print(x <= y)   //以下

型の定義

Swiftは型を指定せずに変数・定数を定義可能です。(型推論を行うため)
少ない記述でプログラミングが行うことが可能です。

var age_1:Int = 20 //型宣言と初期値を記述
var age_2 = 32     //初期値から型を推論
age_2 = 19         //varで宣言した変数の変更

定数も同様です。

let name_1:String = "HCS"   //後で値を変更するとエラーになる
let name_2 = "hcs"          //初期値から型を推論

型変換

Double型以外の型からDouble型への変換にはDouble()を使用する。
Int型以外の型からInt型への変換にはInt()で可能。
同様に、String(),Float()が存在する。
※型変換できない場合は、nilが代入される。(後術)

let i:Int = 5
var a:Double = Double(i)    //iの値をDouble型に変換
var j:Int = Int(a * 1.25)   //jの値は6.25
let f = Float(-9.120)
let y = Int(f)              //yの値は-9となり、小数点以下は切り捨てられる
var t:Double = 4            //実数のインスタンスが作られて代入される
var s = t * 10              //実数の変数tと演算をする実数インスタンスが作られる

文字列

文字列を結合するには、+演算子、+=演算子や.append関数も利用できる。
また、文字列の中に変数を埋め込みたい場合や、特殊文字を表示する場合には、バックスラッシュを使用することで実現可能。

let name = "情報太郎"
let msg = "こんにちは、" + name + "さん。"     //こんにちは、情報太郎さん。
var homework = "情報"
homework += "「データ構造とアルゴリズム」"      //homework = "情報「データ構造とアルゴリズム」"
homework.append("-->")

文字列中の特殊文字を表すにはバックスラッシュを使用することで実現できます。

let msg = "command=\u{2318}, option=\u{2325}"   // "command=⌘, option=⌥"

文字列の中に変数や定数を埋め込むことができます。
埋め込むには、\(変数)を利用することで実現できます。

let n = 8
let str = "\(n)の2乗は\(n * n)です。"

配列

他のプログラミング言語同様に配列が存在します。
配列に格納できる値の個数に制限はありません。
定義の仕方が幅広いので注意してください。

var a : [Int] = [2,2,2,2,2]       //型宣言と初期値を記述
var a = [2,5,3,7,5]               //初期値から型を推論
var a : Array<Int> = [1,3]        //本来の定義の仕方
var s = [String]()                //イニシャライザの呼び出し
var s:[String] = []               //型を指定した変数に空配列を代入

計算式も代入可能です。

var g = 1.2
var f = [g/2.0, g/0.3, g/0.04]
print(f)                          //[0.6, 4.0, 30.0]

配列にアクセスするには以下のように[]を利用します。
添字はゼロオリジンなので注意してください。

let digits = ["00","01","02","03"]
print(digits[2])                    //02を出力
print(digits.count)                 //配列の個数を表す

配列に要素を追加するには、.append()を利用します。
また+演算子や+=演算子も利用できます。

//.append関数を利用
var array = ["①","②","③"]
array.append("④")
print(array)

//+と+=演算子を利用
let m = array + ["5","6"]
print(m)
array += ["Ⅴ","Ⅵ"]
print(array)

インクリメントとデクリメント

Swiftでは++--とは記述できない

var a = 0
a += 1        //a_4のインクリメント
a -= 1        //a_4のデクリメント

Unicode

Unicodeを変数名として使用可能です。(実際は使わないけど)

let 用紙の幅 = 50.0
var 左の余白割合 = 0.10
var 右の余白割合 = 0.08
var 本文横幅 = 用紙の幅 * (1.0 - 左の余白割合 - 右の余白割合)

ジェネリクス(型パラメータ)

型をパラメータとしてプログラムに記述するための機能があります。
他のプログラミング言語と同様ですね。

//指定したジェネリクス型でn分のオプショナル配列を返す関数
func getNilArary<T>(n:Int) -> [T?] {
  return [T?]( repeating:nil, count:n )
}

let hoge:[Int?] = f(n:10)

配列も通常は省略しているが、型パラメータを使用している。

var a:[Int]       //通常はこちらの記法
var a:Array<Int>  //このようにも書ける(本来の記法)

モジュールのインポート

モジュール(ライブラリ)をインポートすることで様々なクラスやメソッドを利用できる。
インポートするにはimport句を使用する。

import UIKit                                //UIKitフレームワークのインポート
import PlaygroundSupport                    //playgroundで使用するためのフレームワーク

// UIKitモジュールを使用することでGUI部品を利用できる。
let color = UIColor.green                   //緑色を取得
let image = UIImage(named: "sample.png")    //ローカルの画像を取得
let imageView = UIImageView(image: image)   //画像をビュー(画面に表示するためのレイヤー)クラスに変換

if

他のプログラミング言語同様に制御構文が存在します。
else句は任意なので省略可能です。
条件式に()は不要なので注意。

if x > y {
    print("x>yは真です。")
} else {
    print("x>yは偽です?")
}

while

繰り返し制御が可能です。
ループから抜けるためのbreak文や次の繰り返しを開始するためのcontinueが使用可能です。

while x < y {
    //繰り返したい処理
}

repeat-while

while文は評価した後で繰り返し処理を行いますが、repeat-while文は最低一度は繰り返し処理を実行するという違いがあります
Javaのdo-while文と同じです。

repeat {
    //繰り返したい処理
} while x < y

for-in

繰り返しの回数が決まっている場合に、for-in文を使用することが可能となっています。
範囲や集合を表す式から1つずつ取り出し、繰り返しの処理を行います。
forの後ろに定義された定数は暗黙的に、letが記述されていることになり、値を変更できないので注意してください。
この定数はfor-in文の中でのみ有効であるため、スコープ外からは参照できないので注意。
・範囲型については以下をご覧ください。
for i in 1..<3 { } i(定数)の値は1,2となり、{}の中を2回実行
for k in 1...3 { } k(定数)の値は1,2,3となって、{}の中を3回実行

//九九の表を作成するプログラム
for i in 1 ..< 10 {
    var l = ""
    for j in 1 ..< 10 {
        let r = i * j
        if r < 10 { l += "\t" }
        l += "\(r)"
    }
    print(l)
}

where句を利用することで、くり返しに条件を付与することが可能。

//for-in文で4と7の倍数を表示しないプログラム
for i in 1..<64 where q % 4 != 0 && q % 7 != 0  {
    print(i,terminator: " ")
}

配列の中を取り出し、逐次繰り返すことも可能

//配列から一致する文字列を探すプログラム
let name = "taro"
let group = ["kojiro","itiro","jiro","taro","musashi"]
for s in group {
    if name == s {
        print("\(s)が見つかりました")
        //繰り返しをやめる
        break
    }
}

switch

他言語と大きく違う点は以下です。
- 分岐先の文の実行が終了しても、breakを記述する必要はない
- default句は必ず書かなければならない
- 分岐に使用する値の型に、文字列や構造体やクラスを使用することが可能

var num = 0
switch num {
case 0:
    print("what")   //breakがなくてもよい
case 1,2:
    print("ohh")    //複数を記述する場合はこのように記述
case 3,4:
    print("me") 
case 6:
    break           //breakを記述してもよい
default:
    print("you")    //最後にdefaultを書く
}

関数

他言語と大きく違いはありませんが、関数定義が独特であるため注意が必要です。

//単純な関数count()とreset()
var total = 0
func count(i:Int) -> Int {  //整数の引数を1つとり、整数を返す
    total += i
    return total
}
func reset() {              //引数も戻り値もなし
    total = 0
}
reset()                     //グローバル変数の初期化
count(i: 5)                 //呼び出し時には、
print("\(count(i: 10))")

func getMessage() -> String {
    return "現在の値は\(total)です。"
}
//こうも書ける(関数単体がreturnだけの場合は、return句も省略可能)
func getMessage() -> String { "現在の値は\(total)です。" }

引数ラベル

関数の引数は適切でわかりやすいものにしておくことが好ましいとされています。
他言語は関数呼び出しを行う際に変数や式を並べるのみでプログラムを読んでも引数の意味が分かりにくいです。
たとえば、カタログ番号29090の商品を15万円で1個購入するという呼び出しを、一般的には以下のように記述します。

buy(29090, 150000, 1)

Swiftでは引数にキーワードを明示して関数を呼び出すという方法が使われます。
そうすることで、関数定義を見ずともプログラムが書くことができ、効率化!というわけです。

//引数ラベルを持つ関数定義の例
func buy(product:Int, price:Int, quantity:Int) {
    print("Product:\(product), amount = \(price * quantity)")
}
buy(product: 29090, price: 150000, quantity: 1)
//長方形の縦と横の長さで面積を計算するプログラム①
func getArea(h:Double, w:Double) -> Double {
    return h * w
}
//引数ラベル(h:やw:)が表示されることで、呼び出しの際に引数間違いを防ぐことができる
let result = getArea(h: 5.0, w: 12.5)

//長方形の縦と横の長さで面積を計算するプログラム②
func getArea(height h:Double, width w:Double) -> Double {
    return h * w
}
//引数ラベルを明示的に書き足したパターン(height,widthが引数ラベル、h,wが仮引数となる)
let result = getArea(height: 5.0, width: 12.5)

結果を無視する_(アンダースコア)

戻り値を利用しない場合や変数、定数として扱う必要がない場合に利用する。
_結果を無視するという意味になる。

//下線の特殊な記法(関数の結果を無視する)
_ = area_a(h: 10.5, w: 12.5)
_ = area_b(height: 10.5, width: 13)

//仮引数の省略(_を記述することで仮引数を省略可能)
func compare(_ a:Int, _ b:Int, _:Bool) {
    // 比較の処理
}
compare(1, 2, false)

//添字を使用しないのあれば以下のように記述可能
for _ in 1...10 {
    // くり返しの処理
}

inout引数

関数の実引数はコピーされて関数に渡される。関数内での処理によって呼び出し側の変数の値を変えたい時は、inout引数を使用することで実現できる。

//値渡しと参照渡し(inout)
func swap(_ x:inout Int,_ y:inout Int) {
    let tmp = x
    x = y
    y = tmp
}

var swapX = 100
var swapY = 0
swap(&swapX, &swapY)
print("swapX",swapX,"swapY",swapY)

オーバロード

引数の型や数、戻り値の型が異なることで、同じ関数名を定義可能となる。

func overLoad(a:String) {
    print(a)
}
func overLoad(a:String) -> String {
    return a
}
func overLoad(a:Int) -> String {
    return String(a)
}

タプル

タプルとは、複数個のデータを組みにしてまとめたものことを指す。
関連し合う複数の値をまとめて扱いたいが、構造体やクラスを定義するほどではない場合に重宝する。
プログラムの中でデータを渡したり、一時的に保管したりするような用途で扱うべきであり、タプルを元にして複雑なデータ構造を作るべきではないとされている。
タプル型は値型のデータであるため、代入にたびに新しいインスタンスが生成される。

let m = ("dog.jpg",161_022)                         //ファイル名とバイト数
let cat: (String,Int,Int) = ("cat.jpg",1024,764)    //型宣言をした場合
var img : (String,Int,Int) = cat                    //互いの要素の個数と型が同じであれば代入可能

//「.0」のような記法を使うことでタプルにアクセス可能。数字に変数を指定することはできないので注意。
print(img.0,img.1,img.2)

//BIMを計算するプログラム
func BMI(tall:Double, weight:Double) -> (Double,Double) {
    let v = 22.0                    //理想的な値
    let t = tall * tall / 10000.0   //cm を m に変換
    let index = weight / t          //BMIを計算
    return (index, v * t)           //目標体重も計算して返す
}
let result = BMI(tall: 170, weight: 55) //戻り値はタプル型
print(result)

オプショナル型

Swiftでは扱う値が存在しない場合は、nilという特殊な値を用意している。
例えば整数の場合、変数や式の型はInt型だが、nilを値として持つことがある場合はInt?という型で扱う。
nilを通常の値と誤って使用してしまうことを防止する目的で導入された。(ぬるぽ防止)

var a : Int = 0                 // 整数のみ代入可能
var b : Int? = 10               // 整数+nilのみ代入可能
b = nil                         // 代入可能
let olympic = "2020"
var year : Int? = Int(olympic)  // 2020が返される
var city : Int? = Int("Tokyo")  // 整数として評価できないため、nilが返る
//以下でも同じことになる
var year_alt : Optional<Int> = Int(olympic)  //2020が返される

オプショナル型のアンラップ

オプショナルInt型(Int?)の値は整数かnilであるが、値がInt型ではないため、そのままInt型の変数に値を代入できない。
Int?型からInt型の値を取り出すように、オプショナル型からデータを取り出すことをアンラップという。
アンラップのためには、オプショナル型に対して「!」という記号を使用する

let olympicYear1 : Int? = Int("2020")
let next1 : Int = olympicYear1! + 4   //アンラップが必要
//オプショナル型の変数の値がnilだった場合は、実行時にエラーとなる。
let olympicYear2 : Int? = Int("令和2年")   //olympicYear2の値は、nil
//let next2 : Int = olympicYear2! + 4     //実行時エラーとなる

nilチェック

オプショナル型の変数、または定数は、比較の演算子「==」または「!=」を使って、格納されている値を調べることが可能。
この時、アンラップする必要がないため実行時エラーにはならない。
また、オプショナル型の値がnilでない場合に役立つ、オプショナルバインディングという記法が用意されている。

//比較演算子を利用したやり方
var nagano : Int? = Int("1995")
if nagano != nil {              //naganoがnilでない場合
    print("長野オリンピックは\(nagano!)年に開催されました")  //ここでアンラップする
} else {
    print("エラー")                //naganoがnilだった場合
}

//オプショナルバインディングを利用したやり方
let tokyo : Int? = Int("2020")
if let t = tokyo {                                  //この書き方がオプショナルバインディング!(varでもOK!)
    print("東京オリンピックは\(t)年に開催されるはずでした")  //tはInt型。アンラップは不要!
} else {
    print("エラー")                                   //tokyoがnilだった場合
}

//オプショナル型を引数とする関数
func nickname(_ name:String?, age:Int) -> String {
    if let nick = name {
        return "浪速の" + nick + "\(age)歳"
    }
    return "浪速の名無し\(age)歳"
}
print(nickname(nil, age: 33))
print(nickname("情報太郎", age:21))

あとがき

細かく記載できていなくてすみません。(タイトル詐欺だろって思わないで!)
時間があれば更新するかもしれませんので、それまではしばしお待ちください。

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

独学でAppStoreにアプリを公開するまで

はじめに

AppStoreにiOSアプリを公開するまでにやったこと・大変だったことを振り返りたいと思います。
これからiOSアプリ開発に挑戦したいと思っている方の参考になればと思います。

自己紹介

私は、実務未経験で独学でプログラミングを学習してます。
2020年の2月末から夏頃まではWebアプリ開発を学習していました。

作ったアプリの紹介記事↓
独学半年の実務未経験がRails+Nuxt.jsでSPA作ったので見て欲しい

秋頃は資格取ったりしてました。
10月末からiOSアプリ開発を学習し始めました。
12月末までに2つアプリをAppStoreに公開することができたので、アプリを紹介したいと思います。

アプリ紹介

学習から開発、申請までを時系列で振り返える前に、アプリをシンプルに紹介しておきます。

一つ目

一つ目は「タグメモ」です。
タグをつけて管理できるシンプルなメモ帳アプリです。

AppStore
リポジトリ
new_iphone12.001.jpeg

11月7日から制作を始めて同月の17日にリリースしました。

二つ目

二つ目は「StudyLevel」です。
毎日の勉強時間を記録するとレベルが上がっていく学習習慣支援アプリです。

AppStore
Untitled.png
iphone12_keynote_StudyLevel.001.jpeg

こちら、下記で紹介記事を書いているWebアプリのiOS版リメイクになります。
独学半年の実務未経験がRails+Nuxt.jsでSPA作ったので見て欲しい

11月17日から開発を始めて、12月29日にリリースしました。

時系列で振り返る

まずは学習

Twitterを振り返ってみると、10月20日から学習を始めたようです。

使用した主なリソース

まずはSwiftの基本を学習したいと思い、最初の方に購入した書籍です。
よくある説明不足の初心者向け書籍とは異なり、基礎的な部分から詳しく説明されていました。
後半部では、「どういう場面で役に立つのか」といった実践的な内容まで解説されており、学んだのどこで使えばいいのかわからないということがありませんでした。
「Webサービスとの連携」の章もあり、APIとの通信を頻繁に行う「StudyLevel」の開発の際にはとても役に立ちました。

紹介したアプリは二つともSwiftUIを使用して開発しました。
SwiftUIの基本的な考え方から、リファレンス的な内容まで詳しく説明されていました。
SwiftUIでUIKitの部品を使う方法まで載っており、SwiftUIでできないことをカバーする方法まで説明されています。
開発中にも何度も読み返しました。

どのくらいインプットに時間をかけるか

私は学習を始めてから3週間弱でアプリの開発を始めました。( 一日平均5時間ぐらい )
振り返ってみると、少し時間をかけすぎてしまったように感じます。
やはり実際にコードを書かないと頭に残りませんし、基礎的なことすらアウトプットできていないのに実践的な知識を取り入れようとしても意味がないなと感じました。
最初はさらっと学習して、開発していく中で疑問に思ったことを再度学習するのが効率的だと思います。

メモアプリを開発し始める

とりあえず簡単なアプリでいいのでAppStoreに公開してみたい!という意気込みで開発を始めました。
シンプルで機能が少ないものでもAppStoreに公開されており、これなら自分にも作れそうだ!という理由でメモアプリを作ることにしました。( しょうもない理由ですいません )

メモの保存にはRealmを使用しました。
開発を始めてから学習しましたが、高度なことしていないこともあり、特に困りませんでした。

開発は順調に進み、一週間ほどで完成させることができました。

申請する

AppStoreへの申請は、手順が多く大変です。(特に一番始めは)
「 AppStore 申請 」で検索すると、手順を説明している記事がたくさんヒットするので、参考にしながら準備を進めました。

画像の加工をあまりしたことがなかったので、アイコンやスクリーンショット(アプリの紹介用の画像)を作成するのに少し苦労しました。
スクリーンショットはKeynoteで作成しています。

参考になった記事↓

iOSアプリを登録、申請して公開するまで
App Store提出用のスクリーンショットを無料で自作する

結果を待つ

申請に関する記事を読んでいると、機能不足でリジェクトされている方が多く、恐怖していました。
が、次の日の昼ごろには審査通過の通知が届き、拍子抜けしました。

シンプルなアプリですが、AppStoreに公開されているのをみたときは感動しました。

学習管理アプリを開発し始める

Webアプリを作成している頃から、ネイティブアプリ版を作成してみたいと思っていたので、作成に取り掛かることにしました。
APIはWebアプリと同じものを流用しています。iOS版用に機能を追加したりはしました。
APIと通信して、画面に表示させるだけですむ部分も多かったので想定よりもスムーズに進みました。

大変だったこと

  • メモリ管理

それぞれの投稿にアイコン画像を表示しているため、画面に表示される画像の量が多くなってしまい、メモリ不足が発生していました。
画像を圧縮してサイズを小さくすることによって、解決しました。
加えて、通信量を減らすために画像データをRealmを使ってキャッシュするようにしました。

  • SwiftUIでは実装できない

まだ新しいフレームワークであるため、サポートされていない機能があります。
ただ、UIKitの部品をSwiftUIで使用することができるので、全く実装できないということはありませんでした。

二回目の申請

二回目なので、スムーズに準備が進みました。
メモアプリに比べて力を入れているので、アイコンやスクリーンショットの完成度を高めることにしました。
前回の時は、アイコンの手を抜いていましたが、今回は少し力を入れました。

参考にした記事↓

イラレやフォトショ要らず!アプリアイコンをCacooで作ろう

リジェクトを経験する

リジェクトされました。ただ、想定済みでした。
というのも、ユーザー投稿機能があるアプリには「ブロック・通報機能」をつける必要があるのです。
先に実装方法を考えておいたので、すぐに実装・テストして、即日再申請しました。

申請が通る

まだ不十分と言われたらどうしようかと思っていましたが、杞憂でした。
無事にAppStoreに公開され、アプリが稼働しているのを確認できました。

まとめ・反省

  • アイデアについて

メモアプリは飽和していますし、学習管理アプリもWebアプリ版の焼き直しみたいなものなので、オリジナリティに欠けると感じています。
もっと実際的なアプリを開発したいと考えているので、企画を練る段階を大事にしたいと思いました。

  • 独学について

独学の限界を感じることが多くなってきました。問題解決を全て自分でしないといけないのは、メンタル的になかなかしんどいです。調べて解決方法が出てくるならいいですが、全く原因がわからないエラーに遭遇すると絶望します。
まあ、なんとかアプリは完成できているのでどうにかして解決しているのですが、勉強効率は悪いと思います。

  • 開発のルールについて

Webアプリ版の記事でも書いているのですが、一人で開発しているとルールの徹底が難しいと感じました。コミットやブランチを切る時の粒度、開発順序、コーディングルールなどは自分の裁量と気分で簡単にねじ曲げられるので、徐々に雑になりがちです。
初期段階でルールを考えておくべきだと感じました。

これからについて

いいアイデアが浮かんだら、またアプリ作りたいと思います。
とりあえず、今月は基本情報の本番なので気を引き締めて頑張ります。
4月の応用情報も受けれたらと思っているので、そちらも勉強を進めていきます。
また、UIKitとStoryBordを使用したアプリ開発も学習したいと考えています。
良さそうな学習リソースが見つからないので、ご存知の方コメントしていだけると幸いです。

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

Apple Silicon MacでiOSアプリ開発は可能なのか?

こんにちは。都内でiOSエンジニアをやっております、 @zrn-ns です。

Apple Silicon搭載MacBook Airを使用し始めて、半月ほどが経過しました。
使うたび、その挙動の機敏さや排熱の少なさ、バッテリー持ちに感動します。

しかし、開発用途で使い始めると、これまでのIntel製チップでは発生しなかった多くの問題にぶち当たり、その対処に多くの時間を割く羽目になりました。
(既存のプロジェクトをビルドできる状態にするまでに1週間ほどを費やしました)

これからApple Silicon搭載Macを買うiOSエンジニアの方向けに、レポートを残そうと思います。

?‍♂️TL;DR

M1 Macはまだ手放しではおすすめできない。
iOSアプリ開発をこれから始める人やあまり熟練度の高くない人はIntel Macを購入したほうが無難。

オフィスワーク用途としてのコスパは申し分ない

オフィスワークやネットサーフィン等の用途であれば、このMacは間違いなく『買い』です。
普段のネットサーフィンや文字書き、オフィスワーク程度の用途であれば、間違いなく今まで出会ったノートPCの中でも最高のノートPCだと思います。

Appleが初めてリリースしたApple Silicon搭載Macであるにも関わらず、ハードウェアの不具合は非常に少なく(全く無いわけではないですが)、驚くほど快適に動作します。

ハードウェアの制約

多くはないのですが、ディスプレイ周りの不具合に遭遇したので、記載しておきます。

スリープ解除時に外付けディスプレイの挙動が安定しない

僕はMacBook AirをCalDigit TS3 Plus(Thunderbolt 3接続のドッキングステーション)に接続して利用しています。
このDockはBelkinなどの他社のDockと比べてもかなり安定して動作するのでかなり気に入って使っているのですが、このDockを経由して外付けディスプレイを接続した場合、スリープ復帰時にディスプレイの接続ができないことがあります。(体感で3日に1回ほどの頻度で発生します)

Dockの問題である可能性もありますが、手元にあるIntel Macでは発生しないため、モニター側の問題でないのは確かです。

DDC/CI系のユーティリティがうまく動作しない

僕は普段Lunarというユーティリティを使用しています。
これはDDC/CIというプロトコルを使用することで、Mac側から外部ディスプレイのコントラストや明るさを調整できるもので、これを使用することでMac側の画面の明るさと外部ディスプレイの明るさを同期させることができます。

しかしApple Silicon搭載Macでは現状DDC/CIに対応していないらしく、これらのユーティリティは動作しないようです。
M1チップの制限なのかはわかりませんが、今後解消することを祈ります。

Apple Silicon搭載MacでiOS開発はできるのか?

少なくとも僕の環境では、M1 Macでアプリ開発ができる状態が整いました。

しかしすべてネイティブで動作しているわけではなく、それなりに痛みを伴っています。

基本的にターミナルはRosettaで起動して使っている

ターミナルをRosettaを使用して開くことで、多くのツールはM1 Macで動作します。
BundlerやFastlane, CocoaPodsについてはRosettaを噛ませることで問題なく動作しました。
(一応オーバーヘッドが発生するはずですが、ほぼ体感できないレベルです)

Homebrewは基本的にRosettaを使用したほうがよさそう

実はHomebrewはM1対応のものがすでにbetaですがリリースされており、利用できる状態になっています。
しかしHomebrewがM1に対応していたとしても、Homebrew経由でインストールするツールにはM1に対応していないものがまだ多く、M1対応のHomebrewをメインで使うのはまだ厳しい印象です。

基本的には、これからしばらくの間は、HomebrewはRosettaを使用して使用することをおすすめします。

Firebase関連はSwiftPackageManagerに移した

FirebaseはこれまでCocoaPodsで管理していたのですが、M1対応は専用のブランチでbeta対応中らしく、Apple SiliconとIntel両方でビルドできるようにするために、FirebaseをSwift Package Managerに移行しました。

SwiftPM版はすでにApple Siliconへの対応が行われていますが、まだいくつか問題があります。

NimbleのアサーションマッチャーはApple Siliconでは動作しない

マッチャーライブラリのQuick/Nimbleの機能であるAssertion Matcherは、x64-86アーキテクチャでしか動作しないため、arm64アーキテクチャを採用するApple Siliconでは動作しないようです。
今後解消される可能性はありますが、fatalError()の検証ができないのは少し不便ですね。

Carthageについては多分うまく動かない

今回のプロジェクトではCarthageを使用していないため検証できていないのですが、CarthageではまだApple Siliconへの対応が不十分なようなので、うまく動作しないと思われます。

具体的には、Xcode12でCarthageを使用するには公式が提示しているワークアラウンドスクリプトを適用する必要があるのですが、これを使用するとApple Silicon Mac向けのバイナリが削除されるため、うまく動作しなくなるはずです。

その他は特に問題なさそう

Mint, CocoaPods, Swift Package Managerを使用しているプロジェクトでは、概ね正常にビルドができる環境が整いました。(おそらく同様の構成であれば、同じように動作させることは可能かと思います)

参考までに、僕の環境で正常に動作したツールの一覧を記載しておきます。

Rosetta2で動作

  • Homebrew
    • vim
    • ggrep
    • Mint
      • SwiftLint
      • LicensePlist
      • periphery
  • Bundler
    • Fastlane
    • CocoaPods
      • Quick/Nimble

ネイティブで動作

  • Xcode12, SwiftPM
    • Alamofire
    • SDWebImage
    • SwiftyJSON
    • RxSwift
    • Firebase
    • RSKImageCropper

総括

iOSアプリ開発において、現行のすべてのMacの中でもかなり上位のパフォーマンスを誇るApple Silicon搭載Macですが、しばらく使ってみて、やはりまだ手放しにおすすめはできないと感じました。

iOSアプリ開発をするとき、Xcodeから吐き出される意味不明なエラーに苦しめられることがよくあります。多くの場合、先人が同様の問題にぶち当たり、何らかのワークアラウンドが提供されていることが多いのですが、Apple Silicon搭載Macで表示されるエラーはその限りではありません。

問題の解決そのものを楽しめる人(もしくはその問題を誰かが解消するまで待てる人)でなければ、メインの開発機としてApple Silicon Macを購入するのはやめたほうが無難かもしれません。
(特にアプリ開発初心者の方にはハードルが高い可能性がありますので、もう少し様子を見ることをおすすめします。)

ただし、よくわからないエラーに多く見舞われる分、問題の調査や原因の切り分け能力はつくかと思います。
たびたび発生する意味不明なエラーを、自力で解決する気概がある方にはおすすめしますので、是非購入を検討してみてはいかがでしょうか。

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

やってみたら簡単!ディープラーニング・オセロを作って自分を負かすまで強くした話(その1)

オセロのAIアルゴリズムをディープラーニングで作成し、私が勝てないぐらいまでには強くなった、という話です。

また私の場合は2ヶ月ぐらいかかってしまいましたが、実装自体はそんなに難しくなかったので、実装方法についても説明したいと思います。

この記事でわかることは、ディープラーニングでオセロのAIアルゴリズムを作る方法です。基本的な考え方は他のボードゲームも同じなので、流用できると思います。

対象読者は、TensorFlowなどディープラーニングのライブラリを使い始めて、MNISTの数字分類など基本的な処理はできたけれど、それ以外の問題だとやり方がわからない、というような方です。

きっかけ

私の所属するエンジニアと人生コミュニティで、リバーシチャレンジなるものが開催されたことがきっかけです。このコンテストは「リバーシならどこにこだわっても良い」というルールでした。

私は、ちょうど少しまえに「将棋AIで学ぶディープラーニング」という本を読んでいて、いつかボードゲームのAIを作ってみたいなと思っていたところだったので、エントリーすることにしました。

わりと気楽にエントリーして、ディープラーニングのモデルを簡単に作ってAIに打たせてみよう、ぐらいの気持ちからスタートしたのですが、途中からだんだん「自分に勝つぐらいにしたい」と思い始めて、改善をしていったのでした。

私のオセロの腕について

「自分を負かしたっていっても、あなたのオセロの腕はどうなのよ?」という疑問もあると思います。

ハッキリ言えば弱いです。

ディープラーニングと対戦を繰り返す中で私も少しずつ強くなりましたが、だいたいこんなことを考えて打っています。

(私の思考)
1. 最初は真ん中あたりの4x4マスから抜けないように打つと良さそう
2. 盤面の端にあるマスは取られづらいからなるべく取りたい
3. 四隅は絶対に取られないので、なんとしても取りたい
4. 先読みは面倒だからしない

出来たもの

モデルの作成と訓練はTensorFlowを使いましたが、最終的にはiOSアプリ(AppStoreには出していません)になりました。GitHubからcloneすれば手元で動かすことができます。
TokyoYoshida/reversi-charenge

機能としては、AIと対戦できるほか、メンテナンスモードにすると自由に盤面を作ることができます。勝ち負けの判定機能は作っていないので、最後に自分の石の数を数える必要があります。

AIの強さは、だいたい私と対戦して8割ぐらいはAIが勝つぐらいです。なぜかたまに私が自分の石の色だけになる完全勝利をすることもあります。理由は不明ですが、このAIは先読みしないのと、後半まで自分の石をあまり増やさずに最後に追い上げる傾向があることが関係しているのかもしれません。

対戦結果の例

私が黒、AIが白です。

・最後に追い上げられて、ほぼ真っ白になってしまいました。

・隅を3つも取ったのに負けてしまった。。

対戦していくうちに、四隅だけを取ろうとしても必ずしも勝てないことや、あまり自分の石が多すぎると、パスしかできなくなってしまうことがあることがわかりました。

作り方の概要

作り方の説明に入っていきます。

盤面のデータを画像として渡すと、次に打つ手を予想してくれるようなニューラルネットのモデルを作り、訓練することでオセロAIを作ることができます。

「次の手の予想」とは、オセロは8x8マスあるので「64マスのどこが最善かを予想する分類問題」ということになります。


※画像といってもピクセルデータではなく盤面を表現した配列です

これは、AlphaGoのSLポリシーネットワークの仕組みを模したもので、教師あり学習を使って棋譜データから盤面の画像とその時に打った手をニューラルネットに学習させることで、盤面を与えると次の手を予想してくれるようになるというものです。

AlphaGoの場合はさらに様々な手法を組み合わせていますが、今回はSLポリシーネットワークだけを作りました。おまけとしてモンテカルロ木探索も組み合わせたものも作りましたが、これはAlphaGoとはだいぶ異なる方法になっています。

参考:
AlphaGoの論文(原題:Mastering the game of Go with deep neural networks and tree search)
AlphaGoを模したオセロAIを作る(1): SLポリシーネットワーク

作り方の手順は次のとおりです。

1.学習データを作る
2.モデルを作る
3.訓練する
----(今回の説明はここまで)----
4.UIを作る(ついでにモンテカルロ木探索をする)

本記事はその1なので訓練するところまでの説明ですが、オセロの手をAIに予想させるところまでは作ります。

また、実装はPythonを使いGoogle Colaboratory上で動作させていますが、Pythonが動作する環境なら何でも良いです。

1.学習データを作る

これが一番大変な作業だったりします。1つ1つ説明します。

・棋譜データをダウンロードする

学習データは、フランスのオセロ連盟が公開しているオセロの棋譜データベースWTHORを使うので、データをダウンロードしておきます。

・棋譜データをcsvに変換する

ダウンロードした棋譜データはバイナリフォーマットになっているため、これを変換用サイトでcsvに変換します。出力形式を選べますが今回は「全項目出力」を使いました。

参考:オセロの棋譜データベースWTHORの読み込み方
(このツールを作った方の記事です。)

棋譜データをcsvに変換したら、1つのファイルにマージします。

・csvデータをデコードする

データセットをcsvで読み込み、ヘッダ行を削除します。

NoteBook
import pandas as pd
import re
import numpy as np

# csvの読み込み
df = pd.read_csv("wthor棋譜データ.csv")

# ヘッダ行の削除
df = df.drop(index=df[df["transcript"].str.contains('transcript')].index)

読み込んだデータのうち、transcriptという列が実際の棋譜の情報です。
文字列が並んでいるのが分かりますが、1つの列値が1試合分のデータとなっています。

文字列はf5f6e6・・・と続いていますが、2文字で1手となっていて次のようにエンコードされています。
1文字目 ・・・ 列(a,b,c・・・h)
2文字目 ・・・ 行(1,2,3・・・8)
例えばf5なら、左から6列目、上から5行目ということです。

このままtranscriptに1試合のデータがすべて入っているのは分かりづらいので、
1行=1手となるように展開します。あわせて、文字列をデコードして列番号と行番号に変換します。

NoteBook
# 正規表現を使って2文字ずつ切り出す
transcripts_raw = df["transcript"].str.extractall('(..)')

# Indexを再構成して、1行1手の表にする
transcripts_df = transcripts_raw.reset_index().rename(columns={"level_0":"tournamentId" , "match":"move_no", 0:"move_str"})

# 列の値を数字に変換するdictonaryを作る
def left_build_conv_table():
  left_table = ["a","b","c","d","e","f","g","h"]
  left_conv_table = {}
  n = 1

  for t in left_table:
    left_conv_table[t] = n
    n = n + 1

  return left_conv_table

left_conv_table = left_build_conv_table()

# dictionaryを使って列の値を数字に変換する
def left_convert_colmn_str(col_str):
  return left_conv_table[col_str]  

# 1手を数値に変換する
def convert_move(v):
  l = left_convert_colmn_str(v[:1]) # 列の値を変換する
  r = int(v[1:]) # 行の値を変換する
  return np.array([l - 1, r - 1], dtype='int8')

transcripts_df["move"] = transcripts_df.apply(lambda x: convert_move(x["move_str"]), axis=1)

こんな感じのデータになりました。

・棋譜データを学習データに変換する

1手1手のデータを盤面に展開して学習の入力データにします。

学習データは、入力データとしてオセロの盤面、教師データとしてその時の1手です。
たとえば、最初の例にあるオセロの盤面を学習データにした場合は、次の図のようになります。
image.png

shapeは、それぞれ次のようになります。
学習データ = (バッチ数, 2(チャンネル。自分の石の配置と敵の石の配置), 8(行), 8(列))
予想データ = (バッチ数, 64(8x8の盤面の位置で左上が0番目で右下が63番目))

注意点は、入力データのチャンネルは黒→白という並びではなく、自分の石→敵の石という並びになっていることです。このようにすることで、黒が打ったデータも、白が打ったデータもこの並び方に従っていれば学習データとして利用できるようにしています。

盤面に展開するためには、1手を打ったら敵の石をひっくり返す処理を入れて、その時の盤面を記録していきます。

パスの場合は次のデータも同じプレイヤーの手になります。単純に黒の番、白の番、、と交互に読んでしまうと盤面がおかしくなるのでパスの時の判定処理を入れます。

NoteBook
# 盤面の中にあるかどうかを確認する
def is_in_board(cur):
  return cur >= 0 and cur <= 7

# ある方向(direction)に対して石を置き、可能なら敵の石を反転させる
def put_for_one_move(board_a, board_b, move_row, move_col, direction):
  board_a[move_row][move_col] = 1

  tmp_a = board_a.copy()
  tmp_b = board_b.copy()
  cur_row = move_row
  cur_col = move_col

  cur_row += direction[0]
  cur_col += direction[1]
  reverse_cnt = 0
  while is_in_board(cur_row) and is_in_board(cur_col):
    if tmp_b[cur_row][cur_col] == 1: # 反転させる
      tmp_a[cur_row][cur_col] = 1
      tmp_b[cur_row][cur_col] = 0
      cur_row += direction[0]
      cur_col += direction[1]
      reverse_cnt += 1
    elif tmp_a[cur_row][cur_col] == 1:
      return tmp_a, tmp_b, reverse_cnt
    else:
      return board_a, board_b, reverse_cnt
  return board_a, board_b, reverse_cnt

# 方向の定義(配列の要素は←、↖、↑、➚、→、➘、↓、↙に対応している)
directions = [[-1,0],[-1,1],[0,1],[1,1],[1,0],[1,-1],[0,-1],[-1,-1]]

# ある位置に石を置く。すべての方向に対して可能なら敵の石を反転させる
def put(board_a, board_b ,move_row, move_col):
  tmp_a = board_a.copy()
  tmp_b = board_b.copy()
  global directions
  reverse_cnt_amount = 0
  for d in directions:
    board_a ,board_b, reverse_cnt = put_for_one_move(board_a, board_b, move_row, move_col, d)
    reverse_cnt_amount += reverse_cnt

  return board_a , board_b, reverse_cnt_amount

# 盤面の位置に石がないことを確認する
def is_none_state(board_a, board_b, cur_row, cur_col):
  return board_a[cur_row][cur_col] == 0 and board_b[cur_row][cur_col] == 0

# 盤面に石が置けるかを確認する(ルールでは敵の石を反転できるような位置にしか石を置けない)  
def can_put(board_a, board_b, cur_row, cur_col):
  copy_board_a = board_a.copy()
  copy_board_b = board_b.copy()
  _,  _, reverse_cnt_amount = put(copy_board_a, copy_board_b, cur_row, cur_col)
  return reverse_cnt_amount > 0

# パスする必要のある盤面かを確認する
def is_pass(is_black_turn, board_black, board_white):
  if is_black_turn:
    own = board_black
    opponent = board_white
  else:
    own = board_white
    opponent = board_black
  for cur_row in range(8):
      for cur_col in range(8):
        if is_none_state(own, opponent, cur_row, cur_col) and can_put(own, opponent, cur_row, cur_col):
          return False
  return True

# 変数の初期化
b_tournamentId = -1 # トーナメント番号
board_black = [] # 黒にとっての盤面の状態(1試合保存用)
board_white = [] # 白にとっての盤面の状態(1試合保存用)
boards_black = [] # 黒にとっての盤面の状態(全トーナメント保存用)
boards_white = [] # 白にとっての盤面の状態(全トーナメント保存用)
moves_black = [] # 黒の打ち手(全トーナメント保存用)
moves_white = [] # 白の打ち手(全トーナメント保存用)
is_black_turn = True # True = 黒の番、 False = 白の番
# ターン(黒の番 or 白の番)を切り変える
def switch_turn(is_black_turn):
  return is_black_turn == False # ターンを切り替え

# 棋譜のデータを1つ読み、学習用データを作成する関数
def process_tournament(df):
  global is_black_turn
  global b_tournamentId
  global boards_white
  global boards_black
  global board_white
  global board_black
  global moves_white
  global moves_black
  if df["tournamentId"] != b_tournamentId:
    # トーナメントが切り替わったら盤面を初期状態にする
    b_tournamentId = df["tournamentId"]
    board_black = np.zeros(shape=(8,8), dtype='int8')
    board_black[3][4] = 1
    board_black[4][3] = 1
    board_white = np.zeros(shape=(8,8), dtype='int8')
    board_white[3][3] = 1
    board_white[4][4] = 1
    is_black_turn = True
  else:
    # ターンを切り替える
    is_black_turn = switch_turn(is_black_turn)
    if is_pass(is_black_turn, board_black, board_white): # パスすべき状態か確認する
      is_black_turn = switch_turn(is_black_turn) #パスすべき状態の場合はターンを切り替える

  # 黒の番なら黒の盤面の状態を保存する、白の番なら白の盤面の状態を保存する
  if is_black_turn:
    boards_black.append(np.array([board_black.copy(), board_white.copy()], dtype='int8'))
  else:
    boards_white.append(np.array([board_white.copy(), board_black.copy()], dtype='int8'))

  # 打ち手を取得する
  move = df["move"]
  move_one_hot = np.zeros(shape=(8,8), dtype='int8')
  move_one_hot[move[1]][move[0]] = 1

  # 黒の番なら自分→敵の配列の並びをを黒→白にして打ち手をセットする。白の番なら白→黒の順にして打ち手をセットする
  if is_black_turn:
    moves_black.append(move_one_hot)
    board_black, board_white, _ = put(board_black, board_white, move[1], move[0])
  else:
    moves_white.append(move_one_hot)
    board_white, board_black, _ = put(board_white, board_black, move[1], move[0])

# 棋譜データを学習データに展開する
transcripts_df.apply(lambda x: process_tournament(x), axis= 1)

上のコードを実行したとき、各変数は次のような状態になっています。
boards_black ・・・ 黒が手を打つ直前の盤面の状態
boards_white ・・・ 白が手を打つ直前の盤面の状態
moves_black ・・・ 黒が打った手
moves_white ・・・ 白が打った手

これらを使って学習データを作ります。
黒と白の盤面と打ち手をつなげて1つのデータにするだけです。

NoteBook
x_train = np.concatenate([boards_black, boards_white])
y_train = np.concatenate([moves_black, moves_white])  
# 教師データは8x8の2次元データになっているので、64要素の1次元データにreshapeする
y_train_reshape = y_train.reshape(-1,64)

学習データが出来ました。

私はWTHORから20年分のデータをダウンロードしてこの処理をしましたが、535万盤面のデータになりました。まだダウンロードしていないデータも合わせるとさらに強いAIが作れるはずです。

2.モデルを作る

いよいよモデルを作ります。

入力データは、上で説明したとおりshape=(バッチ数,2,8,8)の配列です。これを先頭でPermute(転置)をしていますがこれはTensorFlowの都合です。Conv2DにChannels First(batch, C, H, W)と指定すれば大丈夫だろうと思っていたらなぜかConv2DがChannels Firstを受け付けてくれなかったので、急きょChannels Last(batch, H, W, C)に変換しています。(これがわかっていたら最初からそういうデータを用意すればよかったです..)

続けて、カーネルサイズ3の畳み込み層を12層と、カーネルサイズ1の畳み込み層、最後にバイアス層を入れてSoftMax関数にかけています。結果として出力はshape=(バッチ数,64)の配列になります。64の要素にはオセロのマスごとに次の手の確率が0〜1の数字で入ります。1に近いほど良い手ということになります。

バイアス層ですが、tf.kerasにはバイアス層がなかったのでBiasクラスとして自作しています。

NoteBook
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from keras.engine.topology import Layer

class Bias(keras.layers.Layer):
    def __init__(self, input_shape):
        super(Bias, self).__init__()
        self.W = tf.Variable(initial_value=tf.zeros(input_shape[1:]), trainable=True)

    def call(self, inputs):
        return inputs + self.W  

model = keras.Sequential()
model.add(layers.Permute((2,3,1), input_shape=(2,8,8)))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(128, kernel_size=3,padding='same',activation='relu'))
model.add(layers.Conv2D(1, kernel_size=1,use_bias=False))
model.add(layers.Flatten())
model.add(Bias((1, 64)))
model.add(layers.Activation('softmax'))

モデルが出来たらコンパイルをします。

オプティマイザはAlphaGoと同じSGD、損失関数は分類問題なので、categorical_crossentropyを使用します。

NoteBook
model.compile(keras.optimizers.SGD(lr=0.01, momentum=0.0, decay=0.0, nesterov=False), 'categorical_crossentropy', metrics=['accuracy'])

3.訓練する

訓練をします。

訓練は、Google Colabだとタイムアウトが厳しいので、Google AI Platformのインスタンス上で実行しました。tryexceptで囲って途中で中断しても学習したモデルを保存するようにしてあります。

このあたりのことは、Google Colaboratoryで利用していたノートブックをGoogle AI Platformで実行し、さらにコマンドラインからも実行する方法にまとめてあります。

NoteBook
try:
    model.fit(x_train, y_train_reshape, epochs=600, batch_size=32, validation_split=0.2)
except KeyboardInterrupt:
    model.save('saved_model_reversi/my_model')
    print('Output saved')

AI Platformのインスタンス(NVIDIA Tesla T4 GPU)で、6時間ほど学習して4エポックほど学習したところ、val_accuracyが0.59になったので、そこで学習を止めました。

AplhaGoは2840万盤面に対して学習しaccuracyが57%だったそうです。

モデルの訓練で苦労したところと対策

最初の頃は、モデルの訓練を12時間もしたもののAccuracyが0.25程度にとどまり、実際に対戦しても弱々でどうしたものかと思っていました。

原因を調べたところ、次のような問題が見つかりました。

  • 棋譜データから盤面を展開する処理にバグがあった。このため間違った盤面を学習させていた
  • モデルのPermuteの指定方法が間違っていた。これによって、おかしな画像を学習させていた
  • Bias層の後で、reshapeして(8,8)にしてしまっていた。8分類問題になってしまっていた

ディープラーニングは、意味のないデータを読み込ませてもなんとなく答えを出してしまうので、間違いがあることに気づきにくいです。また、処理の途中の状態を抜き出してチェックしても、それが正しいのか間違っているのかは判断しづらいです。

そこで、次のような手順で調査をしました。

1. ディープラーニングの出力の質に対する基準を設定する

今回は「私に勝つ」ということが基準でした。Accuracy(正確さ)についての基準値も設定しました。Accuracyが0.25というのは他の方のオセロAIに比べても低いので、他の事例から推測して少なくとも0.35は超えてほしいと考えていました。

逆に言うとこのような基準がないと調査することもなかったので、上のような問題は見つけることが出来なかったと思います。

2. モデルの問題の原因となりうる箇所をMECEに分類しておく

大分類としてはデータ、モデル、パラメータがあります。これらをツリー状に整理した上で、ツリーの枝を1つ1つを掘り下げていきます。

これを意識しないで手当たりデータや実装を差し替えたりすると、どこまでが正しくてどこからがおかしいのか、というように範囲を狭めることができなくなってしまいます。

3. データ加工からモデルの出力までの全体を少しずつ細分化して原因を見つける

2.と同じような話ですが、こちらはデータの流れに注目して細分化していきます。
例えば、ニューラルネットにすべて0などの単純な値を入力してみて、特定の層の出力を確認してみるといったことをすると、問題に気がつくことがあります。(全くわからないことの方が多かったですが...)

AIに打ち手を予想させてみる

学習が終わったら、打ち手の予想ができる状態になっています。試しに予想してみましょう。

NoteBook
# オセロの初期の盤面データを与える
board_data = np.array([[
[[0,0,0,0,0,0,0,0],
 [0,0,0,0,0,0,0,0],
 [0,0,0,0,0,0,0,0],
 [0,0,0,0,1,0,0,0],
 [0,0,0,1,0,0,0,0],
 [0,0,0,0,0,0,0,0],
 [0,0,0,0,0,0,0,0],
 [0,0,0,0,0,0,0,0]],

[[0,0,0,0,0,0,0,0],
 [0,0,0,0,0,0,0,0],
 [0,0,0,0,0,0,0,0],
 [0,0,0,1,0,0,0,0],
 [0,0,0,0,1,0,0,0],
 [0,0,0,0,0,0,0,0],
 [0,0,0,0,0,0,0,0],
 [0,0,0,0,0,0,0,0]]]],dtype=np.int8)

# 打ち手を予想する
model.predict(board_data)  
# 出力結果
# array([[2.93197723e-11, 1.42428486e-10, 7.34781472e-11, 2.39318716e-08,
#         1.31301447e-09, 1.50736756e-08, 9.80145964e-10, 2.52176102e-09,
#         3.33402395e-09, 2.05685264e-08, 2.49693510e-09, 3.53782520e-12,
#         8.09815548e-10, 6.63711930e-08, 2.62752469e-08, 5.35828759e-09,
#         4.46924164e-10, 7.42555386e-08, 2.38477658e-11, 3.76452749e-06,
#         6.29236463e-12, 4.04659602e-07, 2.37438894e-06, 1.51068477e-10,
#         1.81150719e-11, 4.47054616e-10, 3.75479488e-07, 2.84151619e-14,
#         3.70454689e-09, 1.66316525e-07, 1.27947108e-09, 3.30583454e-08,
#         5.33877942e-10, 5.14411222e-11, 8.31681668e-11, 6.85821679e-13,
#         1.05046523e-08, 9.99991417e-01, 3.23126500e-07, 1.72151644e-07,
#         1.01420455e-10, 3.35642431e-10, 2.22305030e-12, 5.21605148e-10,
#         5.75579229e-08, 9.84997257e-08, 3.62535673e-07, 4.41284129e-08,
#         2.43385506e-10, 1.96498547e-11, 1.13820758e-11, 3.01558468e-14,
#         3.58017758e-08, 8.61415117e-09, 1.17988044e-07, 1.36784823e-08,
#         1.19627297e-09, 2.55619081e-10, 9.82019244e-10, 2.45560993e-12,
#         2.43100295e-09, 8.31343083e-09, 4.34338648e-10, 2.09913722e-08]],
#       dtype=float32)

出力結果は、マスごとの配列なので、最大値をとると打ち手を取得することができます。

NoteBook
np.argmax(model.predict(board_data))  

# 出力結果
# 37

左上から37番目が最善手のようですね。

最後に

いずれ、その2を書きたいと思います。

その2は、iOSデバイス上での動作となります。ディープラーニングをiOSデバイスで動作させる方法や、ミニマックス法やモンテカルロ木探索を取りれようとして失敗した話などを書こうと思います。

(書かなかったらすいません)

また、NoteではiOS開発、とくにCoreML、ARKit、Metalなどについて定期的に発信しています。
https://note.com/tokyoyoshida

Twitterでも発信しています。
https://twitter.com/jugemjugemjugem

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

YouTubeアプリからのshareがFlutterアプリで受け取れずにハマった話

ShareExtensionを利用したFlutterアプリを作っていて、なぜかYouTubeアプリからのshareだけ受け取れなくてハマった...という、誰の役に立つのか分からない、とてもニッチなお話。
(普段iOS開発してないエンジニアの備忘録)

解決したかったこと

receive_sharing_intentプラグインを利用したアプリを作っている。
こちらの記事を参考にさせてもらった)

URLを受け取りたいアプリなので、サンプルに倣い、URL部分だけを使ってほぼほぼ問題なく動くようになった:open_hands:
ただ、なぜかYouTubeアプリからだけURLが受け取れない :frowning2:

shareの候補としてアプリは表示されるものの、アプリを選択してもなぜか何も起こらない。

どうやらSwiftレベルで、何かがうまく動いてない様子。 :thinking:

ログを仕込んでデバグしたいのだが...

Swiftまわりデバグしてていつもハマるのはログ。
Swiftでprint等でログを仕込んでも、Flutterのコンソールには何も表示されず。
何が起きてるのかさっぱりわからん:ghost:

「じゃあ、XCodeで実行したらなんか見えるかなー」となり、実行してみるが
「XCodeのコンソールも何も出ねーじゃーん」...となりーの、
View -> Debug Area -> Activate Consoleをして初めてログが見えることを知る。
ただ、それでもprintNSLogで仕込んだログは表示されず。
(見落としてるだけかなぁ。。。)

ひとまずの打開策

辿りついた自分のwork aroundは、"NSLogを使ってコンソールアプリでログを見る"。

NSLogならここに表示された:raised_hands:

で、本筋の問題

問題だったのは、typeの判定。
サンプルで言えば、if attachment.hasItemConformingToTypeIdentifier(urlContentType)のところで引っかかってた。

iPhoneのYouTubeアプリからshareは、なぜかURLではなく、Plain Textとしてシェアされていた。
なので、if attachment.hasItemConformingToTypeIdentifier(textContentType)の部分も実装することで解決。

何故なのかはいまいちよくわからぬ。
渡ってくるのは間違いなくURLなのだが... :thinking:

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

iOS14以降のPHAuthorizationStatus

はじめに

iOSでは、カメラロールにアクセスするためには、ユーザに対して適切に権限をリクエストする必要があります。
iOS14になり、PHAccessLevel や PHAuthorizationStatus.limited が追加され、挙動が複雑になったので、調べたことの備忘録です。
「写真のアクセスを許可」の状態に応じた PHAuthorizationStatus と、できることのまとめです。

iOS13以下の場合

確認: iOS13.4.1

「写真のアクセスを許可」 authorizationStatus() requestAuthorization()
未確認 .notDetermined ○ (表示される)
読み出し/書き込み .authorized × (表示されない)
許可しない .denied ×

iOS14以降の場合

確認: iOS14.2.1

未確認

「写真のアクセスを許可」 authorizationStatus() authorizationStatus
(for: .addOnly)
authorizationStatus
(for: .readWrite)
requestAuthorization
(for: .addOnly)
requestAuthorization
(for: .readWrite)
未確認 .notDetermined .notDetermined .notDetermined

requestAuthorization(for: .addOnly) のみ確認

  • 設定画面の状態

  • authorizationStatusの結果
「写真のアクセスを許可」 authorizationStatus() authorizationStatus
(for: .addOnly)
authorizationStatus
(for: .readWrite)
requestAuthorization
(for: .addOnly)
requestAuthorization
(for: .readWrite)
写真の追加のみ
(requestAuthorization(for: .addOnly)で「OK」)
.notDetermined .authorized .notDetermined ×
なし
(requestAuthorization(for: .addOnly)で「許可しない」)
.notDetermined .denied .notDetermined ×

requestAuthorization(for: .readWrite) のみ確認

  • 設定画面の状態

  • authorizationStatusの結果
「写真のアクセスを許可」 authorizationStatus() authorizationStatus
(for: .addOnly)
authorizationStatus
(for: .readWrite)
requestAuthorization
(for: .addOnly)
requestAuthorization
(for: .readWrite)
選択した写真
(requestAuthorization(for: .readWrite)で「選択した写真」)
.authorized .limited .limited × ×
すべての写真
(requestAuthorization(for: .readWrite)で「全ての写真へのアクセスを許可」)
.authorized .authorized .authorized × ×
なし
(requestAuthorization(for: .readWrite)で「許可しない」)
.denied .notDetermined .denied ×

requestAuthorization(for: .addOnly) と requestAuthorization(for: .readWrite) の両方を確認

  • 再現手順
    authorizationStatus(for: .addOnly) と authorizationStatus(for: .readWrite) のどちらか片方だけが .notDetermined の時に、.notDetermined の方のAccessLevel で requestAuthorization する

  • 設定画面の状態

  • authorizationStatusの結果
「写真のアクセスを許可」 authorizationStatus() authorizationStatus
(for: .addOnly)
authorizationStatus
(for: .readWrite)
写真の追加のみ .denied .authorized .denied
選択した写真 .authorized . limited . limited
すべての写真 .authorized .authorized .authorized
なし .denied .denied .denied

iOS14以降にできるだけアルバムに写真を保存する

やりたいこと

  1. アルバムを指定して保存できる時は、アルバムを指定して保存する
  2. アルバムを指定して保存できない時は、カメラロールに保存する
  3. カメラロールにも画像を保存できない時は、何もしない

アルバムを指定して保存できる時は、アルバムを指定して保存する

readWrite の AuthorizationStatus が .authorized の時に保存できる

PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized

アルバムを指定して保存できない時は、カメラロールに保存する

addOnly の AuthorizationStatus が .authorized もしくは .limited の時に保存できる

PHPhotoLibrary.authorizationStatus(for: . addOnly) == .authorized or . limited

判定器

class AuthorizationStatusChecker {
    enum PhotoLibraryAuthorizationStatus {
        case readWrite
        case addOnly
        case denied
    }
    static func checkPhotoLibrary(handler: @escaping (PhotoLibraryAuthorizationStatus) -> Void) {
        let readWrite = PHPhotoLibrary.authorizationStatus(for: .readWrite)
        let addOnly = PHPhotoLibrary.authorizationStatus(for: .addOnly)
        switch (readWrite, addOnly) {
        case (.authorized, _):
            handler(.readWrite)
        case (.notDetermined, _):
            PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_: PHAuthorizationStatus) in
                Self.checkPhotoLibrary(handler: handler)
            }
        case (_, .authorized), (_, .limited):
            handler(.addOnly)
        case (_, .notDetermined):
            PHPhotoLibrary.requestAuthorization(for: .addOnly) { (_: PHAuthorizationStatus) in
                Self.checkPhotoLibrary(handler: handler)
            }
        default:
            handler(.denied)
        }
    }
}

使い方

AuthorizationStatusChecker.checkPhotoLibrary { [weak self] (status) in
    switch status {
    case .readWrite:
        self?.saveToAlbum(name: "MyAlbum")
    case .addOnly:
        self?.saveToCameraRoll()
    case .denied:
        break
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリ開発者だからこそ知っておきたい、データベース論理設計の基礎

はじめに

こんにちは、iOSエンジニアの dayossi です。

家族が幸せになれるサービスを提供したいと思って、
HaloHaloという家族日記アプリをリリースしています。

データベース設計を本格的に勉強しはじめて、
「ちゃんと勉強しとかないとまずいな」と感じたので
学んだことをアウトプットします。

本記事の視点

これまでドキュメント型データベースを扱ってきたのですが、
リレーショナル型データベースの考え方や扱い方が設計段階から違う点があったので

リレーショナル型データベースの論理設計段階のざっくりとしたポイントを押さえつつ、
アンチパターンがなぜアンチパターンなのかを考えたいと思います。

そもそもデータベースってなんだ?

データベースは、情報を格納し、検索や更新など整理も容易にしてくれる箱のようなものです。

よく使用される 2つのデータベースを比較すると、以下のような特徴があります。

データベースの大分類 詳細分類 データモデルの特徴 SQL使用
リレーショナルデータベース 行・列で構成される表構造 使用する
NoSQL key-value型
カラム型
カラムストア型
グラフ型
ドキュメント型
各詳細型によって異なる 使用しない

例えば、AWSのRelational DataBaseはリレーショナルデータベースに、
FirebaseのCloud Firestoreは、NoSQLのドキュメント型にそれぞれ該当します。

今回は、リレーショナルデータベースにフォーカスしていきます。

リレーショナルデータベース(RDB)の特徴

以下、リレーショナルデータベースを RDB と略して記載します。

RDBでは、行と列からなる表のようなデータ構造を持ちます。

テーブル カラム1 カラム2 カラム3
レコード1
レコード2
レコード3

類似したデータのまとまりをテーブル、縦の行をカラム、横の列をレコードと呼びます。

カラムには、同じ定義のデータが並び
レコードは、カラムごとのデータをまとめて閲覧できる構造となります。

users user_id email password user_name job
レコード1 11111 hoge@.com HOGEHOGE ほげほげ サーバーサイドエンジニア
レコード2 22222 piyo@.co.jp PIYOPIYO ぴよぴよ iOSエンジニア, Androidエンジニア
レコード3 33333 fuga@.ne.jp FUGAFUGA ふがふが デザイナー, SE

キーと呼ばれる、テーブル内のデータを個別に認識するカラムを
決めるのが一般的です。

上記の users テーブルで、user_id がキーとなっている場合
user_id は一意のものとして認識されます。

そのため、email や passwordなどが重複していても
user_id が重複していなければ一意のレコードとみなされます。

一意なデータの集まりであることを保証するため、
RDBでは正規化という作業を行います。

正規化のポイント

まず正規化の目的は、

  • 人間が理解できる形で、現実世界の事実を表現する
  • 事実の格納方法から冗長性を排除し、データの異常や不整合を防ぐ
  • 整合性製薬をサポートする (5より引用)

正規化を行う際のポイントは、以下の5つです。

  • 行・列いずれも、順番は決まっていないこと
  • 値が同じ行は存在しないこと
  • すべての列は、値の型が決まっており、1つの値だけが入っていること
  • 値にテーブル内独自の意味をもたせない

先ほどの usersテーブルを、もう一度みてみます。

users user_id email password user_name job
レコード1 11111 hoge@.com HOGEHOGE ほげほげ サーバーサイドエンジニア
レコード2 22222 piyo@.co.jp PIYOPIYO ぴよぴよ iOSエンジニア, Androidエンジニア
レコード3 33333 fuga@.ne.jp FUGAFUGA ふがふが デザイナー, SE

jobの部分に複数の値が入っていますので、
ここは正規化を行うべき箇所となります。

users user_id email password user_name job_1 job_2
レコード1 11111 hoge@.com HOGEHOGE ほげほげ サーバーサイドエンジニア
レコード2 22222 piyo@.co.jp PIYOPIYO ぴよぴよ iOSエンジニア Androidエンジニア
レコード3 33333 fuga@.ne.jp FUGAFUGA ふがふが デザイナー SE

正規化は最大で5段階までありますが、今回は1段階目までにとどめます。

また、user_idが番号順に並んでいるように見えますが、
これは順番に整列するために保持しているのか確認が必要そうです。

RDBは、正規化された複数のテーブルを関連づけることで
以下のような個性を発揮します。

更新作業が早い

・関連のあるデータテーブルのキーを保持することで
高速でデータを更新することができます。
 
そのため、正規化とよばれる方法で
一つのテーブルが肥大化しないよう、小さなテーブルに区切って関連づける作業が重要となります。

また正規化を行うと、データが必ず一意であることも保証できるので
メンテナンスも容易となります。

設計によって、検索に時間がかかることも

・一方、正規化を行うとテーブルを細分化したためにネストが深い構造となるため、
特定のデータを取り出したい場合は時間がかかることがあります。

・検索時には、関連づけられた複数のデータテーブルを
元の1つの巨大なテーブルにつなげ直す作業が内部で発生することがあります。

このデータテーブルをつなげ直す作業は負荷が大きいため、
頻発すると検索処理がどんどん遅くなっていきます。

正規化について一長一短があります。

ですが、そもそもデータが重複していては検索も更新も容易にできません。

もし正規化を行わなかった場合、データが重複したまま
データテーブルは大きくなっていきます。

結果、検索でも更新でもバグが発生しやすくなり、余分な時間がかかるリスクが残ります。

バグ発生のリスクを抑える正規化は行う前提で、
いかに検索もしやすいデータテーブルを設計するかがポイントとなります。

まとめ

UserDefaultだと取り扱いにくい、APIレスポンスを保存する場合などで
RDBは威力を発揮してくれます。

RDBを操作する際はSQL言語が必要となりますが、
RealmなどSQLの取り扱いを簡単にしてくれるライブラリもありますので
iOSアプリ開発にも取り入れやすいと思います。

また、RDBとNoSQLのいいとこ取りである、NewSQLなるものもあります。

NoSQLが注目されやすいですが、
企業ではまだまだSQLも活用されているので

ちゃんとデータベースの基礎も知っておくほうが
今後のために良さそうだなと思います。

参照・引用元とキーワード

情勢
1. ついに刈り取られ始めたOracle Database

データベース:
2. NoSQLの比較・ランキング・おすすめ製品一覧
3. 【3分解説】データベースの種類とは? (リレーショナル型データベース)
4. 【3分解説】データベースの種類とは? (key-value型データベース)
5. SQLアンチパターン
6. 2020年現在のNewSQLについて

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

[Swift]Decodable / Generics を利用して API を叩く

この記事で書くこと

・Swift で API を叩く方法

この記事を書いた理由

・忘れた時に確認するため

コード

Sample.swift
import Foundation

// T: Decodable プロトコルを準拠させた構造体
internal struct Sample {
    static func fetchAPI<T:Decodable>(url url:String,completion: @escaping (T) -> Void){

        guard let urlComponents = URLComponents(string: url) else { return }

        // クエリを使用しデータを絞り込む場合は queryItems プロパティに設定
        //urlComponents.queryItems = [URLQueryItem(name: "name", value: "value"),]

        let task = URLSession.shared.dataTask(with: urlComponents.url!){ data,response,error in
            guard let jsonData = data else { return }

            do {
                let decodedData = try JSONDecoder().decode(T.self,from: jsonData)
                completion(decodedData)
            } catch {
                print(error.localizedDescription)
            }  
        }
        task.resume()
    }
}
HowToUse.swift
fetchAPI(url:"https://sample.com",completion:{(data) in /* ここで data に対して処理を実行 */ })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iPhoneアプリにログイン機能をつけてもappleログインがないと審査が通らない

firebaseを使って、TwitterログインとGoogleログインはつけたけれど、
appleログインができないと、リジェクトされる。
知らなかった?
審査メッセージ↓
スクリーンショット 2021-01-11 12.11.57.png

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

FlutterでstatelessなTextFieldを作ったので宣伝したい

はじめに

FlutterやReactにおいて、大きな関心ごとの一つに状態管理がある。
TextFieldはその性質上、自ら状態を持つ設計になりやすい。
しかし、TextFieldの入力情報をcallbackで返し、TextFieldの引数に現在値を与えることでTextFieldが状態を持たないようにできる。
ユースケースはProviderなどの状態管理ツールを入れているプロジェクトに限定されると思うが、私が欲しいと思ったので、今回作成した。
ついでに勉強がてら公開してみた。

メリット

  • TextFiledの状態を自前で管理できるようになる(Provider, redux, Reverpod, BLoc, etc...)
  • 他のwidgetの状態と連動させやすい

デメリット

  • 自前で状態管理するコードを書く必要がある
  • rebuildのタイミングを把握していないと、文字列入力中にwidgetがrebuildされ、文字が消えることがある

先行記事

Flutterにおける状態管理は様々な手法がある。こちらの記事が非常によくまとまっていると感じた。
この記事では状態管理方法については言及せず、外側で状態管理をしやすいTextFieldについて記述する。

こちらの記事と全く目的が同じである。
しかし、こちらの記事では状態管理を完全に外だししているわけではなく、Stateに状態を持たせてしまっているため、外からTextFieldに表示したい文字列を与えても変化しない課題がある。
WidgetとStateの関係についてはこちらの記事がとても参考になった。

使い方

こちらのリポジトリに公開している。
また、pub.devにも公開している。

exampleを見てもらうのが一番早いが、こちらにもコードを載せておく。
githubやpub.devが最新版となるため、そこはご了承を...

class _MyHomePageState extends State<MyHomePage> {
  String _message = "";

  void _setMessage(String str) =>
      setState(() => _message = str);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            StatelessTextField(
              initialValue: _message,
              style: Theme.of(context).textTheme.headline4,
              decoration: InputDecoration(hintText: "first message field"),
              onSubmitted: _setMessage,
            ),
            StatelessTextField(
              initialValue: _message,
              style: Theme.of(context).textTheme.headline6,
              decoration: InputDecoration(hintText: "second message field"),
              onSubmitted: _setMessage,
            )
          ],
        ),
      ),
    );
  }
}

簡単に説明すると、縦に並ぶ二つのStatelessTextFieldinitialVlaue_messageを与えて、onSubmitted_messageを更新してrebuildするexampleである。
このようにStatelessTextFieldは外側で状態管理をしたいときに有効である。

実装(使うだけなら知らなくてもOK)

TextFieldに初期値を与えるためには、TextEditControllerを保持しなければならないため、やむなくStatefulWidgetをextendsして作成した。
が、使用感はStatelessWidgetなので、stateless_textfieldと命名した。

exampleのような構成ではkeyを正しく与えないとWidgetとStateの組み合わせがずれてしまう場合があると思うが、Stateに状態を持たない構成なので、ずれてしまっても問題ない。
なんとなく気持ち悪いと思うが、悪影響はなさそうなので、このような実装にした。

TextEditControllerが曲者

TextFieldに初期値を与えるためにはTextEditControllerを用意しなければならない。
また、これはちゃんとdispose()を呼ばないとメモリリークする危険物であるため、そのハンドリングのためにStateで保持している。
逆に言えば、Stateで保持しているのはこれだけである。(たまにちゃんとdisposeしていないサンプルを見かけるが、大丈夫なのだろうか...)

最後に

Flutterの勉強を初めて半月くらいですが、WidgetとElementの関係や、公式の提供するWidgetの豊富さが面白く、「Flutter凄いー」となっています。
フロントエンドを例えに出すとReact + Material-UIといった感じですね。サクサク作れます。
Flutterの勉強がてら、自分用のflutter_templateを作っていたところ、状態を持たないTextFieldが欲しくなったので作りました。

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

【Swift5】メモ:既存のプロジェクトにCoreDataを追加する

はじめに

アプリ作成時に「Use Core Data」にチェックをつければ自動でこれらが追加されますが、後から使いたくなった時は少し面倒だったので、コピペで使えるようにまとめておきます。

AppDelegate.swift

import CoreData

iCloudにデータを保管できるようにする場合はNSPersistentCloudKitContainer、そうでない場合はNSPersistentContainerを使います。

// MARK: - Core Data stack

lazy var persistentContainer: NSPersistentCloudKitContainer = {
    /*
    The persistent container for the application. This implementation
    creates and returns a container, having loaded the store for the
    application to it. This property is optional since there are legitimate
    error conditions that could cause the creation of the store to fail.
    */
    let container = NSPersistentCloudKitContainer(name: "coredataSample")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

            /*
            Typical reasons for an error here include:
            * The parent directory does not exist, cannot be created, or disallows writing.
            * The persistent store is not accessible, due to permissions or data protection when the device is locked.
            * The device is out of space.
            * The store could not be migrated to the current model version.
            Check the error message to determine what the actual problem was.
            */
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()

// MARK: - Core Data Saving support

func saveContext () {
    let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
      }
}

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

[Xcode]UIScrollViewを最低限動かすための手順・やり方

iOS開発の学習をしています。

最近学んだ中で、特にUIScrollViewの扱いについてはかなり苦労しました。
そのUIScrollViewについて最低限動かすということに焦点を絞って備忘録的に書いています。

「なんだか良く分からないけど、誰かの記事見てそのまま書いて、動いたからオッケー!」
とはならないように、初学者なりに理屈を考えながら実装を試みました!
足りない部分も多々ありますが、私と同じような初学者の方々の参考になれば幸いです。

概要

新規プロジェクトを作成後、storyboradを使用しUIScrollViewをスクロールするところまでを実践する。

環境

  • macOS Catalina 10.15.7
  • Xcode - 11.3.1

完成品

Image from Gyazo

内容

  • UIScrollView
  • UIView
  • UILabel * 2

実践

①ViewにScrollViewを設置

Main.storyboard→View Controller→Viewを選択し、
ViewにScrollViewを設置する。

scroll-1.png

後ほど制約をつけるのでどこでも良いです。ちょっと大きめにしておいた方が見やすいかも。

②ScrollViewに制約をつける

先ほど設置したScrollViewにAutoLayoutで制約を加えていきます。
Add New Constraintsから設定していきます。
ScrollViewのサイズを決める制約となります。
親Viewに対しての余白を決めることで、サイズを決めています。

数字はなんでも良いですが今回は上下に50,左右に0とします。

scroll-2.png

Add 4 Constraintsを選択すると、ScrollViewに以下の4つの制約が追加されます。

- Safe Area.trailing = Scroll View.trailing
- Safe Area.bottom = Scroll View.bottom + 50
- Scroll View.leading = Safe Area.leading
- Scroll View.top = Safe Area.top + 50

また、ScrollViewに対し赤いラインが表示され"Scrollable Content Size Ambiguity"という制約エラーが出ます。

scroll-3.png

scroll-4.png

これは直訳の通り"スクロール可能なコンテンツのサイズが曖昧"という警告です。

補足:Scrollable Content Size Ambiguityとは?

では、このエラーは一体なんなのか?ということを考えていきます。

エラー文によると、

The scrollable size of a UIScrollView is computed automatically based upon the constraints of its subviews.

訳:UIScrollViewのスクロール可能なサイズは、そのサブビューの制約に基づいて自動的に計算されます。
スクロール可能なコンテンツの幅と高さを完全に定義するには、制約が必要です。

という内容が記載されています。
どうやらエラー文で指摘を受けたスクロール可能なコンテンツのサイズを定義するためにサブビューを制約する必要があるようです。

補足2:サブビューとは?

Xcodeのレイアウトはオブジェクトを上に重ねていくような構造になっており、ユーザから見て手前にあるビューをサブビュー(subview)、奥にあるビューをスーパービュー(superview)というようです。

scroll-debug.png

こんな感じ。(画像は完成品のレイアウト)

エラーは一旦スルーで

上記の理由から、現状ScrollViewはサブビューを持っていないのでエラーを解消することは出来ません。
ScrollViewの中にContentsViewなどを設置してからの対応となります。

③ScrollViewの中にViewを設置

scroll-5.png

ただのViewです。
分かりやすくするために、ScrollViewを水色、新たに設置したViewを灰色にしています。

このViewですが、名前は慣例的に"ContentsView"などとするのが良いらしいです。
本記事でも、以後ContentsViewと記載します。

④ContentsViewとContentLayoutGuideに制約をつける

こちらの記事がとても分かりやすく、大変参考になりました。
【Swift】Align制約の使い方。複数部品を整列する制約を追加する。(Swift 2.1、XCode 7.2)

scroll-6.png

コマンドキー押して2つ同時選択し、AutolayoutのAlignを選択。
Add New Alignment Constraintsから上下左右を固定します。

上4つを0にしてチェックを入れます。

  • Leading Edges
    部品の左端から他の部品の左端までの水平方向の距離を指定
  • Trailing Edges
    部品の右端から他の部品の右端までの水平方向の距離を指定
  • Top Edges
    部品の上端から他の部品の上端までの垂直方向の距離を指定
  • Bottom Edges
    部品の下端から他の部品の下端までの垂直方向の距離を指定

以下の制約が追加されます。

- ContentsView.top = Content Layout Guide.top
- ContentsView.leading = Content Layout Guide.leading
- ContentsView.bottom = Content Layout Guide.bottom
- ContentsView.trailing = Content Layout Guide.trailing

この制約手法は、複数のオブジェクトを選択した場合のみ使える制約となっており、一つしか選択していない状態だと、チェックを入れることが出来ません。

ではこれらはどういう制約なのか考えてみます。
Leading Edges 部品の左端から他の部品の左端までの水平方向の距離を指定とあるように2つの部品の距離を指定するものです。

例えばLeading Edgesを0にすることより、
(部品A)ContentLayoutGuideの左端と、(部品B)ContentsViewの左端の
距離を指定した"0"にすることができる。
要するにくっつくと言うことです。

※あくまで現時点の自分の理解では、オブジェクトの開始位置を、他のオブジェクトに依存して決める設定というイメージ。

4箇所指定することで、ContentLayoutGuideとContentsViewの4隅の位置を固定する制約となっている。

⑤FrameLayoutGuideとContentsViewの横幅を合わせる

上記④ではContentLayoutGuideに合わせてContentsViewの位置を設定したが、
ContentsView自身のサイズを決めていないので、こちらも設定していきます。(当然エラー状態となっている)

はじめにContentsViewの横幅を設定します。
高さはContentsView自身で定める必要があるが、横幅はScrollViewの枠に合わせる形が良いと思われます。(※一般的な縦のみスクロールの場合)

ContentsViewからcontrolキーを押しながらFrameLayoutGuideへドラッグし、Equal Widthsを選択。(青いガイドラインが出る)

scroll-7.png

追加された制約

- ContentsView.width = 0.57971 × Frame Layout Guide.width
  (数値は現在のContentsViewのサイズにより変動)

この時、元々のContentsViewの横幅を元に上記制約が作られてしまいます。
直すには制約のSize inspectorからMultiplier1に設定することで、ContentsViewをScrollViewの横幅目一杯に広げることができます。

scroll-8.png

⑥ContentsViewの高さについて制約をつける

続いて高さを決めていきます。
ContentsViewを選択し、AddNewConstraintsへ。
上の余白を0、高さを1000とした。

scroll-9.png

追加された制約

- ContentsView.top = Frame Layout Guide.top

これに加えてContentsView自身にもheight = 1000という制約が追加され、これで高さ&位置を確定することが出来ます。

これで制約エラーは解消されます!

ContentsViewを選択した状態でスクロールしてみてください。
動くハズです!

⑦Labelを配置する

ここからはおまけです。
ContentsViewになんでも好きなものを配置して動かしている気分を味わっていきましょう。

scroll-10.png

完成!

これで冒頭の完成品となりました!

後語り

いかがだったでしょうか?
まだまだ理解出来ていない部分も多いので、学習を進めていく過程で随時更新していきたいと考えています。

また、ContentLayoutGuideFrameLayoutGuideに関しては全然理解が追いついていないので、今後の課題と思ってしっかり学習していこうと思います!

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

[Xcode]UIScrollViewを最低限動かすための手順

iOS開発の学習をしています。

最近学んだ中で、特にUIScrollViewの扱いについてはかなり苦労しました。
そのUIScrollViewについて最低限動かすということに焦点を絞って備忘録的に書いています。

「なんだか良く分からないけど、誰かの記事見てそのまま書いて、動いたからオッケー!」
とはならないように、初学者なりに理屈を考えながら実装を試みました!
足りない部分も多々ありますが、私と同じような初学者の方々の参考になれば幸いです。

概要

新規プロジェクトを作成後、storyboradを使用しUIScrollViewをスクロールするところまでを実践する。

環境

  • macOS Catalina 10.15.7
  • Xcode - 11.3.1

完成品

Image from Gyazo

内容

  • UIScrollView
  • UIView
  • UILabel * 2

実践

①ViewにScrollViewを設置

Main.storyboard→View Controller→Viewを選択し、
ViewにScrollViewを設置する。

scroll-1.png

後ほど制約をつけるのでどこでも良いです。ちょっと大きめにしておいた方が見やすいかも。

②ScrollViewに制約をつける

先ほど設置したScrollViewにAutoLayoutで制約を加えていきます。
Add New Constraintsから設定していきます。
ScrollViewのサイズを決める制約となります。
親Viewに対しての余白を決めることで、サイズを決めています。

数字はなんでも良いですが今回は上下に50,左右に0とします。

scroll-2.png

Add 4 Constraintsを選択すると、ScrollViewに以下の4つの制約が追加されます。

- Safe Area.trailing = Scroll View.trailing
- Safe Area.bottom = Scroll View.bottom + 50
- Scroll View.leading = Safe Area.leading
- Scroll View.top = Safe Area.top + 50

また、ScrollViewに対し赤いラインが表示され"Scrollable Content Size Ambiguity"という制約エラーが出ます。

scroll-3.png

scroll-4.png

これは直訳の通り"スクロール可能なコンテンツのサイズが曖昧"という警告です。

補足:Scrollable Content Size Ambiguityとは?

では、このエラーは一体なんなのか?ということを考えていきます。

エラー文によると、

The scrollable size of a UIScrollView is computed automatically based upon the constraints of its subviews.

訳:UIScrollViewのスクロール可能なサイズは、そのサブビューの制約に基づいて自動的に計算されます。
スクロール可能なコンテンツの幅と高さを完全に定義するには、制約が必要です。

という内容が記載されています。
どうやらエラー文で指摘を受けたスクロール可能なコンテンツのサイズを定義するためにサブビューを制約する必要があるようです。

補足2:サブビューとは?

Xcodeのレイアウトはオブジェクトを上に重ねていくような構造になっており、ユーザから見て手前にあるビューをサブビュー(subview)、奥にあるビューをスーパービュー(superview)というようです。

scroll-debug.png

こんな感じ。(画像は完成品のレイアウト)

エラーは一旦スルーで

上記の理由から、現状ScrollViewはサブビューを持っていないのでエラーを解消することは出来ません。
ScrollViewの中にContentsViewなどを設置してからの対応となります。

③ScrollViewの中にViewを設置

scroll-5.png

ただのViewです。
分かりやすくするために、ScrollViewを水色、新たに設置したViewを灰色にしています。

このViewですが、名前は慣例的に"ContentsView"などとするのが良いらしいです。
本記事でも、以後ContentsViewと記載します。

④ContentsViewとContentLayoutGuideに制約をつける

こちらの記事がとても分かりやすく、大変参考になりました。
【Swift】Align制約の使い方。複数部品を整列する制約を追加する。(Swift 2.1、XCode 7.2)

scroll-6.png

コマンドキー押して2つ同時選択し、AutolayoutのAlignを選択。
Add New Alignment Constraintsから上下左右を固定します。

上4つを0にしてチェックを入れます。

  • Leading Edges
    部品の左端から他の部品の左端までの水平方向の距離を指定
  • Trailing Edges
    部品の右端から他の部品の右端までの水平方向の距離を指定
  • Top Edges
    部品の上端から他の部品の上端までの垂直方向の距離を指定
  • Bottom Edges
    部品の下端から他の部品の下端までの垂直方向の距離を指定

以下の制約が追加されます。

- ContentsView.top = Content Layout Guide.top
- ContentsView.leading = Content Layout Guide.leading
- ContentsView.bottom = Content Layout Guide.bottom
- ContentsView.trailing = Content Layout Guide.trailing

この制約手法は、複数のオブジェクトを選択した場合のみ使える制約となっており、一つしか選択していない状態だと、チェックを入れることが出来ません。

ではこれらはどういう制約なのか考えてみます。
Leading Edges 部品の左端から他の部品の左端までの水平方向の距離を指定とあるように2つの部品の距離を指定するものです。

例えばLeading Edgesを0にすることより、
(部品A)ContentLayoutGuideの左端と、(部品B)ContentsViewの左端の
距離を指定した"0"にすることができる。
要するにくっつくと言うことです。

※あくまで現時点の自分の理解では、オブジェクトの開始位置を、他のオブジェクトに依存して決める設定というイメージ。

4箇所指定することで、ContentLayoutGuideとContentsViewの4隅の位置を固定する制約となっている。

⑤FrameLayoutGuideとContentsViewの横幅を合わせる

上記④ではContentLayoutGuideに合わせてContentsViewの位置を設定したが、
ContentsView自身のサイズを決めていないので、こちらも設定していきます。(当然エラー状態となっている)

はじめにContentsViewの横幅を設定します。
高さはContentsView自身で定める必要があるが、横幅はScrollViewの枠に合わせる形が良いと思われます。(※一般的な縦のみスクロールの場合)

ContentsViewからcontrolキーを押しながらFrameLayoutGuideへドラッグし、Equal Widthsを選択。(青いガイドラインが出る)

scroll-7.png

追加された制約

- ContentsView.width = 0.57971 × Frame Layout Guide.width
  (数値は現在のContentsViewのサイズにより変動)

この時、元々のContentsViewの横幅を元に上記制約が作られてしまいます。
直すには制約のSize inspectorからMultiplier1に設定することで、ContentsViewをScrollViewの横幅目一杯に広げることができます。

scroll-8.png

⑥ContentsViewの高さについて制約をつける

続いて高さを決めていきます。
ContentsViewを選択し、AddNewConstraintsへ。
上の余白を0、高さを1000とした。

scroll-9.png

追加された制約

- ContentsView.top = Frame Layout Guide.top

これに加えてContentsView自身にもheight = 1000という制約が追加され、これで高さ&位置を確定することが出来ます。

これで制約エラーは解消されます!

ContentsViewを選択した状態でスクロールしてみてください。
動くハズです!

⑦Labelを配置する

ここからはおまけです。
ContentsViewになんでも好きなものを配置して動かしている気分を味わっていきましょう。

scroll-10.png

完成!

これで冒頭の完成品となりました!

後語り

いかがだったでしょうか?
まだまだ理解出来ていない部分も多いので、学習を進めていく過程で随時更新していきたいと考えています。

また、ContentLayoutGuideFrameLayoutGuideに関しては全然理解が追いついていないので、今後の課題と思ってしっかり学習していこうと思います!

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

Xcodeのよく使う便利機能・コマンド

はじめに

逆効率厨の僕がインターン中にトレーナーに教えてもらった便利機能とコマンドを紹介します!

今開いているファイルをProject Navigatorで表示する

めちゃくちゃでかいプロジェクトファイルを触っている時、どのグループのファイルか行方不明になったこと、ありませんか?そう、僕ですね。
そんな時、Command + Shift + jを押せば今開いているファイルをProject Navigatorで表示することができます。

検索

プロジェクト内でファイル検索する
Command + Shift + o
Xcodeの検索バーから検索するとコードも表示されるため、ファイルを探したいときはこちらの方が便利です。

ファイル内で検索する
Command + f

シュミレーターをshake

Cmd + Ctr + z
デバッグのときに便利

インデントを表示する

Editor < Invisibles
インデントが・・・で表示されて見やすくなります.
スクリーンショット 2021-01-11 3.13.04 (1).png

スペースのみの行を空行にする

Xcode > Preference > Text Editing > While Editingにチェック
ここにチェックを入れるとSwiftLint警察によってslackを荒らす回数が減ります。
ちなみに範囲選択してCmd +iするとインデントも修正してくれます。
スクリーンショット 2021-01-11 3.18.36.png

該当のコードを書いた人を探す

スクリーンショット 2021-01-11 3.26.11.png
ここから
スクリーンショット 2021-01-11 3.26.28.png
誰がいつ書いたコードかを表示することができます。

いい機能を見つけたらもっと追加していきます!
便利だと思ったものがあったらLGTMくれると嬉しいです!!!

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

【Swift】xibを使ってUITableViewを実装する

はじめに

UITableViewを使用する際にカスタムセルを.xibを使用して実装する方法について書きます。
xibを使用することでカスタムセルのcalss内の記述をStoryboardベースで実装することができます。

この記事でやること

UITableViewCellのxibにあるLabelのテキストを変更する
Simulator Screen Shot - iPhone 12 - 2021-01-11 at 02.00.41.png

実装

TableViewの追加

画面いっぱいにTableViewを追加します。
スクリーンショット 2021-01-11 2.03.14.png

xibファイルを作成

Cocoa Touch Classを選択し、
スクリーンショット 2021-01-11 2.05.40.png

UITableViewCellを選択、Also Create XIB fileにチェックを入れ、ファイルを作成する
スクリーンショット 2021-01-11 2.07.02.png

Labelの追加

CellにLabelを追加し、Outlet接続する
スクリーンショット 2021-01-11 2.16.03.png

TableViewを作成

TableViewとコードをOutlet接続し、extensionに一般的なTableViewのコード()を書きます
スクリーンショット 2021-01-11 2.16.51.png

ここで一般的なTableViewと違うのがRegisterを記述する点と

ViewController.swift
tableView.dataSource = self
tableView.delegate = self
// registerでxibをidentifierとして設定する
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")

ここで設定したidentifierを使用する点です。

ViewController.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
    cell.titleLabel?.text = "aaa"

    return cell
}

ビルドしてみる

これで冒頭の通りxibを使用してUITableViewCellのテキストを変更することができたかと思います。
もし参考になればLGTMくれると嬉しいです!

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

[iOS] SourceryとAspectsを組み合わせて自動でタップイベントをトラッキングする

はじめに

継続的に機能改善を行うにはユーザの行動分析は欠かせません.そのため,多くの企業がユーザがアプリ内で開いた画面やタップしたボタンなどのインタラクションをトラッキングしているのではないでしょうか.

しかし,トラッキング処理は有益な一方でコードの見通しを悪くする要因の1つでもあります.なぜなら,トラッキングの処理はアプリの価値を表す主要な機能(以降,ビジネスルールと呼ぶことにします)とは一切関係が無いからです.それなのにも関わらず,トラッキング処理はビジネスルールが存在するのと同じ上位のレイヤーに現れることが多いため,尚更ノイズに感じてしまうのです.

こういった乱雑な処理は,どうにか隠蔽して意識せず開発を進めたいものです.この記事では,iOSアプリ上のボタンのタップイベントを対象にして,その方法を模索および提案します.

目的

あるViewControllerが持つUIButtonがタップされたときに,ログイベントを貯めているサーバに問い合わせを行います.このとき,実装者がトラッキングの処理を一切書かないことと見ないことを目指します.

実装

それでは,早速どのように実現するかについて説明します.

方針

先述した目的を実現するためには次の2つの処理が必要です.

1.ボタンのタップイベントを自動でハンドルして,タップイベントをサーバへ送信する.
2.対象とするViewController全てで1の処理を実行する.

1の処理はメタプログラミングによって実現できそうです.これは,対象とするViewControllerのプロパティとして保持している全てのUIButtonに対してaddTarget(_:action:for:)を呼び出すイメージです.
2の処理はAspectsをによって実現できそうです.これは,メタプログラミングをした1の処理をViewControllerの任意のライフサイクルにフックさせて実行させるイメージです.

実装例

方針だけではイメージしずらい部分もあると思うので,ここからはコードを交えつつ詳細な説明を行います.

メタプログラミングによるタップイベントのハンドリング

まず最初にメタプログラミングで実現したゴールを説明します.タップイベントをトラッキングしたいと思っている,2つのUIButtonを内部に持つAviewControllerを考えることにします.

class AViewController: UIViewController {
    let hogeButton = UIButton()
    let fugaButton = UIButton()
}

AviewControllerに対して次のようなコードを生成するのが一旦のゴールです.生成されたAviewController.registerTabEventLogger()は,ViewControllerとUIButtonの組み合わせによる文字列をパラメータとしたタップイベントをトラッキングするように各UIButtonに命令します.

extension AViewController {
    fileprivate func registerTabEventLogger() {

        hogeButton.addTarget(self, action: #selector(didTaphogeButton), for: .touchUpInside)
        fugaButton.addTarget(self, action: #selector(didTapfugaButton), for: .touchUpInside)

    }

    @objc private func didTaphogeButton() {
    Logger.send(event: .tap(button: "AViewController.hogeButton"))
    }

    @objc private func didTapfugaButton() {
    Logger.send(event: .tap(button: "AViewController.fugaButton"))
    }
}

ゴールを明示したところで,早速実装の説明に移ります.Swiftにおいて,こういったメタプログラミングをするのにはSourceryが有用です.Sourceryを使って上記のようなコード生成を行うために,次のようなProtoclとテンプレートファイルを記述しました.SourceryのREADMEに従ってセットアップを行いビルドを走らせると,TapEventLoggableControllerに準拠したクラスに対応するコードが自動生成されます.これが,タップイベントをサーバへ伝える処理を自動で生成する処理です.

protocol TapEventLoggableController { }

// 全てのUIViewControllerへ適応させる
extension UIViewController: TapEventLoggableController {}
// あるいは特定のUIViewControllerへ適応させる
extension AViewController: TapEventLoggableController {}
import UIKit

<%_ for vc in types.classes.filter { $0.implements["TapEventLoggableController"] != nil } { -%>
extension <%= vc.name %> {
    fileprivate func registerTabEventLogger() {

    <%_ for button in vc.variables.filter { $0.typeName.name == "UIButton" } { -%>
        <%= button.name %>.addTarget(self, action: #selector(didTap<%= button.name %>), for: .touchUpInside)
    <% } -%>

    }
    <%_ for button in vc.variables.filter { $0.typeName.name == "UIButton" } { -%>

    @objc private func didTap<%= button.name %>() {
        Logger.send(event: .tap(button: "<%= vc.name %>.<%= button.name %>"))
    }
    <% } -%>
}
<% } -%>

Aspectsによるタップイベントの監視の開始

registerTabEventLogger()を呼び出すことによって,タップイベントがサーバへ伝達されるようになりました.それではこのメソッドは誰が呼ぶのでしょうか?ViewControllerの中で決まりごととして毎回呼びますか?それは呼び忘れのミスに繋がるし,トラッキング処理の隠蔽がしきれていません.

今回はAspectsを使ってViewControllerの.viewDidAppear(_:)をhookしてregisterTabEventLogger()を呼び出すようにしました.結果が次のコードです.

// Generated using Sourcery 1.0.0 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT

import UIKit
import Aspects

extension AViewController {    
    fileprivate func registerTabEventLogger() {

        hogeButton.addTarget(self, action: #selector(didTaphogeButton), for: .touchUpInside)
        fugaButton.addTarget(self, action: #selector(didTapfugaButton), for: .touchUpInside)

    }

    @objc private func didTaphogeButton() {
        Logger.send(event: .tap(button: "AViewController.hogeButton"))
    }

    @objc private func didTapfugaButton() {
        Logger.send(event: .tap(button: "AViewController.fugaButton"))
    }
}

enum AspectTracker {
    private static var token: AspectToken?

    static func setup() {
        guard token == nil else { return }

        token = try? UIViewController.hook(
            #selector(UIViewController.viewDidAppear(_:)),
            with: .positionBefore,
            using: { info, animated in
        // 抽象化したい気持ちが湧いてきますが,型の継承関係は暗黙的に変えたくないので,やっていません.
                if let vc = info?.instance() as? AViewController {
                    vc.registerTabEventLogger()
                }
            }
        )
    }
}

import UIKit
import Aspects

<%_ for vc in types.classes.filter { $0.implements["TapEventLoggableController"] != nil } { -%>
extension <%= vc.name %> {
    fileprivate func registerTabEventLogger() {

    <%_ for button in vc.variables.filter { $0.typeName.name == "UIButton" } { -%>
        <%= button.name %>.addTarget(self, action: #selector(didTap<%= button.name %>), for: .touchUpInside)
    <% } -%>

    }
    <%_ for button in vc.variables.filter { $0.typeName.name == "UIButton" } { -%>

    @objc private func didTap<%= button.name %>() {
        Logger.send(event: .tap(button: "<%= vc.name %>.<%= button.name %>"))
    }
    <% } -%>
}
<% } -%>

enum AspectTracker {
    private static var token: AspectToken?

    static func setup() {
        guard token == nil else { return }

        token = try? UIViewController.hook(
            #selector(UIViewController.viewDidAppear(_:)),
            with: .positionBefore,
            using: { info, animated in
                <%_ for vc in types.classes.filter { $0.implements["TapEventLoggableController"] != nil } {-%>
                if let vc = info?.instance() as? <%= vc.name %> {
                    vc.registerTabEventLogger()
                }
                <% } -%>
            }
        )
    }
}

これでAspectTracker.setup()を任意の場所で一回呼べば,それ以降は自動トラッキングが走るようになりました.

デモ

以上までで説明した処理を結合すると動画に示すような処理が可能となります.これは,画面内に有る青いボタンをタップするとトラッキングのログがコンソールへ吐き出されている様子を表しています.

画面内に有る青と赤の領域にあるボタンをタップするとトラッキングのログが吐き出されている様子

改善

実運用するにあたって必要となってきそうなことと,その解決方法の考えを明記しておきます.

タップ時にサーバへ送信するイベントを柔軟に変えたい

ポリモーフィズムを使えば良い

protocol TapEventLoggableController {
    var tapEventMap: [UIButton: Event] { get }
}

extension TapEventLoggableController {
    var tapEventMap: [UIButton : Event] {
        [:]
    }
}

class AViewController: UIViewController {
    let hogeButton = UIButton()
    let fugaButton = UIButton()

    var tapEventMap: [UIButton : Event] {
        [hogeButton: .tapHogeButton]
    }
}

@objc private func didTaphogeButton() {
    if let event = tapEventMap[hogeButton] {
        Logger.send(event: event)
    } else {
        Logger.send(event: .tap(button: "BViewController.hogeButton"))
    }
}

サーバへイベントを送信するクラスをDIしたい

ViewControllerが保持すれば良い.

protocol TapEventLoggableController {
     var logger: Logger { get }
}

おわりに

この記事では自動でタップイベントをトラッキングする方法を紹介しました.この方法を応用すれば,タップイベント以外のイベント,例えばインプレッションのイベントも容易にトラッキングすることが可能だと思います.ぜひ試してみてください.

参考文献

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

Swiftにおけるfinal修飾子とその強制

はじめに

Swiftには他の言語と同じようにfinal修飾子があります.
この修飾子はLanguage Guideに次のような説明がされています.

You can prevent a method, property, or subscript from being overridden by marking it as final. Do this by writing the final modifier before the method, property, or subscript’s introducer keyword (such as final var, final func, final class func, and final subscript).

Any attempt to override a final method, property, or subscript in a subclass is reported as a compile-time error. Methods, properties, or subscripts that you add to a class in an extension can also be marked as final within the extension’s definition.

You can mark an entire class as final by writing the final modifier before the class keyword in its class definition (final class). Any attempt to subclass a final class is reported as a compile-time error.

https://docs.swift.org/swift-book/LanguageGuide/Inheritance.html

要約すると,この修飾子を用いることにより,プロパティやメソッドのオーバーライドおよびクラスの継承を禁止することができます.

継承を禁止する2つの利点

さて,このfinal修飾子を使う利点とはなんでしょうか.
私は大きく分けて次の2つが挙げられると考えています.

  1. パフォーマンスの向上
  2. 変更の影響範囲を限定する

パフォーマンスの向上

これは,Appleの公開しているブログにも言及されています.

The final keyword is a restriction on a class, method, or property that indicates that the declaration cannot be overridden. This allows the compiler to safely elide dynamic dispatch indirection. For instance, in the following point and velocity will be accessed directly through a load from the object’s stored property and updatePoint() will be called via a direct function call. On the other hand, update() will still be called via dynamic dispatch, allowing for subclasses to override update() with customized functionality.

https://developer.apple.com/swift/blog/?id=27

final修飾子を使用してあるクラスの継承を禁止すると,そのクラスのプロパティやメソッドをDynamic Dispatchを介さずに直接読み込めるようです.
その結果,パフォーマンスが向上すると述べられています.

より具体的に考えてみましょう,
例えば,次のように,AnimalクラスとCatクラスがあるとします.
Animalクラスは継承可能となっているため,animal.eat()にて実際に呼び出されるのはAnimalクラスかそのサブタイプ(Catクラス)のeat()となります.
よって,コンパイル時に実際に何が呼ばれるのかを判定する処理(*1)が必要になります.

class Animal {
  func eat() { 
    print("eat")
  }
}

class Cat: Animal {
  override func eat() { 
    super.eat()
    print("cat food")
  }
}

let animal: Animal = Cat()
animal.eat()

しかし,Animalクラスにfinal修飾子が付いていたらどうでしょうか.
その場合はanimal.eat()で呼び出されるのはAnimalクラスのeat()になることが約束されます.
よって,コンパイル時に直接Animalクラスの`eat()を直接アクセスすれば良くなるため,パフォーマンスが向上します.

変更の影響範囲を限定する

final修飾子を使用することは,そのクラスの変更による影響を最小限にすることに繋がります.
結果的に,意図せずバグを生んでしまったり,コードの可読性を向上させることに寄与します.

先述したCatとAnimalを例にとります.
CatはAnimalを継承し,そのメソッドをオーバーライドし,その中でAnimalのメソッドを呼び出しています.
そのため,Animalのメソッドに変更を加えた場合,Catのメソッドの振る舞いが変わります.
つまり,これはAnimalに変更を加える度に,そのサブタイプが破壊されていないか確認する必要があることを示しています.
こういった,不用意に受ける影響の確認と修正を要されるくらいなら,継承させる必要がないクラスにはfinal修飾子を付けた方が良いかもしれません.

final修飾子を強制する

先述したようなfinal修飾子による恩恵を最大限受けるために,継承されていないクラスにはfinal修飾子を付けましょう.
しかし,いくら意識したところで,プログラミング中のトライ&エラーの過程でfinal修飾子のことをすっかり忘れてしまうかもしれません.
もしくは,チームにそういった意識が根付いていないかもしれません.
どうにか,機械的に解決(final修飾子を強制)できないでしょうか?

この方法には次の2つがあると思います.

  • 静的解析ツールを使用して,継承していないクラスを指摘する.
  • 静的解析ツールやフォーマッターを活用して,継承されていないクラスに対して自動的にfinal修飾子を付ける

今回はDangerを用いて,前者の解決方法を試みました.

Dangerプラグインの作成

継承されていないクラスを検知し,PRのコメントにてそれを指摘するDangerプラグインを作成しました.
なお,このプラグインが検知できるのは次のケースです.

  • 既にあるrclassへ付けてあったfinalが誤って外された時
  • 新しく定義したタイプにfinalが必要な時
  • 消去したタイプの親タイプにfinalが必要な時

普段rubyを書き慣れていないため,読みにくいのはご容赦下さい??
(また,何か例外があればご指摘頂けると嬉しいです)

module Danger
  class ForceSwift < Plugin
    Class = Struct.new(:parent_type, :self_type, :is_final?)
    LinePosition = Struct.new(:file_path, :line_number)

    def warn_if_needed(target_paths)
      all_classes = []
      modified_classes = []
      deleted_classes = []
      @position = {}

      diff_swift_files = (git.modified_files + git.added_files)
                         .filter { |file| File.extname(file) == '.swift' }
                         .map { |file| git.diff_for_file(file).patch }

      project_files = target_paths
                      .flat_map { |root| Dir[File.join("#{root}/**", '*.swift')] }

      diff_swift_files.each do |text|
        text.split("\n").each do |line|
          case line
          when /^\+/
            add_class_to_list(line, modified_classes)
          when /^\-/
            add_class_to_list(line, deleted_classes)
          end
        end
      end

      project_files.each do |path|
        line_number = 0
        File.open(path)  do |file|
          file.each_line do |line|
            line_number += 1
            is_success = add_class_to_list(line, all_classes)
            @position[all_classes.last.self_type] = LinePosition.new(path, line_number) if is_success
          end
        end
      end

      modified_classes
        .each { |a_class| warn_if_final_is_needed(a_class, all_classes) }
      deleted_classes
        .flat_map { |a_class| parents(a_class, all_classes) }
        .each { |a_class| warn_if_final_is_needed(a_class, all_classes) }
    end

    private

    def children(reference_class, all_classes)
      all_classes.filter { |a_class| a_class.parent_type.include?(reference_class.self_type) }
    end

    def parents(reference_class, all_classes)
      all_classes.filter { |a_class| reference_class.parent_type.include?(a_class.self_type) }
    end

    def warn_if_final_is_needed(a_class, all_classes)
      children = children(a_class, all_classes)
      is_leaf = children.empty?
      if is_leaf && !a_class.is_final?
        warn(
          "おそらく#{a_class.self_type}にはfinal修飾子が必要です.",
          file: @position[a_class.self_type].file_path,
          line: @position[a_class.self_type].line_number
        )
      else
        children.each { |node| warn_if_final_is_needed(node, all_classes) }
      end
    end

    def add_class_to_list(line, list)
      class_difinication_match = line.match(/class (.+) {/)
      return false unless !class_difinication_match.nil? && !class_difinication_match[1].nil?

      is_final = !line.match(/.*final.+class/).nil?
      hierarchy_match = class_difinication_match[1].match(/(.+): (.+)/)
      if !hierarchy_match.nil? && !hierarchy_match[2].nil?
        parents = hierarchy_match[2].split(',').map { |line| line.strip }
        list << Class.new(parents, hierarchy_match[1], is_final)
      else
        list << Class.new([], class_difinication_match[1], is_final)
      end
      true
    end
  end
end

このプラグインを使用すると次のように,Botが指摘してくれます.

まとめ

  • final修飾子を付けることには2つのメリットがある
  • 付けるのを忘れがちならDangerプラグインを使うなどの解決方法がある

注釈

(*1) たぶんこれがDynamic Dispatch

参考文献

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