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

SwiftUIのおすすめ参考サイトまとめ

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

【iOS】アプリが閉じた状態でUniversal Linksでの起動をデバッグする

アプリが閉じた状態でのUniversal Links経由の起動を繰り返しデバッグしたい時のメモ

手順

  1. EditSchemeWait for executable to be launched をチェック
    Screen Shot 2020-02-14 at 19.51.53.png
    これによって、Runした時にアプリが自動で起動しなくなります

  2. アプリをRun(Wait for executable to be launched)

  3. Terminalで以下のコマンドを実行

$ xcrun simctl terminate booted <bundle_identifier> && xcrun simctl openurl booted <url>

これによって常にアプリが閉じた状態で、Universal Links経由でアプリが起動するようになります。
繰り替えし確認したい場合は2と3を繰り返すことで可能となります。

解説

細かいコマンドの仕様はhelpやdocumentを参照してください。
- xcrun simctl terminate booted <bundle_identifier> は対象のアプリをタスクキルするのと同様です
- xcrun simctl openurl booted url はUniversal Links経由でアプリを起動します

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

Build React Native Custom Checkbox Component for Android and iOS

In this tutorial, we are going to learn how to create a custom Checkbox component in React Native application for Android and iOS platforms. We will learn step by step how to get Multiple checkboxes values on button click using React Native APIs.

READ MORE TO CLICKHERE

https://www.positronx.io/build-react-native-custom-checkbox-for-android-and-ios/

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

UITableViewCellの中のUIImageVewでアニメーションすると勝手に止まる件

UITableView の中に UIImageView を入れていて、その UIImageViewanimationImages を指定してアニメーションを表示している時、テーブルのセルを選択するとアニメーションが止まってしまう。

解決法

setSelected, setHighlightedをオーバーライドしてその中でアニメーションを再開する。

class MyCell: UITableViewCell {

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        restartAnimatingIfNeeded()
    }

    override func setHighlighted(_ highlighted: Bool, animated: Bool) {
        super.setHighlighted(highlighted, animated: animated)
        restartAnimatingIfNeeded()
    }

    func restartAnimatingIfNeeded() {
        if imageView.animationImages != nil {
            imageView.startAnimating()
        }
    }
}

少々無理やりな気がするけど…あと最初のフレームから再生されてしまうのも気になる…

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

【iOS】今すぐ始める自動テスト(XCTest, XCUITest)

iOSアプリのテストを始めたい、やったほうが良いのはわかる、 けど…
という方のための、
公式フレームワークXCTestXCUITestで自動テストを始めるまでの記事です。

セットアップ

※ 既に「アプリ名+Tests/UITests」のグループやTARGETSがある場合は不要
image.png

プロジェクト作成時に「include Unit/UI Tests」にチェック
image.png

もしくはプロジェクト作成後、File→New→Target→
image.png
Unit/UI Testing Bundleを追加して
image.png

TARGETが増えていれば完了。
image.png

XCTest

テストクラスの作成

まず、Testsグループ内にUnitTestクラスを作成。
image.png
↓選択
image.png
すると以下のような雛形が出来る。

import XCTest
class QiitaTest: XCTestCase {
    override func setUp() {
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }
    func testExample() {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }
    func testPerformanceExample() {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }
}

[1] setUp
テスト開始時に動く。共通の準備等に使う。
[2] testExample
名前がtestから始まる関数がテスト扱いとなって実行される。
[3] tearDown
テスト終了時に動く。共通の確認等に使う。

例:test1(), test2()を書いた場合
setUp() > test1() > tearDown() > setUp() > test2() > tearDown()
の順で動く。

Membershipの登録

他のファイルのクラスの呼び出しにはMembershipの登録が必要。
例えばLangというクラスを呼ぶ場合、アプリ内の該当ファイルを選択し…
image.png
Testsにチェックを入れる。
image.png
この操作は複数ファイルを選択して同時に適用できる。グループ単位はできませんでした。

テストの実行

