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

UICollectionViewの引っ張り上げ更新はFooterでやると楽よ

UICollectionViewの引っ張り上げ更新の実装では、よくcontentInsetcontentOffsetを弄くり回して算出してるものが多いのですが、どうにもぱっと見で実装内容が理解できないので(頑張って説明変数使っていても)別の方法を考えていました。
FacebookのiOSアプリ見ているとUICollectionViewのFooterを使っていそうだったので、真似してみたら上手くいったのでご紹介致します。

R.swiftを使っていたので、適時コードは読み替えて実装お願い致します。

ActivityFooterView

今回自分はStoryBoardを使ったのでUICollectionViewにFooter追加して以下のクラスと紐付けしましたが、使わない派の人はviewDidLoadあたりでregisterお願いします。

footer.swift
import UIKit

class ActivityFooterView: UICollectionReusableView {
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
}

UICollectionViewController(or Delegate)

UICollectionViewDelegateのheader/footerが表示される直前に呼ばれる collectionView(_:willDisplaySupplementaryView:forElementKind:at:) delegateメソッドでアニメーション再生の関数を叩いていきます。

そこで非同期の関数を呼び出し、クロージャでstopAnimating()やreloadData()を呼び出すことで引っ張り上げ更新は実現されます。
データ取得関数などの結果がdelegateで帰ってくる場合は、cachedFooterView のようなfooterViewのキャッシュプロパティを使う必要があります。

pullup-update.swift
 /* UICollectionViewでの引っ張り上げ更新に対して、Footerを利用するアプローチを選択した。
    理由としては、contentInsetやoffsetをぐちゃぐちゃ弄ると意図しない挙動を作り込みやすいし、分かりにくい処理になるので避けたかったため
    このアイデアの参考にしたのはFacebookアプリで、おそらく同じような実装をしている。試してみてもらうと分かるが全く同じ挙動をする。
    */
    override func collectionView(_ collectionView: UICollectionView,
                                 viewForSupplementaryElementOfKind kind: String,
                                 at indexPath: IndexPath) -> UICollectionReusableView {
        let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind,
                                                                           withReuseIdentifier: R.reuseIdentifier.footer,
                                                                           for: indexPath)!
        return footerView
    }

    override func collectionView(_ collectionView: UICollectionView,
                                 willDisplaySupplementaryView view: UICollectionReusableView,
                                 forElementKind elementKind: String,
                                 at indexPath: IndexPath) {
     let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: elementKind,
                                                                           withReuseIdentifier: R.reuseIdentifier.footer,
                                                                           for: indexPath)!

        footerView.activityIndicator.startAnimating()
        doSomething() { // closure
            footerView.activityIndicator.stopAnimating()
            collectionView.reloadData()
        }
    }

感想

ものすごくスッキリ実装出来たので、かえって怖い...。
問題なければ今後もこの実装を使っていくつもりですが、問題や改善点あればコメント助かりますー。 :bow:

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

【iOS】オープンソースのOCRライブラリ「SwiftyTesseract」を試してみる

背景

【iOS】オープンソースのOCRライブラリ調査の続きです。

前回の調査の過程で、Tesseract OCR iOSSwiftOCRの2つがGitHubのスター数から見ても人気だということが分かりました。しかしながら、日本語認識に関しては課題が多く、実用向きではない印象を受けました。そこで個人的に注目しているのがSwiftyTesseractです。

というわけで、SwiftyTesseractを実際に試してみることにしました。

SwiftyTesseractについて

SwiftyTesseractの最大の特徴は、LSTMニューラルネットベースのOCRエンジンが追加された最新安定バージョンのTesseract 4をラップしている点です。ディープラーニングは詳しくないので深掘りしませんが、これで何が嬉しいかというと、Tesseract OCR iOSやSwiftOCRと比べて、ある程度スピードと正確性を両立させた日本語OCRが手軽に実現できるということです。

前提

本記事で使用した環境は次のとおりです。

  • macOS 10.14.4
  • Xcode 10.2.1
  • CocoaPods 1.4.0
  • Swift 4.2

Xcodeプロジェクトを作成する

まずはXcodeプロジェクトを新規作成し、Build SettingsでSwift Language Versionを変更します。
SwiftyTesseractのSwiftバージョンに合わせて、Swift 4.2を選択します。
スクリーンショット_2019-05-04_0_37_53.png

SwiftyTesseractをXcodeプロジェクトへ導入する

CocoaPods、または、CarthageでSwiftyTesseractを導入します。
SwiftyTesseractの解説には、CocoaPodsとCarthage双方の導入方法が紹介されていますので、好きな方法で導入してください。

ちなみに今回は、CocoaPodsを使ってSwiftyTesseractを導入してみます。
まずはPodfileを開いて次の一行を追加します。

Podfile
pod 'SwiftyTesseract', '~> 2.0'

次にPodfileを保存して、下記のコマンドでインストールします。

$ pod install

以上でSwiftyTesseractの導入は完了です。
以降は「プロジェクト名.xcworkspace」というファイルを開いて開発を進めていきます。

学習データをXcodeプロジェクトへ追加する

SwiftyTesseractでは、Tesseract 1用にあらかじめ用意されている学習データをそのまま利用することができます。
Tesseract用の学習データには、次の3つのタイプが用意されています。

