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

iOS用OpenVPNの設定ファイルの作り方

iOS版のOpenVPNでもOpenVPNの設定ファイルと秘密鍵などをインポートすると使うことができますが、設定ファイルと秘密鍵など複数のファイルに分かれているとなかなか扱いが面倒だと思います。(iTunesのファイル共有を使ってファイルを転送する必要があるかもしれません。)
OpenVPNの設定ファイルには鍵情報を含めることができますが、PCとiOSによって形式が違うため気をつけないとハマってしまいます。(私のことです…)
備忘録も兼ねて残しておこうと思います。

設定のファイル内に鍵情報が混在することになるため扱いにはご注意ください。

TL;DR

OpenVPN.ovpn
#通常通り設定する
client
dev tun
#...
#鍵ファイルの指定はコメントアウトするか消す
#ca ca.crt
#cert client.crt
#key client.key

#tls-auth ta.key 1
#tls-auth使っている場合は下記を追記
key-direction 1

<ca>
-----BEGIN CERTIFICATE-----
CAの証明書
-----END CERTIFICATE-----
</ca>
<key>
-----BEGIN RSA PRIVATE KEY-----
クライアントの秘密鍵
-----END RSA PRIVATE KEY-----
</key>
<cert>
-----BEGIN CERTIFICATE-----
クライアントの証明書
-----END CERTIFICATE-----
</cert>
<tls-auth>
#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
TLSAuth用の鍵
-----END OpenVPN Static key V1-----
</tls-auth>

設定にファイルを組み込む

ファイルの指定を削除する

通常ca ca.crtkey client.keyなどと鍵や証明書のファイルを指定しますがその箇所を削除します。
ただしtls-auth ta.key 1と指定している場合はkey-direction 1に置き換えてください。

設定ファイルにファイルを追記する

ファイルの指定を削除した代わりに各ファイルを組み込む必要があります。
XMLのようにタグを使い中にファイルの内容を記載します。
ca ca.crtなら<ca>~</ca>
tls-auth ta.key 1なら<tls-auth>~</tls-auth>のようなタグを使います。

生成スクリプト

事前に組み込み前の設定ファイルと鍵ファイル等を用意しているのであれば下記のスクリプトで変換することができます。

#! /bin/sh
if [ ! "$#" = "1" ];then
  echo Usage: $0 openvpn.conf
fi

# Original config file
confname=$1

# CA cert file
caname=`grep  -e "^ca" ${confname} |sed 's/.* //g'`

# Client cert file
certname=`grep  -e "^cert" ${confname} |sed 's/.* //g'`

# Key file
keyname=`grep  -e "^key" ${confname} |sed 's/.* //g'`

# TLS-Auth key file
tlsauthname=`grep  -e "^tls-auth" ${confname} |sed 's/tls-auth \(.*\) 1/\1/g'`

# New config file
ovpnname="${confname%%.*}.ovpn"
if [ -f "${ovpnname}" ]; then
  printf "Overwrite(${ovpnname})?:"
  read ans
  if [ ! "${ans}" = "Y" -a ! "${ans}" = "y" ]; then
    exit
  fi
fi

# Load original config file
cat ${confname} |grep -v -e "^ca" -e "^cert" -e "^key" | sed 's/^tls-auth .*$/key-direction 1/g' > ${ovpnname}

# Append CA cert
if [ ! "${caname}" = "" -a -f ${caname} ]; then
  echo "<ca>" >> ${ovpnname}
  cat ${caname} >> ${ovpnname}
  echo "</ca>" >> ${ovpnname}
fi

# Append client private key
if [ ! "${keyname}" = "" -a -f ${keyname} ]; then
  echo "<key>" >> ${ovpnname}
  cat ${keyname} >> ${ovpnname}
  echo "</key>" >> ${ovpnname}
fi

# Append client cert
if [ ! "${certname}" = "" -a -f ${certname} ]; then
  echo "<cert>" >> ${ovpnname}
  cat ${certname} >> ${ovpnname}
  echo "</cert>" >> ${ovpnname}
fi

# Append TLS Auth key
if [ ! "${tlsauthname}" = "" -a -f ${tlsauthname} ]; then
  echo "<tls-auth>" >> ${ovpnname}
  cat ${tlsauthname} >> ${ovpnname}
  echo "</tls-auth>" >> ${ovpnname}
fi

あとがき

iOSでOpenVPNを設定する際に苦戦したため記事に残しておきました。
この記事で沼から抜け出せる人がいることを願っています…
もし間違いなどがあればご指摘いただければ幸いです。

Reference

https://webnetforce.net/ubuntu-openvpn-server/

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

[Swift] UITextViewに画像を添付する方法

はじめに

GameWith AdventCalendar 2019 の14日目の記事になります。

普段はiOSエンジニアをやっていて、最近はWebの業務を行なっているのでその辺りについても来週のアドベントカレンダーで話したいと思います。