アプリ内のコードを実行し、XCTAssert〜系メソッドでチェックする。
※以下のコード例ではアプリ特有のクラスが出てきますが、要は関数の呼び出し結果が期待値と一致するかをチェックする単体テストです。

import XCTest
class ProductIdCoderServiceTests: XCTestCase {
    override func setUp() {}
    override func tearDown() {}
    func testEncodeLesson() {
        let lesson = Lesson(lang: Lang(rawValue: "ja")!, courseId: 3, lessonId: 6)
        let coderService = ProductIdCoderService()
        let productId = coderService.encodeLesson(lesson)
        XCTAssertEqual(productId, "ja.course.3.lesson.6")
    }
    func testDecodeLesson() {
        let productId = "ja.course.3.lesson.4"
        let coderService = ProductIdCoderService()
        let lesson: Lesson = coderService.decodeLesson(productId)!
        XCTAssertEqual(lesson.lang.rawValue, "ja")
        XCTAssertEqual(lesson.courseId, 3)
        XCTAssertEqual(lesson.lessonId, 4)
    }
}

テスト別に開始ボタンを押すか、Cmd+U等でテストを実行。
image.png
通るとこのように表示される。
image.png

XCUITest

アプリを起動し、外から操作するイメージで行われるテスト。
実際のアプリに近い画面操作をテストできるが、画面に出てこない内部情報を扱うのは難しい。

テストクラスの作成

まず、UITestsグループ内にUITestクラスを作成。
image.png
↓選択
image.png
すると以下のような雛形ができる。

import XCTest
class QiitaUITest: XCTestCase {
    override func setUp() {
        // Put setup code here. This method is called before the invocation of each test method in the class.
        // In UI tests it is usually best to stop immediately when a failure occurs.
        continueAfterFailure = false
        // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
        XCUIApplication().launch()
        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
    }
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }
    func testExample() {
        // Use recording to get started writing UI tests.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }
}

XCTestに比べ continueAfterFailureXCUIApplication() が追加されている。
continueAfterFailure = false
どこかで失敗した時にそこでテストを打ち切るという設定。UIテストは多大な時間をかけて流れを追うものなので、そうするのがオススメというもの。
XCUIApplication().launch()
アプリを起動するという操作。

Membershipの登録

XCTestと同じなので割愛。

ライブラリ探索を追加

UITests => Build Settings => Runpath Search Pathsに$(FRAMEWORK_SEARCH_PATHS)を追加
image.png
この方法がスタンダードかは不明だが、

バンドル“appUITests”は、壊れているか必要なリソースがないため読み込めませんでした。 バンドルを再インストールしてください。

とエラーが出る場合に有効。

テストの実行

テストの実行方法自体はXCTestと同じだが、XCUIApplicationXCUIElementを使ってコードからアプリを操作する必要がある。
操作法の一部を紹介します。

class PurchaseLessonUITests: XCTestCase {
    var app: XCUIApplication!
    override func setUp() {
        continueAfterFailure = false
        app = XCUIApplication()
    }
    override func tearDown() {}
    func testPurchase() {
        app.launch()
        addUIInterruptionMonitor(withDescription: "アラート検知") { (alert) -> Bool in
            alert.buttons["OK"].tap()
            return true
        }
        app.buttons["purchaseButton"].tap()
        XCTAssert(XCUIApplication().alerts["お買い上げありがとうございます"].exists)
    }
}

割り込みへの予約

発生タイミングが定かでないものに対してアクションを予約しておける。
アラートを検知した時に即OKボタンを押す例。

        addUIInterruptionMonitor(withDescription: "アラート検知") { (alert) -> Bool in
            alert.buttons["OK"].tap()
            return true
        }

要素を探す

accessibilityIdentifierをアプリケーション内で設定しておくと、

    @IBOutlet weak var purchaseButton: UIButton! {
        didSet {
            purchaseButton.accessibilityIdentifier = "purchaseButton"
        }
    }

テストでこのように探し出してタップなどができる。

        app.buttons["purchaseButton"].tap()

UIによるが、単にタイトル等で探せる場合もある。

        // `exists`を呼び出して`XCTAssert`に放り込めば存在をチェックできる
        XCTAssert(XCUIApplication().alerts["お買い上げありがとうございます"].exists)