SwiftyTesseractの解説によると、たいていのケースで最良の選択肢はtessdata_fastになるだろうと書いてあるので、今回はtessdata_fastの日本語学習データを利用します。

  1. パソコン上にtessdataという名称でフォルダを作成しておきます。 スクリーンショット_2019-05-03_21_30_16.png
  2. tessdata_fastのリポジトリにアクセスし、jpn.traineddataをクリックします。 スクリーンショット_2019-05-04_8_01_07.png
  3. Downloadボタンをクリックします。 スクリーンショット_2019-05-04_8_01_14.png
  4. あらかじめ作成しておいたtessdataフォルダへjpn.traineddataをダウンロードします。 スクリーンショット_2019-05-03_21_32_53.png
  5. tessdataフォルダをXcodeプロジェクトへドラッグします。 スクリーンショット_2019-05-03_21_54_12.png
  6. オプションは次のように選択します。 スクリーンショット_2019-05-03_22_18_34.png
  7. 水色のアイコンで表示されたtessdataフォルダの配下にjpn.traineddataがあればOKです。スクリーンショット_2019-05-03_22_19_16.png

以上で日本語学習データを利用する準備が整いました。

画像をXcodeプロジェクトへ追加する

目的の画像ファイルを用意し、Xcodeプロジェクトへ取り込んでおきます。
今回は次の画像(sample.png)で試してみます。

sample.png

ロゴや下線があったり、「」<>:・〜※などの記号があったり、よく見ると紙が波打って文字のラインが若干ずれていたりといったトラップが仕込まれていますね。
果たして結果や如何に!?

OCR機能を実装する

ここまで来たら、あとはOCR機能を実装するだけです。
ViewController.swiftを次のとおり書き換えます。

ViewController.swift
import UIKit
import SwiftyTesseract

class ViewController: UIViewController {

    let swiftyTesseract = SwiftyTesseract(language: RecognitionLanguage.japanese)

    override func viewDidLoad() {
        super.viewDidLoad()

        let start = Date()

        let fileName = "sample.png"
        guard let image = UIImage(named: fileName) else { return }

        swiftyTesseract.performOCR(on: image) { recognizedString in
            guard let text = recognizedString else { return }
            print("\(text)")

            print("\(-start.timeIntervalSinceNow)")
        }
    }
}

ポイント解説

SwiftyTesseractのインポート

何はともあれ、SwiftyTesseractをインポートします。
これによってSwiftyTesseractが提供するメソッド等が利用できるようになります。

import SwiftyTesseract

SwiftyTesseractのインスタンスを生成

SwiftyTesseractのインスタンスを生成すると同時に、OCRで認識する言語の指定をここで行います。
下記は日本語を認識する場合の記述ですが、tessdataフォルダに含まれる学習データの言語を指定してあげる必要があります。 2

    let swiftyTesseract = SwiftyTesseract(language: RecognitionLanguage.japanese)

OCRの実行

OCRの実行は、単純にsample.pngから生成したUIImageをperformOCR(on:completionHandler:)メソッドへ渡すだけです。
完了ハンドラでは認識された文字列を受け取って処理します。

        let fileName = "sample.png"
        guard let image = UIImage(named: fileName) else { return }

        swiftyTesseract.performOCR(on: image) { recognizedString in
            guard let text = recognizedString else { return }
            print("\(text)")

            // ... 省略
        }

認識結果を確認する

気になる認識結果です。

1600x900の画像を使ってシミュレータで実行した場合は、1.6秒程度を要しました。

1600x900、シミュレータ実行時の認識結果(tessdata_fast)
「ねんきん定期便」「ねんきんネット」に関する
                           下             ら
お問い合わせは、 下記専用番号へ
マク               昭          2
sm 0570-058-555
※O5Oから始まる電話でおかけになる場合は(東京)03-6700-1144
<受付時間> 月 曜日 午前8:30征後7:00
火金曜日 。 午前8:30っ生後5:15
※祝日、 1 2 月 2 9日1月3日はご利用いただけません。
※月曜日が祝日の場合は、翌日以降の開所日初日に午後 7 : 00まで
相談をお受けします。
お問い合わせの際は、「ねんきん定期便」 (ヘー 1 ページ) の照会番号、 基礎年金番号または個人番号をお知らせください。
ご利用にあたっての留意事項
ら 0エルは、 一般の固定電話からおかけになる場合は、 全国どこからでも市内通話料金でご利用できます。
7当レ、
・「O57O」の最初の「O」 を省略したり、 市外局番を付けて間違い電話になるケースが完生しています.。
おかけ間違いのないよう、 ご注意ください。
・ 月曜日などの休日明けやお手元に通知書が届いた直後 (5日間程度) は、電話がつながりにくくなりのます。
週の後半月の後半はつながりやすくなっています。
・オンライ ン端末の移働時間によっては、  ご照会の回答が翌日以降になる場合があります。

800x450の画像を使ってシミュレータで実行した場合は、1.3秒程度を要しました。

800x450、シミュレータ実行時の認識結果(tessdata_fast)
「ねんきん定期便」「ねんきんネット」に関する
お問い合わせは、下記専用番号へ
トン                            回
記 0570-058-555
※O5Oから始まる電話でおかけになる場合は(東京) 03-6700-1144 g叶)
<受付時間> 月 果 日 。 生前Bi30一年径7:00
※祝日、1 2月29日1月3日はご利用いただけません。
※月曜日が祝日の場合は、避日以降の開所日補日午後7 : 0 0まで
相談をお受けします。
お問い合わせの隊は、「ねんきん定期便」 (ムー 1 ページ) の照会番号、 基礎年金番号または個人番号をお知らせください。
ご利用にあたっての留意事項
 本ルは  のたから33 国どこびらでも中衝料金でご利用できます。
ただし、
・_「O570」の最初の 「O」を省略したり、 市外局番を付けて間違い電話になるケースが発生しています。
おかけ商違いのないよう、 ご注意ください。
・ 月明日などの休日明けやお手元に運知書が届いた直後 (5日間程度) は、重話がつながりにくくなります。
 週の後半月の後半はつながりやすくなっています。
・オインライン端末の知午時聞によっては、 ご暑会の回答が翌日以降になる場合があります。

