20191219のSwiftに関する記事は14件です。

Swiftでメモリマップドファイルを使う

概要

Swiftでメモリマップドファイルを使いたくて、C言語のmmapを使ったコードを実装しました。
そのときのメモです。

コード全体を含めたサンプルコードはGitHubに置いています。
MemoryMappedFileSwift

この記事ではGitHubのコードをベースに解説用に変えています。
GitHubのコードはstructにして使いやすくしています。

メモリマップドファイルとは

メモリマップドファイルは、RAMの代わりにファイルを使ってデータを入れているにもかかわらず、RAMと同じような感覚で使えるものです。他のプログラムと共有するような使い方もあります。

Swiftでのコード

Swift専用APIやライブラリがあるかもしれませんが、C言語の関数を使って作りました。
SwiftからC言語の関数を使うと、Unsafe系の扱いが少し面倒でした。

ファイルの作成またはオープン

メモリマップドファイルで最初に行うのは、ファイルの作成です。
C言語の関数の「open」を使います。取得したファイルデスクリプタは後で使います。

// ファイルパス
let filePath = ...

var pathBuf = [Int8]()
pathBuf.append(contentsOf: filePath.utf8CString)

let fileDescriptor = Darwin.open(pathBuf, O_CREAT | O_RDWR, S_IREAD | S_IWRITE);

ファイルサイズの計算

メモリマップドファイルのファイルサイズはページサイズの倍数になっている必要があります。
ページサイズはgetpagesize関数で取得できます。
必要なサイズ以上でページサイズの倍数になっているサイズで作成します。

// 必要なサイズ
let neededSize = ...

let pageSize = Int(getpagesize())
let mappedSize = ((neededSize + pageSize - 1) / pageSize) * pageSize

ファイルの拡張

メモリマップドファイルは、読み書きする領域が既にファイルにも存在している必要があります。
なかったらメモリ不足と一緒です。
ftruncate関数でファイルサイズを調整します。

ftruncate(fileDescriptor, off_t(mappedSize))

マッピング

作ったファイルにマッピングします。
マッピングにはmmap関数を使います。関数が返したバッファを使って以降は通常のメモリのようにアクセスできます。

var mappedBuffer = Darwin.mmap(UnsafeMutableRawPointer(mutating: nil), mappedSize, 
                               PROT_READ | PROT_WRITE, MAP_SHARED, fileDescriptor, 0)

マッピングされた領域に書き込む

マッピングされた領域にデータを書き込むときは、次の2段階の手順となります。

  1. 通常のメモリアクセスと同様にバッファに書く。
  2. msync関数でファイルと同期させる。

msync関数で同期するまでファイルの内容は更新されません。
メモリマップドファイルはファイルを使用するので、純粋なメモリアクセスよりは遅くなります。
毎回、変更する度にmsyncを呼ぶとパフォーマンスが想定以上に悪くなることもあるようです。
利用する場所に合わせて適切なタイミングを決定するのが良いようです。

if let buf = mappedBuffer.bindMemory(to: UInt8.self, capacity: 256) {
    for i in 0 ..< 256 {
        buf[i] = UInt8(i)
    }

    Darwin.msync(mappedBuffer, mappedSize, 0)
}

マッピングされた領域から読み込む

マッピングされた領域からの読み込みは、通常のメモリアクセスと同じです。

if let buf = mappedBuffer.bindMemory(to: UInt8.self, capacity: 256) {
    for i in 0 ..< 256 {
        let str = String(format: "%02X", buf[i])
        print(str, separator: "", terminator: "")

        if ((i + 1) % 4) == 0 {
            print(" ", separator: "", terminator: "")
        }

        if ((i + 1) % 16) == 0 {
            print()
        }
    }
}

少し上のコードで、先頭256バイトに、0から255までの値を書き込んでいるので、実行すると次のように出力されました。
書き込んだものが読み取れています。

00010203 04050607 08090A0B 0C0D0E0F 
10111213 14151617 18191A1B 1C1D1E1F 
20212223 24252627 28292A2B 2C2D2E2F 
30313233 34353637 38393A3B 3C3D3E3F 
40414243 44454647 48494A4B 4C4D4E4F 
50515253 54555657 58595A5B 5C5D5E5F 
60616263 64656667 68696A6B 6C6D6E6F 
70717273 74757677 78797A7B 7C7D7E7F 
80818283 84858687 88898A8B 8C8D8E8F 
90919293 94959697 98999A9B 9C9D9E9F 
A0A1A2A3 A4A5A6A7 A8A9AAAB ACADAEAF 
B0B1B2B3 B4B5B6B7 B8B9BABB BCBDBEBF 
C0C1C2C3 C4C5C6C7 C8C9CACB CCCDCECF 
D0D1D2D3 D4D5D6D7 D8D9DADB DCDDDEDF 
E0E1E2E3 E4E5E6E7 E8E9EAEB ECEDEEEF 
F0F1F2F3 F4F5F6F7 F8F9FAFB FCFDFEFF 

マッピング終了とファイルのクローズ

必要なくなったらマッピングを解除して、ファイルを閉じて完了です。
テンポラリファイルなどで使用しているときは、このタイミングで削除すれば良いと思います。

Darwin.munmap(mappedBuffer, mappedSize)
Darwin.close(fileDescriptor)

まとめ

大きなファイルのときも少ないメモリで処理できます。
搭載しているメモリよりも大きなデータを扱わなければいけないときなどには便利だと思います。
手元のマシンで5GBのメモリマップドファイルを作って、開いている状態のときのメモリ使用量を確認してみると6.6MBでした。

少しずつファイルを書き込んでいて、パフォーマンスが遅いみたいなときにも、データの書き込みタイミングと本当のファイルの更新タイミングを別々にできて、本当の更新回数を減らすことができ、パフォーマンス改善にもつなげられると思います。

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

Swiftの高階関数をNLP100ノックと絡めてまとめる

この記事は茨大 Advent Calendar 2019 20日目の記事です。

初めに

今回は茨大 Advent Calendar 2019に参加させてもらったのと,
ちょうどNLP100本ノックに挑戦しているので,100本ノックの最初の方の問題で使った高階関数についてまとめたいと思います。

個人的に,高階関数はあまり使っていなかったのですが,ちょうどいい機会なので
習得も含め使ってみようと思いました。

当初はPythonを使ってノックしようと思ってたのですが,ググったところ
@mono0926 さんの [Swift 3] 言語処理100本ノック 2015 第1章: 準備運動
や,
@tikidunpon さんの Swift4で言語処理100本ノック 2015 第1章,第2章
など,Swiftで挑戦されている方がいらっしゃったので,私もSwfitでやっています。

コードはGithubにあげています。
間違っていたり,おかしな点があればご指摘ください!

問題を解いていて個人的によかったものをピックアップして幾つか書いていこうと思います。

100本ノックQ2

まずQ2の問題と回答を以下に書きます。

//02. 「パトカー」+「タクシー」=「パタトクカシーー」
//「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.
func Q2(_ input1: String, _ input2: String) -> String 
   return zip(input1, input2).map { String($0) + String($1) }.reduce("", +)
}

この問題は引数のinput1に「パトカー」が,input2に「タクシー」が入っています。
まず,zip(input1, input2).map { String($0) + String($1) }の部分で,input1とinput2の先頭からの文字列が
それぞれセットになって,配列に格納されます。

この部分をデバックで出力すると以下のようになります。

([String]) $R4 = 4 values {
  [0] = "パタ"
  [1] = "トク"
  [2] = "カシ"
  [3] = "ーー"
}

この結果から,input1とinput2がmapによって配列に格納されていることがわかります。
問題は,文字列「パタトクカシーー」を得ることが目的ですから,[String]からStringにする必要があります。

高階関数を使わずにfor文とかを使って連結しようとしたら,次のようになると思います。

var result = ""
for line in huge {
  result += line
}

別に,動くのでこれでも大丈夫です。
しかし,高階関数のreduceを使えば,1行で,完結することができ,非常にみやすくなります。
(コードが読みやすくなるかはわかりません。)

100本ノックQ3

これもまず,問題文と解答を以下に示します。

//03. 円周率
//"Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."
//という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ.
func Q3(_ input: String) -> [Int] {
   return input.components(separatedBy: " ").map { $0.pregReplace(pattern: "[,|.]", with: "").count }
}

何も考えずに回答すれば,十数行になりそうな問題ですが,高階関数をうまく組み合わせることで1行で終わりました!

まず,inputされる文字をスペースで分ける必要があるので,compnentsを使いスペースで分けました。
デバックで出力すると以下のようなString型の配列が得ることができます。

(lldb) p input.components(separatedBy: " ")
([String]) $R0 = 15 values {
  [0] = "Now"
  [1] = "I"
  [2] = "need"
  [3] = "a"
  [4] = "drink,"
  [5] = "alcoholic"
  [6] = "of"
  [7] = "course,"
  [8] = "after"
  [9] = "the"
  [10] = "heavy"
  [11] = "lectures"
  [12] = "involving"
  [13] = "quantum"
  [14] = "mechanics."
}

これらそれぞれに対して,文字数を得ればいいのですが,「,」や「.」を取り除く必要があります。
また,「,」や「.」が配列のどこに出現するのかはinputの内容によって変化するので,
配列の全ての要素に対して調べる必要があります。

今までなら,配列の全ての要素にアクセスするなんて聞いたらfor文を使っていました。
しかし,mapを使えば,for文を使わなくても,配列の全ての要素にアクセスできます。

map内の処理である,
$0.pregReplace(pattern: "[,|.]", with: "").count
この部分において,今回は正規表現を使って,「,」と「.」を空文字に置き換える処理をしました。
置き換える作業を行った後,countを使って文字数を返す処理をしています。

これにより,各単語の(アルファベットの)文字数を先頭から出現順に並べたリスト(Int型の配列)を作成しています。

100本ノックQ21

問題文と解答を以下に示します。

/// 23. セクション構造
//記事中に含まれるセクション名とそのレベル(例えば"== セクション名 =="なら1)を表示せよ.
func Q23(_ input: String) -> [(secName: String, secLevel: Int)] {
   let wikiUK = Q20(input)
   let sectionLine = wikiUK.text.components(separatedBy: .newlines).filter{ $0.contains("==") }
   let sectionName = sectionLine.map { $0.pregReplace(pattern: "[=]*", with: "")}
   let sectionLevels = sectionLine.map { $0.components(separatedBy: "=")}.map{ ($0.count - 1) / 2 }
   return zip(sectionName, sectionLevels).map{ ($0, $1) }
}

この問題はjsonファイルから「イギリス」に関する記事本文をQ20で取得したものに対して行う問題です。
wikiUK

struct WikiSet: Codable {
    let title: String
    let text: String
}

となっています。

まず,1行目で,イギリスに関する記事を取得しています。
2行目で,イギリスに関する記事を行ごとに分け,それに対して,==が含まれている行を取得しています。
3行目は,セクションの名前を取得しています。正規表現を使い,==を取り除く処理をしています。

4行目のsectionLine.map { $0.components(separatedBy: "=")}では,=で文を分けていきます。
例えば,===hoge===であれば,

[0] = "="
[1] = "="
[2] = "="
[3] = "hoge"
[4] = "="
[5] = "="
[6] = "="

というふうに分割されます。これはsectionLineそれぞれの行に対して行われるので,結果として2次元配列が返されます。

次にこの2次元配列に対しての操作である.map{ ($0.count - 1) / 2 }の部分を考えます。
セクションは必ず,
n個の「=」 + セクション名 + n個の「=」
で構成されることがわかります。つまり,2次元配列の個数は必ず奇数になることがわかります。
ゆえに,2次元配列の個数から1を引いて2で割れば,格セクションレベルを取り出すことができます。

まとめ

高階関数をできるだけ使いながらSwiftで100本ノックをしているのですが,結構大変ということがわかりました。

Pythonでないと解けないというわけではないのですが,
やはりPythonの方が解きやすい気はします。

Swfitで100ノックは大変なのですが,@mono0926さんも指摘しているとおり,
StringをExtentionで拡張しながら解き進めれば,Swfitでも自然言語処理はアリなのかなと思います。

調べた感じたと,Swfitで50本くらいまでいってる人はいなかったです。
挫折しなければ100本Swfitでやりきりたいと思います。

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

Xcode11の新規プロジェクト作成につまずいた

はじめに

Xcode11 で新規にプロジェクトを作成し、iOS13 未満もサポートしようとすると大量のエラーが出てしまいます:dizzy_face:

error

この辺りめんどくさくて最近作ったやつ(下記とか)は大抵 iOS13 以上サポートにしてましたが iOS13 未満もサポートするため調べてみました。

対応

SwiftUI を使わない

とりあえず新規作成時に User Interface で SwiftUI ではなく Storyboard を選択する。

ui

頑張ってバージョンで分岐させてもいいがおとなしく今のところは SwiftUI をあきらめる:poop:

SceneDelegate 対応

出てるエラーを見てもらうとわかる通り SceneDelegate に関連するものばかりです。

SceneDelegate とはなんぞや?というのは下記参考

対応としては下記2パターンです。

  1. SceneDelegate を削除する
  2. エラーに1つずつ対応する

SceneDelegate を削除する

SceneDelegate はどうやら必須ではないらしいので削除しても大丈夫です。

※機能が制限されるのでそことの兼ね合い

  1. Info.plist の Application Scene Manifest キーを削除する
    Application Scene Manifest キーがある場合に必須となるのでこれを削除する。
    plist
  2. SceneDelegate.swift を削除する

さあビルドっとやるとまだエラーが出る:scream_cat:

error_2

ごっそり削除

AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    return true
  }
}

ビルド!!!

画面が真っ黒でこんなエラーが

[Application] The app delegate must implement the window property if it wants to use a main storyboard file.

とりあえず window を追加!!

AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow? // これ追加

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    return true
  }
}

ビルド!!!!