XCTestとXCUITestの基本は以上です。

応用:XCUITestで軽めにモックを挟む

テストにおいて、状況設定に応じたコードを走らせたい場合があると思います。
XCUITestはアプリを外から操作するイメージで行われるため、内部の状態把握やコントロールが少し難しいです。
XCUIApplicationlaunchArgumentslaunchEnvironmentを使ってアプリケーション内のコードを切り替える例を紹介します。

Argumentsを使ってテスト用モックを挟む

https://developer.apple.com/documentation/xctest/xcuiapplication/1500477-launcharguments

まず、launchArgumentsにテストである旨を追加。

import XCTest

class UITests: XCTestCase {
    var app: XCUIApplication!
    override func setUp() {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launchArguments.append("UI_TEST") // ← これ
    }
    override func tearDown() {}
    func testHoge() {
        app.launch()
        // Hogeをテストするコード
    }
}

アプリケーション側にテスト用のクラスを追加で用意し、

HogeService.swift
protocol HogeService {
    // ..略
}
class DefaultHogeService { // 本来使用するクラス
    // ..略
}
class TestHogeService { // テスト用クラスを追加
    // ..略
}

例えばインスタンス生成時に必要に応じて選択するように変更する。

func hogeService() -> HogeService {
    if ProcessInfo.processInfo.arguments.contains("UI_TEST") { // テストかを判定
        return TestHogeService()
    } else {
        return DefaultHogeService()
    }
}

ここはアプリ本体への追記になってしまうが、致し方ない。
これでテスト時はTestHogeService()が使われるようになった。

Environmentsを使って分岐を制御する

https://developer.apple.com/documentation/xctest/xcuiapplication/1500427-launchenvironment

次は、HogeServiceが成功・失敗した後それぞれのケースもテストしたい。
launchEnvironmentに命令を書くことにする。

import XCTest

class UITests: XCTestCase {
    var app: XCUIApplication!
    override func setUp() {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launchArguments.append("UI_TEST")
    }
    override func tearDown() {}
    func testHogeSuccess() {
        app.launchEnvironment["HOGE_SERVICE"] = "success" // HogeServiceの挙動を成功と指定
        app.launch()
        // Hogeが成功した後の挙動が正しいかをテストするコード
    }
    func testHogeFailure() {
        app.launchEnvironment["HOGE_SERVICE"] = "failure" // HogeServiceの挙動を失敗と指定
        app.launch()
        // Hogeが失敗した後の挙動が正しいかをテストするコード
    }
}

TestHogeServiceで読み取り、分岐させる。

protocol HogeService {
    func fuga()
}
class DefaultHogeService {
    // ..略
}
class TestHogeService: HogeService {
    func fuga() {
        if ProcessInfo.processInfo.environment["HOGE_SERVICE"] == "success" {
            // 成功時にやること
        } else if ProcessInfo.processInfo.environment["HOGE_SERVICE"] == "failure" {
            // 失敗時にやること
        } else {
            fatalError()
        }
    }
}

以上です。お役に立てば幸いです。

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

Xcodeでnib(xib)のロードが劇遅だった話

はじめに

昔は速かったXcode、数年前からnibのロードが劇的に遅くなった。
1ファイル10秒とかかかる。
なんだそれー!!!

ありえねー
Appleふざけんなー

色々試行錯誤の結果、劇的に改善できたー:kissing_closed_eyes:

やってみた

その方法は

~/Library/Caches/以下を全部削除。
一応Xcodeは再起動した。

のみ。他にも色々試したけど、多分これで決まり。
macOSをセーフブートすると、キャッシュも削除されるって話なので、多分それでもいける。

まとめ

よかったー
作業効率が劇的に良くなった。
でもたまにxcodeは10秒程度フリーズするのは直らず:sob:
まあたまになので、問題無い。

短か。
でもこれで全部。

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

UIViewController�の全面にUITableViewを設置する場合、`UITableView`を基底viewにしてしまえばよさそう

