20200114のiOSに関する記事は9件です。

IQOSのパックコード入力をできるだけ省力化

IQOSのパックコード入力がめんどくさい

先日まで使用していたiPhone7。カメラの調子が悪かったのでQRコードではなくアルファベットで入力していました。めんどくさい。
iPhone11proに買い替えたことをきっかけに、IQOSのパックコード入力を省力化できないか試してみました。

ところで、iPhone ショートカット LINEで検索するとLINEで帰るコールするショートカットがたくさん出てきますが世の中の皆さんは、そんなに帰るコールを自動化したいものなんですか。

さっそくつまずく

書類 > QR/バーコードをスキャン
共有 > クリップボードにコピー
App > LINE > メッセージをIQOSに送信
の順に追加。

いざ実行。

標準カメラが起動し、QRコードを読み込んだら。。。LINEが起動。
うん。起動はしました。
手動でIQOSのトークルームを探して開いて、TXTボックスにペーストし送信。
登録できました。

違う。そうじゃない。

メッセージをIQOSに送信

検索してみると皆さん、同様に
App > LINE > メッセージをIQOSに送信
でトークルームが開かないみたい。
開きそうなのに。
さらに検索してみると
お使いのAppからショートカット > 全て表示 > メッセージを{ユーザー名}に送信
でショートカットを作成し呼び出すと良いらしい。

メッセージをIQOSに送信を作って呼び出すことにしました。

動け

書類 > QR/バーコードをスキャン
共有 > クリップボードにコピー
に続けて
App > ショートカット > ショートカットを実行
を追加し、実行するショートカットに先ほど作ったメッセージをIQOSに送信を指定します。

いざ実行。

標準カメラが起動し、QRコードを読み込みLINEが起動。
IQOSのトークルームが開いたのでTXTボックスを触ってペースト。
送信!!

動いた

動きました。動いたけど、もうちょっと何とかならないのかと。
アルファベットを入力するよりも、写真AppにQRコードの写真が溜まっていかないし、手間が減った気がします。
いや、気がついていないだけで、もっと簡単な方法があったのかもしれません。

TXTボックスにクリップボードの内容がペーストされてるとなお良いし、送信までしてくれるととても嬉しいんです。
今後、ショートカットAppがもっとできることが増えて、便利になることを祈ります。

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

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Composite~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Compositeパターン概要

  • ディレクトリとファイルのような、ツリー構造を扱うためのパターンです。
  • ディレクトリとファイルのように、「名前」などの同じプロパティや、「削除」などの同じ操作を持つ場合、ディレクトリ(容器)とファイル(中身)を同じように扱うことができます。
  • すなわち、検索や削除などを再帰的に行うことができます。
  • GoFのデザインパターンでは構造に関するパターンに分類されます。

使い所

  • そのものズバリ、ツリー構造を扱う場合には、Compositeパターンを思い浮かべると設計に掛かる時間を短縮できる可能性が高いです。
  • UIViewのView Hierarchyもツリー構造であり、Compositeパターンが使われています。

view_hierarchy.gif
引用:Cocoa Design Patterns (Retired Document)

サンプルコード

Swiftバージョンは 5.1 です。

protocol DirectoryEntry {
    var name: String { get }
    func remove()
}

final class File: DirectoryEntry {
    let name: String

    init(name: String) {
        self.name = name
    }

    func remove() {
        print("\(name)を削除しました")
    }
}

final class Directory: DirectoryEntry {
    let name: String
    private var entryList = [DirectoryEntry]()

    init(name: String) {
        self.name = name
    }

    func add(entry: DirectoryEntry) {
        entryList.append(entry)
    }

    func remove() {
        for entry in entryList {
            entry.remove()
        }
        print("\(name)を削除しました")
    }
}

// Usage
let dir1 = Directory(name: "dir1")
let file1 = File(name: "file1")
dir1.add(entry: file1)
// dir1
//   ∟file1

let dir2 = Directory(name: "dir2")
let file2 = File(name: "file2")
let file3 = File(name: "file3")
dir2.add(entry: file2)
dir2.add(entry: file3)
// dir2
//   ∟file2
//   ∟file3

dir1.add(entry: dir2)
// dir1
//   ∟file1
//   ∟dir2
//     ∟file2
//     ∟file3

let file4 = File(name: "file4")
dir1.add(entry: file4)
// dir1
//   ∟file1
//   ∟dir2
//     ∟file2
//     ∟file3
//   ∟file4

dir1.remove()
// "file1を削除しました"
// "file2を削除しました"
// "file3を削除しました"
// "dir2を削除しました"
// "file4を削除しました"
// "dir1を削除しました"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Command~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Commandパターン概要

  • Command=命令をオブジェクトとして扱うパターンです。
  • Commandオブジェクトは、「パラメータ」と「処理」をカプセル化したモノ。
  • GoFのデザインパターンでは振る舞いに関するパターンに分類されます。

使い所

  1. 並列処理を行いたいケース
  2. Queueing処理(Commandの待ち行列管理)を行いたいケース
  3. (DBを使わずに)ある時点の状態へのロールバックを行いたいケース
  4. Commandオブジェクトの生成元とは異なる場所でCommandを実行したいケース
  • 上記1、2を実現するための言語機能であるOperation(とOperationQueue)は、Commandパターンに則って構築されています。
  • したがってSwiftでは、Commandパターンのコードを自力で実装しなければならないケースは少なそうです。
  • とはいえ前提知識として、Commandパターンのコンセプトは理解しておいて損はないかと思います。

(ご参考)
【Swift】Grand Central Dispatch (GCD)とOperationQueue まとめ > Operation Queue

サンプルコード(Commandパターンの実装例)