iOS12 でも動いた:tada:

エラーに1つずつ対応する

エラーをみていくと全部 @available(iOS 13.0, *) をつけろ!っていうやつです。
下記のように @available(iOS 13.0, *) をつけて削除の時と同様に window を追加します。

SceneDelegate.swift
@available(iOS 13.0, *) // ここ追加
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let _ = (scene as? UIWindowScene) else { return }
    }
  :
  :
  :
AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow? // これ追加

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    return true
  }

  // MARK: UISceneSession Lifecycle
  @available(iOS 13.0, *) // ここ追加
  func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
  }

  @available(iOS 13.0, *) // ここ追加
  func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
    // Called when the user discards a scene session.
    // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
    // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
  }
}

2019/12/21追記。コメントでもっとクールな書き方を教えていただきました:heart_eyes:

AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow? // これ追加

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    return true
  }
}

// MARK: UISceneSession Lifecycle
@available(iOS 13.0, *) // ここ追加
extension AppDelegate {
  func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    // Called when a new scene session is being created.
    // Use this method to select a configuration to create the new scene with.
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
  }

  func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
    // Called when the user discards a scene session.
    // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
    // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
  }
}

ビルド!!!!!

iOS12 でも動いた:tada::tada:

SceneDelegate があると AppDelegate と両方に処理を書かないといけなくなるので 許してくれるなら(お客さんが)削除でいいんじゃないかなと思います:unamused:

2019/12/21追記。
こちらもコメントいただきました。iOS14 などで今後 AppDelegate の処理が SceneDelegate に移行する可能性もあるのでしっかりバージョンを切り分けて対応した方が無難とのこと:frowning2:(iPhone なら Scene とか関係ないし削除しても...でも結局今後どう変わるかわからないし...)

なのでできるならちゃんと対応しましょう!!!

なんどもこの作業をやるのがめんどくさい場合はテンプレート機能とかあるらしいです(...知らなかった:see_no_evil:

さいごに

なんとかまにあった...:mask:

(半角スペース2個で改行できるの知らなかった:speak_no_evil:

参考

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

アニメーションを止めるな!

はじめに

この記事は、ZOZOテクノロジーズ #4AdventCalendar2019の記事です。
昨日は@Kyou13さんの「開発合宿でVue.js+Firebase+Onsen UIで食事の栄養管理アプリを作った話」でした。

さて、iOSでは、アニメーション中に画面を移動するとアニメーションが停止してしまうのは既知の事実かと思います。

基本的には ViewController のライフサイクルに合わせて調整してあげれば良いですが、コンポーネント化されたカスタムViewに関しては、コンポーネント側で管理されている方が利用しやすいケースの方が多いと思います。

この記事では、コンポーネント化されたカスタムView側でアニメーションを管理するTipsを紹介します。

アニメーションが止まるタイミング

まず、アニメーションが止まるタイミングですが、次の2つが考えられます。

  • アプリ内で画面を移動する
  • アプリをバックグラウンドに移動させる

では、下記のカスタムViewを例に、それぞれの対応方法を見ていきます。

動作環境
  • Xcode 10.3
  • Swift 5
  • iOS 12.4
import UIKit

class IndicatorView: UIView {
    private let strokeLayer = CAShapeLayer()
    private var isAnimating = false

    override init(frame: CGRect) {
        super.init(frame: frame)
        initialize()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        initialize()
    }

    override func draw(_ rect: CGRect) {
        let lineWidth: CGFloat = 2.0
        let radius = (rect.width - lineWidth) / 2
        let startAngle = CGFloat.pi * (3 / 2)
        let endAngle = startAngle + CGFloat.pi * 2.0
        let path = UIBezierPath(
            arcCenter: CGPoint(x: rect.width / 2.0, y: rect.height / 2.0),
            radius: radius,
            startAngle: startAngle,
            endAngle: endAngle,
            clockwise: true
        )
        strokeLayer.frame = rect
        strokeLayer.path = path.cgPath
        strokeLayer.lineWidth = lineWidth
    }

    private func initialize() {
        strokeLayer.fillColor = UIColor.clear.cgColor
        strokeLayer.strokeColor = UIColor.blue.cgColor
        strokeLayer.strokeEnd = 0.8
        layer.insertSublayer(strokeLayer, at: 0)
    }

    private func addAnimation() {
        let animation = CABasicAnimation(keyPath: "transform.rotation")
        animation.fromValue = 0.0
        animation.toValue = 2.0 * Double.pi
        animation.duration = 1.0
        animation.repeatCount = .infinity
        animation.timingFunction = CAMediaTimingFunction(name: .linear)

        strokeLayer.add(animation, forKey: "rotate")
    }

    func startAnimating() {
        guard !isAnimating else { return }
        isAnimating = true
        addAnimation()
    }

    func stopAnimating() {
        guard isAnimating else { return }
        isAnimating = false
        strokeLayer.removeAllAnimations()
    }
}

アプリ内で画面を移動した場合

ナビゲーションのプッシュやタブ移動などがこちらの対象になります。

Before

video1.gif

画面に戻ってくるとアニメーションが止まっていることがわかります。

対応方法

UIView の、 didMoveToWindow() メソッドを利用すると、 window が変わったタイミングでイベントを取得することができます。
画面に表示されるタイミングでは、 window プロパティに値が入っているので、そのタイミングでアニメーションを追加します。

    override func didMoveToWindow() {
        super.didMoveToWindow()

        if let _ = window, isAnimating {
            addAnimation()
        }
    }

After

video2.gif

アプリをバックグラウンドに移動させた場合

Before

video3.gif

バックグラウンドに移動後、アプリに戻ってくるとアニメーションが止まっています。

対応方法

NotificationCenter を利用するとアプリがフォアグラウンドなったタイミングでイベントを取得できるので、そのタイミングでアニメーションを追加してあげます。

    private func initialize() {
        strokeLayer.fillColor = UIColor.clear.cgColor
        strokeLayer.strokeColor = UIColor.blue.cgColor
        strokeLayer.strokeEnd = 0.8
        layer.insertSublayer(strokeLayer, at: 0)

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleEnterForeground),
            name: UIApplication.willEnterForegroundNotification,
            object: nil
        )
    }

    @objc func handleEnterForeground(_: Notification) {
        if let _ = window, isAnimating {
            addAnimation()
        }
    }

After

video4.gif

これでアプリを利用している間はアニメーションを止めることなく、動かし続けることができます。

最終的なコードです。

class IndicatorView: UIView {
    private let strokeLayer = CAShapeLayer()
    private var isAnimating = false

    override init(frame: CGRect) {
        super.init(frame: frame)
        initialize()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        initialize()
    }

    override func draw(_ rect: CGRect) {
        let lineWidth: CGFloat = 2.0
        let radius = (rect.width - lineWidth) / 2
        let startAngle = CGFloat.pi * (3 / 2)
        let endAngle = startAngle + CGFloat.pi * 2.0
        let path = UIBezierPath(
            arcCenter: CGPoint(x: rect.width / 2.0, y: rect.height / 2.0),
            radius: radius,
            startAngle: startAngle,
            endAngle: endAngle,
            clockwise: true
        )
        strokeLayer.frame = rect
        strokeLayer.path = path.cgPath
        strokeLayer.lineWidth = lineWidth
    }

    override func didMoveToWindow() {
        super.didMoveToWindow()

        if let _ = window, isAnimating {
            addAnimation()
        }
    }

    private func initialize() {
        strokeLayer.fillColor = UIColor.clear.cgColor
        strokeLayer.strokeColor = UIColor.blue.cgColor
        strokeLayer.strokeEnd = 0.8
        layer.insertSublayer(strokeLayer, at: 0)

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleEnterForeground),
            name: UIApplication.willEnterForegroundNotification,
            object: nil
        )
    }

    @objc func handleEnterForeground(_: Notification) {
        if let _ = window, isAnimating {
            addAnimation()
        }
    }

    private func addAnimation() {
        let animation = CABasicAnimation(keyPath: "transform.rotation")
        animation.fromValue = 0.0
        animation.toValue = 2.0 * Double.pi
        animation.duration = 1.0
        animation.repeatCount = .infinity
        animation.timingFunction = CAMediaTimingFunction(name: .linear)

        strokeLayer.add(animation, forKey: "rotate")
    }

    func startAnimating() {
        guard !isAnimating else { return }
        isAnimating = true
        addAnimation()
    }

    func stopAnimating() {
        guard isAnimating else { return }
        isAnimating = false
        strokeLayer.removeAllAnimations()
    }
}

さいごに

「動作はつづける、アニメーションは止めない!」

明日は@iiizukkeyiii さんです。

お楽しみに!

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

クリスマス前にARでジョークアプリをつくってみた話

この記事はiOS #2 Advent Calendar 2019の19日目の記事になります。
あんまり今年は書くモチベなかったのですが、ちょうどわいたところに空きがあって飛び入り参加しました。

とうようです。

普段は個人でiOSアプリつくったり、大学院でUIの研究室にいたり、中高生にITを教えたりしています。
ちなみに大学生・大学院生向けにちょうどこのLife is Tech !という中高生にITを教える団体のメンター募集がはじまっているので良かったら応募してみてください!笑
楽しい上にいろんなスキルが身につくのでぜひ!

>ーー切り取り線ーー<

さて、本題。

はじまり

はじまりはこの秋、2019年9月にさかのぼります。

増税前のiPhone 11シリーズの発表。僕は長年iPhone 7 Plusを利用していたのですが、そろそろ欲しいと。
Face ID使ってみたいと。

そんなわけでSIMフリーのiPhone 11 Pro Maxを購入しました!(パチパチパチ)

image.png

今までの機体も開発用に残しておこうという判断をした結果、手元にはこのようにiPhone 7 PlusとiPhone 11 Pro Maxの二台が残りました。

image.png

そう二台が...。

あっ!!!!!!!!!!

そこで気付いてしまったのです。この二台がある意味を...。
思いついてしまったものは仕方がないと、早速制作をはじめました。

手始め

まず手始めにARに表示するオブジェクトを制作しようということになりました。

といってもこんなにくだらないアプリにわざわざMayaやBlendarを使うのもなぁということで最初に検討したのがこれ

Reality Composer!!!

これはiOS13とXcode11向けにWWDC2019で発表されたApple公式の3Dモデル製作ツールです。そう、Apple公式の。大事なことなので2回言いました。

触ってみるとすごく簡単にリッチな3Dモデルがつくれてめっっっっちゃ楽しいです。詳しいことはこちらの記事などをご覧ください。

ですがこれは自分のやりたかったことに対して自由度が少し低い(ような気がする)という欠点がありました。

しかも今回つくらなければいけないのはめちゃくちゃ単純なモデルです。
そこで次に採用を決めたのがXcodeについているSceneKitのモデル製作画面です。

こちらの記事が参考になります。

というわけでとても簡単な平面のモデルをつくりました。えいやっ!

image.png

実際の模様はコードで反映していくので現時点ではめちゃくちゃ簡単です。

アプリの機能を変える

続いてこのアプリはiPhone 7 PlusとiPhone 11 Pro Maxの役割を完全に分けようという方針を考えたので、その実装をはじめました。

そのためにはデバイスの種類が取れるようにしなければいけません。なのでこちらを参考にこのような関数を追加しました。

