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

iOS13の設定アプリにつまずいた

はじめに

みなさんご存知かと思いますが iOS13 は設定アプリのクラッシュ祭りでした:confetti_ball:

設定アプリをクラッシュさせるためのアップデートといっても過言ではありません!!:expressionless:

某マッチングアプリの話

iOS13.2.3 で某マッチングアプリがインストールされていると設定アプリがクラッシュするという話もありました。

参考

【iPhone】設定が開けない、落ちる問題がiOS13.2.3で報告 Pairsの削除で改善する事例も

こちらは iOS13.2.3 から各アプリ情報を先読みするようになり某アプリの plist ファイルの値不正(型不正?)によって設定アプリを開くと数秒後にクラッシュするようになっていたようです。

私も調査のためインストールしてみたところ無事クラッシュしました:expressionless:

こちらは某アプリの値不正が原因でしたが、サードパーティ製のアプリが設定アプリに影響するというのは中々のバグなような気もします。(なんか色々悪いことできそう:smiling_imp:

某アプリのバグはすぐに修正され今はもうクラッシュしません:innocent:

みんな大好きLicensePlistの話

ライセンス表示がめっちゃ楽なみんな大好き LicensePlist も iOS13 の設定アプリでクラッシュするというのに遭遇していました:disappointed_relieved:(たぶん全部 iOS 側のバグ:speak_no_evil:)

13

押したらクラッシュ!!

とりあえずLicensePlistの各バージョンについて動作を確認してみました。Root.plist の記載は下記。

Root.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
    <key>PreferenceSpecifiers</key>
    <array>
        <dict>
            <key>Type</key>
            <string>PSGroupSpecifier</string>
            <key>FooterText</key>
            <string>Copyright</string>
        </dict>
        <dict>
            <key>Type</key>
            <string>PSChildPaneSpecifier</string>
            <key>Title</key>
            <string>Licenses</string>
            <key>File</key>
            <string>com.mono0926.LicensePlist</string>
        </dict>
        <dict>
            <key>Type</key>
            <string>PSTitleValueSpecifier</string>
            <key>DefaultValue</key>
            <string>1.0.0</string>
            <key>Title</key>
            <string>Version</string>
            <key>Key</key>
            <string>sbVersion</string>
        </dict>
    </array>
    <key>StringsTable</key>
    <string>Root</string>
    </dict>
</plist>

LicensePlist 2.6.0

これ 2.6.0: SwiftPM (Swift Package Manager) Support

2.7.1: iOS 13 support の前のバージョンです。(2.7.0入れたかったんですが pod install が失敗するのであきらめました:confused:

iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3
:o: :x: :o: :o: :o:

なんかたまにへん(gif わかりにくい場合この Issue の this behavior に動画がありました。)

260

LicensePlist 2.7.1

これ 2.7.1: iOS 13 support

iOS13 でのクラッシュを受けすぐに有志が動きました。(さすがは人気ライブラリ:clap:
生成される plist ファイルに Type: PSGroupSpecifier を加えたらいけるんじゃないか?とのこと。

iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3
:o: :x: :x: :x: :o:

13.1 と 13.2.2 はライセンス詳細から一覧に戻るとクラッシュ!!!

LicensePlist 2.10.0(2019/12/21時点で最新)

これ 2.10.0: Add --single-page option

iOS13 対応後もクラッシュの報告は続き対応されたのがこのバージョン。その後も有志が調査してどうやら3階層(Licenses -> ライブラリ一覧 -> ライセンス情報)push だとクラッシュするとのことでライブラリ一覧を削除した Licenses -> ライセンス情報の2階層にするオプション --single-page が追加されました。

こんな感じ

setting

--single-page なし

iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3
:o: :x: :o: :x: :o:

--single-page あり

iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3
:o: :x: :o: :o: :o:

クラッシュしていた原因の詳細はわかりませんが、クラッシュログにやたらと SwiftUI とあったのでそのへんなんだと思います...:neutral_face:

このあたりのバグは iOS13.3 で無事修正されたそうです:clap:

133

クラッシュしないけど一度表示して戻ると License を押しても画面遷移しない...(まあクラッシュしないし一回目はちゃんとライセンスがみれるのでOK:innocent:

さいごに

LicensePlist はもう iOS のアップデートを待つしかないと思ってましたが対応されてよかったです:laughing:
(某アプリは今後もクラッシュしないか調査を続行しようと思います:neutral_face:

ぎりぎりセーフ:mask:

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

Firebaseで超簡単にiOSアプリを配布する?

今回はFirebaseのApp Distributionを使って手動でアプリを配布する方法を紹介いたします

準備するもの

  • Firebaseプロジェクトの登録
  • Apple Developer Programの登録
  • 共有するメンバーのデバイスをDeveloperアカウントに登録
  • 共有するメンバーのメールアドレス
  • iOS DistributionタイプのCertificates登録
  • AdHoc用のProvisioning Profile登録

アプリのipaファイルを取得

アプリのアーカイブを生成

まずは、配布するアプリのビルドをipaとしてアーカイブします。既にアーカイブしている場合はOrganizerからアーカイブを選択します。
スクリーンショット 2019-12-21 21.04.18.png

アーカイブからipaファイルを生成

AdHocを選択し、手順にそってipaを作成していきます。Select Certificates&ProfileではiOS DistributionタイプのCertificatesとAdHoc用のProvisioning Profileを選択し作成します。

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

ipaファイルをFirebaseにアップロード

ドラッグ&ドロップまたは参照を押してFirebaseにipaファイルをアップロードします.

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

次に共有するメンバーのメールアドレスを登録します。なお、この共有するメンバーのデバイスはApple Developer ProgramのDevicesに登録しておく必要があります。
スクリーンショット 2019-12-21 21.31.36.png

リリースノートを記入して配信しましたボタンを押すとメンバーにFirebaseからメールが届きます。

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

アプリをデバイスにインストール

Download tha latest buildをタップしてProfileをInstallします。

IMG_0640.jpg

Safariでリンクを開きDownloadをタップします

IMG_0641.jpg

次のようなポップアップダイアログが出るのでInstallを押してデバイスにProfileをインストールしていきます。

IMG_0642.jpg

下記の手順の通り設定に移動しProfileをインストールします。

IMG_0643.jpg

ProfileをインストールするとApp Distributionのアプリが追加させていることが確認できます。

IMG_0646.jpg

アプリをタップすると配信したVersionsの一覧が表示されるので任意のVersionをタップしアプリをダウンロードします。

IMG_0645.jpg

アプリがダウンロードできてるのを確認できましたね!
IMG_0647.jpg

最後に

今回はiOSをアプリを手動で配布する方法について紹介しましたが、FastlaneとFirebaseCLIでアップロードを行うと配布を自動化することができるので、次はFastlaneでの配信方法を紹介したいと思います?

公式: https://firebase.google.com/docs/app-distribution/ios/distribute-console?authuser=2

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

iOSアプリとMac CatalystアプリのApp Store申請手順

Mac CatalystでiOSアプリとMacアプリを同時に作る』という記事でMac Catalyst対応アプリの作り方を紹介しました。本記事はiOSアプリおよびMac Catalyst対応アプリのApp Storeへの申請手順を紹介します。

前提
* Apple Developer Programに登録済みであること
* Certificates, Identifiers & ProfilesでCertificatesを登録済みであること
* Xcodeは Xcode 11 を使用
* App Storeへの申請作業は Safari の使用を推奨

1. App IDの登録

  1. Apple Developerにサインイン

    1. https://developer.apple.com/jp/programs/
    2. Accountをクリック
    3. サインイン
  2. Certificates, Identifiers & Profilesをクリック

    1. Identifiersをクリック
    2. +ボタンをクリック

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

  3. Register a New Identifier

    1. App IDsにチェック

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

    2. Continueをクリック

  4. Register an App ID

    1. Platform: iOS, tvOS, watchOSにチェック
    2. Description: 任意のアプリ名などを入力
    3. Bundle ID: Explicitにチェック。iOSアプリのBundle IDを入力

      Mac Catalyst対応アプリのBundle ID(maccatalyst.から始まる)は登録する必要がありません(登録できない仕様のようです)。

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

    4. Capabilities > Macにチェック

      デフォルトのMac Catalyst対応アプリのBundle IDを使う場合はEditはそのままでOKです。既存のMacアプリをリプレイスするなど、デフォルトのIDを使わない場合はEditで設定できるようです: 参考

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

    5. Continueをクリック

    6. Confirm your App ID > Registerをクリック

2. プロビジョニングプロファイルの作成

iOSアプリ用とMacアプリ用の2点用意します。

  1. Certificates, Identifiers & Profilesをクリック

iOS

  1. Profilesをクリック
  2. +ボタンをクリック

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

  3. Register a New Provisioning Profile

    1. Distribution > App Storeにチェック

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

    2. Continueをクリック

  4. Generate a Provisioning Profile

    1. 先ほど登録したApp IDを選択

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

    2. Continueをクリック

    3. 適切なCertificatesにチェック

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

    4. Continueをクリック

    5. Provisioning Profile Nameに任意の名前を入力

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

    6. Generateをクリック

    7. プロビジョニングプロファイルのダウンロード & WクリックしてPCにインストール

Mac

  1. Profilesをクリック
  2. +ボタンをクリック

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

  3. Register a New Provisioning Profile

    1. Distribution > Mac App Storeにチェック

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

    2. Continueをクリック

  4. Generate a Provisioning Profile

    1. 先ほど登録したApp IDを選択

      App IDはiOSアプリと同じのBundle IDを選択する(maccatalyst.が付いていなくてOK)。

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

    2. Continueをクリック

    3. 適切なCertificatesにチェック

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

    4. Continueをクリック

    5. Provisioning Profile Nameに任意の名前を入力

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

    6. Generateをクリック

    7. プロビジョニングプロファイルのダウンロード(ArchiveをアップロードするときAutomatically manage signingを利用する場合はDL不要かも)

3. App Store Connectでアプリ登録

以下の画像のようにiOSアプリとMacアプリをそれぞれ登録します。
スクリーンショット 2019-12-19 10.15.07.png

  1. https://appstoreconnect.apple.com
  2. サインイン
  3. マイAppをクリック

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

iOS

  1. +ボタンをクリック > 新規Appをクリック

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

  2. 項目を埋めて作成をクリック

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

Mac

  1. +ボタンをクリック > 新規macOS Appsをクリック
  2. 項目を埋めて作成をクリック

4. アプリのアップロード

Automatically manage signingにチェックが入っている状態で話を進めます。マニュアルでやる場合は先ほどDLしたプロビジョニングプロファイルをSigning & Capabilitiesでセットします。

iOS

  1. DevicesをiOS端末実機を選択

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

  2. Product > Archiveをクリック

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

  3. Distribute Appをクリック

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

  4. App Store Connectにチェックを入れ、Nextをクリック

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

  5. Uploadにチェックを入れ、Nextをクリック

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

  6. 全項目にチェックを入れ、Nextをクリック

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

  7. Automatically manage signingにチェックを入れ、Nextをクリック

    *Manually manage signingを使う場合は、先ほどDLしたプロビジョニングプロファイルを指定します。

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

  8. Uploadをクリック

Mac

  1. My Macを選択

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

  2. Product > Archiveをクリック

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

  3. Distribute Appをクリック

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

  4. App Store Connectをチェックを入れ、Nextをクリック

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

  5. Uploadにチェックを入れ、Nextをクリック

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

  6. 項目にチェックを入れ、Nextをクリック

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

  7. Automatically manage signingにチェックを入れ、Nextをクリック

    *Manually manage signingを使う場合は、先ほどDLしたプロビジョニングプロファイルを指定します。

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

  8. Uploadをクリック

審査へ提出

iOS

  1. App Store Connectでストアに掲載する情報を入力
  2. 先ほどアップロードしたビルドを選択
  3. 審査へ提出

Mac

  1. App Store Connectでストアに掲載する情報を入力
  2. 先ほどアップロードしたビルドを選択
  3. 審査へ提出

iOSアプリ、Macアプリ、それぞれ審査され無事通過すれば、それぞれのストアに配信されます。

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

【Swift】Vision.frameworkでカメラ画像の顔認識を行う【iOS】

iOS11より、iOS標準フレームワーク Vision.framework を使うと、顔認識ができるらしいので今更ながら使ってみました。

概要

カメラ画像から顔を検出し、顔部分に矩形を表示します。

試した環境

  • Xcode 11.3
  • iOS 13.2
  • swift 5

実行サンプル

ぱくたそフリー素材で実験

IMG_3057.jpg
ディスプレイ画質の問題のせいもありそうですが、顔にちょっと髪がかかってたりすると少し認識が悪い。

Google画像検索「顔」で実験

IMG_3059.jpg
顔が沢山あっても、アップだと良く認識します。
(画像はぼかしてます

コード説明

VNImageRequestHandler
を利用して、 pixelBuffer から、顔情報を配列取得します。

結果は
VNDetectFaceRectanglesRequest
に非同期で戻されます。
顔情報は VNFaceObservation です。

    /// 顔認識情報の配列取得 (非同期)
    private func getFaceObservations(pixelBuffer: CVPixelBuffer, completion: @escaping (([VNFaceObservation])->())) {
        let request = VNDetectFaceRectanglesRequest { (request, error) in
            guard let results = request.results as? [VNFaceObservation] else {
                completion([])
                return
            }
            completion(results)
        }

        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
        try? handler.perform([request])
    }

pixcelBuffer は カメラから取得した sampleBufferCMSampleBufferGetImageBuffer を使って変換します。
imageView には、 sampleBuffer から取得した生成をセット。

    /// カメラからの映像取得デリゲート
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }
        getFaceObservations(pixelBuffer: pixelBuffer) { [weak self] faceObservations in
            guard let self = self else { return }
            let image = self.getFaceRectsImage(sampleBuffer: sampleBuffer, faceObservations: faceObservations)
            DispatchQueue.main.async { [weak self] in
                self?.previewImageView.image = image
            }
        }
    }

またその際、 VNFaceObservation から正規化された画像の位置が取得できるので、
その情報をもとに、矩形を画像に書き込みます。

        let imageSize = CGSize(width: width, height: height)
        let faseRects = faceObservations.compactMap {
            getUnfoldRect(normalizedRect: $0.boundingBox, targetSize: imageSize)
        }
        faseRects.forEach{ self.drawRect($0, context: newContext) }
    /// 正規化された矩形位置を指定領域に展開
    private func getUnfoldRect(normalizedRect: CGRect, targetSize: CGSize) -> CGRect {
        return CGRect(
            x: normalizedRect.minX * targetSize.width,
            y: normalizedRect.minY * targetSize.height,
            width: normalizedRect.width * targetSize.width,
            height: normalizedRect.height * targetSize.height
        )
    }
    /// コンテキストに矩形を描画
    private func drawRect(_ rect: CGRect, context: CGContext) {
        context.setLineWidth(4.0)
        context.setStrokeColor(UIColor.green.cgColor)
        context.stroke(rect)
    }

コード全体

import UIKit
import AVFoundation
import Vision

class FaceViewController: UIViewController {

    @IBOutlet weak var previewImageView: UIImageView!

    private let avCaptureSession = AVCaptureSession()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupCamera()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        self.avCaptureSession.stopRunning()
    }

    /// カメラのセットアップ
    private func setupCamera() {
        self.avCaptureSession.sessionPreset = .photo

        let device = AVCaptureDevice.default(for: .video)
        let input = try! AVCaptureDeviceInput(device: device!)
        self.avCaptureSession.addInput(input)

        let videoDataOutput = AVCaptureVideoDataOutput()
        videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_32BGRA)]
        videoDataOutput.alwaysDiscardsLateVideoFrames = true
        videoDataOutput.setSampleBufferDelegate(self, queue: .global())

        self.avCaptureSession.addOutput(videoDataOutput)
        self.avCaptureSession.startRunning()
    }

    /// コンテキストに矩形を描画
    private func drawRect(_ rect: CGRect, context: CGContext) {
        context.setLineWidth(4.0)
        context.setStrokeColor(UIColor.green.cgColor)
        context.stroke(rect)
    }

    /// 顔認識情報の配列取得 (非同期)
    private func getFaceObservations(pixelBuffer: CVPixelBuffer, completion: @escaping (([VNFaceObservation])->())) {
        let request = VNDetectFaceRectanglesRequest { (request, error) in
            guard let results = request.results as? [VNFaceObservation] else {
                completion([])
                return
            }
            completion(results)
        }

        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
        try? handler.perform([request])
    }

    /// 正規化された矩形位置を指定領域に展開
    private func getUnfoldRect(normalizedRect: CGRect, targetSize: CGSize) -> CGRect {
        return CGRect(
            x: normalizedRect.minX * targetSize.width,
            y: normalizedRect.minY * targetSize.height,
            width: normalizedRect.width * targetSize.width,
            height: normalizedRect.height * targetSize.height
        )
    }

    /// 顔検出位置に矩形を描画した image を取得
    private func getFaceRectsImage(sampleBuffer :CMSampleBuffer, faceObservations: [VNFaceObservation]) -> UIImage? {

        guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return nil
        }

        CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))

        guard let pixelBufferBaseAddres = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) else {
            CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
            return nil
        }

        let width = CVPixelBufferGetWidth(imageBuffer)
        let height = CVPixelBufferGetHeight(imageBuffer)
        let bitmapInfo = CGBitmapInfo(rawValue:
            (CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
        )

        guard let newContext = CGContext(
            data: pixelBufferBaseAddres,
            width: width,
            height: height,
            bitsPerComponent: 8,
            bytesPerRow: CVPixelBufferGetBytesPerRow(imageBuffer),
            space: CGColorSpaceCreateDeviceRGB(),
            bitmapInfo: bitmapInfo.rawValue
            ) else
        {
            CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
            return nil
        }

        let imageSize = CGSize(width: width, height: height)
        let faseRects = faceObservations.compactMap {
            getUnfoldRect(normalizedRect: $0.boundingBox, targetSize: imageSize)
        }
        faseRects.forEach{ self.drawRect($0, context: newContext) }

        CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))

        guard let imageRef = newContext.makeImage() else {
            return nil
        }
        let image = UIImage(cgImage: imageRef, scale: 1.0, orientation: UIImage.Orientation.right)

        return image
    }
}