Swiftバージョンは 5.1 です。

class Command {
    let receiver: Receiver

    init(receiver: Receiver) {
        self.receiver = receiver
    }

    func execute() {}
}

class HelloCommand: Command {
    override func execute() {
        receiver.action(message: "Hello")
    }
}

class WorldCommand: Command {
    override func execute() {
        receiver.action(message: "World")
    }
}

protocol Receiver {
    func action(message: String)
}

class PrintReceiver: Receiver {
    func action(message: String) {
        print(message)
    }
}

class Invoker {
    private var commands = [Command]()

    func addCommand(_ command: Command) {
        self.commands.append(command)
    }

    func execute() {
        for command in commands {
            command.execute()
        }
    }
}

let invoker = Invoker()
let receiver = PrintReceiver()
let hello = HelloCommand(receiver: receiver)
let world = WorldCommand(receiver: receiver)
invoker.addCommand(hello)
invoker.addCommand(world)

invoker.execute()

// 処理結果
// "Hello"
// "World"

引用元:
しめ鯖日記 - Swiftで学ぶデザインパターン22 (Commandパターン)

サンプルコード(Operationの実装例)

import Foundation
class MyCommand: Operation {
    var commandName: String
    var delay: Int

    init(commandName: String, delay: Int) {
        self.commandName = commandName
        self.delay = delay
    }

    override func main() {
        for _ in 0..<delay {
            sleep(1)
        }
        print(commandName)
    }
}

let operationQueue = OperationQueue()
// 最大同時実行数
operationQueue.maxConcurrentOperationCount = 3


let command1 = MyCommand(commandName: "command1", delay: 1)
let command2 = MyCommand(commandName: "command2", delay: 3)
let command3 = MyCommand(commandName: "command3", delay: 2)

operationQueue.addOperation(command1)
operationQueue.addOperation(command2)
operationQueue.addOperation(command3)

// 処理結果
// "command1"
// "command3"
// "command2"

OperationとOperationQueueを使ってみると、Commandパターンになっていることが理解できるかと思います。

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

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Chain Of Responsibility~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Chain Of Responsibilityパターン概要

  • 全体を統率するクラスを作らず、個々のクラスが責務を「たらい回し」して、責務を果たせるクラスに行き当たった時点でそのクラスが処理を行います。
  • GoFのデザインパターンでは振る舞いに関するパターンに分類されます。

使い所

  • iOSエンジニアにとってはおなじみの、UIKitの「Responder Chain」という仕組みがChain Of Responsibilityパターンに則って構築されています。

Responder Chainとは?

UIResponderオブジェクトの繋がりのことで、この繋がりの順番にしたがって
touchイベントなどの処理ができるUIResponderオブジェクト(=first responder)を見つけて処理を行う仕組みです。

引用:【Swift】Responder Chainの仕組み

  • アプリ開発での使い所は思い当たらなかったのですが、iOSフレークワーク内部で使われている仕組みであるわけですから、概念を理解しておいて損はないかと思います。

サンプルコード

Swiftバージョンは 5.1 です。

protocol Withdrawing {
    // 引き出す
    func withdraw(amount: Int) -> Bool
}

// 札束クラス
final class MoneyPile: Withdrawing {
    // 額面
    let value: Int
    // 枚数
    var quantity: Int
    // 責務をたらい渡す次のオブジェクト
    var next: Withdrawing?

    init(value: Int, quantity: Int, next: Withdrawing?) {
        self.value = value
        self.quantity = quantity
        self.next = next
    }

    func withdraw(amount: Int) -> Bool {

        var amount = amount

        func canTakeSomeBill(want: Int) -> Bool {
            return (want / self.value) > 0
        }

        var quantity = self.quantity

        while canTakeSomeBill(want: amount) {

            if quantity == 0 {
                break
            }

            amount -= self.value
            quantity -= 1
        }

        guard amount > 0 else {
            return true
        }

        if let next = self.next {
            return next.withdraw(amount: amount)
        }

        return false
    }
}

// ATMクラス
final class ATM: Withdrawing {
    // $100の札束
    private var hundred: Withdrawing
    // $50の札束
    private var fifty: Withdrawing
    // $20の札束
    private var twenty: Withdrawing
    // $10の札束
    private var ten: Withdrawing

    private var startPile: Withdrawing {
        return self.hundred
    }

    init(hundred: Withdrawing,
           fifty: Withdrawing,
          twenty: Withdrawing,
             ten: Withdrawing) {

        self.hundred = hundred
        self.fifty = fifty
        self.twenty = twenty
        self.ten = ten
    }

    func withdraw(amount: Int) -> Bool {
        return startPile.withdraw(amount: amount)
    }
}

// Usage
// 札束オブジェクトを生成して $10 -> $20 -> $50 -> $100 と数珠つなぎにする
let ten = MoneyPile(value: 10, quantity: 6, next: nil)
let twenty = MoneyPile(value: 20, quantity: 2, next: ten)
let fifty = MoneyPile(value: 50, quantity: 2, next: twenty)
let hundred = MoneyPile(value: 100, quantity: 1, next: fifty)

// ATMオブジェクトを札束オブジェクトを格納して生成する
var atm = ATM(hundred: hundred, fifty: fifty, twenty: twenty, ten: ten)

// ATMには合計$300しか入っていないため$310は引き出せない
atm.withdraw(amount: 310)   // false

// $300は引き出せる
atm.withdraw(amount: 100)   // true

引用:
https://github.com/ochococo/Design-Patterns-In-Swift#-chain-of-responsibility

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

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Bridge~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Bridgeパターン概要

  • 機能を提供する層と、実装を提供する層を分離するパターンです。
  • 各層の橋渡し(Bridge)をiOSエンジニアにはおなじみの委譲(delegate)で行います。
  • GoFのデザインパターンでは構造に関するパターンに分類されます。