func getDeviceInfo() -> String {
    var size: Int = 0
    sysctlbyname("hw.machine", nil, &size, nil, 0)
    var machine = [CChar](repeating: 0, count: Int(size))
    sysctlbyname("hw.machine", &machine, &size, nil, 0)
    let code: String = String(cString: machine)

    let deviceCodeDic: [String: String] = [
        /* Simulator */
        "i386"      : "Simulator",
        "x86_64"    : "Simulator",
        /* iPod */
        "iPod1,1"   : "iPod Touch 1st",            // iPod Touch 1st Generation
        "iPod2,1"   : "iPod Touch 2nd",            // iPod Touch 2nd Generation
        "iPod3,1"   : "iPod Touch 3rd",            // iPod Touch 3rd Generation
        "iPod4,1"   : "iPod Touch 4th",            // iPod Touch 4th Generation
        "iPod5,1"   : "iPod Touch 5th",            // iPod Touch 5th Generation
        "iPod7,1"   : "iPod Touch 6th",            // iPod Touch 6th Generation
        /* iPhone */
        "iPhone1,1"   : "iPhone 2G",                 // iPhone 2G
        "iPhone1,2"   : "iPhone 3G",                 // iPhone 3G
        "iPhone2,1"   : "iPhone 3GS",                // iPhone 3GS
        "iPhone3,1"   : "iPhone 4",                  // iPhone 4 GSM
        "iPhone3,2"   : "iPhone 4",                  // iPhone 4 GSM 2012
        "iPhone3,3"   : "iPhone 4",                  // iPhone 4 CDMA For Verizon,Sprint
        "iPhone4,1"   : "iPhone 4S",                 // iPhone 4S
        "iPhone5,1"   : "iPhone 5",                  // iPhone 5 GSM
        "iPhone5,2"   : "iPhone 5",                  // iPhone 5 Global
        "iPhone5,3"   : "iPhone 5c",                 // iPhone 5c GSM
        "iPhone5,4"   : "iPhone 5c",                 // iPhone 5c Global
        "iPhone6,1"   : "iPhone 5s",                 // iPhone 5s GSM
        "iPhone6,2"   : "iPhone 5s",                 // iPhone 5s Global
        "iPhone7,1"   : "iPhone 6 Plus",             // iPhone 6 Plus
        "iPhone7,2"   : "iPhone 6",                  // iPhone 6
        "iPhone8,1"   : "iPhone 6S",                 // iPhone 6S
        "iPhone8,2"   : "iPhone 6S Plus",            // iPhone 6S Plus
        "iPhone8,4"   : "iPhone SE" ,                // iPhone SE
        "iPhone9,1"   : "iPhone 7",                  // iPhone 7 A1660,A1779,A1780
        "iPhone9,3"   : "iPhone 7",                  // iPhone 7 A1778
        "iPhone9,2"   : "iPhone 7 Plus",             // iPhone 7 Plus A1661,A1785,A1786
        "iPhone9,4"   : "iPhone 7 Plus",             // iPhone 7 Plus A1784
        "iPhone10,1"  : "iPhone 8",                  // iPhone 8 A1863,A1906,A1907
        "iPhone10,4"  : "iPhone 8",                  // iPhone 8 A1905
        "iPhone10,2"  : "iPhone 8 Plus",             // iPhone 8 Plus A1864,A1898,A1899
        "iPhone10,5"  : "iPhone 8 Plus",             // iPhone 8 Plus A1897
        "iPhone10,3"  : "iPhone X",                  // iPhone X A1865,A1902
        "iPhone10,6"  : "iPhone X",                  // iPhone X A1901
        "iPhone11,8"  : "iPhone XR",                 // iPhone XR A1984,A2105,A2106,A2108
        "iPhone11,2"  : "iPhone XS",                 // iPhone XS A2097,A2098
        "iPhone11,4"  : "iPhone XS Max",             // iPhone XS Max A1921,A2103
        "iPhone11,6"  : "iPhone XS Max",             // iPhone XS Max A2104
        "iPhone12,5"  : "iPhone 11 Pro Max",         // iPhone 11 Pro Max

        /* iPad */
        "iPad1,1"   : "iPad 1 ",                     // iPad 1
        "iPad2,1"   : "iPad 2 WiFi",                 // iPad 2
        "iPad2,2"   : "iPad 2 Cell",                 // iPad 2 GSM
        "iPad2,3"   : "iPad 2 Cell",                 // iPad 2 CDMA (Cellular)
        "iPad2,4"   : "iPad 2 WiFi",                 // iPad 2 Mid2012
        "iPad2,5"   : "iPad Mini WiFi",              // iPad Mini WiFi
        "iPad2,6"   : "iPad Mini Cell",              // iPad Mini GSM (Cellular)
        "iPad2,7"   : "iPad Mini Cell",              // iPad Mini Global (Cellular)
        "iPad3,1"   : "iPad 3 WiFi",                 // iPad 3 WiFi
        "iPad3,2"   : "iPad 3 Cell",                 // iPad 3 CDMA (Cellular)
        "iPad3,3"   : "iPad 3 Cell",                 // iPad 3 GSM (Cellular)
        "iPad3,4"   : "iPad 4 WiFi",                 // iPad 4 WiFi
        "iPad3,5"   : "iPad 4 Cell",                 // iPad 4 GSM (Cellular)
        "iPad3,6"   : "iPad 4 Cell",                 // iPad 4 Global (Cellular)
        "iPad4,1"   : "iPad Air WiFi",               // iPad Air WiFi
        "iPad4,2"   : "iPad Air Cell",               // iPad Air Cellular
        "iPad4,3"   : "iPad Air China",              // iPad Air ChinaModel
        "iPad4,4"   : "iPad Mini 2 WiFi",            // iPad mini 2 WiFi
        "iPad4,5"   : "iPad Mini 2 Cell",            // iPad mini 2 Cellular
        "iPad4,6"   : "iPad Mini 2 China",           // iPad mini 2 ChinaModel
        "iPad4,7"   : "iPad Mini 3 WiFi",            // iPad mini 3 WiFi
        "iPad4,8"   : "iPad Mini 3 Cell",            // iPad mini 3 Cellular
        "iPad4,9"   : "iPad Mini 3 China",           // iPad mini 3 ChinaModel
        "iPad5,1"   : "iPad Mini 4 WiFi",            // iPad Mini 4 WiFi
        "iPad5,2"   : "iPad Mini 4 Cell",            // iPad Mini 4 Cellular
        "iPad5,3"   : "iPad Air 2 WiFi",             // iPad Air 2 WiFi
        "iPad5,4"   : "iPad Air 2 Cell",             // iPad Air 2 Cellular
        "iPad6,3"   : "iPad Pro 9.7inch WiFi",       // iPad Pro 9.7inch WiFi
        "iPad6,4"   : "iPad Pro 9.7inch Cell",       // iPad Pro 9.7inch Cellular
        "iPad6,7"   : "iPad Pro 12.9inch WiFi",      // iPad Pro 12.9inch WiFi
        "iPad6,8"   : "iPad Pro 12.9inch Cell",      // iPad Pro 12.9inch Cellular
        "iPad6,11"  : "iPad 5th",                    // iPad 5th Generation WiFi
        "iPad6,12"  : "iPad 5th",                    // iPad 5th Generation Cellular
        "iPad7,1"   : "iPad Pro 12.9inch 2nd",       // iPad Pro 12.9inch 2nd Generation WiFi
        "iPad7,2"   : "iPad Pro 12.9inch 2nd",       // iPad Pro 12.9inch 2nd Generation Cellular
        "iPad7,3"   : "iPad Pro 10.5inch",           // iPad Pro 10.5inch A1701 WiFi
        "iPad7,4"   : "iPad Pro 10.5inch",           // iPad Pro 10.5inch A1709 Cellular
        "iPad7,5"   : "iPad 6th",                    // iPad 6th Generation WiFi
        "iPad7,6"   : "iPad 6th",                    // iPad 6th Generation Cellular
        "iPad8,1"   : "iPad Pro 11inch WiFi",        // iPad Pro 11inch WiFi
        "iPad8,2"   : "iPad Pro 11inch WiFi",        // iPad Pro 11inch WiFi
        "iPad8,3"   : "iPad Pro 11inch Cell",        // iPad Pro 11inch Cellular
        "iPad8,4"   : "iPad Pro 11inch Cell",        // iPad Pro 11inch Cellular
        "iPad8,5"   : "iPad Pro 12.9inch WiFi",      // iPad Pro 12.9inch WiFi
        "iPad8,6"   : "iPad Pro 12.9inch WiFi",      // iPad Pro 12.9inch WiFi
        "iPad8,7"   : "iPad Pro 12.9inch Cell",      // iPad Pro 12.9inch Cellular
        "iPad8,8"   : "iPad Pro 12.9inch Cell",      // iPad Pro 12.9inch Cellular
        "iPad11,1"  : "iPad Mini 5th WiFi",          // iPad mini 5th WiFi
        "iPad11,2"  : "iPad Mini 5th Cell",          // iPad mini 5th Cellular
        "iPad11,3"  : "iPad Air 3rd WiFi",           // iPad Air 3rd generation WiFi
        "iPad11,4"  : "iPad Air 3rd Cell"            // iPad Air 3rd generation Cellular
    ]

    if let deviceName = deviceCodeDic[code] {
        return deviceName
    } else {
        if code.range(of: "iPod") != nil {
            return "iPod Touch"
        }else if code.range(of: "iPad") != nil {
            return "iPad"
        }else if code.range(of: "iPhone") != nil {
            print(code)
            return "iPhone"
        }else{
            return "unknownDevice"
        }
    }
}

ちなみにdeviceCodeがなんなのかは参考記事以外全然情報を見つけられなかったので自分の機体のcodeを一旦printして付け足しました(本当は全デバイスに対応したい...)

これで準備が整ったので次に特定のデバイスの時だけ処理を切り分けるということを行います。これはデバイスの正式名称が手に入っているのでcontainsを使えばいいです。

たとえば今回はiPhone 7系の時だけ別のストーリーボードから出したかったのでAppDelegateにこのように書きました(もちろんSceneDelegateがあるプロジェクトの場合はそちらに書くべき処理です。)

AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    let deviceName = getDeviceInfo()
    let imageStoryboard = UIStoryboard(name: "SevenImage", bundle: nil)
    if deviceName.contains("iPhone 7") {
        window?.rootViewController = imageStoryboard.instantiateInitialViewController()
    }
    return true
}

以降の処理の書き分けも同様のif文を用いてできます。

画像に反応させる

(だんだんタイトルが真面目になってきましたが)次に考えたのは画像に反応してAR上で何かをするというものです。
代表的なところでいうとAR名刺などが有名ですね。あとは海外の中高生デベロッパーがARKit出たてのころ、ゲームのパッケージをアンカーにゲームの紹介映像を流すサンプルアプリを制作してバズったりなんかもしてました。

僕もこれらを参考にやっていきます。
今回アンカーにしたのがこちらの画像

seven.png

最終目標はこの画像をアンカーにしてそこに3DオブジェクトをARで重ねるというものになります。

これはiPhone 7系でアプリを実行すると全画面表示されるようになっています。

つまり!

iPhone 7系ともう一台のiPhoneがあってはじめて成立するアプリ、それがこのアプリの最終形です!!!!!!!

気を取り直して、

この具体的なやり方はAR名刺の記事や最後に載せる完成形コードを見てもらえればいいのですが、ひとつ注意する点としては画像マーカーは必ずAssets.xcassetsの中にAR Resourcesというグループをつくって登録しなければならないという点です。Assets.xcassetsにそのまま入れた画像をマーカーにしようとしてもうまくいかなかったので注意しましょう。

...さてなんやかんやアニメーションなども適当に加えて、AR系の処理はiPhone 11系でしか実行されないように変えて、完成です。

実際にやってみた

さて実際にやってみた動画がこちら(YouTube)になります。

iPhone 7で画像マーカーが出て、それをiPhone 11で見てみると

そう!iPhone 7とiPhone 11がウーン!(CV:ピコ太郎)ってなってセブンイレブンロゴが出てきました!!!!!!!セブンとイレブンがあわさってセブンイレブン!!!!

image.png

......

..........

あ、ごめんなさい。死ぬほど引っ張ってきて結論これです...完全にオヤジギャグです。。。はい。

つまったところ

今回地味につまったところとしてARSCNViewを動かしたときに以下のWarningが出て画面が真っ暗のままになるという現象がありました。

2019-12-19 16:55:33.111128+0900 SevenElevenAR[22084:3756213] [Session] Session (0x113e3e9d0): did fail with error: Error Domain=com.apple.arkit.error Code=102 "Required sensor failed." UserInfo={NSLocalizedFailureReason=A sensor failed to deliver the required input., NSUnderlyingError=0x280329f20 {Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSLocalizedFailureReason=An unknown error occurred (-12780), NSLocalizedDescription=The operation could not be completed, NSUnderlyingError=0x280329740 {Error Domain=NSOSStatusErrorDomain Code=-12780 "(null)"}}}, NSLocalizedRecoverySuggestion=Make sure that the application has the required privacy settings., NSLocalizedDescription=Required sensor failed.}

これはいくつかフォーラムやStackOverflowにも報告されていて、iPhone側の設定でコンパスのキャリブレーションをONにするというものもあるそうなのですが、とりあえずは以下のコードを書くことによって直りました。

func restartSessionWithoutDelete() {
    arScnView.session.pause()
    arScnView.session.run(imageConfiguration, options: [.resetTracking, .removeExistingAnchors])
}

func session(_ session: ARSession, didFailWithError error: Error) {
    if let arError = error as? ARError {
        switch arError.errorCode {
        case 102:
            imageConfiguration.worldAlignment = .gravity
            restartSessionWithoutDelete()
        default:
            restartSessionWithoutDelete()
        }
    }
}

締め

というわけで今回は、完全に思いつきからARKitでちょっとしたダジャレアプリをつくってみました。

ハマりどころはたしかにあったものの、どんどんと手軽にこのぐらいのアプリなら作れるようになってきていていい世の中になっていってるなぁという気持ちです。
こちら完成品のコードを置いておくのでぜひ試してみてください笑

今日の記事以外にもARKitまわりではいくつか記事を書いているので興味があれば読んでみてください!

最近どんどんとAppStoreの審査基準は厳しくなっていますがこんなふうに遊びのアプリだったらどんどん自由な発想で作ってみるのが面白いんじゃないかなと思います。ぜひ面白いものをつくって技術力を上げていきましょう!

追伸

友達の面白い記事の雰囲気を参考に書いてみたのですが、Qiitaよりももっと自由に装飾できる媒体でやるべきでしたね。。。笑

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

Swiftでクリスマスツリーを描画してみた

久々の投稿でSwift縛りでどんな記事にしよか…

Swiftでなにかクリスマスっぽい事をしたいと思ったけど
案が何も思いつかない。。。。

ひらめいた!!

昔、フラクタル図形の木をObjective-Cで作った事があるから
それをSwiftで作ったらええんや??

そうと決まれば真似インスパイアできるコードを探すんや!!

全然ええ感じのやつが見つからへん…

ちょっと微妙な木の形のやつならインスパイアできそうやし
こうなったらちょっと妥協するか。。

どや!
Simulator Screen Shot - iPhone 8 - 2019-12-16 at 21.27.58.png
(これだとただのフラクタル図やな、、、せや!クリスマスっぽく装飾しよ!)

でやぁぁ!!
Simulator Screen Shot - iPhone 8 - 2019-12-16 at 21.42.42.png
(なんかまだまだクリスマスっぽくないな、、、
黄色一色の飾り付けがあかんねやな、カラフルにしたろ!)

とうぅぅぅ!!
Simulator Screen Shot - iPhone 8 - 2019-12-16 at 21.44.56.png
(だいぶクリスマスっぽいけどまだなんか足らんなーー、、
クリスマスって夜のイメージやから背景は黒やな!)

はぁッッッ!!!!
Simulator Screen Shot - iPhone 8 - 2019-12-16 at 21.45.52.png

もう一声ェェ!!!!!
Simulator Screen Shot - iPhone 8 - 2019-12-16 at 21.51.52.png

意外といい感じ!!www