extension FaceViewController : AVCaptureVideoDataOutputSampleBufferDelegate{

    /// カメラからの映像取得デリゲート
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            return
        }
        getFaceObservations(pixelBuffer: pixelBuffer) { [weak self] faceObservations in
            guard let self = self else { return }
            let image = self.getFaceRectsImage(sampleBuffer: sampleBuffer, faceObservations: faceObservations)
            DispatchQueue.main.async { [weak self] in
                self?.previewImageView.image = image
            }
        }
    }
}

github

becky3/face_detection: 【Swift】Vision.frameworkでカメラ画像の顔認識を行う【iOS】
https://github.com/becky3/face_detection

参考サイト

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

端末の画面サイズごとにレイアウトを変更できる"Vary for Traits"の使い方

はじめに

いきなりですが、XcodeのVary for Traits という機能をご存知ですか?

スクリーンショット 2019-12-21 13 05 31

InterfaceBuilderの下のバーにある、グレーのボタンで使うことができます。

一体どういうことができる機能かというと、コードの記述なしに端末の画面サイズや回転状況に応じて、AutoLayoutの制約を付け替えたり、制約の数値を変更、色の変更などのレイアウト周りの設定の変更を行うことができます。

それによってiPhoneとiPad間でのレイアウト崩れを防いだり、画面回転時に全く違うレイアウトを表示するといった活用ができます。