使い所

  • 実装をMockに差し替えることができ、ユニットテストがしやすくなります。
  • VIPERなどのアーキテクチャーパターンは、Bridgeパターンによって成立しています(と私は認識しています)。

サンプルコード

Swiftバージョンは 5.1 です。

// VIPERでのPresenterに当たる層
protocol Presentation {
    var interactor: Usecase { get set }
    func searchButtonTapped()
}

final class Presenter: Presentation {
    var interactor: Usecase

    func searchButtonTapped() {
        self.interactor.fetch()
    }

    init(interactor: Usecase) {
        self.interactor = interactor
    }
}

// VIPERでのInteractorに当たる層
protocol Usecase {
    func fetch()
}

final class ProductionUsecase: Usecase {
    func fetch() {
        print("fetch for Production")
    }
}

final class MockUsecase: Usecase {
    func fetch() {
        print("fetch for Mock")
    }
}

// VIPERでのViewに当たる層
let production = Presenter(interactor: ProductionUsecase())
production.searchButtonTapped()    // "fetch for Production"

let test = Presenter(interactor: MockUsecase())
test.searchButtonTapped()    // "fetch for Mock"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Facade~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Facadeパターン概要

  • 煩雑なモノの窓口(=Facade、ファサード)を作ることでシンプルに扱えるようにするパターンです。
  • Facadeクラスは複雑な実装は持たず、(大抵は複数の)モジュールに対する問合せ窓口としての機能だけを提供します。
  • GoFのデザインパターンでは構造に関するパターンに分類されます。

使い所

アプリ開発の実務で適用できるケースはたくさんあると思いますが、例えば

  • UserDefaultsの読み込み・書き込み とか
  • KeyChainの読み込み・書き込み とか

サンプルコード

Swiftバージョンは 5.1 です。

// Example
final class Defaults {

    private let defaults: UserDefaults

    init(defaults: UserDefaults = .standard) {
        self.defaults = defaults
    }

    subscript(key: String) -> String? {
        get {
            return defaults.string(forKey: key)
        }

        set {
            defaults.set(newValue, forKey: key)
        }
    }
}

// Usage
let storage = Defaults()

// Store
storage["Bishop"] = "Disconnect me. I’d rather be nothing"

// Read
storage["Bishop"]

引用:
https://github.com/ochococo/Design-Patterns-In-Swift#-fa%C3%A7ade

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

初の個人アプリを App Store に審査出したら 30 分足らずでリリースされてビビったからその全てをお伝えします

明けましておめでとうございます。新しい年ということでアイコンも一新した lovee です。はい、あまりにもビビったのでラノベっぽいタイトルにしてみました。後悔はしていない。

なぜアプリ作ろうと思ったの?

いやーそれなりに長い間 iOS エンジニアやってきたけど、業務ではそれなりにたくさんのアプリを作ってきたけど、完全に個人でアプリを作ったことがまだ全くないんですよね。というわけで去年「今年中に個人アプリを出す」という目標を立てました。結局去年中には出せなかったがとりあえずまだ今年度中ということで開き直ってます

どんなアプリ?

簡単に言うとただ単にユーザが入力した文字列を QR コードに変換するだけのアプリです。それだけです。でも本当の使い方はアプリをいちいち立ち上げて QR コードを作るのではありません。このアプリは iOS ネイティブの共有メニューを対応しているので、別のアプリから共有メニュー呼び出して共有したいものを QR コードに変換するのが本来の使い方というかアイデアです。

言葉では説明しにくいので、こちらの画像をご覧いただければ分かりやすいかもしれません:

quickshare.gif

はい、こちらは bilibili 動画で周りにいる人に布教したい動画があったときに、サクッとその動画の URL を QR コード化した動画です。これで例えば周りにいる人が AirDrop が使えなかったり、その人の SNS アカウントを探すのが面倒だったり、そもそも繋がってなくてたまたま懇親会とかで初対面の人に何かを布教したいときにこのサクッと作られた QR コードを読み取ってもらえばミッションコンプリートです。とても楽です。もちろん bilibili 動画だけでなく、iOS のネイティブ共有メニューが使えるアプリで共有内容がテキストもしくは URL に落とし込めるものならなんでも使えます。もう一つの例としては周りにいる友達と何処かを目指したいとき、その場所も地図アプリから共有できたりします:

quickshare_map.gif

App Store で無料リリースしており、しかも広告もないので是非試してみてください!

https://apps.apple.com/app/quickshare-share-via-qr-code/id1494461342

ちなみに実はソースコードも公開しており、興味ある方是非覗いてみてください:

https://github.com/el-hoshino/QuickshaRe

どんな技術を使ってる?

QR コード生成

QR コードの生成自体は特に尖った技術はありません、単純に CoreImage の CIFilter.qrCodeGenerator() から作られてるだけです。ただ何か特筆したいことがあるとすれば、その生成コード読めばわかるように、今の CoreImage の Swift 対応がだいぶ様になっているようです。ひと昔はまだ CIFilter(name: String) から作らなくてはならず、そのため作られたインスタンスはただの CIFilter 型なので、それはなんのためのフィルターか、どんなパラメーターが設定できるかなどはドキュメントを読まなくてはならず、全て文字列で Key-Value コーディングする必要がありました。ところが今の作り方では CIFilter & CIQRCodeGenerator 型が作られ、CIFilter はこれまでと同じく、そして CIQRCodeGenerator プロトコルは設定できるパラメーターとかが定義されているので、QR コードで設定できる correctionLevelmessage がプロパティーとして直接設定できるようになり非常に使いやすいです。