今回の記事ではUITextViewNSTextAttachmentクラスを用いて画像を添付する方法をまとめています。
入力中の文字のpositionなどを取得して、その上下にUIImageViewを配置するなどといった事をしなくても簡単に実装できるので具体例をまじえて紹介したいと思います。

具体例

SNSサービスなどでカメラロールから画像を選択し、テキストと一緒に投稿などを行う場合のUIイメージを想定しています。

カメラロールから画像を取得

シンプルにUIImagePickerControllerを使うケースです。

let picker = UIImagePickerController()
picker.delegate = self
picker.sourceType = .photoLibrary
present(picker, animated: true, completion: nil)

UIImagePickerControllerDelegateを設定する

extension SampleViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let image = info[.originalImage] as? UIImage {

            let fullString = NSMutableAttributedString(string: textView.text)

            let imageWidth = image.size.width
            // 画像の幅を調整したい場合paddingなどをframeから引く
            let padding: CGFloat = 16

            let scaleFactor = imageWidth / (textView.frame.size.width - padding)

            let imageAttachment = NSTextAttachment()
            imageAttachment.image = UIImage(cgImage: image.cgImage!, scale: scaleFactor, orientation: .up)

            let imageString = NSAttributedString(attachment: imageAttachment)
            fullString.append(imageString)

            // TextViewに画像を含んだテキストをセット
            textView.attributedText = fullString
        } 
        dismiss(animated: true, completion: nil)
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }

}

終わりに

NSTextAttachment

今回利用している NSTextAttachment ですが、こちらを使う事でUILabelにも画像を適用する事ができるので非常に便利なクラスになっています。
今後文字と画像を結合するUIを実装する際には是非使ってみてください!

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

Flutter でアプリ開発してみる

はじめに

個人的にスマホアプリをいくつか作りリリースしているのですが、作ったアプリを友達などに見せると、必ず言われるのが、
「iPhoneで動かないの?じゃいいや」
でした。
自分の周りでは、iPhoneを使っている率が高い気がしますが、実際のところ Android と iOSの日本でのシェア率は、ほぼ半々ぐらいのようです。

そのような状況なので、モバイルアプリをリリースする場合は、Android, iOS の両対応が必須です。

ただ、Android と iOS はまったく別物なので、Native なアプリを作るとなると、工数は倍近く掛かってしまいます。

ということで、クロスプラットフォーム対応のフレームワークがいくつかあります。
たとえば、 Xamarin や PhoneGap、うちの会社なら Qt もその一つだと思います。

その中で、最近注目され、実際に使われ始めているものに Flutter があります。
この記事では、その Flutter について紹介致します。

Flutter って?

flutter1.png

Flutter は、Google が、これまで蓄積してきたモバイルアプリ作成の知見を基に、新たに作ったクラスプラットフォーム対応のフレームワークです。

使用する言語は Dart です。Java に比較的近い言語なので、Java を理解していれば、容易に使えるようになるかと思います。

パフォーマンスは AndroidでもiOSのそれぞれ、Native code にコンパイルして実行するらしく、Nativeアプリに比べても遜色は無いパフォーマンスだと思います。

UI周りでは、マテリアルデザインの UI Widget が揃っているので、マテリアルデザインに沿った アプリを、Android Native で作成するよりも比較的簡単に実現できるかと思います。
flutter.png

Flutter の導入事例はすでに多くあり、Google だと、Google Assistant で使用されています。
https://flutter.dev/showcase

また、個人的雑感ですが、Android アプリをJavaで開発するのは、そろそろ限界に来ていると思います。これまで、スマホの使われ方の進歩に併せて機能拡張をして来た結果の構造的欠陥があちこちに出てきていると思います。
よって、Android だけにリリースするアプリでも、Flutter で作るのが良いと感じています。

開発環境

iOS アプリとして動作させる場合は、最終的に Mac が必要になりますが、開発自体は、Windows, Mac, Linux のどれでも大丈夫です。

開発環境としては、Android Studio か、VS Code を使用します。
https://flutter.dev/docs/get-started/install
に従って進めてゆけば、開発環境の構築は完了します。
もちろん全て無料で開発できます。

Flutter の特徴として、HotReload というものがあります。
エミュレータや実機などで開発中のアプリを動作させておくと、ソースコードを書いて保存した瞬間に、その挙動が反映されます。
ビルドやアプリの再インストールが不要なため、結果を見ながらのコーディングが可能です。
特にUI周りの挙動などのコーディング時に役立ちます。

入門

公式にサンプルやチュートリアルが豊富に用意されているので、それから始めるのがお勧めです。
またcodelabs を順に実施すると、基本的なところから学習できます。
https://flutter.dev/docs/codelabs

Flutter のデメリット?

英語が必須

正式版のリリースから1年経った今でも頻繁に update されるので、公式ドキュメントを参照する場面がよく出てきます、ただ、公式ドキュメントはすべて英語なので、そこが多少の障壁になってしまうかと思います。

Dart のエラーメッセージが不親切

エラーメッセージがちょっと分かりにくい場合があります。
例えば、以下のようなソースだと、