今回は、そんなVary for Traits機能を使用する方法を紹介します。

画面サイズの定義を知る

Vary for Traits機能は画面サイズの定義ごとにレイアウト設定を変更するので、使用する前に、まずは画面サイズの定義を知ることが必要不可欠です。
画面サイズの定義は、横幅と縦幅のw(Width)h(Height)、サイズの大小のR(Regular)C(Compact)、の二つの概念の組み合わせでできています。

以下の画像はInterfaceBuilderでiPhone 11 Pro Maxの縦向き状態でViewを表示した際のスクリーンショットです。
左下に、View as: iPhone 11 Pro MAX(wC hR)と書かれています。

スクリーンショット 2019-12-20 17 18 38

iPhone 11 Pro Maxを縦向きにしている状態の画面サイズはwC hR、つまり横幅が小さく、縦幅が大きい端末と定義されていることがわかります。

ちなみに、それぞれの端末がどのように定義されているかは以下の表に起こしたので、参考にしてください

w h
iPhone(縦) C R
4s・SE・8・11 Pro(横) C C
8Plus・11・11 Pro MAX(横) R C
iPad(縦横両方) R R

縦向きのiPhoneのwC hR、横向きのiPhoneがhC、iPadのwR hRなどはよく使うので覚えておくと良いでしょう。