まとめ

認識精度はお世辞にも実用レベルとは言い難いながらも、ハードル高めな画像を使ったわりには頑張ってくれてる気がします。(個人の感想です)

無料のOCRライブラリで、画像や学習データに手を加えずとも、ここまで認識できるのは凄いことではないでしょうか。

認識範囲を限定したり、前処理で画像を適切に加工したり、学習データをチューニングしたりなど、認識精度をさらに向上させる方法を試す余地も大いにありそうです。 3

今後もSwiftyTesseractの動向を追っていきたいと思います。

おまけ

参考までに、tessdata_bestとtessdataの学習データを使った認識結果も掲載しておきます。

tessdata_bestでの認識結果

1600x900の画像を使ってシミュレータで実行した場合は、認識に5.3秒程度を要しました。

1600x900、シミュレータ実行時の認識結果(tessdata_best)
「 ね ん きん 定期 便 」「 ね ん きん ネッ ト 」 に 関す る
三 |
お 問い 合わ せ は 、 下 記 専 用 番号 へ
や -。
記 2 0570-058-555
※O050 か ら 始 まる 電話 で お か け に な る 場合 は (東京 ) 09-6 700-1144 mm
< 受付 時 間 > 月 曜 日
年 前 9 30 午後 4:00
※ 祝 日 、1 2 月 2.9 日 1 月 3 日 は ご 利用 いた だ け ま せん 。
※ 月 曜日 が 祝日 の 場合 は 、 翌 日 以降 の 開所 日 初日 に 午後 7 : 00 ま で
相談 を お 受け し ます 。
お 問い 合わ せ の 際 は 、「 ね ん きん 定期 便 」 (Aー 1 ペー ジ ) の 照会 番号 、 基礎 年 金 番号 また は 個人 番号 を お 知ら せく だ さい 。
ご 利用 に あたっ て の 留意 事項
* ヤル は 、 一 般 の 固定 電話 か ら お か け に な る 場合 は 、 全国 どこ か ら で も 市 内 通話 料金 で ご 利用 で きま す 。
に だ し 、
・「O570」 の 最初 の 「O」 を 省略 し た り 、 市 外 局 災 を 付け て 問 違 い 電 話 に な る ケ ー ス が 発生 し て いま す 。
お か け 間 違い の な いよ う 、 ご 注意 くだ さい 。
* 月 曜日 な どの 休日 明け や お 手元 に 通知 書 が 届い た 直後 (5 日 間 程度 ) は 、 電 話 が つなが り に くく な の ます 。
週 の 後半 や 月 の 後半 は つなが りや すぐ な っ て いま す 。
* オン ライ ン 端 末 の 稼働 時 間 に よ っ て は 、 ご 照会 の 回 答 が 翌日 以降 に な る 場合 が あり ます 。

tessdataでの認識結果

1600x900の画像を使ってシミュレータで実行した場合は、認識に3.0秒程度を要しました。