Merry Christmasの文字はフリー素材を使わせていただきました!
描画で描こうと思いましたが時間がなかったorz(公開前日に記事を書いてます
それとツリーも本当はもっとクリスマスツリーっぽいものを描画したかったけど
時間が(ry
だれかもっとちゃんとしたクリスマスツリーを25日までに描いてくれー(他力本願

とまあ、めっちゃ中途半端な感じやけど
最後まで見てくれてありがとうございます!!
では 良いクリスマスを!!
(プレゼントにコードを置いていきます!)

追記

関連記事にシダの木を書いてる記事があったから
それを真似インスパイアしてまた挑戦したいな(´;ω;`)www

TreeView.swift
extension CGFloat {
  func degrees_to_radians() -> CGFloat {
    return CGFloat(Double.pi) * self / 180.0
  }
}

extension Double {
  func degrees_to_radians() -> Double {
    return Double(Double.pi) * self / 180.0
  }
}

class TreeView: UIView {

    func drawTree(x1: CGFloat, y1: CGFloat, angle: CGFloat, depth:Int){
        if depth == 0 {
            return
        }

        let ang = angle.degrees_to_radians()
        let x2:CGFloat = x1 + ( cos(ang) as CGFloat) * CGFloat(depth) * (self.frame.width / 60)
        let y2:CGFloat = y1 + ( sin(ang) as CGFloat) * CGFloat(depth) * (self.frame.width / 60)

        let line = self.drawLine(x1: x1, y1: y1, x2: x2, y2: y2)
        line.stroke()

        self.drawTree(x1: x2, y1: y2, angle: angle - 20, depth: depth - 1)
        self.drawTree(x1: x2, y1: y2, angle: angle, depth: depth - 1)
        self.drawTree(x1: x2, y1: y2, angle: angle + 20, depth: depth - 1)

        if depth < 4 && self.randomBool(percent: 5){
            let circle = UIBezierPath(arcCenter: CGPoint(x: x1, y: y1), radius: 3, startAngle: 0, endAngle: CGFloat(Double.pi)*2, clockwise: true)
            UIColor.yellow.setFill()
            circle.fill()
        } else if depth < 4 && self.randomBool(percent: 5) {
            let circle = UIBezierPath(arcCenter: CGPoint(x: x1, y: y1), radius: 3, startAngle: 0, endAngle: CGFloat(Double.pi)*2, clockwise: true)
            UIColor.red.setFill()
            circle.fill()
        } else if depth < 4 && self.randomBool(percent: 5) {
            let circle = UIBezierPath(arcCenter: CGPoint(x: x1, y: y1), radius: 3, startAngle: 0, endAngle: CGFloat(Double.pi)*2, clockwise: true)
            UIColor.blue.setFill()
            circle.fill()
        }
    }

    func drawSnow() {
        let circle = UIBezierPath(arcCenter: CGPoint(x: x1, y: y1), radius: 3, startAngle: 0, endAngle: CGFloat(Double.pi)*2, clockwise: true)
        UIColor.red.setFill()
        circle.fill()

    }

    func drawLine(x1:CGFloat, y1:CGFloat, x2:CGFloat, y2:CGFloat) -> UIBezierPath
    {

        let path = UIBezierPath()
        path.move(to: CGPoint(x: x1,y: y1))
        path.addLine(to: CGPoint(x: x2,y: y2))
        path.lineWidth = 0.5
        return path
    }

    override func draw(_ rect: CGRect) {

        let color = UIColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0)
        color.set()

        self.drawTree(x1: self.frame.width / 2 , y1: self.frame.height * 0.8, angle: -90 , depth: 9 )
    }

    func randomBool(percent: Int) -> Bool {
        return arc4random_uniform(100) < percent
    }    
}


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

[Swift]堅牢で崩れにくいコードを実現する

swiftでできない2つのことを利用

associatedtype

  • genericなprotocolを作れる
protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
    func makeIterator() -> Iterator
 }
  • contrete type(class, struct, enum)に準拠させる際に実際の型を指定して使える
  • generic functionで型を指定して使える

associatedtypeを持つprotocolをpropertyの型として利用することができない

extension

  • protocolやcontrete type(class, struct, enum)の機能を拡張できる

extensionブロックの中でstored propertyを持つことができない(黒魔術を使わない限り)

実装例

1. 依存用と公開用のprotocolを分ける

二つのprotocolを用意する

  • 何に依存しているかを明確にする
protocol Repository {
    var remoteStore: StoreRemoteStore! { get }
    var memoryStore: StoreMemoryStore! { get }
}
  • 何を公開しているかを明確にする
protocol StoreRepository {
    func find(id: Int, completion: (Store) -> Void)
}
  • 実装
struct StoreRepositoryImpl: Repository, StoreRepository {
    let remoteStore: StoreRemoteStore!
    let memoryStore: StoreMemoryStore!

    func find(id: Int, completion: (Store) -> Void) {
        fatalError("need to implement")
    }
}
  • 使い所
import UIKit

final class ViewController: UIViewController {

    private let storeRepo: StoreRepository = StoreRepositoryImpl()

    override func viewDidLoad() {
        super.viewDidLoad()

        // StoreRepositoryの持つfindしかアクセスできない
        storeRepo.find(id: 1) { (store) in
            print(store)
        }
    }
}
  • 問題点
import UIKit

final class ViewController: UIViewController {

    private let storeRepo: Repository = StoreRepositoryImpl() // 依存用プロトコルがpropertyの型として利用されてしまう

    override func viewDidLoad() {
        super.viewDidLoad()

        storeRepo.remoteStore.find(id: 1) { (store) in
            print(store)
        }
    }
}

2. 依存用のprotocolをpropertyとして持てなくする

associatedtypeを使って依存protocolを作る

  • 依存用protocol (blueprint)
protocol Repository {
    associatedtype RemoteStore
    associatedtype MemoryStore

    var remoteStore: RemoteStore! { get }
    var memoryStore: MemoryStore! { get }
}
  • 公開用protocol (interface)
protocol StoreRepository {
    func find(id: Int, completion: (Store) -> Void)
}
  • 実装
struct StoreRepositoryImpl: Repository, StoreRepository {
    typealias RemoteStore = StoreRemoteStore
    typealias MemoryStore = StoreMemoryStore

    let remoteStore: RemoteStore!
    let memoryStore: MemoryStore!

    func find(id: Int, completion: (Store) -> Void) {
        fatalError("need to implement")
    }
}
  • 使い所
final class ViewController: UIViewController {

    private let storeRepo: StoreRepository = StoreRepositoryImpl()

    private let storeRepo2: Repository = StoreRepositoryImpl() // これができなくなる

    override func viewDidLoad() {
        super.viewDidLoad()

        storeRepo.find(id: 1) { (store) in
            print(store)
        }
    }
}

3.余計なpropertyを生やさせない

extensionを利用する

  • 依存protocol (blueprint)
protocol Repository {
    associatedtype RemoteStore
    associatedtype MemoryStore

    var remoteStore: RemoteStore! { get }
    var memoryStore: MemoryStore! { get }
}
  • 公開protocol (interface)
protocol StoreRepository {
    func find(id: Int, completion: (Store) -> Void)
}
protocol UserRepository {
    func findAll(completion: ([User]) -> Void)
}
  • 依存protocolの実装
struct RepositoryImpl<R, M>: Repository {
    let remoteStore: R
    let memoryStore: M
}
  • 公開protocolの実装
extension RepositoryImpl: UserRepository where R: UserRemoteStore, M: UserMemoryStore {
  func findAll(completion: ([User]) -> Void) {
    fatalError("need to implement")
  }
}

extension RepositoryImpl: StoreRepository where R: StoreRemoteStore, M: StoreMemoryStore {
  func find(id: Int, completion: (Store) -> Void) {
    fatalError("need to implement")
  }
}
  • 使い所
final class ViewController: UIViewController {

    private let userRepo: UserRepository = RepositoryImpl<UserRemoteStoreImpl, UserMemoryStoreImpl>()
    private let storeRepo: StoreRepository = RepositoryImpl<StoreRemoteStoreImpl, StoreMemoryStoreImpl>()

    override func viewDidLoad() {
        super.viewDidLoad()

        userRepo.fetchAll { (users) in
            print(users)
        }

        storeRepo.find(id: 1) { (store) in
            print(store)
        }
    }
}

まとめ

インスタンスをpropertyとして持つことが前提となりますが、
使う側からは依存用protocolのpropertyにアクセスすることができず、
また実装クラスが依存するproperty以外の余計なpropertyを増やすことができなくなります。(大勢で作業する上で好き勝手されない)

※簡易的な実装例のため省略していますが、↑の例だと依存protocolの実装をpropertyとして利用できるため、Default Initializerを利用せず、依存protocolの実装クラスを利用するクラスから参照できないようにするといったアプローチが必要になります。

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

XCTest+αでJSON形式のテストデータを使い倒す

テストを拡充させていくにあたり,検証に都合のよいデータを手軽に用意できるようにした作業内容のメモ.
サンプルコード→gaussbeam/TestDataloader

やりたいこと

  • あらかじめ用意しておいた.jsonファイルと型を指定することで,jsonファイル内のデータをもとに必要な形式のデータを取得できるようにする

なぜやりたいのか

  1. テストコード内でinit()すると,テスト自体とは直接関わりのないコードが増え,テストの内容が把握しづらくなる
  2. そもそもinit(from decoder: Decoder)しか実装していないようなオブジェクトの場合,コードからのテストデータ生成のために別途イニシャライザを定義する必要がある
  3. 異なるテストケースで同様のデータを利用する場合,その都度データ生成用のコードを書くのは冗長

やったこと

以下に示す方法でjsonファイルから任意の型のテストデータを生成できるようにした.

1. Loader Protocolを定義

このProtocolにより,バンドル内のファイル取得→Dataまでの変換を共通的に行うようにした.
また,変換後の型を任意のものにできるよう,associatedtypeを指定させるようにしている.

実装は以下の通り.

protocol Loader {
    associatedtype DataType

    var bundle: Bundle { get }
    func load(resourceName: String) -> DataType
    func loadData(resourceName: String) -> Data
    func convert(data: Data) -> DataType
}

extension Loader {
    var bundle: Bundle {
        return Bundle(identifier: "jsonファイルを含んだバンドルのID")!
    }

    func load(resourceName: String) -> DataType {
        let data = loadData(resourceName: resourceName)
        return convert(data: data)
    }

    func loadData(resourceName: String) -> Data {
        let path = bundle.path(forResource: resourceName, ofType: "json")!
        let url = URL(fileURLWithPath: path)
        return try! Data(contentsOf: url)
    }
}

Loaderは以下のような流れでjsonからオブジェクトへの変換を行う.

  1. 所定のバンドル内から,指定されたjsonファイルを取得
  2. 取得したjsonファイルの内容をData型に変換
  3. Data型からDataType型(associatedtypeとして紐付け)に変換
  4. 変換されたDataType型のオブジェクトを返す

上記の処理について,1~4までの全体の流れはload()メソッドの,1~2はloadData()としてデフォルト実装としてそれぞれ定義している.
3の変換処理についてはconvert()が該当するが,この処理(およびDataTypeの指定)については,Loaderの適合先でそれぞれ実装させることで,任意の型への変換ができるようにした.

2. 型ごとのLoaderを定義

JSON(=[String: Any])とDecodableに適合したオブジェクト用のLoaderを定義する場合,以下のようになる.

/// jsonファイルから取得した内容を`[String: Any]`に変換するLoader
struct JsonLoader: Loader {
    typealias DataType = [String: Any]

    func convert(data: Data) -> DataType {
        return try! JSONSerialization.jsonObject(with: data, options: []) as! DataType
    }
}

/// jsonファイルから取得した内容を`Decodable`に適合した任意の型に変換するLoader
struct DecodableObjectLoader<T: Decodable>: Loader {
    typealias DataType = T

    func convert(data: Data) -> DataType {
        return try! JSONDecoder().decode(DataType.self, from: data)
    }
}

上記の例以外にも,Dataから変換可能な型はLoaderを定義できる.
例えば,JSONのパースにObjectMapperを使っているような場合,以下のように定義できる.

struct MappableObjectLoader<T: Mappable>: Loader {
    typealias DataType = T

    func convert(data: Data) -> DataType {
        let jsonString = String(data: data, encoding: .utf8)!
        return DataType(JSONString: jsonString)!
    }
}

3. Loaderとテストデータをプロジェクトファイル内に組み込み,テストを記述.

Loaderとテストデータ,およびテストコードを以下のように配置することで,XCTestで利用可能になる.
※ Frameworkを分けている場合に,テストデータやLoaderを共通化したい場合は注意が必要(これはそのうち別記事で書く)
スクリーンショット 2019-12-18 17.11.45.png

Loaderによるテストデータの取得(およびテストを実行)例は以下の通り.
(詳細はサンプルコードを参照)

class CouponTests: XCTestCase {
    func testIsUsed() {
        // ジェネリクスでデコード先の型を指定
        let loader = DecodableObjectLoader<Coupon>()

        XCTContext.runActivity(named: "usedDate==nilの場合,isUsedはfalse") { _ in
            // 引数としてテストデータのファイル名を指定
            let c = loader.load(resourceName: "unusedCoupon")
            XCTAssertFalse(c.isUsed)
        }

        XCTContext.runActivity(named: "usedDate!=nilの場合,isUsedはtrue") { _ in
            let c = loader.load(resourceName: "usedCoupon")
            XCTAssertTrue(c.isUsed)
        }
    }
}

なお,上記のコードで使用したオブジェクトの定義およびjsonファイルは以下の通り.

// (参考)Loader経由で取得するオブジェクトの型
struct Coupon: Decodable {
    var id: Int
    var title: String
    var usedDate: Date?
    var isUsed: Bool {
        return usedDate != nil
    }
}
unusedCoupon.json
{
    "id": 123,
    "title": "Some coupon",
    "usedDate": null
}

APIのモックに利用する

上記の例ではCoupon型にデコードした値のアサーションを行っているが,別の用法として[String: Any]としてデコードした結果をAPIのモックに利用することも可能.

例:Mockingjayと組み合わせた場合

class SomeApiTests: XCTestCase {
    func testSomeApi() {
        
        stubHttp(invitationRequest) { request in
            let body = JsonLoader().load(resourceName: "usedCoupon")
            return json(body, status: 200)(request)
        }
        
        // usedCoupon.jsonの内容で書き換えたAPIレスポンスに対するテスト
    }
}

やってみて

よかったこと

  1. テストデータ生成を1行で行えるようになった
    • 当初の動機であった「テスト自体とは直接関わりのないコードの増加を避ける」ことができた.
      また,所定のBundle内にあるファイルからData型で取得しそれを変換し…というプロセスもLoader内に集約したので,テストコード自体の見通しが良くなった.
  2. 1つのデータをいろいろな形式で使えるようになった
    • 例で挙げたように,Decodableに適合したオブジェクトやJSON(=[String: Any])など任意の型で取得できるので,異なる観点・やり方のユニットテストにおいて,1つのjsonファイルを使い回せるようになった.
  3. レスポンスの書き換えにも利用できる
    • Charlesと組み合わせることで,APIレスポンスの書き換えにも利用できる.
      当初は想定していなかったことだが,ユニットテストのみならず,デバッグ実行時の振る舞い確認においても任意の状況を作ることができ,より開発がしやすくなった.

改善したいこと

  1. テストデータの置き場
    • 先にも少し触れたが,特定のテストターゲット内にバンドルしているため,Frameworkを分けている場合にテストデータを共用できない.
      • :pencil: Kickstarterでは共用Framework(Library)の中にTestHelperというグループを作り,Frameworkをまたいでテスト用のヘルパを共有している
        • この方法をそのまま使うとテストデータもアプリ本体にバンドルされてしまうのでもうひと工夫必要になりそう :thinking:
  2. Bundleの取得方法
    • Bundle自体もIdentifierをベタ書きしているのでポータビリティが低い(一箇所指定するだけなのでそこまで実害はないが…).
      • これについては,Extensionのデフォルト実装でやっていることに起因しているので,役割を分けてBundleを取得するクラスを使うのがよさそう.

「もっとうまいやり方あるよ!」という場合にはコメントにてお願いします :pray:

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

UIButtonのアイコンのダークモード対応

背景

私もダークモード対応したかっこいいアプリをリリースしたい?
でも試しにやってみたら、思ったより気を使う場所がたくさん!
例えばボタンのTextColorを変えただけだと…
Simulator Screen Shot - iPhone 11 Pro Max - 2019-12-19 at 11.36.43.JPG

?

デザイナーさんも忙しそうだし、ボタンに設定してる単色アイコンくらいならコードで色を変えた方が楽なのでは…?
ということで、UIButtonのカスタムクラスを作成することにしました。

やってみる

こちらが今回作ったものです。
ボタンのアイコンとテキストを別な色で設定したい人向けです。

カスタムクラスを用意する

まず@IBInspectableでStoryboardから色を設定できるようにします。
そして画面を表示している途中でモードを切り替えた時のためにdraw()の中で色を変更します。
ついでにハイライトの色もセットしておきます。
(ダークモード用にテキストのハイライトも忘れずに)

class IconButton: UIButton {
    @IBInspectable var iconColor: UIColor?

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        self.setTitleColor(self.currentTitleColor.withAlphaComponent(0.5), for: .highlighted)

        if let image = self.image(for: .normal), let iconColor = self.iconColor {
            self.setImage(image.tint(color: iconColor), for: .normal)
            self.setImage(image.tint(color: iconColor.withAlphaComponent(0.5)), for: .highlighted)
        }
    }
}

extension UIImage {
    func tint(color: UIColor) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(self.size, false, 0)
        color.setFill()
        let drawRect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)
        UIRectFill(drawRect)
        self.draw(in: drawRect, blendMode: .destinationIn, alpha: 1)
        let tintedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return tintedImage
    }
}

Storyboardで設定する

ボタンのCustomClassに先ほど作成したクラスを設定します。
スクリーンショット 2019-12-18 11.57.03.png
iconColorに画像の色(ダークモード対応ずみのもの)を設定します。
スクリーンショット 2019-12-19 11.26.19.png

できました

ライトモード

Simulator Screen Shot - iPhone 11 Pro Max - 2019-12-19 at 11.28.48. jpeg

ダークモード

Simulator Screen Shot - iPhone 11 Pro Max - 2019-12-19 at 13.24.30.JPG

ハイライトはそれぞれこんな感じ
Simulator Screen Shot - iPhone 11 Pro Max - 2019-12-19 at 11.28.49 2.JPG

Simulator Screen Shot - iPhone 11 Pro Max - 2019-12-19 at 11.29.01 2.JPG

完成?

本当は…

SF Symbolsを使えばサクッとダークモード対応ができるみたいなので、もし可能であればそちらを使った方が良いかもしれません?
https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/

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

スクロールビューとボタンを使ったウォークスルーの実装

初めまして,テシマ(@tessy_0901)と申します.
こちらではPageViewControllerとボタンを使ったウォークスルーの実装を書きましたが,スクロールビューを使って同じ動作をさせたいと思います.
先駆者様の最高の記事がめちゃくちゃ多いため,適宜リンクを貼って省略させていただきます.

PageViewControllerの代わりにScrollViewを使うメリットとしては,

  • StoryBoard内のViewControllerの1画面で完結する
  • PageControlの位置を変更しやすい(←ここがでかい)
  • swiftファイルの数を1つにできる

などがあります.(実際にこっちの方が簡単だった)
また,Xcode11からはScrollViewをXcode上でスワイプさせて確認することができるようになったため,こちらを使う方がベターだと感じました.

では本文です.

この記事でできること

  • UIViewControllerでPageControlができる
  • ボタンでページ移動ができる

完成品

Unknown.gif
GitHub
目に悪い!!

実装

UIPageViewControllerの実装

project作成

省略します!(上にあるPageControllerの記事を参考にしたりしなかったりしてください)

UIScrollViewによるPageViewの作成(StoryBoard)

UIScrollViewとPageControlの設置や設定は,この記事参考にしてください(丸投げ)
それとXcode11からUIScrollViewの方式が変わりました.
以前(Xcode10)は,

View
┠Safe Area
┠UIScrollView
┃ ┗View
┃   ┠View1
┃   ┠View2
┃   ┠...
┗PageControl

のように,UIScrollViewの下にScrollView全体を示すViewを置き,その下に各Viewを置く階層構造でしたが,
Xcode11からは

Xcode11
View
┠Safe Area
┠UIScrollView
┃ ┠Content Layout Guide
┃ ┠Frame Layout Guide
┃ ┠View1
┃ ┠View2
┃ ┠...
┗PageControl

このように,Content Layout GuideFrame Layout Guideという要素が追加され,それぞれが全てのViewのガイド,描画範囲のガイドとなりました,
詳しくはこちらこちらをご覧ください.
ここからが大変なのですが,上記の記事にもあるように,AutoLayoutの設定が変わりました.

AutoLayoutの設定

完成形はこうなります(白目)
スクリーンショット 2019-12-18 20.09.46.png

ちゃんと一つ一つ説明していきます.
まずView1,View2,View3の制約ですが,

View1.trailing = View2.leading
View2.trailing = View3.leading

として,View1の右端にView2の左端,View2の右端にView3の左端をくっつけます.
次にView1のWidth,HeightをViewのWidth,Heightと等しくします.

次にContent Layout Guideの設定です.

View1.top = ContentLayoutGuide.top
View2.top = ContentLayoutGuide.top
View3.top = ContentLayoutGuide.top
View1.buttom = ContentLayoutGuide.button
View2.buttom = ContentLayoutGuide.buttom
View3.buttom = ContentLayoutGuide.buttom
View1.leading = ContentLayoutGuide.leading
View3.trailing = ContentLayoutGuide.trailing

全てのviewのtop,buttomをContent Layout Guideのtop,buttomと合わせて,
leading,trailingはそれぞれ端のview(leading = View1, trailing = View3)と合わせます.

最後にView1のheight, widthを大元のViewのheight, widthに合わせ,

View1.height = height
View1.width = width
View2.height = View1.height
View2.width = View1.width
View3.height = View1.height
View3.width = View1.width

View1=View2=View3と合わせることで,どの端末でも画面の大きさが変わらないようにしました.
これで画面は完成です.

UIScrollViewによるPageViewの作成(ソースコード)

追加したScrol, PageControllをBIOutletとして, ButtonをIBActionとしてそれぞれつなぎます.
そしてScrollViewのdelegateを設定するとこうなります.

ViewController
import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var pageScrollView: UIScrollView!
    @IBOutlet weak var pageControl: UIPageControl!
    @IBAction func onButtonTapped(_ sender: Any) {
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.pageScrollView.delegate = self
    }
}

ここまでできたら中身を書いていきましょう


ボタンによる遷移の実装

とりあえず完成したソースコード載せます

ViewController
import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var pageScrollView: UIScrollView!
    @IBOutlet weak var pageControl: UIPageControl!
    @IBAction func onButtonTapped(_ sender: Any) {
        let contentSize: CGSize = self.pageScrollView.contentSize
        let currentPage: Int = pageControl.currentPage
        pageScrollView.setContentOffset(CGPoint(x: floor(contentSize.width/3 * CGFloat(currentPage + 1)), y: 0), animated: true)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.pageScrollView.delegate = self
    }

}

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView:UIScrollView) {
        let pageControlNumber = pageScrollView.contentOffset.x / pageScrollView.frame.size.width
        if pageControlNumber.truncatingRemainder(dividingBy: 1) == 0 {
            pageControl.currentPage = Int(pageControlNumber)
        }

    }
}
@IBAction func onButtonTapped(_ sender: Any) {
        let contentSize: CGSize = self.pageScrollView.contentSize
        let currentPage: Int = pageControl.currentPage
        pageScrollView.setContentOffset(CGPoint(x: floor(contentSize.width/3 * CGFloat(currentPage + 1)), y: 0), animated: true)

ここではボタンを押した際に,ScrollView全体の中身のサイズを取得して,PageControl分だけずらすという動作にしています.

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView:UIScrollView) {
        let pageControlNumber = pageScrollView.contentOffset.x / pageScrollView.frame.size.width
        if pageControlNumber.truncatingRemainder(dividingBy: 1) == 0 {
            pageControl.currentPage = Int(pageControlNumber)
        }

    }
}

また,UIScrollのdelegateを指定しているためスクロールのオフセットの変化を検知するメソッドを呼び出せます.
そのためそのメソッドで,currentPageをスクロールが終わった際(→ Scrollのオフセット/画面の横幅 が整数の時)にcurrentPageが与えられるようにしています.

最後に

流石に雑すぎるので追記でもっと丁寧に解説します・・・
それと弊部のアドカレもよろしくお願いします!!!!!!!

参考文献

https://techblog.recochoku.jp/7237
https://program-life.com/538
https://developer.apple.com/documentation/uikit/uiscrollview/2865772-framelayoutguide
https://developer.apple.com/documentation/uikit/uiscrollview/2865870-contentlayoutguide

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

FeliCa が遅いしフルスキャンも不可能 前編【iOS 13 Core NFC】

気づけば Advent Calendar も後半戦…。
私はこれまで FeliCa を iOS 13 の Core NFC で読み取るサンプルを紹介してきました。

そして、前回の

では iOS での FeliCa の取り扱いの制限(システムの切り替えを IDm で行うことができない)について述べました。

…ですが、現在の iOS 13 での FeliCa に関する制限はまだまだあるのです…。

この記事で紹介する iOS での FeliCa の制限

まとめを先に書いておきます。

  • FeliCa の読み取りで得られる最初の currentSystemCode は Info.plist での FeliCa システムコードの記述する順番に依存する
  • FeliCa の読み取り速度は Info.plist での FeliCa システムコードの記述する順番に依存する
  • FeliCa のフルスキャンを iOS で行いたい場合、「じゃあ Info.plist に存在する全ての FeliCa システムコードを記載すればいいのでは…?」と考えつくが、それは現実的な解決策ではない → 後編につづく

FeliCa の読み取りで得られる最初の currentSystemCode は Info.plist での FeliCa システムコードの記述する順番に依存する

異なる FeliCa システムコードを持つカードを読み取ろうとしてみる

これまでに書いた「楽天Edy、nanaco、WAON」を読み取る方法と、「交通系IC(Suica、PASMO、ICOCA、…etc.)」を読み取る方法で、あらかじめ 使う FeliCa システムコードを Info.plist に追加しておく必要がある と記載してきました。楽天Edy、nanaco、WAON で使われている FeliCa システムコードは 0xFE00、交通系IC で主に使われている FeliCa システムコードは 0x0003 になります。

では、異なる FeliCa システムコードを持つ 楽天Edy と 交通系IC の残高を読み取るための iOS App を開発するにはどうすればいいでしょうか。
Capability と Entitlements の設定は済ませておくとして、まずは使う FeliCa システムコードを Info.plist に追加します。

スクリーンショット 2019-12-18 11.30.44.png

こんな感じで Item 00xFE00Item 10x0003 を指定しました。
そして、NFCTagReaderSessionDelegatetagReaderSession(_:didDetect:) を書いて検出された FeliCa タグに接続、データの読み取る準備をします。

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
    let tag = tags.first!
    session.connect(to: tag) { (error) in
        if let error = error {
            session.invalidate(errorMessage: error.localizedDescription)
            return
        }

        guard case NFCTag.feliCa(let feliCaTag) = tag else {
            session.invalidate(errorMessage: "FeliCa ではない")
            return
        }

        session.alertMessage = "カードを読み取っています…"

        var serviceCode: Data!
        var startIndex = 0
        var endIndex = 0
        switch feliCaTag.currentSystemCode {
        case Data([0x00, 0x03]):
            // 交通系IC
            serviceCode = Data([0x00, 0x8B].reversed())
            startIndex = 11
            endIndex = 12
            break
        case Data([0xFE, 0x00]):
            // 楽天Edy
            serviceCode = Data([0x13, 0x17].reversed())
            startIndex = 0
            endIndex = 3
            break
        default:
            fatalError()
        }

        let blockList = [Data([0x80, 0x00])]
        feliCaTag.readWithoutEncryption(serviceCodeList: [serviceCode], blockList: blockList) { (status1, status2, blockData, error) in
            if let error = error {
                session.invalidate(errorMessage: error.localizedDescription)
                return
            }

            guard status1 == 0x00, status2 == 0x00 else {
                print("ステータスフラグがエラーを示しています", status1, status2)
                session.invalidate(errorMessage: "ステータスフラグがエラーを示しています s1:\(status1), s2:\(status2)")
                return
            }

            let data = blockData.first!
            let balance = data.toIntReversed(startIndex, endIndex)

            print(data as NSData)
            print("残高: ¥\(balance)")
            session.alertMessage = "残高: ¥\(balance)"
            session.invalidate()
        }
    }
}

こちらに示した tagReaderSession(_:didDetect:) 以外のコードは全て前々回の記事などと同じですので、そちらを参考にしてください。

検出された FeliCa カードの currentSystemCode が交通系ICのものか、楽天Edy のものかによって switch し、それぞれの serviceCode 等を指定しています。

ではビルドして実機で動作を確認してみましょう。

楽天Edy(カード型)を読み取った場合


残高が正しく表示されました。

Suica(Apple Pay)を読み取った場合


カード型でない、Suica が入っている Apple Watch に iPhone を近づけると…これも正しく残高を読み取ることができました。

Suica(カード型)を読み取った場合


では普通のカード型の Suica を読み取ると……と、ステータスフラグがエラー……?

status11status2166 (0xA6)というのは、"サービスコードリスト不正" にあたるので、サービスコードの指定が間違っていることになります。どうして Apple Pay の Suica は読み取れたのに、カードの Suica はダメだったんだろう…?

それぞれが持つ FeliCa システムコードを調べる

では、Japan NFC Reader を用いて Apple Pay の Suica とカード型の Suica が持つ FeliCa システムコードを調べてみましょう。
「その他」にある「IDm・PMm を読み取る」からカードをスキャンすると、入っている FeliCa システムコードを調べることができます。

楽天Edy(カード型)が持つ FeliCa システムコード


カード型の楽天Edyは 0x8B610xFE00 の2つの FeliCa システムコードを持っていることがわかります。

Apple Pay の Suica が持つ FeliCa システムコード


Apple Pay の Suica には確かに 0x0003 があることを確認できますね。

Suica(カード型)が持つ FeliCa システムコード


先ほど 読み取りに失敗したカード型の Suica は 0x0003 の他に… 0xFE00 も持っている!? 1

FeliCa の読み取りで得られる最初の currentSystemCode は Info.plist での FeliCa システムコードの記述する順番に依存する

なぜこのような問題が起きたのでしょうか。
今回、switchcurrentSystemCode が何かによって Suica か 楽天Edy かを判別しました。そして、0x0003 のみを持つ Apple Pay の Suica は currentSystemCode0x0003 であったため、無事に Suica と判別できましたが、カード型の Suica には 0x0003 の他に 0xFE00 も持っていたため、currentSystemCode0xFE00 となり 楽天Edyルートに入ることになってしまいました。

もし、FeliCa システムコードを用いてカードを判別しようとするとき、Info.plist に記述する FeliCa システムコードの順番が鍵となる場合があります。

スクリーンショット 2019-12-18 11.30.44.png

いまは Item 00xFE00Item 10x0003 を指定しました。この順番を逆にします。

スクリーンショット 2019-12-19 5.02.43.png

この Info.plist 以外はコードの変更を行わないまま、ビルドしてもう一度カード型の Suica をスキャンしてみましょう。

無事に正しい残高が表示されました!

FeliCa の読み取り速度は Info.plist での FeliCa システムコードの記述する順番に依存する

PaSoRi などでワイルドカードを使用して FeliCa カードに Polling をした場合、FeliCa に搭載されている 0 番目のシステムが検出されるかと思いますが、iOS 13 の Core NFC の場合は検出された FeliCa カードにあるシステムのうち、Info.plist に記載され 最も順番が前にある FeliCa システムコードに一致するシステム が検出、currentSystemCode および currentIDm にはそれが入ることになります。

そのため、多数の FeliCa システムコードに対応しようとした場合、Info.plist に記載する順番が非常に重要になります。

スクリーンショット 2019-12-19 5.21.09.png

取り上げたサンプルでは、FeliCa システムコードが2つだったので特に問題はありませんでしたが、上記スクリーンショットにある Item 4 の 0x8008 が検出されるまでには前4つ(0xFE000x00030x865E0x8592)に比べて時間がかかります。

これが iOS 13 Core NFC で FeliCa を扱う上でまた足を引っ張ることに………。

後編に続く


  1. Suica や PASMO 等、カードの発行時期などによってカードに含まれる FeliCa システムコードの数が異なります。 

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

CGAffineTransformを知る

はじめに

SwiftにはCGAffineTransformという構造体が存在します。2Dグラフィックの描画に使われる、アフィン変換を表します。

アフィン変換を知らなくても、変更したい値を渡せばスケーリング、回転、移動を行うCGAffineTransformを作成することができて、デフォルトにリセットできる機能があります。

CGAffineTransformはUIViewに存在するtransformというPropertyを用いて、Viewに対して変更を加えます。

var transform: CGAffineTransform { get set }

これはアフィン変換を知らないけど、CGAffineTransformを使っていた自分が、CGAffineTransformの仕組みを理解するための記事になります。

アフィン変換とは

幾何学の分野で、ある図形を回転させたり引き延ばしたりする変換をアフィン変換と呼ぶ。
もう少しきちんと説明すると、「アフィン変換とは平行移動と線形変換を組み合わせた変換」のこと。

具体的には、線形変換(拡大縮小、剪断、回転)、平行移動があり、これらの組み合わせで表現されます。

equation01_2x_fabc9070-1967-4d6f-a086-17ab5fcfef6d.png

CGAffineTransformで使用するアフィン変換は、上記の3行3列の行列式で表されます。

3列目は常に(0,0,1)ですので、CGAffineTransformのデータ構造には最初の2列のみの値が含まれます。

ここからはframeがCGRect(x: 0, y: 0, width: 50, height: 50)のUIViewに対して、どうやって計算しているかを見ていきます。

スクリーンショット 2019-12-21 16.33.36.png

・本来はSwiftにはanchorPointというPropertyで回転など幾何学的変更における中心点が決まっていて、デフォルトでは中央なのですが、今回は左上を設定しています。

rotation

CGAffineTransformでは

init(rotationAngle angle: CGFloat)

となっています、angleをラジアンで指定すると

iOSでは、正の値は反時計回りの回転を指定し、負の値は時計回りの回転を指定します。
macOSでは、正の値は時計回りの回転を指定し、負の値は反時計回りの回転を指定します。

angle = 40を指定した場合

スクリーンショット 2019-12-21 23.56.00.png

このようにcenterを軸に40度反時計回りに回転します

回転するためのアフィン変換の行列はこのようになっており、受け取ったangleを元にsin,cosを割り出し、各値が決定します。

equation10_2x_37c5a3fe-695a-4a02-983a-8e807267ffc0 (1).png

計算としては、angleを受け取ったことによって決定した上記の値を回転の計算式に当てはめます。

変換後のx座標 = x座標 x cosθ - y座標 x sinθ + tx
変換後のy座標 = x座標 x sinθ + y座標 x cosθ + ty

を行なっています。

上記の式を元に各座標を求めることができます。

scale

CGAffineTransformでは

init(scaleX sx: CGFloat, y sy: CGFloat)

となっています、scaleX, Yを指定するとViewはそれぞれx,y方向に指定したScaleの倍率分拡大されます。

sx: 2, sy: 1を指定した場合

スクリーンショット 2019-12-22 2.55.03.png

このように指定した方向に値の倍率分拡大します。

スケーリングするためのアフィン変換の行列はこのようになっており、受け取ったscaleX, Yを元に各値が決定します。

equation08_2x_1294061e-22f0-48f7-962a-51d31c17614f.png

計算としては、受け取ったscale値をそのまま座標に乗算します

変換後のx座標 = x座標 x ScaleX
変換後のy座標 = y座標 x ScaleY

を行なっています。

上記の式を元に各座標を求めることができます。

translation

CGAffineTransformでは

init(translationX tx: CGFloat, y ty: CGFloat)

となっています、translationX,Yを指定するとViewはそれぞれx,y方向に指定したtranslationの分移動します。
tx: 50, ty: 30を指定した場合

スクリーンショット 2019-12-22 3.14.50.png

このように指定した方向に値の分移動します。

スケーリングするためのアフィン変換の行列はこのようになっており、受け取ったtranslationX,Yを元に各値が決定します。

equation06_2x_7bc0c515-45dd-4adf-a3f1-426c6ef109f1.png

計算としては、受け取ったtranslation値をそのまま座標に加算します

変換後のx座標 = x座標 + translationX
変換後のy座標 = y座標 + translationY

上記の式を元に各座標を求めることができます。

おまけ

CGAffineTransformには

init(a: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat, tx: CGFloat, ty: CGFloat)

というinitが存在して、今までのいずれかのアフィン変換の行列を指定してあげれば、これからでも操作を行うことが可能です。

試しに40度の回転をしてみます。回転の行列はこうなので、

equation10_2x_37c5a3fe-695a-4a02-983a-8e807267ffc0 (1).png

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var affineView: UIView! {
        didSet {
            affineView.layer.anchorPoint = .init(x: 0, y: 0)
            affineView.frame = .init(x: 0, y: 0, width: 50, height: 50)

            let angle: CGFloat = 40.0

            let radian = (angle * (CGFloat.pi / 180))

            let affineSin = sin(radian)
            let affineCos = cos(radian)

            let affine = CGAffineTransform(a: affineCos,
                                           b: affineSin,
                                           c: -affineSin,
                                           d: affineCos,
                                           tx: 0,
                                           ty: 0)

            affineView.transform = affine
        }
    }


    override func viewDidLoad() {
        super.viewDidLoad()

    }
}


このように同じ行列に指定してあげると、この方法でもinit(rotationAngle angle: CGFloat)と結果を得ることができます。

最後に

本来はデフォルトである、中心を軸にした説明を行っていないので、今後加筆していきたいと思います。

ありがとうございました。

参考リンク

CGAffineTransform

アフィン変換とは

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

Swift の講師?‍?をすることになって、工夫したこと、学び直したこと、学習方法を考えたこと✏️

はじめに?

G's ACADEMY TOKYO (以降、ジーズ)で Swift の講師をしているやまたつです!
人生の初のアドベントカレンダーです!!!頑張って書きすぎて長くなりましたが読んでもらえると嬉しいです?‍♂️
(本記事は 12/14に書いており、まだ授業をしておりません。12/18に加筆しております。)

2019/12/15 からジーズで講師をさせていただいております!
講師をするためにどのようなことを行ったかや、工夫したこと、学び直したことなどを書いていこうと思います!

普段はフリーランスとして企業で iOSアプリ開発に従事しております?‍?
現場で学んできたことをできるだけ咀嚼して、学びやすく、
わかりやすく教えることができるようにしていきたいと思っています。

まだまだ新米の講師ですので、
他に良い方法があれば先輩方の皆様のアドバイスをいただきたいです?‍♂️
よろしくお願いいたします!

目標?

受講生の方が自走できるようにすることと、
プログラミングを楽しく学んでくれることを目標にしています!

Udemy などで学んでいると海外の講師の方は本当に楽しそうにプログラミングをしていると感じます。
ラベルの文字が変わると、「Great !!!!!!」と「Goooooooooooood!!!!」とたくさん言います笑
そのくらい自分も楽しみながら講師ができたら良いなと思っております。
講師の自分が楽しめると、受講生の方も楽しんでくれると信じています☺️

前提

ジーズの受講生は、プログラミング未経験の方 がほとんどです。
PCの扱いも不慣れな方もいらっしゃいます。
講師が専門用語を使わずにどれだけ説明できるか、
そして、受講生の方がプログラミングに苦手意識をもたないようにできるかが重要です。
そのため、挫折をできるだけ防ぐような仕組みづくりを考えています。

インプットしたこと?

講師をするために必要そうなことを考えて、インプットしました。

学び直したこと

  • Swift の基礎的なところ
    • フワッと理解してきたところを言語化(delegate や クロージャ)
  • 最初に躓きやすいところの洗い出し
    • 遷移方法、メソッドの作成、View のライフサイクル
  • iOS13 と iOS12以前のこと
    • ダークモードとハーフモーダルの簡単な対応方法
  • Xcode11 と Xcode10 のこと

新しく学習したこと

  • 「教える」ために必要なことを本からインプット
    • 教える技術
      • 失敗(エラー)を怖がらせない → 授業で人よりも遅れていても許容できるようにする
      • 具体的な形にして説明する
        • ?‍♂️「Todoアプリを作ります」
        • ?‍♂️「Todoアプリを作るために、タスクを追加、読み込みの機能を作ります」
      • ゴールを提示して、イメージできるようにする
        • ?‍♂️「授業始めます。まずは追加機能から作成しましょう〜」
        • ?‍♂️「授業始めます。今回の完成図はこれです(アプリを動かす)では、追加機能から作成しましょう〜」
      • 教えるだけでなく、理解度を図る
        • なぜエラーが出ているかを言語化してもらう。言語化のアシスタントをする。
    • 教える技術2
      • 最初はみんなできないことを伝える
        • 自分の失敗談を話す
        • バグを生みまくった話する
      • まずは聞く
        • エラーが出たときに説明してもらってから解説する
      • 話やすい雰囲気づくりを心がける
        • エラーが出ても言えない空気にしない
        • 自分が積極的にエラーを出したり、質問を投げかける
        • 自分の時は講師が呆れるくらい、質問をしていたことを話す
  • 教えるときに心がけることを動画や記事からインプット
    • リアクション
    • 具体例を話す方法
      • Udemy や 記事で例えを用いられているところを洗い出す
      • 例:class → 車、鯛焼き機
    • できるだけコードに対してコメントを残す
      • コメントを残すことで復習ができる
      • 処理の内容が日本語でわかるので、一気に覚えずに済む
  • 昔のプログラミングができなかった頃の自分を思い出す

以下、参考にさせていただいた資料です!
他にもたくさんありました。全て最高でした。ありがとうございます?‍♂️

自走するための教材情報

教材といっても自分が作成しているわけではないです?
今自分がSwiftを全く知らない状態から学んだときに、どのように学習したら効率的に学べるかを考えて教材を選びました。
もちろん、自分が使ったことのないものをお勧めすることはできないので、新しくUdemyで初心者向けの動画を買ったり、プログラミングの本を買ったりしています笑(メルカリで売るか〜)

本よりも動画の方が学びやすいと考えているので、動画をお勧めしています。

Udemy

  1. 【iOS13対応】未経験者がiPhoneアプリ開発者になるための全て iOS Boot Camp
  2. みんなのiOS講座 ゼロからSwiftで学ぶiPhoneアプリ開発の基礎

英語ができる方ならこちら↓↓↓↓↓
iOS 13 & Swift 5 - The Complete iOS App Development Bootcamp

自分が最初に学ぶ時は、日本企業の動画教材を使っていたのですが、残念ながらアップデートが全くないのでお勧めできなくなってしまいました?
Swift に関して は Udemy の講座の質が良いと感じております(主観)

どうしても本が読みたいなら

動画見たいけどギガがないよって人はこっちですかね?
本当に挫折できないくらいわかりやすかったです。

受講生のPCのセットアップ?

ここは重要かと思います!!!
エンジニアなら誰でもやっている設定かもしれませんが?‍?
初心者の方向けです!

1. キーリピートと、認識時間を最大にする

バックスペースの長押しの時間が長い時ありませんか?
自分は最初、「長い。。。一気に消したい」と思って設定しました!!!
結構重要です。

スクリーンショット 2019-11-23 午前9.28.46.png

2. 検索には GoogleChrome を使って、検索結果を1年以内にする

ここがSwiftを学ぶ上で一番重要なことかもしれません。(他の言語もそうかも)
Swiftは2014年に出た新しい言語で、毎年バージョンアップを繰り返しており、1年以上前の情報が役に立たないこともあります?

そのため、書いてあることが古くて動かなかったり、冗長なコードを含んでいたりします。
自分が初心者の時は Swift2 や Swift3 (3~4年前)のコードを検索して使っていたので、時間を無駄にしていたな〜と思いました。
検索結果を1年以内にすることで、無駄な検索時間を大幅に減らすことができると考えています!

自分はこの拡張機能を使っています!名前のセンスも良い笑→ ato-ichinen

3. 2回クリックしたら単語選択、3回クリックしたら行選択ができることを教える

セットアップとは関係ないですがこれはPCを使う上で必要不可欠かと?‍?
また、基本的なショートカットキーも!
最初はコピペだけでも大丈夫!(Mac: cmd + c, cmd + v)

4. Finder のサイドバーに作業スペース(workspace)を確保

デスクトップにファイルがたくさん置かれていると作業効率がとても悪いです。
今まで教えてきた中で、どこになんのファイルがあるか分からなくなる人がたくさんいました?
サイドバーからアクセスできることでファイルを探す手間がなくなります!

スクリーンショット 2019-12-13 11.49.47.jpg

5. キャッシュ削除コマンドを使えるようにする

Xcode で No such module だったり、 /Library/Developer/Xcode/DerivedData/ にデータが残ってるよ!
みたいなエラーがよく出てきませんか?その時によくやるコマンドを alias で定義させます。

以下の記事の引用です!

-----引用始まり

iOSアプリ開発でのキャッシュ削除方法まとめ

rm -rf ~/Library/Developer/Xcode/DerivedData/

Tip: 個人的に開発中にもっとも使うことの多いコマンドです。
~/.bashrcファイルにエイリアスを定義してcleanだけで簡単に実行できるようにしています。

~/.bashrc
alias clean='rm -rf ~/Library/Developer/Xcode/DerivedData'

--------引用終わり

授業で工夫していること?

ポインタを大きくする

どこをクリックしたかどうかがわかりやすくなります。

スクリーンショット 2019-11-23 午前10.13.26.png

ズーム機能をつけて拡大可能

コードを強調したいときに便利です。

スクリーンショット 2019-11-23 午前9.27.33.png

こんな感じで拡大できます!

zoom_swift_teacher.gif

Xcode、ターミナル の文字を拡大

授業中は遠くの席に座る方もいるので、支障がない程度に拡大しています。
iTerm 使ってたら ブラウザみたいにトラックパッドで文字が拡大することを初めて知りました。

よくあるエラー集を用意して、前で実演する

  • 削除するときは絶対に「Move to Trash」 、 「Remove Reference」はダメ!絶対!
    • 同じファイル名を再作成ができない。ファイルが残ってるから。
  • 参照(接続)が残っているとき
    • 右クリックして接続が残ってるかを確認する
  • エラーが出たら、コンソールで「reason」を打ち込む
    • 最初のうちはどこにエラーの原因が書いてあるかわからないから、「reason」で検索するようにします。
  • 謎のエラーが出たらこちら → Xcodeでデバッグ実行中にクラッシュした時に捗るブレークポイント設定
  • かぶるモジュール名はダメ!絶対!例:SwiftUI をアプリ名にしちゃう

エラーが出た時に、知識で解決するのではなく、「エラー見る → ググる → 解決」の手順を見せる

前述したとおり、「自走できるようにする」が目標です!
そのため、自分でエラーが解決できるようになることは最低条件です。

  1. エラーを見る。
  2. Google翻訳にぶっ込む(筆者は英語が苦手なこともあり最初はぶっ込みまくってました。今はなんとなく読めますよ笑)←できる人は飛ばして良い
  3. 内容を理解する
  4. ぐぐる
  5. それっぽい記事を見つける
  6. 試す
  7. 解決!解決できなかったら4に戻る。

上記の手順を授業中に行うようにしています。

今、必要のない知識を話さない。盛り込まない。

受講生側は、覚えることに必死です
より多くの知識を話してしまうと、「そんなことも覚えなきゃいけないのか。。。?」となりかねません。

例えば、
UILabel を扱う場合, 最初は、.text のプロパティさえ分かれば開発できます。
nameLabel.text = "やまたつ" ですね。
それなのに、「attributedText というプロパティもあって、こっちは〜」と言ってしまうのはよくないということです。
今は必要ではない知識を与えると混乱してしまいます。
できるだけ複雑化させないように努力しています?‍?

プログラミング学習では遅延評価勉強法が良いと思っています。
漠然と勉強するのではなく、必要な時に必要なものを勉強することが重要です。

勉強が苦手な人向けの「遅延評価勉強法」

前述の UILabel の話では、最初は .text プロパティだけ知ってるだけで十分です。
実装する時に、「色を変えたいな〜。フォントを変えたいな〜。アンダーラインひきたいな〜」と思った時に、 .textColor や、 .font 、.attributedText を勉強して使えるようになればいいです。
必要になるまでは最低限の知識だけを教えるようにしています。

遷移の方法を限定する

以下のようなコードで遷移できるようにするために、
Storyboard ではなく xib を使っています(Storyboard が xib の上位互換だということは承知しています)

FirstViewController.swift
let vc = SecondViewController()
present(vc, animated: true)

ここは賛否両論あると思いますが、できるだけスムーズに画面遷移ができるようにしたいと思っています。
Xib だと 「Cocoa Touch Class」からデザインファイルとコードファイルが同時に作成できるのも良い点です。

Storyboard の遷移はハマるポイントが多いため教えるのを辞めました。
(segue の id, storyboard の id, Is Initial View Controller のチェックのし忘れとか)
いろいろな現場を見てきましたが、一つの Storyboard で開発しているところはほぼありませんでした。
設定画面の簡単な遷移のところだけセグエを用いるというのはありましたが、ほとんどが 1Storyboard1ViewCotroller でした。
セグエでの遷移は紹介だけに留めておきました。

絶対に辞めた方がいい!!!と思う方はこっそり教えてください?‍♂️

イメージしやすいようにする

以下は Oshidori というアプリの View Hierarchy です。
どのように描写されているかをイメージするためにこの画像を見せています。
例えば、present の処理で上に ViewController が重なっていることを見せたり、
NavigationController があるから横に遷移できたりすることを教えるために使っています。

階層 斜めから見た図
貼り付けた画像_2019_12_18_午後11_35.png 貼り付けた画像_2019_12_18_午後11_35.png

めちゃ勉強になります↓↓↓
デバッグのためにView階層を把握する

Slack で情報交換する

チャンネルは2つ作成し、dev15xlab8, dev15xlab8_reference を作成しています。
Slack はフロー型の情報交換ツールなので、フロー型とストック型のチャンネルを一つずつ用意しています。

Slack を使用することで、授業中に書いたコードや、参考になるサイトを簡単に提供できたり、質問もすぐに受けることができます。

また、Slack のスタンプ文化がとても好きなので絵文字を200個ほど新しく追加しました。笑
コミュニケーションが容易になり、フレンドリーになる効果もあると思います?

Slackの絵文字(emoji)を一括エクスポート&インポートする

参考にさせていただいた資料まとめ

第一回授業を終えて(2019/12/18加筆)

最初の方はとてもスムーズに話せたり、受講生も理解してくれて手を動かすことができて良かったです!
めちゃくちゃ準備したので嬉しかったです☺️

しかし、「遷移の方法を限定する」を教えていた時に受講生の顔が全員、はてな?に変わりました。。。
UINavigationCotroller を持っているから、横に遷移できて、左上の「戻る」で戻れるんですよ!!!」
present で次の画面がニョキって出てきます。」
(本当はもっと詳しく、View Hierarchy を使って説明した)

でも全然納得してくれる顔をしてくれず、怖すぎました。。。
みんながわかっていない顔をするとこっちも怖いもんなんですね。。。つらかった。。。心のダメージえぐかったです。

受講生にフィードバックしてもらった?‍?

全力で教えたいので、不満があるところは教えてもらって、次の授業に生かそうと思ってやりました。
匿名制度にしているので誰が書いたか分からないので素直な意見を書いてもらえます。
Google Form とても便利です。すぐにアンケート作れて、回答ももらえます!
「分かりやすい」と言ってもらえるのはとても嬉しい反面、受講生の「はてな???」の顔が脳裏に焼きついています?
このフィードバックをもとに次の授業に取り組みます。

スクリーンショット 2019-12-18 午後11.54.31.png
スクリーンショット 2019-12-18 午後11.54.49.png

次回の目標?

フィードバックを糧にして、コードの見やすさや、資料作成をもっと分かりやすくできるようにしていきます。
画面を大きくしたり、コードを見やすいようにしたりなどは自分ではできていると思っていましたが、受講生目線で見るとできていないんだなと痛感しました?
まだまだ講師としての経験値も足りないので、どんどんフィードバックをもらってスキルアップしていこうと思います!!!
受講生の方が「自走できる」ようになることを目標に取り組んでいきます!

最後に

とても長い投稿になってしまいました!最後まで読んでいただきありがとうございました?‍♂️
講師の方や、後輩にプログラミングを教える方の参考になっていただければ幸いです!

ジーズでは講師やチューターを外部から雇っておらず、卒業生や関係者の方で行っております!!!
そのため、本気で教えるし、本気で考えて授業に取り組んでいます。
受講生も何か変わろうとして入校してくる方が多く、本気で授業に取り組み、質問も積極的にします。
受講生同士で切磋琢磨しています!

私も受講生の方から学ぶこともたくさんあり、お互いにWin-Winの関係で成長できていければと思っております!
気になった方は是非ジーズに遊びに来てみてください?

講師をするチャンスを与えてくれたジーズ運営にも感謝しています?‍♂️
ありがとうございました!!!

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

iOSアプリの講師?‍?をすることになって、工夫したこと、学び直したこと、学習方法を考えたこと✏️

はじめに?

G's ACADEMY TOKYO (以降、ジーズ)で iOSアプリ の講師をしているやまたつです!
人生の初のアドベントカレンダーです!!!頑張って書きすぎて長くなりましたが読んでもらえると嬉しいです?‍♂️
(本記事は 12/14に書いており、まだ授業をしておりません。授業後の12/18に加筆しております。)

2019/12/15 からジーズで講師をさせていただいております!
講師をするためにどのようなことを行ったかや、工夫したこと、学び直したことなどを書いていこうと思います!

普段はフリーランスとして企業で iOSアプリ開発に従事しております?‍?
現場で学んできたことをできるだけ咀嚼して、学びやすく、
わかりやすく教えることができるようにしていきたいと思っています。

まだまだ新米の講師ですので、
他に良い方法があれば先輩方の皆様のアドバイスをいただきたいです?‍♂️
よろしくお願いいたします!

目標?

受講生の方が自走できるようにすることと、
プログラミングを楽しく学んでくれることを目標にしています!

Udemy などで学んでいると海外の講師の方は本当に楽しそうにプログラミングをしていると感じます。
ラベルの文字が変わると、「Great !!!!!!」と「Goooooooooooood!!!!」とたくさん言います笑
そのくらい自分も楽しみながら講師ができたら良いなと思っております。
講師の自分が楽しめると、受講生の方も楽しんでくれると信じています☺️

前提

ジーズの受講生は、プログラミング未経験の方 がほとんどです。
PCの扱いも不慣れな方もいらっしゃいます。
講師が専門用語を使わずにどれだけ説明できるか、
そして、受講生の方がプログラミングに苦手意識をもたないようにできるかが重要です。
そのため、挫折をできるだけ防ぐような仕組みづくりを考えています。

インプットしたこと?

講師をするために必要そうなことを考えて、インプットしました。

学び直したこと

  • Swift の基礎的なところ
    • フワッと理解してきたところを言語化(delegate や クロージャ)
  • 最初に躓きやすいところの洗い出し
    • 遷移方法、メソッドの作成、View のライフサイクル
  • iOS13 と iOS12以前のこと
    • ダークモードとハーフモーダルの簡単な対応方法
  • Xcode11 と Xcode10 のこと

新しく学習したこと

  • 「教える」ために必要なことを本からインプット
    • 教える技術
      • 失敗(エラー)を怖がらせない → 授業で人よりも遅れていても許容できるようにする
      • 具体的な形にして説明する
        • ?‍♂️「Todoアプリを作ります」
        • ?‍♂️「Todoアプリを作るために、タスクを追加、読み込みの機能を作ります」
      • ゴールを提示して、イメージできるようにする
        • ?‍♂️「授業始めます。まずは追加機能から作成しましょう〜」
        • ?‍♂️「授業始めます。今回の完成図はこれです(アプリを動かす)では、追加機能から作成しましょう〜」
      • 教えるだけでなく、理解度を図る
        • なぜエラーが出ているかを言語化してもらう。言語化のアシスタントをする。
    • 教える技術2
      • 最初はみんなできないことを伝える
        • 自分の失敗談を話す
        • バグを生みまくった話する
      • まずは聞く
        • エラーが出たときに説明してもらってから解説する
      • 話やすい雰囲気づくりを心がける
        • エラーが出ても言えない空気にしない
        • 自分が積極的にエラーを出したり、質問を投げかける
        • 自分の時は講師が呆れるくらい、質問をしていたことを話す
  • 教えるときに心がけることを動画や記事からインプット
    • リアクション
    • 具体例を話す方法
      • Udemy や 記事で例えを用いられているところを洗い出す
      • 例:class → 車、鯛焼き機
    • できるだけコードに対してコメントを残す
      • コメントを残すことで復習ができる
      • 処理の内容が日本語でわかるので、一気に覚えずに済む
  • 昔のプログラミングができなかった頃の自分を思い出す

以下、参考にさせていただいた資料です!
他にもたくさんありました。全て最高でした。ありがとうございます?‍♂️

自走するための教材情報

教材といっても自分が作成しているわけではないです?
今自分がSwiftを全く知らない状態から学んだときに、どのように学習したら効率的に学べるかを考えて教材を選びました。
もちろん、自分が使ったことのないものをお勧めすることはできないので、新しくUdemyで初心者向けの動画を買ったり、プログラミングの本を買ったりしています笑(メルカリで売るか〜)

本よりも動画の方が学びやすいと考えているので、動画をお勧めしています。

Udemy

  1. 【iOS13対応】未経験者がiPhoneアプリ開発者になるための全て iOS Boot Camp
  2. みんなのiOS講座 ゼロからSwiftで学ぶiPhoneアプリ開発の基礎

英語ができる方ならこちら↓↓↓↓↓
iOS 13 & Swift 5 - The Complete iOS App Development Bootcamp

自分が最初に学ぶ時は、日本企業の動画教材を使っていたのですが、残念ながらアップデートが全くないのでお勧めできなくなってしまいました?
Swift に関して は Udemy の講座の質が良いと感じております(主観)

どうしても本が読みたいなら

動画見たいけどギガがないよって人はこっちですかね?
本当に挫折できないくらいわかりやすかったです。

受講生のPCのセットアップ?

ここは重要かと思います!!!
エンジニアなら誰でもやっている設定かもしれませんが?‍?
初心者の方向けです!

1. キーリピートと、認識時間を最大にする

バックスペースの長押しの時間が長い時ありませんか?
自分は最初、「長い。。。一気に消したい」と思って設定しました!!!
結構重要です。

スクリーンショット 2019-11-23 午前9.28.46.png

2. 検索には GoogleChrome を使って、検索結果を1年以内にする

ここがSwiftを学ぶ上で一番重要なことかもしれません。(他の言語もそうかも)
Swiftは2014年に出た新しい言語で、毎年バージョンアップを繰り返しており、1年以上前の情報が役に立たないこともあります?

そのため、書いてあることが古くて動かなかったり、冗長なコードを含んでいたりします。
自分が初心者の時は Swift2 や Swift3 (3~4年前)のコードを検索して使っていたので、時間を無駄にしていたな〜と思いました。
検索結果を1年以内にすることで、無駄な検索時間を大幅に減らすことができると考えています!

自分はこの拡張機能を使っています!名前のセンスも良い笑→ ato-ichinen

3. 2回クリックしたら単語選択、3回クリックしたら行選択ができることを教える

セットアップとは関係ないですがこれはPCを使う上で必要不可欠かと?‍?
また、基本的なショートカットキーも!
最初はコピペだけでも大丈夫!(Mac: cmd + c, cmd + v)

4. Finder のサイドバーに作業スペース(workspace)を確保

デスクトップにファイルがたくさん置かれていると作業効率がとても悪いです。
今まで教えてきた中で、どこになんのファイルがあるか分からなくなる人がたくさんいました?
サイドバーからアクセスできることでファイルを探す手間がなくなります!

スクリーンショット 2019-12-13 11.49.47.jpg

5. キャッシュ削除コマンドを使えるようにする

Xcode で No such module だったり、 /Library/Developer/Xcode/DerivedData/ にデータが残ってるよ!
みたいなエラーがよく出てきませんか?その時によくやるコマンドを alias で定義させます。

以下の記事の引用です!

-----引用始まり

iOSアプリ開発でのキャッシュ削除方法まとめ

rm -rf ~/Library/Developer/Xcode/DerivedData/

Tip: 個人的に開発中にもっとも使うことの多いコマンドです。
~/.bashrcファイルにエイリアスを定義してcleanだけで簡単に実行できるようにしています。

~/.bashrc
alias clean='rm -rf ~/Library/Developer/Xcode/DerivedData'

--------引用終わり

授業で工夫していること?

ポインタを大きくする

どこをクリックしたかどうかがわかりやすくなります。

スクリーンショット 2019-11-23 午前10.13.26.png

ズーム機能をつけて拡大可能

コードを強調したいときに便利です。

スクリーンショット 2019-11-23 午前9.27.33.png

こんな感じで拡大できます!

zoom_swift_teacher.gif

Xcode、ターミナル の文字を拡大

授業中は遠くの席に座る方もいるので、支障がない程度に拡大しています。
iTerm 使ってたら ブラウザみたいにトラックパッドで文字が拡大することを初めて知りました。

よくあるエラー集を用意して、前で実演する

  • 削除するときは絶対に「Move to Trash」 、 「Remove Reference」はダメ!絶対!
    • 同じファイル名を再作成ができない。ファイルが残ってるから。
  • 参照(接続)が残っているとき
    • 右クリックして接続が残ってるかを確認する
  • エラーが出たら、コンソールで「reason」を打ち込む
    • 最初のうちはどこにエラーの原因が書いてあるかわからないから、「reason」で検索するようにします。
  • 謎のエラーが出たらこちら → Xcodeでデバッグ実行中にクラッシュした時に捗るブレークポイント設定
  • かぶるモジュール名はダメ!絶対!例:SwiftUI をアプリ名にしちゃう

エラーが出た時に、知識で解決するのではなく、「エラー見る → ググる → 解決」の手順を見せる

前述したとおり、「自走できるようにする」が目標です!
そのため、自分でエラーが解決できるようになることは最低条件です。

  1. エラーを見る。
  2. Google翻訳にぶっ込む(筆者は英語が苦手なこともあり最初はぶっ込みまくってました。今はなんとなく読めますよ笑)←できる人は飛ばして良い
  3. 内容を理解する
  4. ぐぐる
  5. それっぽい記事を見つける
  6. 試す
  7. 解決!解決できなかったら4に戻る。

上記の手順を授業中に行うようにしています。

今、必要のない知識を話さない。盛り込まない。

受講生側は、覚えることに必死です
より多くの知識を話してしまうと、「そんなことも覚えなきゃいけないのか。。。?」となりかねません。

例えば、
UILabel を扱う場合, 最初は、.text のプロパティさえ分かれば開発できます。
nameLabel.text = "やまたつ" ですね。
それなのに、「attributedText というプロパティもあって、こっちは〜」と言ってしまうのはよくないということです。
今は必要ではない知識を与えると混乱してしまいます。
できるだけ複雑化させないように努力しています?‍?

プログラミング学習では遅延評価勉強法が良いと思っています。
漠然と勉強するのではなく、必要な時に必要なものを勉強することが重要です。

勉強が苦手な人向けの「遅延評価勉強法」

前述の UILabel の話では、最初は .text プロパティだけ知ってるだけで十分です。
実装する時に、「色を変えたいな〜。フォントを変えたいな〜。アンダーラインひきたいな〜」と思った時に、 .textColor や、 .font 、.attributedText を勉強して使えるようになればいいです。
必要になるまでは最低限の知識だけを教えるようにしています。

遷移の方法を限定する

以下のようなコードで遷移できるようにするために、
Storyboard ではなく xib を使っています(Storyboard が xib の上位互換だということは承知しています)

FirstViewController.swift
let vc = SecondViewController()
present(vc, animated: true)

ここは賛否両論あると思いますが、できるだけスムーズに画面遷移ができるようにしたいと思っています。
Xib だと 「Cocoa Touch Class」からデザインファイルとコードファイルが同時に作成できるのも良い点です。

Storyboard の遷移はハマるポイントが多いため教えるのを辞めました。
(segue の id, storyboard の id, Is Initial View Controller のチェックのし忘れとか)
いろいろな現場を見てきましたが、一つの Storyboard で開発しているところはほぼありませんでした。
設定画面の簡単な遷移のところだけセグエを用いるというのはありましたが、ほとんどが 1Storyboard1ViewCotroller でした。
セグエでの遷移は紹介だけに留めておきました。

絶対に辞めた方がいい!!!と思う方はこっそり教えてください?‍♂️

イメージしやすいようにする

以下は Oshidori というアプリの View Hierarchy です。
どのように描写されているかをイメージするためにこの画像を見せています。
例えば、present の処理で上に ViewController が重なっていることを見せたり、
NavigationController があるから横に遷移できたりすることを教えるために使っています。

階層 斜めから見た図
貼り付けた画像_2019_12_18_午後11_35.png 貼り付けた画像_2019_12_18_午後11_35.png

めちゃ勉強になります↓↓↓
デバッグのためにView階層を把握する

Slack で情報交換する

チャンネルは2つ作成し、dev15xlab8, dev15xlab8_reference を作成しています。
Slack はフロー型の情報交換ツールなので、フロー型とストック型のチャンネルを一つずつ用意しています。

Slack を使用することで、授業中に書いたコードや、参考になるサイトを簡単に提供できたり、質問もすぐに受けることができます。

また、Slack のスタンプ文化がとても好きなので絵文字を200個ほど新しく追加しました。笑
コミュニケーションが容易になり、フレンドリーになる効果もあると思います?

Slackの絵文字(emoji)を一括エクスポート&インポートする

参考にさせていただいた資料まとめ

第一回授業を終えて(2019/12/18加筆)

最初の方はとてもスムーズに話せたり、受講生も理解してくれて手を動かすことができて良かったです!
めちゃくちゃ準備したので嬉しかったです☺️

しかし、「遷移の方法を限定する」を教えていた時に受講生の顔が全員、はてな?に変わりました。。。
UINavigationCotroller を持っているから、横に遷移できて、左上の「戻る」で戻れるんですよ!!!」
present で次の画面がニョキって出てきます。」
(本当はもっと詳しく、View Hierarchy を使って説明した)

でも全然納得してくれる顔をしてくれず、怖すぎました。。。
みんながわかっていない顔をするとこっちも怖いもんなんですね。。。つらかった。。。心のダメージえぐかったです。

受講生にフィードバックしてもらった?‍?

全力で教えたいので、不満があるところは教えてもらって、次の授業に生かそうと思ってやりました。
匿名制度にしているので誰が書いたか分からないので素直な意見を書いてもらえます。
Google Form とても便利です。すぐにアンケート作れて、回答ももらえます!
「分かりやすい」と言ってもらえるのはとても嬉しい反面、受講生の「はてな???」の顔が脳裏に焼きついています?
このフィードバックをもとに次の授業に取り組みます。

スクリーンショット 2019-12-18 午後11.54.31.png
スクリーンショット 2019-12-18 午後11.54.49.png

次回の目標?

フィードバックを糧にして、コードの見やすさや、資料作成をもっと分かりやすくできるようにしていきます。
画面を大きくしたり、コードを見やすいようにしたりなどは自分ではできていると思っていましたが、受講生目線で見るとできていないんだなと痛感しました?
まだまだ講師としての経験値も足りないので、どんどんフィードバックをもらってスキルアップしていこうと思います!!!
受講生の方が「自走できる」ようになることを目標に取り組んでいきます!

最後に

とても長い投稿になってしまいました!最後まで読んでいただきありがとうございました?‍♂️
講師の方や、後輩にプログラミングを教える方の参考になっていただければ幸いです!

ジーズでは講師やチューターを外部から雇っておらず、卒業生や関係者の方で行っております!!!
そのため、本気で教えるし、本気で考えて授業に取り組んでいます。
受講生も何か変わろうとして入校してくる方が多く、本気で授業に取り組み、質問も積極的にします。
受講生同士で切磋琢磨しています!

私も受講生の方から学ぶこともたくさんあり、お互いにWin-Winの関係で成長できていければと思っております!
気になった方は是非ジーズに遊びに来てみてください?

講師をするチャンスを与えてくれたジーズ運営にも感謝しています?‍♂️
ありがとうございました!!!

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