Vary for Traitsを使う

それぞれの端末のサイズ定義がわかったところで実際にVary for Traits機能を使用してみましょう。

準備

例として、まずは画面の中心に横幅300×高さ400の制約を設定したViewを配置します。

スクリーンショット 2019-12-21 12 56 14

その状態で画面を横向きに変更すると、画面自体の高さが400以下になってしまうので、画面から高さ400の制約を設定したViewが見切れて表示されてしまいます。

スクリーンショット 2019-12-21 13 21 28

この状態からVary for Traits機能を使って、画面が横になった場合にレイアウトを変えていきます。

定義したい画面サイズを選択する

早速、Vary for Traitsのボタンをクリックすると、以下の画像の吹き出しが現れます。
WidthとHeightのチェックボックスは、現在表示しているInterfaceBuilderで選択されている画面のサイズ定義で個別に設定したいものにチェックを入れます。

スクリーンショット 2019-12-21 13 25 14

現在表示している画面はiPhone 11 Pro Maxの横向きなので、画面サイズの定義はwR hCです。

両方にチェックを入れるとwR hCの組み合わせのである、8Plus・11・11 Pro MAXの横向きで表示した際に、有効になるレイアウトを設定できます。

Widthのみにチェックを入れた場合はwRが指定されている、横向きの8Plus・11・11 Pro MAXとiPadで表示した際に、有効になるレイアウトを設定できます。

Heightのみにチェックを入れた場合はhCが指定されている、横向き状態のiPhoneで表示した際に、有効になるレイアウトを設定できます。

今回の場合、iPhoneの横向きで表示した際にViewが画面外にはみ出してしまうので、Heightの方にのみチェックを入れます。

スクリーンショット 2019-12-21 13 25 31

これでhCの画面サイズのみで有効になるレイアウトが設定できる状態になりました。

特定の画面サイズでレイアウトを設定する

1.制約の数値を変更する

まずは、Viewの高さの制約を変更してみましょう。
高さの制約の設定を確認すると以下のようになっています。

スクリーンショット 2019-12-21 13 46 11

この中のConstantの左側の+をクリックすると、以下の画像の吹き出しが出ます。
そのままAdd Variationをクリックします。

スクリーンショット 2019-12-21 14 23 47

そうするとConstantの入力欄の下にhCの欄が追加されるので、そこに250を設定してみます。

スクリーンショット 2019-12-21 14 27 11

そうすると、Viewの高さがちゃんと画面内に収まるようになりました。

スクリーンショット 2019-12-21 14 45 45

hCでの数値を変更したので、もちろん縦向きに変更しても影響はありません。

スクリーンショット 2019-12-21 14 50 15

2.制約の有効・無効を設定する

次は横向きの時には、赤いViewの中央揃えではなく、左上に配置するように設定してみましょう。

先ほどのように、View.Center Yの制約を選択し、今度はInstalledの横の+をクリックして、吹き出しのAdd Variationをクリックします。

スクリーンショット 2019-12-21 14 59 04

すると、Installedの下にhC用のチェックボックスが追加されるので、チェックを外すと、制約が無効化されます。

スクリーンショット 2019-12-21 15 00 20

同じようにView.Center Xの制約もhCのInstalledのチェックボックスを追加して、チェックを外しておきましょう。

スクリーンショット 2019-12-21 15 02 15

すると、以下の画像のような状態になるので、ここから新たに制約を設定していきましょう。

スクリーンショット 2019-12-21 15 04 14

今回は左上に配置するので、赤いViewのTopとLeadingに各10ずつの制約を設定します。

スクリーンショット 2019-12-21 15 06 20

設定したView.Leadingの制約の詳細を見てみると、Installedのチェックが外れていて、hCの方のInstalledはチェックが入っています。

つまり、Vary for Traits機能を使用している時に、新たに設定した制約はVary for Traitsボタンをクリックしたときに表示された吹き出しでチェックを入れた画面サイズでのみ有効になるということです。
(今回は、Heightに選択を入れていたので、hC時にのみ有効になる制約ということになります。)

スクリーンショット 2019-12-21 15 11 30

そして、制約の一覧で現在表示している画面サイズで有効な制約はアイコンが明るく表示され、無効な制約はアイコンが暗く表示されるので、それでチェックすることもできます。

スクリーンショット 2019-12-21 15 14 47

この設定が完了すれば、横向きの画面で以下のような表示になります。

スクリーンショット 2019-12-21 15 24 57

3.Viewの背景色の変更を設定する

次に横向きの時に、赤いViewの背景色を別の色にしてみましょう。

今度はViewを選択して、Bacgkroundの横の+をクリックしてAdd Variationをクリックします。
スクリーンショット 2019-12-21 15 30 59

hC用の背景色設定が表示されるので、適当な色を設定すると、横向きの時にのみその色になります。
スクリーンショット 2019-12-21 15 31 17

4.特定のViewを消去・配置する

次は、Viewの上のラベルを設定しましょう。

ラベルには、「300(w)×400(h)」と記載されていますが、横向きの時には、Viewのサイズが300(w)×250(h)なので、「300(w)×250(h)」に変更したいところですが、UILabelのTextは、Vary for Traits機能で差し替えることはできません。