概要

  • UIViewControllerの全面にUITableViewを設置する場合、UITableViewを基底viewにしてしまえばよさそう
  • メリット
    • 管理するviewが一つ減る
    • AutoLayoutの制約が不要になる
  • UIViewControllerviewインスタンス(基底view)がUITableViewのインスタンスになる

スクリーンショット

スクリーンショット 2020-02-14 16.34.53.png

手順

  1. StoryboardにUIViewControllerを設置
  2. 1.で設置したUIViewControllerの基底ViewをStoryboard上で削除する
  3. UITableViewを選択しドラッグアンドドロップで基底ViewとしてUIViewControllerに設置 スクリーンショット 2020-02-14 16.29.15.png

その他

  • 基本的にどんなViewも基底viewに設定できる
    • UITableViewCellUINavigationItemなど、他のView上に設置されるようなViewも設定できる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSでホーム画面に表示されるアプリ名はどこで決まるのか?変更するには?

iOSでホーム画面に表示されるアプリ名はどこで決まるのか?変更するにはどうすればよいか?を整理してまとめました。

確認環境

  • Xcode 11.3.1
  • iOS 13.3

結論

  1. アプリ名は Info.plist ファイルの CFBundleDisplayName で決まる
  2. アプリ名を変更するには、その CFBundleDisplayName の値を変更すればよい
  3. アプリ名をローカライズするには、対象言語毎に InfoPlist.strings を用意する
  4. Build Configuration に応じてアプリ名を切り替えるには、 User-Defined Setting を使うと良い
  5. Build Configuration に応じてアプリ名を切り替えつつローカライズするには、ビルド時にがんばるしかなさそう

ホーム画面のアプリ名はどこで決まるのか?

新規プロジェクトを作成して確認していく。

プロジェクト作成時に Product Name を入力する。
1.png

以下が先ほど入力した Product Name になっていることがわかる。

  • プロジェクトファイル名
  • ターゲット名
  • ターゲット設定の General タブにある Display Name

このうち、最後の Display Name がホーム画面に表示されるアプリ名となる。
2.png

アプリ名を変更するには?

この Display Name を変更してみると、プロジェクトファイルの PRODUCT_NAME が変わる。また、変更前の PRODUCT_NAME はターゲット名 TARGET_NAME であったこと、そして PRODUCT_NAME がアプリケーションファイル (.app) 名になることがわかる。
3.png

アプリケーションファイル名が変わるのは気持ち悪いので PRODUCT_NAME を変更しないようにしたい。そんな時のために Bundle display name という設定がある。これはターゲット設定の Info タブで追加できるので、追加してアプリ名を入力してみる。
4.png
5.png

General タブの Display Name は入力した名前に変わった。ホーム画面上でも変わっている。
6.png

11.png

その正体は Info.plist ファイルの CFBundleDisplayName キー。公式ドキュメントにも ホーム画面で表示される名前 という説明がある。

The user-visible name for the bundle, used by Siri and visible on the iOS Home screen.

7.png

アプリ名をローカライズするには?

アプリ名は Info.plist の CFBundleDisplayName で決まるので、これをローカライズしたい。その方法は公式ドキュメント About Information Property List Files の Localizing Property List Values という章で説明されているとおり、InfoPlist.strings という Info.plist 国際化用の strings リソースファイルを用意する。

Localized values are not stored in the Info.plist file itself. Instead, you store the values for a particular localization in a strings file with the name InfoPlist.strings.

InfoPlist.strings ファイルの作成方法、記述方法、言語別に用意する方法は、通常の文字列リソースファイルをローカライズする場合と同様。プロジェクト設定でローカライズ対象言語を設定する。
8.png

InfoPlist.strings ファイルをプロジェクトに追加する。
9.png

言語別に CFBundleDisplayName を設定する。
10.png

これでデバイスの言語設定によってアプリ名が変わる。
11.png 12.png

Build Configuration に応じてアプリ名を切り替えるには?