void main() {
  double x;
  double y = 0.2;
  double z = x + y;
  print ("answer = $z");
}

実行時に

The method '+' was called on null.
Receiver: null

のようなエラーメッセージが表示されます。
これは、x を初期化していないことが原因ですが、ちょっと分かりにくいかな?と思いました。

UI Widget の挙動を変えるのが大変

実際のアプリを作成する際、用意された UI Widget を使って実装するわけですが、UI の挙動を変えたい場合など、苦労することがあります。
以前、Tar bar を使ったアプリを作ったのですが、TabView の追加、削除はサポートされていなかったので、それを実現するために、結構泥臭い実装が必要になりました。

 で、作ってみたアプリ

お試しでアプリを Flutter で作成し、リリースしてみました。
消費税計算機です。
https://play.google.com/store/apps/details?id=com.gmail.kazutoto.works.flutter_consumption_tax
calc.png

ちなみに、Flutterで作ったので iOSでも動くのですが、Apple税を払うの辞めてしまったので、公式にリリースはしていません。

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

SwiftUI×HealthKitでiOSデータ入門(後編)

HealthKitデータ取得 & Firebase 編

ここまで SwiftUI のキャチアップをしつつ前編で環境を構築し,中編で HealthKit を利用可能にしました.今回は実際に HealthKit で歩数データを取得します.

HealthKitで歩数データを取得

前回の記事で権限は得られたのでデータの読み出しが可能となっているはずです.次にデータを取得していきます.少しややこしいです.細かいところは順次付け足していくとして,とりいそぎで書き加えたコードだけを載せます.

DataCollection.swift を編集していきます.body より前に以下の変数を定義していきます.

let type = HKObjectType.quantityType(forIdentifier: .stepCount)!  // allTypeとは別にtypeを定義
    var calendar = Calendar.current  // カレンダーを取得

このように書きます(追加したのは下2行).

DataCollection.swift
struct DataCollection: View {
    // @Stateを使ってUIの状態と同期をとる
    @State var labelText = "Tap Here"
    @State var flag = false
    let healthStore = HKHealthStore()  // HealthKitのデータを格納するHealthStoreを定義
    // アクセス許可が欲しいデータタイプを指定
    let allTypes = Set([HKObjectType.quantityType(forIdentifier: .stepCount)!])  //今回は歩数のみ