スクリーンショット 2019-12-21 15 38 37

なので、今回の場合は「300(w)×400(h)」のラベルをhCの時に消去して、「300(w)×250(h)」と記載されたラベルをhCの時にのみ表示されるようにすれば望んだ通りの表示になりそうです。

まずは今までの手順と同じように「300(w)×400(h)」ラベルの詳細から、hCInstalledのチェックを外します。

スクリーンショット 2019-12-21 15 43 51

すると、ラベルがViewの上から消去されます。
スクリーンショット 2019-12-21 15 46 10

あとは、新たにラベルを追加します。

スクリーンショット 2019-12-21 15 49 04

制約を追加した時と同じように、新たに追加された「300(w)×250(h)」LabelはhCでのみInstalledになります。

スクリーンショット 2019-12-21 15 50 52

制約と同じようにViewもInstalledかどうかをアイコンの明暗で確認できます。

スクリーンショット 2019-12-21 15 50 01

5.Vary for Traitsを終了する

Done VryingをクリックするとVary for Traitsを終了することができます。

スクリーンショット 2019-12-21 15 17 37

6.確認する

InterfaceBuilderのOrientationを変えて確認してみましょう。
 VaryForTraits

まとめ

Vary for Traits機能を使うことで、縦画面と横画面それぞれに対応したレイアウトを作成する方法を紹介しました。

自分もこの機能を知るまでは、Viewの比率の制約や、制約のPriorityを細かく設定したり、コードで画面サイズや方向で制約を操作して、複数の画面サイズに対応していましたが、この機能を使うことで、コードを使うことなく、制約もシンプルな形で想定している表示を実現できたので、皆さんも使ってみてはいかがでしょうか?

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

Apple Watch で名言表示アプリを作る

 この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。

 こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
 年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。

 弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
 そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
スクリーンショット 2019-12-21 0.42.53.png

 しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
 「遊んでいる時にも時々思い出したいな。。」
 「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
 「エンジニアチームだけずるいな。。」
 (※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)

 そのようなお客様の思いを解決するために、このアプリを作成しました!!

実装

 まず、Xcodeで新しいProjectを作り Watch App を選択します。
スクリーンショット 2019-12-20 23.42.34.png

Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelButtonのみ追加します。

スクリーンショット 2019-12-20 23.50.06.png

 作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
 するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
スクリーンショット 2019-12-20 23.55.57.png

 以上の作業を行うと、以下のようなコードがInterfaceController.swiftに追加されています。

  @IBOutlet weak var maximLabel: WKInterfaceLabel!

    @IBAction func onTapButton() {
    }

 このonTapButtonでボタンが押された際の処理を書いていきます。

 まず、classの先頭で以下のコードを追加します。

    let maxim_words = [
        "プログラミング言語界のノンシリコンシャンプー",
        "食は質より量",
        "俺の辞書の中にカロリーはない",
        "夕方の12時",
        "もうそろそろグルコサミンの時期かな",
        "1の3乗は1だから"
    ]

 これは名言の中でも、自分が選び抜いた精鋭たちになります。
 この名言たちから、"技術を追い求めている人なのかな"、"太っているのかな"、"足腰が弱いのかな" 等の想像ができると思います。
 謎に包まれたFさんのベールも少しずつ剥がれてきましたね!!

 そして、onTapButtonの関数でランダムに名言を選んでLabelに表示する処理を書きます。

    let maximum_word = self.maxim_words.randomElement()
    maximLabel.setText(maximum_word)

これで完成です!!!!!!!!!

View

完成したアプリの起動後画面は以下です。
スクリーンショット 2019-12-21 1.14.50.png

ボタンを押すと次々に名言を吐いてくれます。
スクリーンショット 2019-12-21 0.26.18.png
スクリーンショット 2019-12-21 0.26.32.png

最後に

 これで仕事モードでないOFFの時も名言を思い出すことができます。
 Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。

 自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。

参考記事

初心者向け】はじめてのApple Watchアプリ

 

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

10分で作成!名言表示アプリ for Apple Watch

 この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。

 こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
 年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。

 弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
 そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
スクリーンショット 2019-12-21 0.42.53.png

 しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
 「遊んでいる時にも時々思い出したいな。。」
 「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
 「エンジニアチームだけずるいな。。」
 (※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)

 そのようなお客様の思いを解決するために、このアプリを作成しました!!

実装

 まず、Xcodeで新しいProjectを作り Watch App を選択します。
スクリーンショット 2019-12-20 23.42.34.png

Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelButtonのみ追加します。

スクリーンショット 2019-12-20 23.50.06.png

 作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
 するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
スクリーンショット 2019-12-20 23.55.57.png

 以上の作業を行うと、以下のようなコードがInterfaceController.swiftに追加されています。

  @IBOutlet weak var maximLabel: WKInterfaceLabel!

    @IBAction func onTapButton() {
    }

 このonTapButtonでボタンが押された際の処理を書いていきます。

 まず、classの先頭で以下のコードを追加します。

    let maxim_words = [
        "プログラミング言語界のノンシリコンシャンプー",
        "食は質より量",
        "俺の辞書の中にカロリーはない",
        "夕方の12時",
        "もうそろそろグルコサミンの時期かな",
        "1の3乗は1だから"
    ]

 これは名言の中でも、自分が選び抜いた精鋭たちになります。
 この名言たちから、"技術を追い求めている人なのかな"、"太っているのかな"、"足腰が弱いのかな" 等の想像ができると思います。
 謎に包まれたFさんのベールも少しずつ剥がれてきましたね!!

 そして、onTapButtonの関数でランダムに名言を選んでLabelに表示する処理を書きます。

    let maximum_word = self.maxim_words.randomElement()
    maximLabel.setText(maximum_word)

これで完成です!!!!!!!!!

View

完成したアプリの起動後画面は以下です。
スクリーンショット 2019-12-21 1.14.50.png

ボタンを押すと次々に名言を吐いてくれます。
スクリーンショット 2019-12-21 0.26.18.png
スクリーンショット 2019-12-21 0.26.32.png

最後に

 これで仕事モードでないOFFの時も名言を思い出すことができます。
 Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。

 自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。

参考記事

初心者向け】はじめてのApple Watchアプリ

 

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

[入門] 10分で開発!名言表示アプリ for Apple Watch

 この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。

 こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
 年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。

 弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
 そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
スクリーンショット 2019-12-21 0.42.53.png

 しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
 「遊んでいる時にも時々思い出したいな。。」
 「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
 「エンジニアチームだけずるいな。。」
 (※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)

 そのようなお客様の思いを解決するために、このアプリを作成しました!!