ターゲット設定の Build Settings タブにある設定項目は Build Configulation (Debug, Releaseなど) ごとに値を変更できる。ここには User-Defined というユーザー定義の設定を追加することができる。
14.png

ここに、例えば BUNDLE_DISPLAY_NAME のようなキーを追加し、Build Configuration ごとにアプリ名となる値を入力する。
15.png

そして、Info.plist の Bundle display name の部分に、先ほどの BUNDLE_DISPLAY_NAME を参照させる。
16.png

General タブの Display Name は、Run を実行する Build Configuration のものが表示されている。
17.png

アプリ名にもちゃんと反映されている。
18.png

Build Configuration に応じてアプリ名を切り替えつつローカライズするには?

上記の手段では、最終的に InfoPlist.strings の方のアプリ名が優先されてしまい、両立することができない。Xcode に用意されている仕組みでは、これを両立する手段はなさそう。

ただ、開発の要件上、このような対応が必要になったため、次のようにして力技で対応してみた。

まず、全言語の InfoPlist.strings に対して、CFBundleDisplayName_{Build Configuration} というキーで、Build Configuration ごとにアプリ名を定義する。

en.lproj/InfoPlist.strings
"CFBundleDisplayName_Debug" = "HogeDebug"; 
"CFBundleDisplayName_Release" = "HogeRelease"; 
ja.lproj/InfoPlist.strings
"CFBundleDisplayName_Debug" = "ほげデバッグ"; 
"CFBundleDisplayName_Release" = "ほげリリース"; 

そして Build Phases の Run Script で全言語の InfoPlist.strings に対して、ビルド対象の Build Configuration に該当する CFBundleDisplayName_{Build Configuration}CFBundleDisplayName に置換する。例えば Release の場合は `CFBundleDisplayName_Release が置換対象となる。

置換
sed -i "s/\"CFBundleDisplayName_$CONFIGURATION\"/\"CFBundleDisplayName\"/" path/to/InfoPlist.strings 
en.lproj/InfoPlist.strings
"CFBundleDisplayName_Debug" = "HogeDebug"; 
"CFBundleDisplayName" = "HogeRelease"; 
ja.lproj/InfoPlist.strings
"CFBundleDisplayName_Debug" = "ほげデバッグ"; 
"CFBundleDisplayName" = "ほげリリース"; 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIAppearanceの優先度

UIAppearance の基礎

これですべての UILabeltextColor が茶色に変わります。
(現実的には UILabeltextColorUIAppearance で設定するのは稀だと思いますが、あくまでサンプルとして)

UILabel.appearance().textColor = .brown

特定のView上にあるUILabelのtextColorを変える

View階層が1つだけの時

すべての ViewA 上の UILabel を変えたいなら、こんなふうに書けば良いですね。

UILabel.appearance(whenContainedInInstancesOf: [ViewA.self]).textColor = .red

View階層が2つ以上の時

SampleViewController 上の ViewA 上の UILabel だけを変えたい時など、
階層が2つ以上の時は、対象となるViewから親View/ViewControllerを順に配列の要素に加えます。

UILabel.appearance(whenContainedInInstancesOf: [ViewA.self, SampleViewController.self]).textColor = .red

優先度

例えば以下のような階層構造の時

SampleViewController
┣ UILabel
┣ ViewA
┃ ┗ UILabel
┗ ViewB
  ┗ ViewA
    ┗ UILabel

このように設定するとどうなるでしょうか。

UILabel.appearance().textColor = .brown
UILabel.appearance(whenContainedInInstancesOf: [SampleViewController.self]).textColor = .magenta
UILabel.appearance(whenContainedInInstancesOf: [ViewA.self]).textColor = .blue
UILabel.appearance(whenContainedInInstancesOf: [ViewB.self, SampleViewController.self]).textColor = .red

この場合、より外側の要素での指定が優先され、以下のように解決されます。

UIAppearance_demo.png

ViewA 上の UILabelblue にはならず、
SampleViewController 上の UILabelmagenta にする処理が優先されます。

SampleViewController 上の ViewBUILabelred にする処理は、指定した順に解決され問題なく実行されます。

参考

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