QRCodeGenerator
private extension CIQRCodeGenerator where Self: CIFilter {

    func qrCodeImage(for text: String, correctionLevel: String) -> CIImage? {

        self.correctionLevel = correctionLevel
        self.message = text.data(using: .utf8)!

        return outputImage

    }

}

QR コード表示

CIFilter で生成された QR コードはサイズが非常に小さく、各ビットは 1 ピクセルで表現されているため、そのまま画面に出すととても小さいです。どれくらい小さいかというとこんな感じの画像になります:

Simulator Screen Shot - iPhone 8 - 2020-01-13 at 18.24.22のコピー.png

もちろん確かにこの画像では QR コードとして必要なすべての情報が詰まっていますが、小さすぎてカメラからは認識できませんので画面に合わせて大きく表示する必要があります。ちなみにメインアプリでは SwiftUI を使っています。さて次の問題はどうやって小さい画像を SwiftUI で大きく表示させるかの問題です。

まず画面に合わせて大きく表示すること自体は簡単です、Image 要素の後ろに .resizable().scaledToFit() を追加してあげればいいです。前の .resizable()Image 要素を親要素に合わせてリサイズし、後ろの .scaledToFit() はリサイズしたときアスペクト比を保持して親要素にフィットさせるようにします。UIKit 時代の .aspectFit ですね。

しかしこれだけでは大きな問題点が残っています、それは SwiftUI では(UIKit も同じですが)画像をリサイズするとき、特に拡大するときに補間が働いて、画像がぼやけてしまい、こんなふうに表示されます:

Simulator Screen Shot - iPhone 8 - 2020-01-13 at 18.38.29.png

もちろんこれでスキャンするときちゃんと認識はできますが、やはり見た目は非常に残念ですね。ではこれをどうすればいいかというと、Image 要素を .interpolation(.none) で補完を無効化すればいいです。ですので最終的なコードはこんなものになります:

QRCodeImageView
            Image(uiImage: generator.qrPicture(for: content).uiImage)
                .interpolation(.none)
                .resizable()
                .scaledToFit()

こうすれば下記のようにキレイに QR コードが表示されます:

Simulator Screen Shot - iPhone 8 - 2020-01-13 at 18.47.54.png

SwiftUI

せっかく SwiftUI の話が出てきたので、ここでもう一歩踏み入れた話をしましょう。なぜかというととにかく SwiftUI は今現時点ではすごい罠だらけですので()

まずは入力画面の話をしましょう。入力画面はこんな感じのとてもシンプルな画面です。

Simulator Screen Shot - iPhone 11 Pro Max - 2020-01-13 at 21.29.23.png

とても簡単ですね、画面には左側に文字入力の TextField、そしてその右側に入力確定の Button しかありません。少なくともそんな風に見えます。…本当にそうでしょうか?

まず TextField について、今回はしたに線を引いてあります。UIKit の時代、まあ下線を引くのは決して簡単とは言えませんが、まあ愚直な形でできてました。しかし現段階の SwiftUI では、四角のボーダーや背景色を入れるなら楽に対応できますが下に線を一本引くだけの描画は直接にはできません。そこでいろいろ調べた結果、どうやら Divider() を下に入れればそれっぽく見えることがわかりましたので、こんな感じで対応しました:

TextInputView
extension View {

    func underline() -> some View {
        return VStack {
            self
            Divider()
        }
    }

}
TextInputView
    TextField(/* ... */)
        .underline()

次は右側のボタンですが、まずこのボタンの動作は Push 遷移になるので、そもそも Button ではなく NavigationLink になります。まあこれはそんなに難しい話ではないですかね。

問題はこのボタンの周りにあるボーダーです。

え?さっきボーダー入れるなら楽って言ってたよね?と思うかもしれません。実際筆者もこの対応をするまで楽だと思ってました。そもそも昔 beta の時代は確かに楽に対応できてた気はします。確か .cornerRadius(10).border(Color.secondary, width: 1) みたいなこと書いてればできた覚えがあります。ところが今現時点(Xcode 11.3)でやってみたら、なんとボーダーが直角のままでした。.cornerRadius.border の順番入れ替えても直角のボーダーのままで角の線がなくなっただけです。

というわけでいろいろ調べてみたら、どうやら .overlay で設定しなくてはいけないことが判明しましたので、こんな風に作ることになりました:

TextInputView
extension View {

    func border(color: Color, cornerRadius: CGFloat, lineWidth: CGFloat) -> some View {

        return overlay(
            RoundedRectangle(cornerRadius: cornerRadius)
            .stroke(color, lineWidth: lineWidth)
        )

    }

}
TextInputView
    NavigationLink(/* ... */)
        .border(color: .secondary, cornerRadius: 10, lineWidth: 1)

面倒ですね。

面倒と言えば、さらにこのままこれを NavigationView の中に表示すると、機種によってはキーボードを呼び出したとき TextField が隠されちゃうので、さらにこれらを .offset(y: -100) で場所をずらしました。面倒です。よしなりにレイアウトやってくれるならキーボード表示時の対応くらい自動的によしなりにやって欲しいです。でないとちゃんとしたレイアウトを作りたいとき結局 GeometryReader とか使う羽目になるからとても面倒です。

面倒な話はまだここで終わっていません。Push 遷移の話をちょろっとしましたが、さてこの Push 遷移をするためには、この入力ビューを NavigationView の中に入れなくてはいけません。というわけで SceneDelegate を対応させましょう:

SceneDelegate
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let contentView = NavigationView(content: { TextInputView() })
        // ...
    }

これで手持ちの iPhone X で動作確認してみましょう。よっしちゃんと表示されました。文字列入力して Generate ボタンタップしたらちゃんと QR コードも表示されました。