実装

 まず、Xcodeで新しいProjectを作り Watch App を選択します。
スクリーンショット 2019-12-20 23.42.34.png

Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelButtonのみ追加します。

スクリーンショット 2019-12-20 23.50.06.png

 作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
 するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
スクリーンショット 2019-12-20 23.55.57.png

 以上の作業を行うと、以下のようなコードがInterfaceController.swiftに追加されています。

@IBOutlet weak var maximLabel: WKInterfaceLabel!

@IBAction func onTapButton() {
}

 このonTapButtonでボタンが押された際の処理を書いていきます。

 まず、classの先頭で以下のコードを追加します。

let maxim_words = [
    "プログラミング言語界のノンシリコンシャンプー",
    "食は質より量",
    "俺の辞書の中にカロリーはない",
    "夕方の12時",
    "もうそろそろグルコサミンの時期かな",
    "1の3乗は1だから"
]

 これは名言の中でも、自分が選び抜いた精鋭たちになります。
 この名言たちから、"技術を追い求めている人なのかな"、"太っているのかな"、"足腰が弱いのかな" 等の想像ができると思います。
 謎に包まれたFさんのベールも少しずつ剥がれてきましたね!!

 そして、onTapButtonの関数でランダムに名言を選んでLabelに表示する処理を書きます。

@IBAction func onTapButton() {
    let maximum_word = self.maxim_words.randomElement()
    maximLabel.setText(maximum_word)
}

これで完成です!!!!!!!!!

View

完成したアプリの起動後画面は以下です。
スクリーンショット 2019-12-21 1.14.50.png

ボタンを押すと次々に名言を吐いてくれます。
スクリーンショット 2019-12-21 0.26.18.png
スクリーンショット 2019-12-21 0.26.32.png

最後に

 これで仕事モードでないOFFの時も名言を思い出すことができます。
 Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。

 自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。

参考記事

初心者向け】はじめてのApple Watchアプリ

 

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

アプリテスト自動化への旅【AppiumとJestで簡単に試してみよう!】

皆さんはアプリのテスト自動化をしたことがありますか?
おそらく手元で手動テストをし、Excelか何かにまとめられたテスト仕様書にチェックを入れている人もいるのではないでしょうか。

しかし、自動化しようとしても、どこまで自動化ができるのだろうか。。。だとか、こんな表現をテストで実装は出来ないよね?だとか、時間かかりそうだし、今はそういうフェーズじゃない。だとか...そう思っている人は結構いるのではないかと私は思っています。

そんな人たちに、意外とアプリのテスト自動化はイケるぞ!ってのを伝えるために、本記事にて、AppiumJestを組み合わせた自動テストを紹介します。

皆さんの手元で簡単にセットアップできるので、是非お試しください!

なお、今回紹介する内容のソースコードは以下のリポジトリから参照できますので、是非ご利用ください!
https://github.com/minakawa-daiki/AppiumJestSample

目標

今回の記事では、以下が出来ることをゴールにしていきたいと思います。

  • ページがしっかりと表示されている
  • テキストを自動入力できる
  • ボタンが押せる
  • スワイプができる
  • ブラウザアプリを開いて、テスト対象のアプリに戻ってくる
  • スクリーンショットを保存してみる
  • 一連のテストフローを動画として保存する

Appiumについて

公式サイト: https://appium.io/

公式サイトの言葉を借りて、Google翻訳すると

Appiumは、ネイティブ、 ハイブリッド、およびモバイルWebアプリで利用可能なオープンソーステスト自動化フレームワークです。
WebDriverプロトコルを使用してiOS、Android、およびWindowsアプリで動きます。

要するにアプリのテストを自動化できます。使う前は、どこまで自動化できるのだろうか?と疑問でしたが、かなり自動化できる印象です。様々な言語に対応しているので、とても使いやすいです。

また、GUIのデバッグツールなども用意されているので、とても開発者に優しい、素晴らしいプロダクトだと思います。

Appium

画像は https://www.3pillarglobal.com/insights/appium-a-cross-browser-mobile-automation-tool より

Appiumは様々な言語に対応していますが、今回はJest + TypeScriptで記述していきます。
また、ライブラリはwdを使って書かれたサンプルが多いですが、今回はwebdriverioでやっていきます。

下準備

AndroidとiOSの両方のテスト自動化を行なっていきたいため、 React Nativeでサンプルアプリを作成します。
今回使用するバージョンは以下の通りになります。

  • Expo SDK: v36
  • Appium: 1.16.0-beta.3

Appiumは執筆当時、iOS10系周りでバグが出ていたため、betaを使用しています。
https://github.com/appium/appium/issues/13627

iOS Simulatorの準備

テストに使用するiOS Simulatorの準備をまずしましょう。

Xcodeを開いて、使用したいOSバージョンのシミュレーターをまずはインストールします。

image.png

インストールした後、Simulatorの一覧に存在する端末であれば、その端末を利用してテストが実行されます。
存在しない端末だと毎回新しいSimulatorが作られてしまうので、テストが落ちる原因になるので少し注意が必要です。

image.png

今回の例だと、iPhone11 Pro MaxのiOS 13.2という感じです。

Android Emulatorの準備

AndroidのEmulatorも以下の手順に従って準備していきます。

  1. Android Studioで適当なサンプルアプリを開いてAVDマネージャを開きます image.png
  2. Create Virtual Deviceからエミュレータを追加します
  3. Nameの部分を利用するのでメモっておくと良いでしょう

ここで注意点なのですが、Android 6系以下のEmulatorはChromeではないため(実機はChrome)、WebViewを使用しているアプリでは、そもそもテスト自動化の検証端末に使用しない方がいいです。実機を利用しましょう。

参考: https://qiita.com/masakura/items/210261c954256a7879e6

Appium doctorの実行と修正

projectフォルダに移動し、 yarn appium-doctor を実行しましょう。そして、エラーが出た部分を直していきます。

Macユーザーの方は以下の項目でエラーが出る確率が高いと思います。

  • ✖ Xcode is NOT installed!
  • ✖ Carthage was NOT found!
  • ✖ ANDROID_HOME is NOT set!
  • ✖ JAVA_HOME is NOT set!
  • ✖ adb could not be found because ANDROID_HOME is NOT set!
  • ✖ android could not be found because ANDROID_HOME is NOT set!
  • ✖ emulator could not be found because ANDROID_HOME is NOT set!
  • ✖ Bin directory for $JAVA_HOME is not set

まず、 Xcode is NOT installed!https://github.com/nodejs/node-gyp/issues/569#issuecomment-94917337 を参考に、Xcodeをインストールした後、 udo xcode-select -s /Applications/Xcode.app/Contents/Developer を実行しましょう。