    // 以下を追加
    let type = HKObjectType.quantityType(forIdentifier: .stepCount)!  // allTypeとは別にtypeを定義
    var calendar = Calendar.current  // カレンダーを取得

さらにボタンがタップされた時の処理を以下のように書き換えます.ここでは「昨日から今日まで」に歩いた歩数を取得します.

DataCollection.swift
// ボタンがタップされた時
// もしHealthKitが利用可能なら
if HKHealthStore.isHealthDataAvailable() {
    self.labelText = "HealthKit Available"

    self.healthStore.requestAuthorization(toShare: nil, read: self.allTypes) { (success, error) in}

        // 以下を追加
        let date = Date()  // 今日の日付を取得
        let yesterday = self.calendar.date(byAdding: .day, value: -1, to: date)  // "昨日"は上で定義したカレンダーを使って計算する

        // 取得するデータの開始(きのう)と終わり(きょう)を入れる
        let predicate = HKQuery.predicateForSamples(withStart: yesterday, end: date)
        var step = 0.0  // 結果を格納するための変数

        // クエリを作る
       let query = HKStatisticsQuery(quantityType: self.type, quantitySamplePredicate: predicate, options:.cumulativeSum){query, result, error in
       let query_result = result?.sumQuantity() as Any
       step = (query_result as AnyObject).doubleValue(for: HKUnit.count())  // HKQuantity型をdoubleに変換
       print(step)  // コンソールに出力
       self.labelText = String(step) + "歩" // 文字列に変換してデバイスに表示

これで間違いがなければビルドが通り,コンソールに歩数データが表示されるはず.
Screen Shot 2019-12-14 at 20.03.20.png

また,デバイスのボタンをタップすると歩数が表示されるはず.
IMG_8263.PNG

ここまでで,iPhone からデータを取得することに成功しました!

Firebaseへデータを格納

ここからは,先ほど取得したデータをデータベースへ格納します.他のアプリと連携できるように FirebaseFirestore にデータを格納します.当初 Realm を使う予定でしたがハッカソンの戦略上,Firebase へ変更しました.

Firebase (Firestore)

Firebase の環境構築は公式のインストラクションがわかりやすいのでここでは省略します.「Firebase を iOS プロジェクトに追加する」方法をみながら設定をしてください.この設定が済んでいる前提でデータを送るところをやっていきます.

まずは以下の3つのコード

import Firebase
var window: UIWindow?
FirebaseApp.configure()

をAppDelegate.swift に追記します.以下のように書き変えていきます.

AppDelegate.swift
import UIKit
import Firebase  // 追加した行

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow? // 追加した行
    func application(_ application: UIApplication, didFinishLaunchingWithOptions 
    launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        // Firebase
        FirebaseApp.configure()  // 追加した行
        return true
    }

そして DataCollection.swift のボタンがタップされた時の処理を書いた部分に以下の処理を追加します.

DataCollection.swift
<>
 // クエリを作る
let query = HKStatisticsQuery(quantityType: self.type, quantitySamplePredicate: predicate, options:.cumulativeSum){
    query, result, error in
    let query_result = result?.sumQuantity() as Any
    step = (query_result as AnyObject).doubleValue(for: HKUnit.count())  // HKQuantity型をdoubleに変換
    print(step)  // コンソールに出力
    self.labelText = String(step) + "歩" // デバイスに表示

    // ここからが追加したFirebaseの処理
    let db = Firestore.firestore()
    var ref: DocumentReference? = nil
    ref = db.collection("Health").addDocument(data: [
        "歩数": step
    ]) { err in
        if let err = err {
            print("Error adding document: \(err)")
        } else {
            print("Document added with ID: \(ref!.documentID)")
        }
       }
     }
     self.healthStore.execute(query)         
}else{
<>

これで問題なければ,ボタンを押すたびにデータが FireStore にも格納されているはずです.
Screen Shot 2019-12-15 at 0.52.36.png

実験で作った余分なコレクションも見えていますが,こんな感じでうまくいきました!

まとめ

ここまでSwift×HealthKitでiOSデータ入門の 前編中編後編 を通してざっくり

  1. Swift (SwiftUI) のキャッチアップとして画面遷移とボタンの実装
  2. HealthKit からのデータの取得
  3. Firebase へのデータの格納

の3つを行いました.iOS開発どころか Xcode や Swift, HealthKit を触るのも初めてだったので説明不足や理解が不十分な点がまだまだあります.このあたりは今後も引き続きキャッチアップとアップデートを行っていこうと考えています.もし気づいた点などあればコメントなどいただけると大変助かります.

以上,SwiftUI×HealthKitでiOSデータ入門 でした.

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

Codemagicの設定手順&ハマりそうなところをまとめてみた

はじめに

  • ネイティブアプリを開発したことがない方
  • Flutterを触っている、もしくは触る予定の方

へ向けた記事になります。

私自身、ネイティブアプリの開発がなく、Flutterに入門しており、リリースの時にあたふたしたため、同じような境遇の方のためになればと思い執筆しました。

Codemagic とは

Newsletter-second.jpg

Flutter専用のマネージド CI / CD サービスです。
登録から最初のビルドまで簡単に出来てしまう大変便利なサービス。

Codemagic に登録

Codemagic - SignUp

任意のリポジトリサービスを選択して簡単にSignUpすることができます。

スクリーンショット 2019-12-14 15.09.31.png

任意のリポジトリを選択し、即ビルド

codemagic-device-farm-import-1024x474-2.png

ビルドが終わると APK ファイルが生成されています。

1_6E9A_JrjZCXL0Own0_AyFQ.png

めっちゃ簡単。。
実際にアプリをリリースビルドして自動でアップロードするには Android / iOS 共に設定が必要になります。
それぞれ分けて説明します。

Android に関する設定

Keystore ファイルの作成

Keystore ファイルは Google Play に公開するアプリの署名の際に必要になります。
Keystore ファイルは紛失するとアプリのアップグレードが出来なくなりますし流出すると成りすまされてアプリを公開されてしまう可能性があるので取り扱いには注意しましょう。

Ksystore生成コマンド
keytool -genkey -v -keystore keystorename.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000

Keystore のパスワードと Key のパスワードの2種類入力しなければならないので覚えておいてください。のちに使用します。

Codemagic 上での Code Signing

Android Code Signing
基本的に公式のサイトにまとまっています。

Keystore ファイルのアップロード

Workflow 内の Publish の項目の Android code signing に、作成した keystore をアップロードします。
その際に、

  • keystore password
  • Key alias
  • Key password

も合わせて入力しておきます。

code signing.png

環境変数の設定

公式にもある通り、Keystore に関する情報を環境変数で与えなければいけません。

  • FCI_KEYSTORE_PASSWORD = myKeystorePassword
  • FCI_KEY_ALIAS = MyReleaseKey
  • FCI_KEY_PASSWORD = myKeypassword
  • FCI_KEYSTORE_FILE = myKeystoreEncodedByBase64

スクリーンショット 2019-12-14 14.16.12.png

最後の FCI_KEYSTORE_FILE は最初何を設定するのか分かりませんでしたがどうやら Base64 に encode した Keystore 文字列を突っ込む必要があるらしい。先程の項目で Keystore アップロードしていたので必要ないと勝手に思い込んでました。ドキュメントちゃんと読まないとですね。

Base64_encode
$ base64 -i /path/to/keystore -o output.txt

Post-clone script の追加

Post-clone scriptに以下のコードを追加します。

#!/usr/bin/env sh
set -e # exit on first failed commandset
echo $FCI_KEYSTORE_FILE | base64 --decode > $FCI_BUILD_DIR/keystore.jks

Play Console でのアプリケーション作成

Google Play の Developer 登録が済んでいない人は公式サイトから登録しましょう。

SignInしたらアプリの作成を押して新規アプリを作成します。

スクリーンショット 2019-12-14 14.38.56.png

Service Acount の作成

Codemagic から APK ファイルを自動アップロードするために Service Account を作成する必要があります。
手順に関してはこちらのサイトが参考になるかと思います。

Codemagic 上での Google Play の設定

Workflow の Publish の項目の Google Play を開きます。

スクリーンショット 2019-12-14 14.52.38.png

Service Account の作成の項目で生成した Service Account の APIKey ( JSON )Credentials にアップロードし、APK ファイルをどの Track にアップロードするか選択します。

※ 注意

App を作成した後に、一度手動で APK ファイルをアップロードする必要があります。

Android に関しては以上になります。

iOSに関する設定

iOS Code Signing
公式で手順が解説されています。
順を追って解説していきます。

Apple Developer Portal と連携

User Settings もしくは Team Integration にある Developer Portal の connect ボタンを押して連携します。
連携できると以下のように緑色に変わります。

スクリーンショット 2019-12-14 16.48.48.png

Apple Developer Portal で App ID ( Bundle ID ) を生成

Apple Developer Portal へアクセスし、 Account => Certificates, Identifiers & Profiles の順にクリックします。

ボタンを押してこちらを参考に App ID を生成します

App Store Connect でのアプリ作成

Apple Developer Portal へアクセスし、 Account => Certificates, Identifiers & Profiles => App Store Connect の順にクリックします。

左上の ボタンから 新規 App を押すと以下の画面になります。

スクリーンショット 2019-12-14 17.20.33.png

  • 名前 ( アプリ名 )
  • プライマリ言語 ( 第一言語選択 )
  • バンドルID ( 先程作った Bundle ID )
  • SKU ( アプリを識別するための ID )

を入力して作成を押します。 SKU に関しては変更ができないのでよく考えて入力する必要があります。

Codemagic 上での Code Signing

iOS は Autmatic という項目があり、自動で設定することができます。めっちゃ便利ですね!

※ Pre-build scriptの追加 ( Option )

Build flavor を設定している場合、 Pre-build script に xcconfig ファイルを上書きする処理を記述しなければいけません。詳しくは monoさんの記事をご覧ください。

Codemagic 上での Apple Store Connect の設定

WorkflowPublish 内にある App Store Connect を開きます。

  • Apple ID ( アカウントの Email Address )
  • App-spcific password (App 用パスワード )
  • App ID ( App Store Connect で作成した App の ID )

スクリーンショット 2019-12-14 17.28.20.png

App-specific password を作成するには、
Apple ID Service にアクセスします。アカウントの管理の セキュリティ の項目があるのを確認します。

スクリーンショット 2019-12-14 17.35.49.png

パスワードを生成... をクリックし、任意のラベルをつけて作成します。その際に表示されるパスワードをメモしておきます。パスワードはこれ以降確認できないので注意が必要です。
また、2ファクタ認証 が有効になっていない場合は App 用パスワードを生成することができないので済ませておきましょう。

App ID は App Store Connect へアクセスし、作成したアプリをクリックして確認することができます。

スクリーンショット 2019-12-14 17.43.47.png

全般の設定

Build triggers の設定

  • Trigger on push
  • Trigger on pull request update
  • Trigger on tag creation

の3つの Trigger を設定できます。
ワイルドカードを使って特定の Branch を対象にすることも可能です。
下の画像では、 featurefix で始まる Branch から PullRequest が出された時に実行するパターンです。

スクリーンショット 2019-12-14 17.52.48.png

連携の設定

Slack

Publish もしくは Team integrationに画像のような項目があるので connect を選択

スクリーンショット 2019-12-14 13.47.16.png

Slack の integration の設定ページに飛ぶので許可する
その後、Codemagic でビルドを開始して終了すると Slack に Artifact のリンクを含んだメッセージが post されます。

スクリーンショット 2019-12-14 13.41.07.png

終わりに

Flutter 自体もとっつき易く、新しく始めるのに敷居が低いので嬉しい限りです。
Flutterを触り始めてから Swift や Kotlin でも開発してみたいなと思うようになったので挑戦しようかなと目論んでいます。
ありがとうございました!

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

Xcode 11 の Test Plan を、試しながらゆるく解説

この記事では WWDC 2019 の 「Testing in Xcode」 をベースにして、Test Plan
について調べたことや試してみたことを書こうと思います。すぐに Test Plan を試せるプロジェクトは↓こちら。
https://github.com/akatsuki174/TestPlanSample

Test Plan の存在意義

例えばローカライズ対応をしているアプリで、開発時のデフォルト言語ではテストがパスしても、別の言語で実行した時に UI 崩れが発生するかもしれない。複数の条件(言語、テストの実行順、サニタイザー、引数や環境変数)を跨いで常にテストを実行することは不具合の発見率を高めることができる。

今までの Xcode でもスキームエディタを使うことによって様々な設定をすることが可能だったが、実行は1回しかできない。一方 Test Plan では設定を変えてテストを何回も実行できるところがメリットになる。

Test Plan でできること

  • 設定を変えてテストを複数回実行
  • 全てのパターンを1箇所に定義
  • 設定を複数のスキームで共有
  • CI サーバや Xcode Server で使う(xcodebuild にも対応している)
  • 既存プロジェクトに導入

xctestplan ファイルの作り方

Test Plan を使うために xctestplan ファイルというテスト設定ファイルを作成する。

①Edit Scheme を選択
スクリーンショット 2019-12-08 21.35.51.png

②Test タブを選択し、下の Convert to use Test Plans をクリック
スクリーンショット 2019-12-08 21.37.40.png

③自分に合った変換方法を選択

既存の設定を引き継いで Test Plan を作る、空の Test Plan を作る、すでにある Test Plan を使うの3種類の方法を取ることができる。
スクリーンショット 2019-12-08 21.54.42.png

ちなみにここからでも Test Plan ファイルの新規作成や編集ができるっぽい。
スクリーンショット 2019-12-09 23.12.04.png

xctestplan ファイルの中身 - Tests タブ

Test タブはこの通り。存在しているテストターゲットが一覧になっている。Option をクリックすると、並列でテストを走らせるか否かなどの設定を行うことできる。

特定のターゲットのテストを無効にしたい場合は、Enabled のチェックを外す。
スクリーンショット 2019-12-08 22.35.29.png

xctestplan ファイルの中身 - Configurations タブ

実行の方法を定義している部分。設定できる項目の大区分は以下の通り。

  • 引数
  • ローカライズ
  • UI Test
  • アタッチメント
  • テスト実行
  • コードカバレッジ
  • ランタイムサニタイザー
  • ランタイムAPIチェック
  • メモリ管理

Share Settings では毎回のテストに共通するオプションを設定する。太字になっているところは、カスタム値を設定しているという意味。
スクリーンショット 2019-12-09 23.20.48.png

条件を変えてテストを実行したい場合は新しく Config ファイルを作成してカスタマイズする。詳細は次の例の中にて。

使ってみる

2ヶ国語の翻訳表示テスト

日本語と英語での翻訳表示がちゃんとできているか確認する UI テストの例。ただ Test Plan を試したいだけなのでテストの内容がアレなのはご容赦。
Label に表示されている文字列が正しいかをチェックする。

class ViewController: UIViewController {

    @IBOutlet weak var greetingLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        greetingLabel.text = NSLocalizedString("Top_title_label", comment: "")
    }
}
class TestPlanSampleUITests: XCTestCase {

    var app: XCUIApplication!

    override func setUp() {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launch()
    }

    func testLabelLocalization() {
        XCTAssertEqual(app.staticTexts["greetingLabel"].label, localizedString(key: "Top_title_label"))
    }

    private var currentLanguage: (langCode: String, localeCode: String)? {
        let currentLocale = Locale(identifier: Locale.preferredLanguages.first!)
        guard let langCode = currentLocale.languageCode else {
            return nil
        }
        var localeCode = langCode
        if let scriptCode = currentLocale.scriptCode {
            localeCode = "\(langCode)-\(scriptCode)"
        } else if let regionCode = currentLocale.regionCode {
            localeCode = "\(langCode)-\(regionCode)"
        }
        return (langCode, localeCode)
    }

    private func localizedString(key:String) -> String {
        let localizationBundle = Bundle(path: Bundle(for: TestPlanSampleUITests.self).path(forResource: currentLanguage?.langCode, ofType: "lproj") ?? "")
        let result = NSLocalizedString(key, bundle:localizationBundle!, comment: "")        
        return result
    }
}

準備は各言語用の Config ファイルを作るだけ。Config の追加は見慣れたこの+ボタンをクリックすればOK。
スクリーンショット 2019-12-13 20.53.43.png

設定を変えるのは Localization の部分。

↓日本語の Config
スクリーンショット 2019-12-13 20.51.21.png

↓英語の Config
スクリーンショット 2019-12-13 20.57.13.png

これを実行すると、このように2回連続でテストが走る。
testplan-lang.gif

もし1言語だけテストを回したいなら、テストファイルで Option を押しながらひし形をクリックする。すると使用する Config の選択肢が出てくるので、使いたい方を選ぶ。
スクリーンショット 2019-12-13 21.13.07.png

ではここでわざと日本語のテストが失敗するようにしてみる。

override func viewDidLoad() {
    super.viewDidLoad()
    var text = NSLocalizedString("Top_title_label", comment: "")
    let lang = Locale.preferredLanguages.first
    if lang == "ja" {
        text += "hoge"
    }
    greetingLabel.text = text
}

想定通り失敗する。
スクリーンショット 2019-12-13 23.55.25.png

どのメソッドで、どの Config の時に失敗したのかは、レポートナビゲータからチェックできる。
スクリーンショット 2019-12-14 0.38.52.png
Allタブでは全ての結果が、Passed タブではテストが通ったメソッドが、Failed タブではテストが通らなかったメソッドが、Mixed では Config によって結果が異なるメソッドが表示される。
スクリーンショット 2019-12-14 0.04.04.png
特定の Config の結果だけ見たいときはこのボタンをクリックして選択する。
スクリーンショット 2019-12-14 0.40.55.png

Test Plan の活用例

異なるサニタイザーを組み合わせて使う

サニタイザーは Xcode に内蔵されているツールで、再現しにくいバグを特定してくれる。例えば ASan と UBSan を組み合わせて使うといったことができる。

Language, Location, Locale 別に Config を用意する

各国の言語で UI テストを実行しつつ、Shared Settings でスクショを撮る設定をする。Xcode 11 では成功したテストでも全てのスクショを保存できる。

異なる種類の項目を組み合わせる

  • メモリの安全性を重視:Address Sanitizer + Zombie Objects
  • 並列処理を重視:Thread Sanitizer + Undefined Behavior Sanitizer + Random Order
  • 追加診断:ENABLE_LOGGING=1(カスタム環境変数) + Keep Attachments(テストが成功してもAttachmentsを保持)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】簡単にKeyboardを表示したときにTextFieldが隠れないようにする

この記事は ZOZOテクノロジーズ #1 Advent Calendar 2019 14日目の記事になります。
昨日の記事は @hkame さんによる「Aurora->CloudSQLへMySQLレプリケーションはできるのか」でした。

はじめに

本記事はKeyboardを表示したときにTextFieldが隠れてしまわないようにするための実装についてです。
TextFieldが隠れないようにする実装のやり方は色々あると思いますが、本記事ではFrame計算などのない簡単な方法を紹介したいと思います。

結果

このような動きになります。
日本語キーボードにしてキーボードの高さが変わってもTextFieldの位置が変わっているのがいいですね。

demo.gif

実装について

実装は単純で、キーボードが表示されたらScrollViewのcontentInset.bottomの値を調整します。

ポイントとしては、contentOffsetの調整ではなく、contentInsetの値を調整することです。
contentInsetの値を調整するメリットは、TextFieldの表示位置を気にしなくてもいいことです。
contentOffsetの値を変更する際に単純にキーボードの高さを足してしまうと、TextFieldの位置の制御をするためにキーボードがどの位置に表示されているかにより設定するcontentOffsetを求める計算が必要になります。

contentOffset.yにキーボードの高さを足した場合 contentInset.bottomにキーボードの高さを足した場合
Dec-14-2019 00-35-40.gif Dec-14-2019 00-27-48.gif
import UIKit

class ViewController: UIViewController {

    @IBOutlet private weak var scrollView: UIScrollView!
    @IBOutlet private weak var nicknameTextField: UITextField!
    @IBOutlet private weak var birthdayTextField: UITextField!
    @IBOutlet private weak var submitButton: UIButton!

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidAppear(_:)), name: UIResponder.keyboardDidShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillDisappear(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    @objc private func keyboardDidAppear(_ notification: Notification) {
        guard let keyboardFrame = (notification.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? NSValue)?.cgRectValue else { return }

        scrollView.contentInset.bottom = keyboardFrame.height
    }

    @objc private func keyboardWillDisappear(_ notification: Notification) {
        scrollView.contentInset.bottom = 0
    }
}

キーボードの高さの監視タイミングについて

キーボードの表示、非表示のタイミングをそれぞれまとめてみました。

キーボードの表示

keyboardWillAppear keyboardDidShowNotification
Dec-14-2019 00-43-12.gif Dec-14-2019 00-48-45.gif

キーボードの非表示

keyboardWillAppear keyboardDidShowNotification
Dec-14-2019 00-49-38.gif Dec-14-2019 00-46-07.gif

まとめ

キーボードを表示した際にTextFieldを隠れないようにするには、scrollViewのcontentInset.bottomの値を変更する実装がとても簡単です。
キーボードの非表示時監視タイミングをkeyboardDidShowNotificationにするとカクつくのでおすすめしません。

明日の記事は @r-tezuka さんによる公開予定です。どうぞお楽しみに。

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

UIColor から CIColor に変換するときの注意

前提

UIColor から CIColor にする方法は2種類あります

# 変数 color は UIColor のインスタンスとする

# initializer
CIColor(color: color)

# property
color.ciColor

注意

color.ciColor で取得する時、 color が Core Image でイニシャライズされたものでない (= CIColor で作成されてない) 場合は例外を返されます。
公式ドキュメントにはかいてありますが、抜け穴だと思います。

This property throws an exception if the color object was not initialized with a Core Image color.

結論

基本的に CIColor(color:) の方を使っておけば問題ない。

環境

  • Swift 5

参考

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

Flutter Interact 2019のまとめ

はじめに

この記事はFlutter #2 Advent Calendar 2019の11日目分です。

今年もすぐに枠が埋まってしまっていたのでFlutter Advent Calendarに参加する予定はなかったのですが、ふとカレンダーを覗いていみると11日枠が投稿されずに空いていたのでちょうどFlutter Interactが開催されたので急遽この記事を書くことにしました。

Flutterの最新情報に興味がある方はFlutterウィークリーもよかったらご覧ください
https://qiita.com/tags/flutterweekly

Flutter Interact 2019とは

https://developers.google.com/events/flutter-interact

Flutter Interact is a day dedicated to creation and collaboration with the world, inspired by the makers of brilliant experiences.

一言でいうとGoogle公式のFlutter年次イベントです。12月11日にニューヨークのブルックリンで開催されました。昨年はFlutter Live 2018が同じ時期に開催されました。来年はまた名称が変わっているかもしれませんね。

セッション一覧

Vision Keynote (Flutter Interact '19)

https://youtu.be/NfNdXgJZfFo
ビジョンキーノートを努めたのはMatias Duarte、以前はPalmのVPで、2010にGoogleに入社しAndroid User Experienceのディレクターをやっていて最初にリリースしたのはHoneycombと呼ばれるAndroid 3.0のデザインになります。次にMaterial Designを手掛けたという輝かしい経歴の彼が、現在はFlutterに取り組んでいます。GoogleがFlutterにかける意気込みもこの辺りから伝わってきますね。マテリアルデザインを世界中のスクリーンに広めるためにFlutterに取り組んでいると言っても良さそうです。キーノートでも言及されていますが、マテリアルデザインコンポーネントは専門のエンジニアがFlutterチームにいて新しいコンポーネントのドキュメントが出来るよりも先にFlutterで実装されていることも多いそうです。

Google FontsもFlutterから簡単に使えるようになりました。
https://pub.dev/packages/google_fonts
Gogole Fontsを使えば.ttfファイルをアセットに組み込むことなく自由に1000ものフォントを利用することが出来ます。

使い方は

Text(
  'This is Google Fonts',
  style: GoogleFonts.lato(),
),

Text(
  'This is Google Fonts',
  style: GoogleFonts.lato(
    textStyle: TextStyle(color: Colors.blue, letterSpacing: .5),
  ),
),

のように指定するだけでGoogle Fontsを利用可能です。まだバージョン0.2.0のベータ版ですが利用することが出来ます。

Very Early Experimental

アダプティブUIもシングルコードベースで実現できるようになりました。Flutter Webやマルチサイズのスクリーンに簡単に対応できるようにするには必要な機能ですね
Adaptive UI

開発中のレイアウトコードエディタ。レイアウトの細かな調整もプレビューで確認しながらデザイナーが調整しそのままコードを出力できるようになります。
Layout Editor

Product Keynote (Flutter Interact '19)

https://youtu.be/ukLBCRBlIkk

a portable UI framework for an ambient computing world

Flutterはモバイルアプリのクロスプラットフォームとして知られていますが、様々なスクリーンが増え続ける中すべてのスクリーン向けのアプリを開発できるようにすることを目指してFlutterの開発が続いています。

Flutter 1.12リリース

Flutter 1.12

ダークモードのiOS対応

Dark mode support for iOS

新しいCupertinoウィジェット

Cupertino widgets

Android Xがデフォルト

Android X

既存アプリへの追加

add-to-app support

Googleフォント

Google Fonts

Flutterギャラリー

Flutter Gallery

StadiaをFlutterで開発

Stadia

eBayがFlutterでアプリを作り直した

https://play.google.com/store/apps/details?id=com.ebay.mobile
ebay

Flutter on Desktop

Flutter On Desktop

Flutter on the Web

Flutter on the Web

Dart

Screen Shot 2019-12-14 at 00.00.00.png

Dartpad

https://dartpad.dev/
Dartpad
Web上のPlaygroundでFlutterも動くようになりました

Codelabs

https://flutter.dev/docs/codelabs
Codelabo
コードラボもそのままFlutterの動作を確認できるようになりました

Flutter outline Editor

Flutter outline Editor

レイアウトエクスプローラー

Layout Explorer

Adobe XD

Screen Shot 2019-12-14 at 00.09.54.png

キーノート以外のセッション

Welcome to the Sparkle Party (Flutter Interact '19)
https://youtu.be/1AxXF038-lY

Material Theming with Flutter (Flutter Interact '19)
https://youtu.be/stoJpMeS5aY

Designing for the Web with Flutter (Flutter Interact '19)
https://youtu.be/tot-R_q5-0o

Building in Accessibility with Flutter (Flutter Interact '19)
https://youtu.be/bWbBgbmAdQs

Design and Build Clock Displays with Flutter (Flutter Interact '19)
https://youtu.be/i60HG1TtKJo

Use Rive and Flutter for dynamic, interactive, & animated experiences (Flutter Interact '19)
https://youtu.be/6QZy5sYozVI

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