Simulator Screen Shot - iPhone 11 Pro Max - 2020-01-14 at 02.23.32.png

めでたしめでたし。これで App Store に申請出しましょう。

と思うじゃん?また後でテストについてもちょっと話しますが、実はこれ、iPad ではちゃんと表示されません(!?)。アプリ開いたら真っ白な画面が表示されます。

Simulator Screen Shot - iPad Pro (12.9-inch) (3rd generation) - 2020-01-13 at 23.41.32.png

なんでや!?と思っていろいろ画面いじってみたら、どうやら指が左から右スワイプしないとこの入力画面が出てこないことが判明しました。

Simulator Screen Shot - iPad Pro (12.9-inch) (3rd generation) - 2020-01-13 at 23.42.24.png

さらにいうと iPad だけでなく、iPhone でも横画面にするとき横の Size Class が Regular になる iPhone 11 Pro Max とかの機種も同じく真っ白で左から右スワイプしないといけません。

Simulator Screen Shot - iPhone 11 Pro Max - 2020-01-13 at 23.43.39.png

というわけでこの動きについてさらにいろいろ調べてみたら、とりあえず DoubleColumnNavigationViewStyle にすれば表示はできることがわかりました。ただしこれは本来すべき対応というより、現状のバグのような動きに対するワークアラウンドですから本当はこれに依存すべきではないですが、まあトレードオフですかね、多機種/多画面向きの対応を取るのか、SwiftUI がバグらない環境を取るのか。とりあえず私は前者を取りました。そしてもちろんこうすれば QR コードの表示画面は iPad ではフル画面ではなく常に右側の限られた幅で表示されることにはなっちゃいますが。というわけでコードをこんなふうに修正しました:

SceneDelegate
        let contentView = NavigationView(content: { TextInputView() })
            .navigationViewStyle(DoubleColumnNavigationViewStyle())
            .padding()        

Simulator Screen Shot - iPad Pro (12.9-inch) (3rd generation) - 2020-01-13 at 23.44.55.png

しかし、これで万事解決と思ったら、まだまだ罠があります。先ほど言いました iPhone 11 Pro Max の横画面問題ですが、こうしても横画面では結局これまでと同じく右スワイプしないと入力画面出てきません。もう現在これについては SwiftUI のバグということで開き直って放置することにしました。別にアプリが落ちるわけでもないし

こんなにシンプルな画面が 2 つだけでも対応がこんなに大変なので、現時点で SwiftUI で複雑な画面を組むのはやはりやめたほうがいいですね。Swift が 3.0 でようやくまともっぽくなったのと同じように、SwiftUI 3.0 くらいまで気長に待ちましょう。

Share Extension 対応

共有メニューに自分のアプリを表示させるためには Share Extension を対応すればいいです。まずは Target を追加します:

スクリーンショット 2020-01-13 18.53.02.png

あとはダイアログに合わせてポチポチすれば Share Extension 用の Target が追加されます。簡単です。

ところがこれで共有画面用の ShareViewController が自動的に追加されますが、その VC のクラスは Twitter とかの SNS に投稿するための iOS ネイティブで用意されている SLComposeServiceViewController のサブクラスです;しかしこのアプリではそんな SNS 投稿とかをせず、単純に共有内容を QR コード化して表示するだけの画面が欲しいです。どうすればいいですか?

答えは意外と簡単です:SLComposeServiceViewController の継承を UIViewController に変えるだけです。

ShareViewController
- class ShareViewController: SLComposeServiceViewController {
+ class ShareViewController: UIViewController {

    // ...

}

こうすることで、簡単に必要な共有画面を組み込めます。

また、共有項目の取得は、ShareViewControllerextensionContext?.inputItems から取得できます。ただしこの inputItems[Any] ですので、中身を NSExtensionItem に落とし込む必要があります。落とし込んだら、さらに attachments プロパティーから forEach して loadItem の非同期処理で共有アイテムを取り出さなくてはなりませんし、そのアイテムの種類も Uniform Type Identifiers で特定しないといけなかったりします。割と結構面倒な処理ですが、愚直なコードにするとこんな感じです:

ShareViewController
class ShareViewController: UIViewController {

    private typealias SharingItem = (type: String, content: String)
    private var sharingItems: [SharingItem] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        guard let inputItem = extensionContext?.inputItems.first as? NSExtensionItem else {
            return
        }

        DispatchQueue.global().async {

            for attatchment in inputItem.attachments ?? [] {
                for identifier in attatchment.registeredTypeIdentifiers {

                    attachment.loadItem(forTypeIdentifier: identifier) { [weak self] (coding, error) in

                        guard let item = coding else {
                            assertionFailure("Failed to load item for \(identifier). Error: \(error as Any)")
                            return
                        }

                        switch identifier as CFString {
                        case kUTTypeURL:
                            guard let url = item as? URL else { assertionFailure("Failed to load item as URL."); break }
                            self?.sharingItems.append(("URL", url.absoluteString))

                        case kUTTypeText, kUTTypePlainText:
                            guard let text = item as? String else { assertionFailure("Failed to load item as Text."); break }
                            self?.sharingItems.append(("Text", text))

                        default:
                            break
                        }

                    }

                }

            }

        }

    }

}

見ての通り、非常に長い処理をしなくてはいけないです。ですので流石にアプリではいくつかの細かいメソッドに切り分けていますが。