Carthage was NOT found!brew install carthage してください。

ANDROID_HOME is NOT set!JAVA_HOME is NOT set! は以下のような感じで環境変数を設定しましょう。

export ANDROID_HOME=$HOME/Library/Android/sdk
export JAVA_HOME=`/usr/libexec/java_home`
export PATH=$JAVA_HOME/bin:$PATH

そして、もう一度 yarn appium-doctor をすると解消しているはずです。

そして今回は、テストの実行状況を録画するため、ffmpegをインストールしておきます

brew install ffmpeg

今回のサンプルアプリの概要

今回使用するサンプルアプリは以下のようなページを持っています。コードの内部実装は詳しく説明しませんが、ソースコードは公開しているので、気になる方はご覧ください。

  • 1ページ目はテキストを入力する画面があります
  • 2ページ目は1ページ目で入力されたテキストが表示されます。ボタンを押すことで内容が変わります
  • 3ページ目は画像が配置されたWebViewで、画像をクリックするとSafariに遷移します
  • 4ページ目は空のページです
  • 5ページ目は動画が配置されたWebViewで、動画をクリックするとSafariに遷移します

hoge.gif

アプリを取得する

GitHubにてapkとappを公開してますが、自分でビルドしたい場合はexpo経由で以下のコマンドを実行してください。

自分でビルドする場合

  • iOSの場合は yarn build:ios
  • Androidの場合は yarn build:android

直接ダウンロードする場合

https://github.com/minakawa-daiki/AppiumJestSample/releases/tag/v1.0

また、生成されたアプリはappフォルダ直下にAppiumJestSample.appAppiumJestSample.apkでそれぞれ配置してください。(AppiumJestSample.app.zipを解凍してご利用ください)

テスト自動化対象端末のconfigを設定する

今回はテストコードを書く上のドライバーとしてwebdriverioを採用していますので、jest-environment-webdriverioというライブラリを使用しています。
https://www.npmjs.com/package/webdriverio
https://www.npmjs.com/package/jest-environment-webdriverio

また、よくAppiumのテストで使用されているドライバーでは wd があります。
https://www.npmjs.com/package/wd

webdriverioを採用した理由は、非同期処理をより綺麗に書ける点もありますが、執筆時点のwdでは動画撮影周りにバグがあったため採用を見送りました。

jest-environment-webdriverioのおかげで、webdriverioの設定を簡単に記述できます。jest.config.js を実際に見てみましょう。

必要な設定はtestEnvironmentjest-environment-webdriverioを指定し、testEnvironmentOptionsに端末情報を記述するだけです。

iOSでは以下のような設定になります。

jest.config.js
{
  port: 4723,
  capabilities: {
    platformName: "iOS",
    platformVersion: "11.3",
    deviceName: "iPhone X",
    automationName: "XCUITest",
    wdaLocalPort: 8100,
    nativeWebTap: true,
    app: "./app/AppiumJestSample.app"
  }
}
  • portはAppiumが起動しているポートを指定します
  • platformVersionはiOSのバージョンです
  • deviceNameはiOS Simulatorに存在する端末を指定します
  • automationNameは実際に使用するUIテストフレームワークを指定します
  • wdaLocalPortは並列化するときに違うポート番号を指定します
  • appはiOSのアプリケーションのpathを指定します

Androidはこんな感じです。

jest.config.js
{
  port: 4723,
  capabilities: {
    platformName: "Android",
    deviceName: "Android Emulator",
    automationName: "Appium",
    avd: "Pixel_3_API_29",
    systemPort: 8200,
    nativeWebTap: true,
    app: "./app/AppiumJestSample.apk"
  }
}
  • deviceNameはエミュレータを使用する場合はAndroid Emulatorを指定します
  • avdはエミュレータのnameのスペースを_で埋めたものを記述します
  • systemPortは並列化するときに違うポート番号を指定します

また、実機を接続してる場合は、以下のような設定で動かせます。

jest.config.js
{
  port: 4723,
  capabilities: {
    platformName: "Android",
    systemPort: 8201,
    app: "./app/AppiumJestSample.apk"
  }
}

自動テストを動かす

初回は自動化するためのアプリをインストールするため、時間がかかります。テストが失敗することが多々あるので、その場合は再度実行しましょう。

テストを実行する前にバックグラウンドで yarn appium を実行し、Appiumサーバーを立ち上げておきましょう。

その上で、テストの実行はyarn testで動作します。デフォルトはiOSが起動します。
明示的にOSを指定したい場合はそれぞれyarn test:android, yarn test:iosを実行してください。

テストが開始されると、シミュレータやエミュレータが起動し、テストが行われます。
全てのテストが終わったら、tests/screenshotsにテスト途中のスクショや全体の動画が以下のような感じで保存されていると思います。

image.png

hoge.gif

Appiumのスタイルはjestなどのテストフレームワークで表示チェックを担保するのと、スクリーンショットや動画なのでアプリの全体的な動きのテストを担保することで自動化していくのが良いと思っています。

無理に全てテストコードで担保しようとせず、スクリーンショットや動画も積極的に使っていきましょう。

テストコードの説明

まず、ReactNativeとNativeで書かれたコードのテストコードは結構違う感じになることを念頭に置いておいてください。
また、今回はほんの一部しか機能を紹介しないので興味を持った方はドキュメントを見てみると良いでしょう。

ドキュメントの例: http://appium.io/docs/en/commands/element/find-element/

それに加え、テストコードを書くときにはアプリ内のそれぞれのElementに対してaccessibilityLabelを振っておくと要素を特定しやすくなるのでオススメです。

動画を撮影する

index.test.ts
beforeAll(async () => {
  try {
    await browser.startRecordingScreen({ videoType: 'mpeg4' });
  } catch (e) {}
});

afterAll(async () => {
  try {
    const movie = await browser.stopRecordingScreen();
    const decode = Buffer.from(movie, 'base64');
    fs.writeFileSync(
      `${baseResultPath}/result.mp4`,
      decode
    );
  } catch (e) {}
});

動画は簡単に撮れます。browser.startRecordingScreen({ videoType: 'mpeg4' })で撮影を開始した後、browser.stopRecordingScreen()で結果を保持し、後はデコードして書き出すだけです。

ffmpegをインストールしておくのを忘れずに。

「1ページ目が存在している」かどうかのテスト

index.test.ts
test('1ページ目が存在している', async () => {
  expect((await browser.$('~slide1')).elementId).not.toBeUndefined();
  await browser.saveScreenshot(`${baseResultPath}/page1.png`);
});