1600x900、シミュレータ実行時の認識結果(tessdata)
` ね ん き ん 定 期 便 」` ね ん き ん ネ ッ ト 」 に 関 す る
ミ ロ
お 問 い 合 わ せ は 、 下 記 専 用 番 号 ヘ
ン シ 許
啓 0⑤⑦0-0⑤⑧-⑤⑤⑤
※O⑤O か ら 始 ま る 電 話 で お か け に な る 場 合 は ( 東 京 ) 0③-⑥⑦00- ⑪④④ c_-awms
< 受 付 時 間 > 月 _ 曜 _ 日
平 前 ⑨ : ③ 0 へ 平 後 ④ : 0 0
※ 祝 日 、① ② 月 ② ⑨ 日 ー① 月 ③ 日 は ご 利 用 い た だ け ま せ ん 。 *
※ 月 曜 日 が 祝 日 の 場 合 は 、 翌 日 以 降 の 開 所 日 初 日 に 午 後 ⑦ : 0 0 ま で
相 談 を お 受 け し ま す 。
お 問 い 合 わ せ の 際 は 、 ` ね ん き ん 定 期 便 」 (Aー ① ペ ー ジ ) の 照 会 番 号 、 基 礎 年 金 番 号 ま た は 個 人 番 号 を お 知 ら せ く だ さ い 。
ご 利 用 に あ た っ て の 留 意 事 項
* 戸昇乞イ ヤ ル は 、 一 般 の 固 定 電 話 か ら お か け に な る 場 合 は 、 全 国 ど こ か ら で も 市 内 通 話 料 金 で ご 利 用 で き ま す 。
に だ し 、
・ `0⑤⑦0」 の 最 初 の `O」 を 省 略 し た り 、 外局を付けて間這い電話になるケ ー ス が 発 生 し て い ま す 。
お か け 閾 違 い の な い よ う 、 ご 注 意 く だ さ い 。
・月鱧貝萱との休日明けやお手元仁通知害か届いた直後 (⑤ 日 間 程 度 ) は 、 電 話 が つ な が り に く く な り ま す 。
週の欝キゆ貝の後半はつなかりやすくなっています。
・ オ ン ラ イ ン 端 末 の 稼 働 時 間 に よ っ て は 、 ご 照 会 の 回 答 が 翌 日 以 降 に な る 場 合 が あ り ま す 。


  1. TesseractはGoogleによって開発が進められているOCRエンジンです。 

  2. 今回は日本語のみ指定していますが、複数の言語を指定することもできます。 

  3. SwiftyTesseractの解説を見ても、予測可能な結果を返すために画像加工などの最適化をほどこすのは、ライブラリ側ではなくアプリ側であるとも受け取れる記述が確認できます。 

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

【iOS】オープンソースのOCRライブラリ調査

はじめに

iOSで利用可能なオープンソースのOCRライブラリを調査してみました。

前提条件

下記条件で調査しました。

  • 無料であること
  • オフラインで利用できること
  • サーバサイドやクラウドサービスを介さず利用できること
  • 手書き文字は考慮しないこと
  • 日本語の認識が可能であること
  • 日本語学習データの作成 or チューニングが可能なこと

調査結果

以下の3つのライブラリをピックアップしてみました。

Tesseract OCR iOS SwiftOCR SwiftyTesseract
OCRエンジン パターン認識 ニューラルネットワーク LSTMニューラルネットワーク
日本語学習データの提供
ライセンス MIT license Apache License,
Version 2.0
MIT License
依存ライブラリ Tesseract 3
Leptonica
libpng
libjpeg
libtiff
Swift-AI
GPUImage
Union-Find
Tesseract 4
Leptonica
libpng
libjpeg
libtiff

Tesseract OCR iOS

OCRエンジンにGoogleが開発を進めているTesseractを採用しているだけあって、情報量が多く人気もあるライブラリです。ただし、Tesseract本体、及び、対応する学習データのバージョンが若干古いため、日本語の認識精度に関しては疑問符が付きます。現在進行系でTesseract最新バージョンの実装を進めている(?)模様。

SwiftOCR

Tesseractよりパフォーマンスに優れる点をアピールしているライブラリです。ただし、短い英数字コードの認識が得意らしく、誌や文を認識したい場合はTesseractが良いと説明しています。日本語を認識するためには学習データを作成する必要があり、トレーニングツールが提供されているとは言え、十分な精度を望むのであれば導入に相応の時間を要しそうです。

SwiftyTesseract

Tesseract OCR iOSに同じく、TesseractをOCRエンジンとして採用しているライブラリです。Tesseract OCR iOSとの違いは最新安定バージョンのTesseractを採用している点で、パフォーマンスと認識精度が大きく向上しています。ただし、Tesseract本体のAPIを完全にはサポートしておらず、利用できない機能があるので注意が必要です。

まとめ

ここ数年で見聞きする機会が多くなった「ディープラーニング」によって、OCRライブラリの文字認識精度が飛躍的に向上している印象を受けました。
調査したライブラリの中では、SwiftyTesseractが最も手軽かつ実用向きではないかと考えます。
他にも有用なライブラリをご存知の方がいらっしゃいましたら、ぜひご教示ください。

おまけ

クラウド可かつ課金可まで条件を緩めると、Firebase向けML Kitのテキスト認識(OCR)一択な気がします。

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

【Xcode】GitHubにpushした瞬間にTabの幅が変わってしまう

先輩エンジニアにコードレビューで
「インデントの乱れは心の乱れ!!!! :anger:
と叱られて
「あれっ!?!?Xcode上だと何もおかしくないのに!?!?」
と混乱してしまったそこのあなたのためにこの記事を残します。

tl;dr

tabの幅がおかしい時は XCode -> Preferences... -> Text Editing -> Indentationの設定を弄れ

症状

Xcode上では何も違和感なかった(むしろ control+iコマンドでインデント調整をしている)コードをGitHubリモート上にpushした途端インデント幅が変わる
(macOS Mojave: 10.14.4, XCode: 10.2.1)

  • ローカル(Xcode)上
    スクリーンショット 2019-05-07 17.24.44.png
    (よしよし、インデント調整もしたしpushしよう)

  • リモート(GitHub)上
    スクリーンショット 2019-05-07 17.25.24.png
    (? :thinking: )
    (リモートだとTab幅が2文字分深くなってしまいました)

解決法

XCode -> Preferences... -> Text Editing -> Indentation内の
Prefer indent using
Tabs-> Spacesに変更。これだけ。
スクリーンショット 2019-05-07 17.36.45.png
TabsによるインデントだとXcode上の見た目が何文字でも6文字に固定されてしまう?ことを知りませんでした
もちろん、シンプルに Tab widthIndent widthが想定のものと違っている可能性もあるので要確認です

まとめ

設定できる項目と設定の影響範囲はちゃんと覚えようね

おまけ

人のコードを見る時にインデント幅を変えたい場合はURLに ?ts=4とかしてあげましょう
Source: https://github.com/tiimgreen/github-cheat-sheet#adjust-tab-space

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

UIRequiredDeviceCapabilitiesが原因でFlutter製iOSアプリの審査がリジェクトされた際の対処方法

事象

Flutterで作ったiOSアプリをAppleの審査に提出した結果、下記の理由でリジェクトされました。

差出人: Apple
2. 3 Performance: Accurate Metadata
Guideline 2.3 - Performance - Accurate Metadata


We were unable to install the app on iPad and iPhone. The UIRequiredDeviceCapabilities key in the Info.plist is set in such a way that the app will not install on an iPhone and iPad .

Next Steps

To resolve this issue, please check the UIRequiredDeviceCapabilities key to verify that it contains only the attributes required for your app features or the attributes that must not be present on the device. Attributes specified by a dictionary should be set to true if they are required and false if they must not be present on the device.

Resources

Please review the Technical Q&A 1397: Understanding the UIRequiredDeviceCapabilities key for information on the UIRequiredDeviceCapabilities key.

You may also wish to review the dictionary keys table in the "Declaring the Required Device Capabilities" section of the App Programming Guide for iOS.

この記事では試行錯誤の過程を記載しています。
途中経過よりも最終的な対処方法を知りたい方は、最後の項目対処その4まで読み飛ばしてください。

対処その1

社内のiOS開発者に伺ったところ、iOSプロジェクトを新規作成するとUIRequiredDeviceCapabilitiesはデフォルトで設定されるとのことでした。
Flutterで新規作成したプロジェクトにはこれが設定されていませんでした。
UIRequiredDeviceCapabilitiesをInfo.plistに追加して、さらにその中のアイテムにarmv7を追加しました。

armv7を設定した理由は、iOSアプリを新規作成した際にデフォルトで設定される値がarmv7であること、またこちらの公式ドキュメントによるとarmv7は一部の機能を包含する形になっていること、この上記2点を参考にしたためです。

結果その1

再度同じ理由でリジェクトを受けました。

対処その2

シミュレーター上で問題なく動作することを動画でまとめ添付し、審査に提出したところこの問題は解決しました。

詳しくはこちら

この投稿を参考にして、アプリが動作している動画と下記のような文言を添えてレビューチームに返信しました。

I can install the my app into emulators and physical devices.
I upload the movie that shows my app is running.
I want you to review my app again.

結果その2

再度同じ理由でリジェクトを受けました。

対処その3

社内の別のiOS開発者から得た情報で、似たような理由でリジェクトを受けた際にInfo.plistの中身をそのまま渡したら無言で承認されたことがあると伺いました。
一説によるとApp Storeのバグではないかと言われているそうです。

私も下記の文言を添えて、Info.plistを添付ファイルとしてアップロードして返信してみました。

Hello,

Thank you for your reply.

I checked my app Info.plist.
It has the UIRequiredDeviceCapabilities key in Info.plist.
I upload my Info.plist.
I want you to check it.

Best regards

結果その3

再度同じ理由でリジェクトを受けました。

対処その4

社内のFlutterアプリ開発者に伺ったところ、Flutter製のiOSアプリをストアへアップロードする際はflutter build ios --releaseを実行してからでないと送れなかったことがあるとアドバイスをいただきました。
そこで、アドバイス通り上記コマンドを実行した後にArchiveを選択してバイナリをストアへアップロードして再度審査に提出しました。

結果その4

無事に審査を通過しました!

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

Flutter製iOSアプリの審査がリジェクトされた際の対処方法

事象

Flutterで作ったiOSアプリをAppleの審査に提出した結果、下記の理由でリジェクトされました。

差出人: Apple
2. 3 Performance: Accurate Metadata
Guideline 2.3 - Performance - Accurate Metadata


We were unable to install the app on iPad and iPhone. The UIRequiredDeviceCapabilities key in the Info.plist is set in such a way that the app will not install on an iPhone and iPad .

Next Steps

To resolve this issue, please check the UIRequiredDeviceCapabilities key to verify that it contains only the attributes required for your app features or the attributes that must not be present on the device. Attributes specified by a dictionary should be set to true if they are required and false if they must not be present on the device.

Resources

Please review the Technical Q&A 1397: Understanding the UIRequiredDeviceCapabilities key for information on the UIRequiredDeviceCapabilities key.

You may also wish to review the dictionary keys table in the "Declaring the Required Device Capabilities" section of the App Programming Guide for iOS.

この記事では試行錯誤の過程を記載しています。
途中経過よりも最終的な対処方法を知りたい方は、最後の項目対処その4まで読み飛ばしてください。

対処その1

社内のiOS開発者に伺ったところ、iOSプロジェクトを新規作成するとUIRequiredDeviceCapabilitiesはデフォルトで設定されるとのことでした。
Flutterで新規作成したプロジェクトにはこれが設定されていませんでした。
UIRequiredDeviceCapabilitiesをInfo.plistに追加して、さらにその中のアイテムにarmv7を追加しました。

armv7を設定した理由は、iOSアプリを新規作成した際にデフォルトで設定される値がarmv7であること、またこちらの公式ドキュメントによるとarmv7は一部の機能を包含する形になっていること、この上記2点を参考にしたためです。

結果その1

再度同じ理由でリジェクトを受けました。

対処その2

シミュレーター上で問題なく動作することを動画でまとめ添付し、審査に提出したところこの問題は解決しました。

詳しくはこちら

この投稿を参考にして、アプリが動作している動画と下記のような文言を添えてレビューチームに返信しました。

I can install the my app into emulators and physical devices.
I upload the movie that shows my app is running.
I want you to review my app again.

結果その2

再度同じ理由でリジェクトを受けました。

対処その3

社内の別のiOS開発者から得た情報で、似たような理由でリジェクトを受けた際にInfo.plistの中身をそのまま渡したら無言で承認されたことがあると伺いました。
一説によるとApp Storeのバグではないかと言われているそうです。

私も下記の文言を添えて、Info.plistを添付ファイルとしてアップロードして返信してみました。

Hello,

Thank you for your reply.

I checked my app Info.plist.
It has the UIRequiredDeviceCapabilities key in Info.plist.
I upload my Info.plist.
I want you to check it.

Best regards

結果その3

再度同じ理由でリジェクトを受けました。

対処その4

社内のFlutterアプリ開発者に伺ったところ、Flutter製のiOSアプリをストアへアップロードする際はflutter build ios --releaseを実行してからでないと送れなかったことがあるとアドバイスをいただきました。
そこで、アドバイス通り上記コマンドを実行した後にArchiveを選択してバイナリをストアへアップロードして再度審査に提出しました。

結果その4

無事に審査を通過しました!

おわりに

今回の原因はアプリのビルド方法が正規の方法ではなかったことが原因による問題でした。
余談ですが、Flutter製のアプリはflutter build ios --releaseでビルドしないとpubspec.yamlで指定したアプリのversionCodeversionNameがバイナリに反映されないようです。
今回は、各プラットフォーム標準のやり方ではなく、FlutterではFlutter独自の方法を用いるべきだということを学びました。
今後も何かしら問題が起きた際にはそこを疑ったほうがよさそうです。

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

SwiftGenとR.swift 導入するにあたって

はじめに

SwiftGenR.swiftはiOS開発上でリソース管理を楽にできるライブラリ。というより、ツールと言った方がしっくりくるかと思います。
コードに存在するハードコーディングをなくし、運用保守しやすくなります。
そして今回弊社の新規プロジェクトでどちらか採用しようと考え、いろいろ比較したのをまとめます。

対応しているリソース比較

以下のように対象プロジェクトの欲しい部分は両方ありました。
プロジェクトによってはCoreData扱えるSwiftGenは良いですね。(CoreData扱ったことないので、詳しくわかりませんがw)

SwiftGen R.swift
image
font
color
localize string
storyboard
segue
nib
reusable cell
core data ×
plist △(※1)
file △(※2)

※1. File読み込みまでは簡潔にできる。
※2. jsonとyamlは設定できる。

導入比較

Xcodeの設定上でBuild Phaseに各々のshellを追加し、走らせる点は同様ですね。
両者の導入方法を見てみましょう。

SwiftGen

https://github.com/SwiftGen/SwiftGen/blob/master/README.md#installation

以下のswiftgen.ymlファイルでカスタマイズできますので、汎用性が高いですね。

swiftgen.yml
strings:
  inputs: Resources/Base.lproj
  filter: .+\.strings$
  outputs:
    - templateName: structured-swift4
      output: Generated/strings.swift
xcassets:
  inputs:
    - Resources/Images.xcassets
    - Resources/MoreImages.xcassets
  outputs:
    - templateName: swift4
      output: Generated/assets-images.swift

加えてbrewでも用意されているため、xcodeでいちいちビルドして、リソースファイル生成という手間がありません。

$ brew update
$ brew install swiftgen

R.swift

https://github.com/mac-cain13/R.swift/blob/master/README.md#cocoapods-recommended

特に設定ファイルもなく、ライブラリをプロジェクトに導入し、Build Phasesで設定し、ビルドすれば、リソースファイルが生成されます。

一律Rprefixで呼び出せるのは一意性があって、良いですね。

まとめ

  • SwiftGen

    • カスタマイズできる
    • コマンド実行でき、ビルドの手間が省ける
    • CoreDataが扱える
  • R.swift

    • 導入が簡単
    • カスタマイズできないため、プロジェクト間の揺れはない
    • 対応リソース間の呼び出し方に揺れがない

結局今回のプロジェクトでは知見がないSwiftGenの方をお試し採用しました。

今回は導入するにあたっての比較となりましたが、いかがでしょうか?ぜひ皆さんの今後の開発のてだすけになれば、幸いです。

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

fastlane match を使用して iOS の証明書管理を行う

前書き

iOS 開発において「証明書」という単語を聞くだけでアレルギー反応を起こす人も少なくないかもしれません。
iOS 開発チームにメンバーが追加された際は、少なくとも以下のような作業を行ってもらう(もしくは、行う)必要があります。

  • Apple Developer アカウントの作成
  • Apple Develper Portal(以下、Dev Portal)にメンバーを招待
  • コード署名証明書(以下、証明書)の作成
  • プロビジョニングプロファイル(以下、プロファイル)の更新

これらの作業を手動で行うのには非常に手間がかかります。
Xcode 8 から Automatic Signing が導入されましたが、証明書管理が iOS 開発チームの各メンバーに依存することにより発生する証明書周りのトラブルは後を絶ちません。個人開発では発生しなかったトラブルがチーム開発では付いて回ります。
また、CI 上で証明書とプロファイルをどのように扱うかといった課題も残ります。

そこで fastlane match(以下、match) です。
match のアプローチについてはこちらに記載されています。

match とは

match は、iOS 開発チームで共通の証明書とプロファイルの作成や維持を行い、プライベートリポジトリに格納、そして全ての開発者間で共有できるようにします。
つまり、メンバーが増える度に Apple Developer アカウントや証明書を作成する必要がなくなります。

構成は以下をご覧下さい。

fastlane match
ざっくり説明すると、以下の2点が運用される状態を作ろうと思いました。
- adminに任命された人は、matchを使って証明書& Provisioning Profile の作成&commitを行う
- memberは、matchを使って証明書& Provisioning Profile をダウンロードして使う

引用元:fastlane match でiOSアプリ開発者を「証明書管理の苦しみ」から解放せよ! - BizReach Tech Blog

match の導入

インストール

match は fastlane の action のため、fastlane をインストールすることで match を使用することができます。
fastlane は RubyGems や Homebrew を利用してインストールすることができます。
Gemfile を使用するにはこちらを参照して下さい。

# Using RubyGems
sudo gem install fastlane -NV

# Alternatively using Homebrew
brew cask install fastlane

事前準備

プライベートリポジトリの作成

証明書やプロファイルを保存するためのプライベートな Git リポジトリを作成します。
リポジトリ名は certificates などにします。

Apple Developer アカウントの用意

チーム全体で共有する Apple Developer アカウントを用意します。(例:office@company.com
権限を App Manager にして下さい。

App Manager 以上の権限でないと、match を実行した際に以下のようなエラーが発生する場合があります。

[App Store Connect unification] Spaceship::UnexpectedResponse: [!] Apple provided the following error info: Access Unavailable - You currently don't have access to this membership resource

詳しくは、こちらをご覧下さい。

Xcode プロジェクト設定

Xcodeプロジェクトのプロファイルを自動に設定しないでください。常に正しいプロファイルが選択されるわけではありません。
Xcode プロジェクト設定「 General > Automatically manage signing 」を無効にしておきましょう。

詳しくは、こちらをご覧下さい。

App ID の用意

iOS アプリのための App ID を用意します。

produce を使用することで、Dev Portal と App Store Connect の両方に新しい iOS アプリを作成することが可能です。また、アプリケーションサービスの変更やアプリケーショングループの変更も可能です。

-a もしくは --app_identifier オプションを使用して、アプリの Bundle Identifier を指定できます。

fastlane produce -a <app_identifier>

-i もしくは --skip_itc オプションを使用して、App Store Connect でのアプリ作成をスキップできます。

fastlane produce -a <app_identifier> -i

初期設定

match を使用するための初期設定を行います。

fastlane match init

Git リポジトリの URL や、パスフレーズが要求されるため入力して下さい。

証明書やプロビジョニングプロファイルは openssl を使用してパスフレーズによって暗号化され、Git リポジトリに保存されます。
そのため、パスフレーズはチームに共有する必要があります。
環境変数を使用して復号化するようにパスフレーズを設定するには、MATCH_PASSWORD を使用します。

また、上記コマンドを実行すると、カレントディレクトリに Matchfile が作成されます。
fastlane init を実行したプロジェクト(以下、fastlane プロジェクト)のプロジェクトルートで上記コマンドを実行した場合は ./fastlane/ に作成されます。

match の実行

fastlane プロジェクトのプロジェクトルート外で match を実行した場合、チーム全体で共有する Apple Developer アカウントの Apple ID や、アプリの Bundle Identifier が要求されるため入力して下さい。
入力を省略したい場合は、カレントディレクトリの Matchfile を修正して下さい。

Matchfile
app_identifier("<app_identifier>")
username("<username>")

作成

証明書とプロビジョニングプロファイルを作成します。
基本的に管理者が行う作業です。

$ fastlane match development
$ fastlane match adhoc
$ fastlane match appstore
Fastfile
match(type: "development")
match(type: "adhoc")
match(type: "appstore")

既存の証明書とプロファイルを残したまま、match で作成した証明書とプロファイルを混在させ、徐々に移行することも可能です。

match を実行した例は以下をご覧下さい。

match appstore small

引用元:match - fastlane docs

具体的な処理の流れ

上記コマンドの具体的な処理の流れは以下をご覧下さい。

  1. Dev Portal に証明書とプロファイルを作成
  2. Git リポジトリに証明書(とその秘密鍵)とプロファイルを格納
  3. プロビジョニングプロファイルが ~/Library/MobileDevice/Provisioning Profiles にインストール
  4. 証明書と秘密鍵が、キーチェーンにインストール

※ 既に証明書とプロビジョニングプロファイルが作成されている場合は更新されます。

リポジトリの構造

上記コマンド実行後、Git リポジトリには2つのディレクトリが含まれます。

  • 証明書とその秘密鍵を含む certs ディレクトリ
  • プロビジョニングプロファイルを含む profiles ディレクトリ

具体的にどのような構造になるか確認したい場合は、こちらのサンプルを参照して下さい。

取得

Git リポジトリから証明書とプロビジョニングプロファイルのインストールを行います。
基本的にメンバーが行う作業です。
新規メンバーのマシンに証明書とプロビジョニングプロファイルをインストールしたい場合など、Dev Portal にアクセスする必要がない場合は --readonly オプションを使用します。

$ fastlane match development --readonly
Fastfile
match(type: "development", readonly: true)

削除

無効や期限切れ、Xcode によって管理された証明書やプロビジョニングプロファイルを Dev Portal から削除します。

$ fastlane match nuke
$ fastlane match nuke development
$ fastlane match nuke distribution

削除される予定の証明書とプロファイルのリストが表示され、削除するかどうかの確認が要求されるため、yn を入力して下さい。
y を入力し削除しても、App Store と TestFlight で既に利用可能なアプリは引き続き機能します。

※ 初めて match を実行する前に、上記コマンドを使用して既存の証明書とプロファイルの消去を考慮することが推奨されています。match の検証や、既存の証明書とプロファイルを残したい場合は上記コマンドの使用はお勧めしません。

nuke を実行した例は以下をご覧下さい。

match nuke

引用元:match - fastlane docs

パスフレーズの変更

パスフレーズを変更します。

$ fastlane match change_password

実行後、全てのマシンで新しいパスフレーズが要求されます。

新しいデバイスの登録

register_device を使用することで、Dev Portal に新しいデバイスを登録することが可能です。

$ fastlane run register_device name:"<name>" udid:"<udid>"
Fastfile
register_device(
  name: "<name>",
  udid: "<udid>"
)

--force_for_new_devices オプションを使用して、Dev Portal のデバイス数が変更されたかどうかを確認し、必要に応じてプロビジョニングプロファイルを自動的に更新します。

$ fastlane match development --force_for_new_devices
$ fastlane match adhoc --force_for_new_devices
Fastfile
match(type: "development", force_for_new_devices: true)
match(type: "adhoc", force_for_new_devices: true)

CI で使用

取得

CI 上で match を実行するときは、常に --readonly オプションを使用することが推奨されています。

Fastfile
lane :beta do
  match(type: "appstore", readonly: is_ci)

  gym(scheme: "Release")
end

キーチェーンの作成

CI 上で match を実行するときは、キーチェーンを名前とパスワード付きで作成する必要があります。

詳しくは以下をご覧下さい。

具体的には、キーチェーンのパスワードを入力して許可する必要がありました。自分のマシンの場合はログインパスワードを入力すれば良いのですが、CIマシン上での場合は新しくキーチェーンを名前・パスワード付きで作成して、match実行時にそれを用いる、という手順が必要でした。

コードとしては、MATCH_KEYCHAIN_NAME・MATCH_KEYCHAIN_PASSWORDに適当な値を環境変数としてセットした上で、以下をmatchコマンド前に実行すれば解決しました。

引用元:Bitrise Xcode 8.1などのSierra製CIマシンとfastlaneの組み合わせでビルドが固まる問題が解決?

Fastfile
before_all do
  if is_ci?
    create_keychain(
      name: ENV['MATCH_KEYCHAIN_NAME'],
      password: ENV['MATCH_KEYCHAIN_PASSWORD'],
      timeout: 1800
    )
  end
end

※ AppStore のプロビジョニングプロファイルにはデバイス情報が含まれていないため、プロファイルタイプが appstore の場合無視されます。

参考文献

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

【Swift5】自動スクロール(auto scroll)機能を実装

はじめに

今回、自動スクロール(auto scroll)機能を実装する機会があったので、その方法を備忘録として残しておきます。

実装

調べてみたところ、Timerを使って一定時間ごとにスクロール(OffSetやIndexPathなどを変更)させていく、という方法があるようだったので、その方針でやってみました。

今回はUIScrollViewを継承したUICollectionViewとUITableViewを自動スクロールさせてみました。

UICollectionView

まずは自動スクロール動作を管理するTimer型の変数を定義します。

CollectionViewController.swift
private var autoScrollTimer = Timer()

自動スクロールを開始・停止するメソッドを用意します。UICollectionViewは一定時間ごとに表示IndexPathを変更して、左右に自動スクロールさせています。

CollectionViewController.swift
// 自動スクロールを開始する
    private func startAutoScroll(duration: TimeInterval, direction: ScrollDirectionType) {
        // 表示中のIndexPathを取得
        var indexPath = collectionView.indexPathsForVisibleItems.sorted { $0.item < $1.item }.first ?? IndexPath(item: 0, section: 0)
        // 最初のItem
        let firstItem = IndexPath(item: 0, section: 0)
        // 最後のItem
        let lastItem = IndexPath(item: collectionView.numberOfItems(inSection: 0) - 1, section: 0)
        // 自動スクロールを終了させるかどうか
        var shouldFinish = false
        autoScrollTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: true, block: { [weak self] (_) in
            guard let self = self else { return }
            // item2つ分ずつスクロールさせる
            switch direction {
            case .left:
                indexPath = (indexPath.item + 2 >= self.collectionView.numberOfItems(inSection: 0)) ? lastItem : IndexPath(item: indexPath.item + 2, section: 0)
                shouldFinish = indexPath.item == lastItem.item
            case .right:
                indexPath = (indexPath.item - 2 <= 0) ? firstItem : IndexPath(item: indexPath.item - 2, section: 0)
                shouldFinish = indexPath.item == firstItem.item
            default: break
            }
            DispatchQueue.main.async {
                self.collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
                if shouldFinish { self.stopAutoScrollIfNeeded() }
            }
        })
    }

    // 自動スクロールを停止する
    private func stopAutoScrollIfNeeded() {
        if autoScrollTimer.isValid {
            view.layer.removeAllAnimations()
            autoScrollTimer.invalidate()
        }
    }

~~~ 省略

enum ScrollDirectionType {
    case upper, under, left, right
}

今回はボタンタップによって自動スクロールの開始・停止を行ってみます。自動スクロールの間隔は1秒としています。

CollectionViewController.swift
@IBAction func didTapLeft(_ sender: UIButton) {
        startAutoScroll(duration: 1.0, direction: .left)
    }

@IBAction func didTapStop(_ sender: UIButton) {
        stopAutoScrollIfNeeded()
    }

動作結果

UICollectionView-AutoScroll

UITableView

CollectionViewの場合と同様に、Timer型の変数を定義します。

TableViewController.swift
private var autoScrollTimer = Timer()

自動スクロールを開始・停止するメソッドを用意します。UITableViewは一定時間ごとにContentOffsetを変更して、上下に自動スクロールさせています。

TableViewController.swift
    // 自動スクロールを開始する
    private func startAutoScroll(duration: TimeInterval, direction: ScrollDirectionType) {
        // 表示されているTableViewのOffsetを取得
        var currentOffsetY = tableView.contentOffset.y
        // 自動スクロールを終了させるかどうか
        var shouldFinish = false
        autoScrollTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: true, block: { [weak self] (_) in
            guard let self = self else { return }
            // 10ずつY軸のoffsetを変更していく
            switch direction {
            case .upper:
                currentOffsetY = (currentOffsetY - 10 < 0) ? 0 : currentOffsetY - 10
                shouldFinish = currentOffsetY == 0
            case .under:
                let highLimit = self.tableView.contentSize.height - self.tableView.bounds.size.height
                currentOffsetY = (currentOffsetY + 10 > highLimit) ? highLimit : currentOffsetY + 10
                shouldFinish = currentOffsetY == highLimit
            default: break
            }
            DispatchQueue.main.async {
                UIView.animate(withDuration: duration * 2, animations: {
                    self.tableView.setContentOffset(CGPoint(x: 0, y: currentOffsetY), animated: false)
                }, completion: { _ in
                    if shouldFinish { self.stopAutoScrollIfNeeded() }
                })
            }
        })
    }

    // 自動スクロールを停止する
    private func stopAutoScrollIfNeeded() {
        if autoScrollTimer.isValid {
            view.layer.removeAllAnimations()
            autoScrollTimer.invalidate()
        }
    }

同じくボタンタップによって自動スクロールの開始・停止を行ってみます。自動スクロールの間隔は0.1秒としています。

TableViewController.swift
@IBAction func didTapUp(_ sender: UIButton) {
        startAutoScroll(duration: 0.1, direction: .upper)
    }

@IBAction func didTapStop(_ sender: UIButton) {
        stopAutoScrollIfNeeded()
    }

動作結果

UITableView-AutoScroll

ソースコード

以下のリポジトリに作成したサンプルのソースを置いてあります。
https://github.com/ddd503/AutoScroll-CollectionView-TableView

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