簡単にちょっと説明を付け加えると、上記の attachments 配列に対してこんなことを行っています:

  1. attachments を回して、さらに各々の attachment に対してそれぞれに登録されている registeredTypeIdentifiers 配列を回します。二重ループです。
  2. registeredTypeIdentifiers ループ内で取得された identifier を使って、attachmentloadItem(forIdentifier:completionHandler:) 処理を掛けます。登録されてるはずの identifier ですので、completionHandler 内では coding が必ず取得できるはずですから、取得できなかった時をアサートします。当然ですが completionHandler は非同期処理ですので注意しましょう。
  3. identifier は Uniform Type Identifiers(UTType) ですので、ここを参考に、URL とテキストを指す kUTTypeURLkUTTypeTextkUTTypePlainText のときだけ共有アイテムを取り出して、自身の sharingItems に追加します。

まあなかなか大変な作業ですね。特に気をつけないといけないのは、kUTTypeURL とかを定義しているのは MobileCoreServices モジュールですので、これも import しないといけないですし、これらはそもそも String ではなく CFString ですので,switch 文回す時もキャストが必要です。さらに、kUTTypeURL の時と、kUTTypeText kUTTypePlainText の時、取り出した coding は違う型で、前者は URL、後者は String になるのでここでもさらにキャストしないといけないです。まあ面倒です。

ちなみにここももちろん生成された小さい QR コード画像を拡大して表示しないといけないですが、これは SwiftUI ではなく UIKit の画面になるので対応が少々違います、UIImageViewlayer を弄ることになります:

ShareViewController
    qrImageView.layer.magnificationFilter = .nearest

さてここまで弄ってミッションコンプリート思ってるあなた、甘いです。まだ画面を閉じる処理が書かれていません。

共有画面を閉じるなんて、画面の上からしたスワイプすれば閉じれるんじゃないの?と思うかもしれませんが、大体の場合はこれでいけます。ところが筆者がいろんなアプリで動作テストしてたときに、なんとなぜか Amazon アプリからの共有画面は閉じれないことが判明しました。ほとんどのアプリでは Modal 遷移するときにカードのような表示になりますが、なぜか Amazon のアプリだけ Fullscreen の表示になります。そのため下スワイプでの画面終了はできないのです。

いやできなくても普通に dismiss すればいいじゃん?と思うかもしれませんが、実際筆者も最初そう思いましたが、やってみたら全然閉じれませんでした。いろいろ調べてみたらなんとまた VC の extensionContext から completeRequest もしくは cancelRequest を呼び出さなくてはいけないことが判明しました。まあ QR コードの生成は終わってるし他にやることもないので complete でいいと思いましたので、こんなふうに素直に閉じるボタンを作って,閉じる処理を書きました:

ShareViewController
    @IBAction private func dismiss() {
        extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
    }

どうやら Share Extension 対応の肝は extensionContext っぽいですね。

さて、ここまで対応したらプログラム上の対応はほぼ終わりましたが、審査に提出するためにはまだもう一つ直すところがあります:Info.plist です。

Share Extension 対応を追加するときに、Xcode は自動的に Share Extension ターゲットの Info.plist に NSExtensionActivationRule を書き込みますが、初期値は開発がしやすいように TRUEPREDICATE になっています。これはどういう意味かというと、アプリから共有メニューを呼び出すときに、共有内容がどんなものでもこのアプリは受け付けられるよという意味です。しかしこれは本当に初期開発がしやすいためだけのものですので、このまま審査に提出するとリジェクトを喰らいます。ですのでこれを直す必要があります。

単純な直し方でしたら、この NSExtensionActivationRule の値タイプを String から Dictionary に変えて、その下にさらに NSExtensionActivationSupportsAttachmentsWithMaxCountNSExtensionActivationSupportsWebURLWithMaxCount などの項目を追加すればいいです。このアプリも受け付ける共有は文字列と URL だけですので、それっぽく設定してみました。ところがいろんなアプリで試してみたところ、TRUEPREDICATE なら対応できてたマップアプリの場所共有はどう変更しても出てこなくなりました。受付タイプを文字列と URL に限定せずに全てのファイルを受け付けるとかにしてもダメでした。というわけでここで更にいろいろ探してみたら、どうやらこれは Subquery で対応できるらしい。というわけで最終的に NSExtensionActivationRule はこんな風になりました:

Info.plist
            <key>NSExtensionActivationRule</key>
            <string>SUBQUERY(extensionItems, $extensionItem, SUBQUERY($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text").@count &gt;= 1).@count &gt;= 1
                 OR SUBQUERY(extensionItems, $extensionItem, SUBQUERY($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text").@count &gt;= 1).@count &gt;= 1
                 OR SUBQUERY(extensionItems, $extensionItem, SUBQUERY($extensionItem.attachments, $attachment, SUBQUERY($attachment.registeredTypeIdentifiers, $uti, $uti UTI-CONFORMS-TO "public.url" AND NOT $uti UTI-CONFORMS-TO "public.file-url").@count &gt;= 1).@count &gt;= 1).@count &gt;= 1
            </string>

本当はソースコードと同じように kUTTypeURL とかを使いたかったのですが、やはり Info.plist ではそれが使えませんでしたから文字列をそのまま書くしかないです。残念。

初めてリリースした個人アプリどんな感想?

とりあえずまずは審査の速さにビビっていますね。はい。

思い返せば、自分が初めて iPhone アプリ(当時はまだ iPad がない)開発に(一応)携わった時はまだ iPhone 3GS の時代で、当時は Retina ディスプレイとか多種多様な画面サイズとかとは無縁な世界でした。その時にメモオフの iPhone 移植の仕事に携わって、当時は審査を出してから結果が出るのに 1 週間かかってもおかしくない時代でした(コンシューマゲーム機の場合、ソニーやニンテンドーなどの審査は更に時間かかるので特にそれほど違和感がなかったっぽい)。その後インディーズの iOS 開発者が増え、(特に Android の無審査と比べた場合の)審査の待ち時間の不満が高まっていき、徐々に審査が早くなり、1 週間以内、3 ~ 4 日内、2 日以内、そして割と最近でも大体当日か翌日とかのイメージでしたので、まさか 30 分以内に審査が通って App Store に並ばれたなんて思いもしなかったから、とても感慨深いです。

スクリーンショット 2020-01-12 13.16.51.png

そしてまだ超シンプルな機能しかありませんが、むしろ「とりあえずリリースしようぜ」の精神でリリースしたわけです。でないとまじでずるずるして今年度すら出せないじゃないかと危機感を抱きました

Ck9_MOJWYAAR_Rm.jpeg

ところが去年の WWDC の二大目玉の一つである SwiftUI ですが、最初はざわざわして、みんな大興奮して、そして時とともになんとなくみんな冷静になり始めて、「あれ?これ罠多くね?」って流れになって、最終的に「やはりプロダクトに使うのはまだ早すぎた」という結論に着地したのが身をもって理解しました。本当にまだ罠が多いしちょっとでもカスタムなレイアウトを組もうとするとすぐ面倒な書き方が強いられます。特に iPhone と iPad 両方、画面向きも横縦両方対応しようとすると本当に面倒です。まあそれでも個人的には Auto Layout よりは将来性があると思う信じ込んでいますけどね。

将来性といえば、そういえば beta の時 SwiftUI のプレビューをポチポチするだけでビューの添削とかカスタマイズとかができたはずですが、いつの間になくなった!?

そして自分でアプリをリリースして気づいたのが、AppStoreConnect のアプリ紹介でスクショやキーワードの追加や修正は新しいバージョンを出さないとできないですね。静止画のスクショしかないとやはりメリットが伝わりにくいと思って、この記事の最初に貼り付けたような共有メニューからの利用を画面録画したから、それを AppStoreConnect に追加しようと思ったらできませんでした。一つ勉強になりました。

どれくらいダウンロードされた?

残念ながら本記事を執筆した時点ではまだダウンロード数とかのレポートがないんですよね…一応おかげさまで評価が 1 件星 5 ついただいておりますがレビューも今のところまだありません。今後に期待です。皆さんのレビューもお待ちしております。

今後のロードマップは?

テストをたくさん書く

今現時点でも一応最低限の Unit Test がありますが、それに加えて UI Test もたくさん追加していきたいと思います。実際現状まだリリースしていない開発ブランチに最初の UI Test を追加しました。やはりテスト大事ですね。実は上にも書いた iPad の NavigationView 問題、これもとりあえず iPad で UI Test 動かした時に発覚した問題です。

UI Test 書いてて思ったのは、やはり結構 API が独特というか普段の開発とは全然違うスタイルですね。手元の iOS テスト全書がとても頼りになりましたが、それでも今 iPhone と iPad でそれぞれ微妙に違いが生じる操作法をどうやって UI Test に落としこむかちょっと悩んでいます。そしてそれとは別にもしかすると XCUITest 用のラッパーフレームワークを作るかもしれません。いろんな API をもう少し分かりやすいようにしたいです。

そしてまだメインアプリの最低限の UI Test しか書いてませんが、何しろ目玉機能は共有メニューからの呼び出しなので、共有メニューからの UI Test も書きたいです。はい。

更にテストと関連するもので、CI/CD 環境も充実にしたいですね。CI 面として現状まだ SwiftLint も入れてないし Danger も入れていません、Bitrise の Workflow でそれらの Step を用意していますがスキップさせてるだけです。早いうちになんとか直したいです。そして CD 面として開発者アカウントの 2FA 問題もあってなぜか AppStoreConnect にログインできなくなっています。2FA 対応のためのセッション同期やアプリパスワード設定を全部マニュアル通りに設定したはずなのに…というわけで今は Bitrise の方に上げられた .xcarchive ファイルを落として手動で上げています。あとは UI Test で撮ったスクショも自動的にアップロードしていきたいですね。毎回毎回自分で手動で撮って上げるのは流石にだるいです。

機能を追加する

とりあえず今考えているのは履歴機能、お気に入り機能と QR コード画像のカスタマイズ機能です。

履歴と気に入りは文字通り、これまで生成した QR コードの履歴と、お気に入りの QR コードをまとめる機能です。QR コードのカスタマイズ機能は、例えば QR コードの色を変えるとか、ユーザが提供した画像を QR コード画像に組み込むとかの機能です。多分こちらのライブラリーを使った機能かな。

また履歴機能とお気に入り機能に関しては共有メニューからの利用も連動させる予定です。つまり共有メニューから生成したものも履歴に残るし、お気に入りに追加できるようにしたいです。

有料アプリにする(?)

はい、上記の機能を全部組み込んだら有料アプリにしようかなと思っています。オープンソースなのに有料アプリ。

まあもちろんそんな高いアプリにするつもりは全然ないですが、やはりお金が入るのと入らないのとではモチベーションが全然違う(と思う)んですね。

じゃあアプリ内購入にするか広告にすればいいのでは?と思うかもしれませんが、まずアプリ内購入はプログラムをかなり複雑化してしまう(気がする)ので気が進まないです;そして広告はそもそも個人的に嫌いです。せっかくシンプルで使いやすい画面を作ったのにそれを広告で汚したくないです。

逆に言うとつまり今のうちダウンロードしておかないと、今後有料アプリになったらお金払わなくてはならなくなるかもしれませんよ?(悪魔の囁き)(でもそもそも Qiita のユーザならソースコードから自分でフォークしてビルドすればええやん)

他に何か言いたいことある?

実はこのアプリを Slack で宣伝したら、@takasek さんにショートカットで似たようなことができるよーと教えられました。手順としてはまず共有メニューからの入力を受け入れて、その入力から「書類」で QR コードを生成して、更に「書類」で生成された QR コードを Quick Look で表示すればいいです。ただ実際自分でやってみたところ、QR コードの表示はできるが、例えばマップアプリとかの共有内容が複数ある時に、どの QR コードがなんの内容に対応したものかというのは自力で目パースする実際一回誰かにスキャンしてもらわないとわからないのでちょっと大変です。その点このアプリでは共有内容を UISegmentedControl で切り替えられ、更にその内容も UILabel で下に表示しているから分かりやすいです。あと羅小黒戦記マジ面白いからとりあえず日本語字幕版上映中の映画からでいいからみんな見て

参考リンク

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

[Swift5][Firestore]'Failed to get FirebaseApp instance. Please call FirebaseApp.configure() before using Firestore'のエラーを解消

はじめに

iOSアプリにFirestoreを導入する際に、上記のエラーが出ました。エラー内容を直訳すると「'FirebaseAppのインスタンスを取得できませんでした。 Firestoreを使用する前に,FirebaseApp.configure()をコールしてください。」です。
自分が解消できた方法を共有します。

間違ったコード

AppDelegate.swift
import UIKit
import Firebase 

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure() //ここに書いていた
        let db = Firestore.firestore()
        return true
    }

Firebase公式リファレンスのソースコードではこのような記述方法でしたが、自分のプロジェクトではエラーとなってしまいました。なぜ公式の方法でエラーになってしまったかはまだ分かっていません。

解決方法

AppDelegate.swift
import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    override init() { //初期化メソッドを追記
            FirebaseApp.configure()
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let db = Firestore.firestore()
        return true
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactNativeバージョンアップ作業は新規プロジェクトに移植方式が良い (v0.48.4 -> v0.60.5)

2年ぶりにiOSアプリのReact Nativeのバージョンアップ作業を行いました。v0.48.4 -> v0.60.5。バージョンをひとつ上げるだけでも苦しめられていたのに、これだけ大幅なバージョンアップは気が遠くなる苦行に違いありません。だけど、Google アナリティクス開発者サービス SDK が終わってしまうため、他のアクセス解析を導入する必要がありました。

新規プロジェクト立ち上げ方式

この困難な道のりを出来るだけ平坦にする良い方法はないか模索してみました。そして、既存プロジェクトのReactNativeバージョンを上げてエラーと格闘するよりも、最新のReactNativeで新規プロジェクトを作成し、そこにソースコードを移植する方が簡単ではないかという結論になりました。実際どうなのかは不明ですが、この新規プロジェクト立ち上げ方式に託してみることに。

react-native init NewProject
cd NewProject
npm install
react-native run-ios

新規プロジェクトはまぁ普通に動いたので、とりあえず使いたいモジュールをpackage.jsonに追加してみました。すると、ReactNative v0.60から追加されたautolinkなるものが非常に便利。package.jsonにモジュールを追加してnpm installした後にpod installすると、Xcodeプロジェクトへの追加やバイナリのinclude設定などを自動的にしてくれます。autolink以前はXcodeのモジュール設定でうまく行かずclean&buildを繰り返していた記憶があるので、この作業時間がなくなるのは素晴らしいです。モジュールを削除してからnpm installすればちゃんとXcodeからも削除もしてくれます。

ただし、autolink未対応のモジュールや古いモジュールについては、対応されるまで待つか、以前のように手動でXcodeに追加する必要があります。手動追加する場合は、プロジェクト直下にreact-native.config.jsというファイルを作って以下の記載をすることで、指定したモジュールのautolinkを動かなくしておきます

module.exports = {
   dependencies: {
    'realm': {
      platforms: {
      ios: null,
      },
    },
  },
}

使っているモジュールのうち、realmのみこの手動追加が必要でした。realmについてはautolink対応がissuesに上がっていて、現時点では対応済みみたいなので、次からはautolinkに切り替える必要があります。

realm動作確認用の簡易ソース

import React from 'react'
import { Text } from 'react-native'
import Realm from 'realm'

const ExampleComponent = (props) => {
    return ( Hello world!)
}

export default class App extends React.Component {

  render() {
    let realm = Common.getRealm()
    return (
    );
  }
}

React Navigationアップデート

というわけで、まずはrealmが動く状態にしてから、既存プロジェクトからappディレクトリ、index.jsなどをコピーして再びrun-ios。そして、React Navigationのバージョンを上げないとダメっぽいので最新版に。更に、いくつかのエラーに対応しました。

エラー「unable to raesolve module reactnative-gesture-handler」-> いくつかのモジュールがスピンアウトしたらしく、Getting started · React Navigationを参照してインストールしたらOK。

エラー「bundling failed: Error: Unable to resolve module `./navigators/createContainedStackNavigator」-> キャッシュクリアで解消

npm start --reset-cache

エラー「undefined is not a function (evaluating '(0,_reactNavigation.stacknavigator)')」-> StackNavigatorが消滅してCreateStackNavigatorになっていたので修正。更に、同じような感じで以下の関数を修正。
TabNavigator -> createBottomTabNavigator
StackNavigator -> createStackNavigator

addNavigationHelpersが消滅していたので修正。

無事?アップデート完了

色々修正したりしましたが、事前に想像していたよりも作業がサクサク進んだ気がします。もっと死ぬかと思ってた。すべてをクリアして一からやり直す下記コマンドはそんなに何度も使うことなくエミュレータ動作確認まで漕ぎ着けることができました。

rm -r node_modules && npm i && cd ios && pod install && cd .. && react-native run-ios
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む