browser.$('~slide1')).elementIdundefinedとならない場合、そのページは確かに存在している。という風にしてテストを担保しています。これは各ページで行っています。

また、browser.saveScreenshot('${baseResultPath}/page1.png')でスクリーンショットを保存しています。簡単ですね!

「1ページ目で入力された内容が2ページ目で表示されている」かどうかのテスト

index.test.ts
test('1ページ目で入力された内容が2ページ目で表示されている', async () => {
  const textInputElement = await browser.$('~TextInput');
  await textInputElement.addValue('おりばー');
  await browser.pause(2000);
  await browser.hideKeyboard();
  await swipe('left');
  // 2ページ目の存在確認
  expect((await browser.$('~slide2')).elementId).not.toBeUndefined();

  // https://github.com/appium/appium/issues/13288 をみる限り
  // iOSのバージョンによってはgetText()がaccessibilityLabelと同じ文字列を返すバグがある
  // なのでaccessibilityLabelの値をinputTextと一緒にしている
  // 最新のiOSとAndroidは問題ない
  const textInputResultElement = await browser.$(`~${inputText}`);
  expect(await textInputResultElement.getText()).toBe(inputText);
});

async function swipe(direction: 'left' | 'right') {
  if(platformName === 'iOS') {
    // iOSはswipeを呼び出せるのでそれを使う
    await browser.execute('mobile: swipe', { direction });
  } else if(platformName === 'Android') {
    // Androidは代わりにflickを利用する
    const rootElement = await browser.$('//*');
    const x = direction === 'left' ? -1000 : 1000;
    await browser.touchFlick(x, 0, rootElement.elementId, 100);

    // 現状ReactNativeはこれだとSwipeできない?(AndroidのNativeだと動くことを確認)
    // await browser.touchPerform([
    //   { action: 'press', options: { x: 1000, y: windowSize.height / 2 } },
    //   { action: 'moveTo', options: { x: 100, y: windowSize.height / 2 } },
    //   { action: 'release' },
    // ]);
  }
}

ここでは、1ページにて、文字を入力し、それが2ページ目で入力された内容が表示されているかどうかのテストを行っています。

textInputElement.addValue('おりばー')とすることで、textInputElementに対して「おりばー」という文字が入力されます。

また、browser.hideKeyboard()でキーボードを閉じることができるのも意外と盲点だったりします。

swipe('left')はスワイプを行なっています。iOSとAndroidでスワイプの仕方が様々あるので、気になる方はコードを読んでみてください。

browser.$(~${inputText})ここの部分はコメントにも書いているのですが、iOSの特定のバージョンにおいて、getText()accessibilityLabelで指定した内容になってしまうというバグが存在しています。

追記: Xcodeのバージョンも大きく関係しているらしいです。詳しくはコメント欄をご覧ください。

ですので、その応急処置として、accessibilityLabelにそのまま値を入れるという実装になってしまっています。

「2ページ目のボタンをタップすると内容が変化する」かどうかのテスト

index.test.ts
test('2ページ目のボタンをタップすると内容が変化する', async () => {
  await browser.saveScreenshot(`${baseResultPath}/page2-1.png`);
  const textChangeButtonElement = await browser.$('~textChangeButton');
  await textChangeButtonElement.click();
  const textInputResultElement = await browser.$('~タップされたよ!');
  await browser.saveScreenshot(`${baseResultPath}/page2-2.png`);
  expect(await textInputResultElement.getText()).not.toBe(inputText);
});

ここではtextChangeButtonElement.click()でボタンをタップして、文字の内容が変わっていることをテストしています。

「3ページ目が存在している」かどうかのテスト

index.test.ts
test('3ページ目が存在している', async () => {
  await swipe('left');
  expect((await browser.$('~slide3')).elementId).not.toBeUndefined();
  await browser.saveScreenshot(`${baseResultPath}/page3.png`);

  // 画像をクリックして戻ってこれることを確認
  await browser.pause(2000);
  const imageWrap = await browser.$("~imageWrap");
  await imageWrap.click();
  await browser.pause(5000);
  // アプリに戻ってくるための処理
  if (platformName === 'Android') {
    await browser.pressKeyCode(4);
  } else {
    await browser.execute('mobile: activateApp', {
      bundleId: app.expo.ios.bundleIdentifier,
    });
  }
});

3ページ目では存在確認に加え、「画像をクリックして、別アプリに行った後、戻ってこれることを確認」しています。
別アプリに遷移してしまった後はAndroidとiOSとで指定の仕方は変わるのですが、それぞれ以下のコマンドで可能になっています。

Androidの場合
browser.pressKeyCode(4)

iOSの場合
browser.execute('mobile: activateApp', {bundleId:app.expo.ios.bundleIdentifier,})

簡単ですね!

「5ページ目が存在している」かどうかのテスト

index.test.ts
test('5ページ目が存在している', async () => {
  await swipe('left');
  expect((await browser.$('~slide5')).elementId).not.toBeUndefined();

  // 動画が再生されていることをスクショで判別する
  await browser.saveScreenshot(`${baseResultPath}/page5-1.png`);
  await browser.pause(3000);
  await browser.saveScreenshot(`${baseResultPath}/page5-2.png`);
});

5ページ目では動画が自動再生されますので、スクリーンショットを撮って、しっかりと再生されているかをチェックしています。

こんな感じでスクリーンショットを撮りつつ、テストを実行していく流れになります。

余談

今回のテストの中で、Androidの場合はChromeDriverを利用するのですが、ChromeDriverはChromeのバージョンと強く紐づいています。
ですので、本来であればAndroidのChromeバージョンによって使い分けなければいけないのですが、Appiumの起動時にchromedriver_autodownloadと指定することで、Appium側でそこをよしなにやってくれています。すごいですね!

並列化について

並列化はwebdriverioが提供しているものもありますが、それを利用して並列化すると特定の端末のテストが落ちたときに全てのテストが失敗してしまうことになるのでオススメはあまりしないです。

なので、自分で別プロセスでそれぞれ実行して並列化させることをオススメします。

並列化するときの注意点はiOSの場合はwdaLocalPortを、Androidの場合はsystemPortをずらして実行することです。

終わりに

アプリのテスト自動化はAppiumのおかげで想像よりかは書きやすいです。
しかし、バグも多く潜んでいるので、根気よく戦っていく形にはやっぱりなってしまいます。
無理して全てを自動化するのではなく、今手動でやっているテストを少しでも簡単にしようという目線で、少しずつ自動化していくことが良いのではないかと思います。是非皆さんもAppiumに触れてみてください。

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