- 投稿日:2019-12-21T23:15:15+09:00
iOS13の設定アプリにつまずいた
はじめに
みなさんご存知かと思いますが iOS13 は設定アプリのクラッシュ祭りでした
設定アプリをクラッシュさせるためのアップデートといっても過言ではありません!!某マッチングアプリの話
iOS13.2.3 で某マッチングアプリがインストールされていると設定アプリがクラッシュするという話もありました。
参考
【iPhone】設定が開けない、落ちる問題がiOS13.2.3で報告 Pairsの削除で改善する事例も
こちらは iOS13.2.3 から各アプリ情報を先読みするようになり某アプリの plist ファイルの値不正(型不正?)によって設定アプリを開くと数秒後にクラッシュするようになっていたようです。
私も調査のためインストールしてみたところ無事クラッシュしました
こちらは某アプリの値不正が原因でしたが、サードパーティ製のアプリが設定アプリに影響するというのは中々のバグなような気もします。(なんか色々悪いことできそう)
某アプリのバグはすぐに修正され今はもうクラッシュしませんみんな大好きLicensePlistの話
ライセンス表示がめっちゃ楽なみんな大好き LicensePlist も iOS13 の設定アプリでクラッシュするというのに遭遇していました
(たぶん全部 iOS 側のバグ
)
押したらクラッシュ!!
とりあえず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 が失敗するのであきらめました)
iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3 なんかたまにへん(gif わかりにくい場合この Issue の this behavior に動画がありました。)
LicensePlist 2.7.1
これ 2.7.1: iOS 13 support
iOS13 でのクラッシュを受けすぐに有志が動きました。(さすがは人気ライブラリ)
生成される plist ファイルにType: PSGroupSpecifier
を加えたらいけるんじゃないか?とのこと。
iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3 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
が追加されました。
こんな感じ
--single-page
なし
iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3
--single-page
あり
iOS12.4.1 iOS13.0 iOS13.1 iOS13.2.2 iOS13.3 クラッシュしていた原因の詳細はわかりませんが、クラッシュログにやたらと SwiftUI とあったのでそのへんなんだと思います...
このあたりのバグは iOS13.3 で無事修正されたそうです
クラッシュしないけど一度表示して戻ると License を押しても画面遷移しない...(まあクラッシュしないし一回目はちゃんとライセンスがみれるのでOK
)
さいごに
LicensePlist はもう iOS のアップデートを待つしかないと思ってましたが対応されてよかったです
(某アプリは今後もクラッシュしないか調査を続行しようと思います)
ぎりぎりセーフ
- 投稿日:2019-12-21T22:18:01+09:00
Firebaseで超簡単にiOSアプリを配布する?
今回はFirebaseのApp Distributionを使って手動でアプリを配布する方法を紹介いたします
準備するもの
- Firebaseプロジェクトの登録
- Apple Developer Programの登録
- 共有するメンバーのデバイスをDeveloperアカウントに登録
- 共有するメンバーのメールアドレス
- iOS DistributionタイプのCertificates登録
- AdHoc用のProvisioning Profile登録
アプリのipaファイルを取得
アプリのアーカイブを生成
まずは、配布するアプリのビルドをipaとしてアーカイブします。既にアーカイブしている場合はOrganizerからアーカイブを選択します。
アーカイブからipaファイルを生成
AdHocを選択し、手順にそってipaを作成していきます。Select Certificates&ProfileではiOS DistributionタイプのCertificatesとAdHoc用のProvisioning Profileを選択し作成します。
ipaファイルをFirebaseにアップロード
ドラッグ&ドロップまたは
参照
を押してFirebaseにipaファイルをアップロードします.次に共有するメンバーのメールアドレスを登録します。なお、この共有するメンバーのデバイスはApple Developer ProgramのDevicesに登録しておく必要があります。
リリースノートを記入して
配信しました
ボタンを押すとメンバーにFirebaseからメールが届きます。アプリをデバイスにインストール
Download tha latest build
をタップしてProfileをInstallします。Safariでリンクを開き
Download
をタップします次のようなポップアップダイアログが出るのでInstallを押してデバイスにProfileをインストールしていきます。
下記の手順の通り
設定
に移動しProfileをインストールします。ProfileをインストールするとApp Distributionのアプリが追加させていることが確認できます。
アプリをタップすると配信したVersionsの一覧が表示されるので任意のVersionをタップしアプリをダウンロードします。
最後に
今回はiOSをアプリを手動で配布する方法について紹介しましたが、FastlaneとFirebaseCLIでアップロードを行うと配布を自動化することができるので、次はFastlaneでの配信方法を紹介したいと思います?
公式: https://firebase.google.com/docs/app-distribution/ios/distribute-console?authuser=2
- 投稿日:2019-12-21T22:13:13+09:00
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の登録
Apple Developerにサインイン
- https://developer.apple.com/jp/programs/
- Accountをクリック
- サインイン
Certificates, Identifiers & Profilesをクリック
Register a New Identifier
Register an App ID
- Platform: iOS, tvOS, watchOSにチェック
- Description: 任意のアプリ名などを入力
Bundle ID: Explicitにチェック。iOSアプリのBundle IDを入力
Mac Catalyst対応アプリのBundle ID(
maccatalyst.
から始まる)は登録する必要がありません(登録できない仕様のようです)。Capabilities > Macにチェック
デフォルトのMac Catalyst対応アプリのBundle IDを使う場合はEditはそのままでOKです。既存のMacアプリをリプレイスするなど、デフォルトのIDを使わない場合はEditで設定できるようです: 参考
Continueをクリック
Confirm your App ID > Registerをクリック
2. プロビジョニングプロファイルの作成
iOSアプリ用とMacアプリ用の2点用意します。
- Certificates, Identifiers & Profilesをクリック
iOS
- Profilesをクリック
+ボタンをクリック
Register a New Provisioning Profile
Generate a Provisioning Profile
Mac
- Profilesをクリック
+ボタンをクリック
Register a New Provisioning Profile
Generate a Provisioning Profile
3. App Store Connectでアプリ登録
以下の画像のようにiOSアプリとMacアプリをそれぞれ登録します。
- https://appstoreconnect.apple.com
- サインイン
マイAppをクリック
iOS
Mac
- +ボタンをクリック > 新規macOS Appsをクリック
- 項目を埋めて作成をクリック
4. アプリのアップロード
Automatically manage signingにチェックが入っている状態で話を進めます。マニュアルでやる場合は先ほどDLしたプロビジョニングプロファイルをSigning & Capabilitiesでセットします。
iOS
DevicesをiOS端末実機を選択
Product > Archiveをクリック
Distribute Appをクリック
App Store Connectにチェックを入れ、Nextをクリック
Uploadにチェックを入れ、Nextをクリック
全項目にチェックを入れ、Nextをクリック
Automatically manage signingにチェックを入れ、Nextをクリック
*Manually manage signingを使う場合は、先ほどDLしたプロビジョニングプロファイルを指定します。
Uploadをクリック
Mac
My Macを選択
Product > Archiveをクリック
Distribute Appをクリック
App Store Connectをチェックを入れ、Nextをクリック
Uploadにチェックを入れ、Nextをクリック
項目にチェックを入れ、Nextをクリック
Automatically manage signingにチェックを入れ、Nextをクリック
*Manually manage signingを使う場合は、先ほどDLしたプロビジョニングプロファイルを指定します。
Uploadをクリック
審査へ提出
iOS
- App Store Connectでストアに掲載する情報を入力
- 先ほどアップロードしたビルドを選択
- 審査へ提出
Mac
- App Store Connectでストアに掲載する情報を入力
- 先ほどアップロードしたビルドを選択
- 審査へ提出
iOSアプリ、Macアプリ、それぞれ審査され無事通過すれば、それぞれのストアに配信されます。
- 投稿日:2019-12-21T17:06:27+09:00
【Swift】Vision.frameworkでカメラ画像の顔認識を行う【iOS】
iOS11より、iOS標準フレームワーク
Vision.framework
を使うと、顔認識ができるらしいので今更ながら使ってみました。概要
カメラ画像から顔を検出し、顔部分に矩形を表示します。
試した環境
- Xcode 11.3
- iOS 13.2
- swift 5
実行サンプル
ぱくたそフリー素材で実験
ディスプレイ画質の問題のせいもありそうですが、顔にちょっと髪がかかってたりすると少し認識が悪い。Google画像検索「顔」で実験
顔が沢山あっても、アップだと良く認識します。
(画像はぼかしてますコード説明
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
は カメラから取得したsampleBuffer
をCMSampleBufferGetImageBuffer
を使って変換します。
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参考サイト
[iOS 11] 画像解析フレームワークVisionで顔認識を試した結果
https://dev.classmethod.jp/smartphone/iphone/ios-11-vision/[iOS]リアルタイムで画像処理をする時の カメラの内部パラメーターの取得方法 - Qiita
https://qiita.com/shirahama_x/items/421d0d343d9629e66794
- 投稿日:2019-12-21T16:08:08+09:00
端末の画面サイズごとにレイアウトを変更できる"Vary for Traits"の使い方
はじめに
いきなりですが、XcodeのVary for Traits という機能をご存知ですか?
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)
と書かれています。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を配置します。
その状態で画面を横向きに変更すると、画面自体の高さが400以下になってしまうので、画面から高さ400の制約を設定したViewが見切れて表示されてしまいます。
この状態からVary for Traits機能を使って、画面が横になった場合にレイアウトを変えていきます。
定義したい画面サイズを選択する
早速、Vary for Traitsのボタンをクリックすると、以下の画像の吹き出しが現れます。
WidthとHeightのチェックボックスは、現在表示しているInterfaceBuilderで選択されている画面のサイズ定義で個別に設定したいものにチェックを入れます。現在表示している画面は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の方にのみチェックを入れます。
これで
hC
の画面サイズのみで有効になるレイアウトが設定できる状態になりました。特定の画面サイズでレイアウトを設定する
1.制約の数値を変更する
まずは、Viewの高さの制約を変更してみましょう。
高さの制約の設定を確認すると以下のようになっています。この中の
Constant
の左側の+をクリックすると、以下の画像の吹き出しが出ます。
そのままAdd Variation
をクリックします。そうするとConstantの入力欄の下に
hC
の欄が追加されるので、そこに250を設定してみます。そうすると、Viewの高さがちゃんと画面内に収まるようになりました。
hC
での数値を変更したので、もちろん縦向きに変更しても影響はありません。2.制約の有効・無効を設定する
次は横向きの時には、赤いViewの中央揃えではなく、左上に配置するように設定してみましょう。
先ほどのように、
View.Center Y
の制約を選択し、今度はInstalled
の横の+をクリックして、吹き出しのAdd Variation
をクリックします。すると、Installedの下に
hC
用のチェックボックスが追加されるので、チェックを外すと、制約が無効化されます。同じように
View.Center X
の制約もhC
のInstalledのチェックボックスを追加して、チェックを外しておきましょう。すると、以下の画像のような状態になるので、ここから新たに制約を設定していきましょう。
今回は左上に配置するので、赤いViewのTopとLeadingに各10ずつの制約を設定します。
設定した
View.Leading
の制約の詳細を見てみると、Installed
のチェックが外れていて、hC
の方のInstalled
はチェックが入っています。つまり、Vary for Traits機能を使用している時に、新たに設定した制約はVary for Traitsボタンをクリックしたときに表示された吹き出しでチェックを入れた画面サイズでのみ有効になるということです。
(今回は、Heightに選択を入れていたので、hC
時にのみ有効になる制約ということになります。)そして、制約の一覧で現在表示している画面サイズで有効な制約はアイコンが明るく表示され、無効な制約はアイコンが暗く表示されるので、それでチェックすることもできます。
この設定が完了すれば、横向きの画面で以下のような表示になります。
3.Viewの背景色の変更を設定する
次に横向きの時に、赤いViewの背景色を別の色にしてみましょう。
今度はViewを選択して、Bacgkroundの横の+をクリックして
Add Variation
をクリックします。
hC
用の背景色設定が表示されるので、適当な色を設定すると、横向きの時にのみその色になります。
4.特定のViewを消去・配置する
次は、Viewの上のラベルを設定しましょう。
ラベルには、「300(w)×400(h)」と記載されていますが、横向きの時には、Viewのサイズが300(w)×250(h)なので、「300(w)×250(h)」に変更したいところですが、UILabelのTextは、Vary for Traits機能で差し替えることはできません。
なので、今回の場合は「300(w)×400(h)」のラベルを
hC
の時に消去して、「300(w)×250(h)」と記載されたラベルをhC
の時にのみ表示されるようにすれば望んだ通りの表示になりそうです。まずは今までの手順と同じように「300(w)×400(h)」ラベルの詳細から、
hC
のInstalled
のチェックを外します。あとは、新たにラベルを追加します。
制約を追加した時と同じように、新たに追加された「300(w)×250(h)」Labelは
hC
でのみInstalledになります。制約と同じようにViewも
Installed
かどうかをアイコンの明暗で確認できます。5.Vary for Traitsを終了する
Done VryingをクリックするとVary for Traitsを終了することができます。
6.確認する
InterfaceBuilderのOrientationを変えて確認してみましょう。
まとめ
Vary for Traits機能を使うことで、縦画面と横画面それぞれに対応したレイアウトを作成する方法を紹介しました。
自分もこの機能を知るまでは、Viewの比率の制約や、制約のPriorityを細かく設定したり、コードで画面サイズや方向で制約を操作して、複数の画面サイズに対応していましたが、この機能を使うことで、コードを使うことなく、制約もシンプルな形で想定している表示を実現できたので、皆さんも使ってみてはいかがでしょうか?
- 投稿日:2019-12-21T08:55:26+09:00
Apple Watch で名言表示アプリを作る
この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。
こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
「遊んでいる時にも時々思い出したいな。。」
「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
「エンジニアチームだけずるいな。。」
(※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)そのようなお客様の思いを解決するために、このアプリを作成しました!!
実装
まず、Xcodeで新しいProjectを作り Watch App を選択します。
Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelとButtonのみ追加します。作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
以上の作業を行うと、以下のようなコードが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
最後に
これで仕事モードでないOFFの時も名言を思い出すことができます。
Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。
参考記事
- 投稿日:2019-12-21T08:55:26+09:00
10分で作成!名言表示アプリ for Apple Watch
この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。
こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
「遊んでいる時にも時々思い出したいな。。」
「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
「エンジニアチームだけずるいな。。」
(※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)そのようなお客様の思いを解決するために、このアプリを作成しました!!
実装
まず、Xcodeで新しいProjectを作り Watch App を選択します。
Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelとButtonのみ追加します。作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
以上の作業を行うと、以下のようなコードが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
最後に
これで仕事モードでないOFFの時も名言を思い出すことができます。
Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。
参考記事
- 投稿日:2019-12-21T08:55:26+09:00
[入門] 10分で開発!名言表示アプリ for Apple Watch
この記事は 、ユアマイスター Advent Calendar 2019 の21日目の記事です。
こんにちは!ユアマイスターでエンジニアインターンをしている土佐鰹です。
年末にPRIDE、K1などで盛り上がっていた時代から10年飛んで、RIZINが年末の格闘技放送を定例化してきて嬉しい限りでございます。弊社には格闘技のように、長く愛されるサービスを作りたいエンジニアがたくさんいます。その中の1人に、息を吐くように名言を生み出す方がおられます。(以降、Fさん)
そんなFさんが吐き出す名言を忘れたくないと、過去に弊社では全精力をかけて名言を返してくれるSlackBotを作成しました。
しかし、Slackを使うのは仕事モードの時がほとんどですよね。。。
「遊んでいる時にも時々思い出したいな。。」
「ジムで自分を追い込んで疲れてる時に癒されたいな。。」
「エンジニアチームだけずるいな。。」
(※弊社ではエンジニアチーム以外はSlack以外のツールをメインで使っています)そのようなお客様の思いを解決するために、このアプリを作成しました!!
実装
まず、Xcodeで新しいProjectを作り Watch App を選択します。
Inerface.storyboardでAppleWatchのUIを作っています。
今回はsimple is the bestの精神で、LabelとButtonのみ追加します。作成したLabelから controlを押しながらInterfaceControllerにスワイプしていきます。
するとLabelのNameを設定する画面が出てきますので適当に入力します。今回は名言表示部分なのでmaximumLabelとしました。(Buttonに関しても同様。)
以上の作業を行うと、以下のようなコードが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
最後に
これで仕事モードでないOFFの時も名言を思い出すことができます。
Fさんも自分の名言がみんなに見てもらえて嬉しいことこの上ないはず。自分はARKitでARの開発を行なっているのですが、AppleWatchのフレームワークもさほど変わらないことがわかったので、最近買ったAppleWatchで暇があったらアプリを作ってみようかなと思いました。
参考記事
- 投稿日:2019-12-21T06:44:41+09:00
アプリテスト自動化への旅【AppiumとJestで簡単に試してみよう!】
皆さんはアプリのテスト自動化をしたことがありますか?
おそらく手元で手動テストをし、Excelか何かにまとめられたテスト仕様書にチェックを入れている人もいるのではないでしょうか。しかし、自動化しようとしても、どこまで自動化ができるのだろうか。。。だとか、こんな表現をテストで実装は出来ないよね?だとか、時間かかりそうだし、今はそういうフェーズじゃない。だとか...そう思っている人は結構いるのではないかと私は思っています。
そんな人たちに、意外とアプリのテスト自動化はイケるぞ!ってのを伝えるために、本記事にて、AppiumとJestを組み合わせた自動テストを紹介します。
皆さんの手元で簡単にセットアップできるので、是非お試しください!
なお、今回紹介する内容のソースコードは以下のリポジトリから参照できますので、是非ご利用ください!
https://github.com/minakawa-daiki/AppiumJestSample目標
今回の記事では、以下が出来ることをゴールにしていきたいと思います。
- ページがしっかりと表示されている
- テキストを自動入力できる
- ボタンが押せる
- スワイプができる
- ブラウザアプリを開いて、テスト対象のアプリに戻ってくる
- スクリーンショットを保存してみる
- 一連のテストフローを動画として保存する
Appiumについて
公式サイト: https://appium.io/
公式サイトの言葉を借りて、Google翻訳すると
Appiumは、ネイティブ、 ハイブリッド、およびモバイルWebアプリで利用可能なオープンソーステスト自動化フレームワークです。
WebDriverプロトコルを使用してiOS、Android、およびWindowsアプリで動きます。要するにアプリのテストを自動化できます。使う前は、どこまで自動化できるのだろうか?と疑問でしたが、かなり自動化できる印象です。様々な言語に対応しているので、とても使いやすいです。
また、GUIのデバッグツールなども用意されているので、とても開発者に優しい、素晴らしいプロダクトだと思います。
画像は 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/13627iOS Simulatorの準備
テストに使用するiOS Simulatorの準備をまずしましょう。
Xcodeを開いて、使用したいOSバージョンのシミュレーターをまずはインストールします。
インストールした後、Simulatorの一覧に存在する端末であれば、その端末を利用してテストが実行されます。
存在しない端末だと毎回新しいSimulatorが作られてしまうので、テストが落ちる原因になるので少し注意が必要です。今回の例だと、iPhone11 Pro MaxのiOS 13.2という感じです。
Android Emulatorの準備
AndroidのEmulatorも以下の手順に従って準備していきます。
- Android Studioで適当なサンプルアプリを開いてAVDマネージャを開きます
![]()
- Create Virtual Deviceからエミュレータを追加します
- 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に遷移します
アプリを取得する
GitHubにてapkとappを公開してますが、自分でビルドしたい場合はexpo経由で以下のコマンドを実行してください。
自分でビルドする場合
- iOSの場合は
yarn build:ios
- Androidの場合は
yarn build:android
直接ダウンロードする場合
https://github.com/minakawa-daiki/AppiumJestSample/releases/tag/v1.0
また、生成されたアプリは
app
フォルダ直下にAppiumJestSample.app
とAppiumJestSample.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
を実際に見てみましょう。必要な設定は
testEnvironment
にjest-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
にテスト途中のスクショや全体の動画が以下のような感じで保存されていると思います。Appiumのスタイルはjestなどのテストフレームワークで表示チェックを担保するのと、スクリーンショットや動画なのでアプリの全体的な動きのテストを担保することで自動化していくのが良いと思っています。
無理に全てテストコードで担保しようとせず、スクリーンショットや動画も積極的に使っていきましょう。
テストコードの説明
まず、ReactNativeとNativeで書かれたコードのテストコードは結構違う感じになることを念頭に置いておいてください。
また、今回はほんの一部しか機能を紹介しないので興味を持った方はドキュメントを見てみると良いでしょう。ドキュメントの例: http://appium.io/docs/en/commands/element/find-element/
それに加え、テストコードを書くときにはアプリ内のそれぞれのElementに対して
accessibilityLabel
を振っておくと要素を特定しやすくなるのでオススメです。動画を撮影する
index.test.tsbeforeAll(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.tstest('1ページ目が存在している', async () => { expect((await browser.$('~slide1')).elementId).not.toBeUndefined(); await browser.saveScreenshot(`${baseResultPath}/page1.png`); });
browser.$('~slide1')).elementId
でundefined
とならない場合、そのページは確かに存在している。という風にしてテストを担保しています。これは各ページで行っています。また、
browser.saveScreenshot('${baseResultPath}/page1.png')
でスクリーンショットを保存しています。簡単ですね!「1ページ目で入力された内容が2ページ目で表示されている」かどうかのテスト
index.test.tstest('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.tstest('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.tstest('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.tstest('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に触れてみてください。