- 投稿日:2020-02-08T21:50:13+09:00
#24 チュートリアル的な初回起動時のみ表示されるViewController1例
はじめに
個人のメモ程度の出来なのであまり参考にしないで下さい.
環境
Xcode:11.2.1
Swift:5.1.2
2019/11part1
Main.storyboard
でsegue
を選択する.part2
segue
を選択した状態で,Attributes inspector
のIdentifier
に任意の文字列を入れる.part3
AppDelegate.swift
のapplication(_:didFinishLaunchingWithOptions:)
に4行追加する.func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { //この下の4行を追加 let ud = UserDefaults.standard let firstLunchKey = "firstLunch" let firstLunch = [firstLunchKey: true] ud.register(defaults: firstLunch) return true }part4
ホーム画面の
ViewController
のStartViewController.swift
のviewDidAppear(_)
に7行追加する.override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) //この下の7行を追加 let ud = UserDefaults.standard let firstLunchKey = "firstLunch" if ud.bool(forKey: firstLunchKey) { ud.set(false, forKey: firstLunchKey) ud.synchronize() self.performSegue(withIdentifier: "toAboutApp", sender: nil) } }part5
チュートリアル画面の
ViewController
のAboutAppViewController.swift
の「閉じる」ボタンのアクションにに1行追加する.@IBAction func toback(_ sender: UIButton) { //この下の1行を追加 self.dismiss(animated: true, completion: nil) }
- 投稿日:2020-02-08T21:49:16+09:00
JenkinsとfastlaneでiOSビルド環境を構築@Mac mini
前置き
AndroidをEC2インスタンス(Linux)のJenkinsでビルドしていたので、同じようにiOSもビルドしたい。
しかしAndroidはソース書いてビルドもしているのでなんとかJenkinsの設定もできるが、iOS開発はほとんどわからない。自分の開発環境にiOS開発環境をセットアップしたぐらいだ。そんな状態のほとんどAndroid開発しかしたことない自分でもfastlaneを使えばiOSも簡単にビルド自動化ができたので、Jenkins × fastlane × Mac miniの構成をご紹介します。
構成
iOSのビルドにはMacが必要なので、そのままLinuxのインスタンス上のJenkinsでは出来ない。
そのため、JNLPというJavaのサービスを利用してEC2のJenkinsとローカルの机の上のMac miniちゃんを連携してビルドできるようにする。
- iOSをビルドするMac miniにはJenkinsはセットアップする必要は無い。fastlaneを使ってiOSのビルドに詳しくなくても楽チン
- ビルド自体の他に以下も行ってCI/CD(CDだけかな?)を回して、社内でipaの検証サイクルを効率化できた。
- ビルドしたらipaをS3にアップロードしてリンクをBacklogにコメントする
- ビルド中に何かエラーになったらslackのチャンネルに通知してすぐ気づけるように
Macへfastlaneのセットアップ
- iOS/Androidのビルドツール fastlane でコマンドラインからビルドしてipaを作成する
Rubyのセットアップ
- fastlaneはRubyのgemで提供されているのでRubyが必要
- rbenvでRuby2.5.1をシステム全体にセット (バージョンは適当)
fastlaneのセットアップ
上記ドキュメントを参照してプロジェクトのルート(xxxxx.xcodeprojが存在するディレクトリ)で
$ fastfile initによりFastfile, Appfileを作成してソース管理する
- gymというアクションでiOSをビルドする
fastlaneのgemインストール
$ bundle install —path vendor/bundleでGemfileに記述してプロジェクト毎にセットしていると毎回のビルドでfastlaneをインストールしないといけないので、以下でシステムに直接インストールする
$ gem install fastlaneビルド時のタイムアウトとリトライを調整する
デフォルトの設定だと以下ビルド情報を表示するコマンドでタイムアウトとリトライの上限を超えてたまにエラーになってしまう場合がある。
xcodebuild -showBuildSettings -workspace ./xxxxx.xcworkspace -scheme xxxxxデフォルトだと10秒と4回の上限のようなので、Fastfileに以下のように設定しエラーとならないように
ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "180" ENV["FASTLANE_XCODEBUILD_SETTINGS_RETRIES"] = "10"JenkinsへのMac miniのslave登録
EC2インスタンスで起動しているJenkinsへローカルの机の上にあるMac miniをslaveとして登録する。
- Jenkinsの管理 -> ノードの管理 -> 新規ノード作成 -> Permanent Agentを選択してOK
- 以下必要な項目を記入して保存
値 メモ 同時ビルド数 1 2にすると並行して2つ同時にビルドできる? リモートFSルート Users/jenkins/jobs/ Mac側のルートディレクトになる場所を指定する ラベル mac-mini(識別するラベル) ビルドするジョブを作った時にこのラベル名を指定してビルドするマシンを指定する 用途 「このマシンを特定ジョブ専用にする」を選択 iOSのビルドしかしないのでこの設定にする 起動方法 「 Launch agent via Java Web Start 」を選択 この方法がJavaのJNLPという通信で↓のjarファイルを実行して通信を開始する 可用性 「keep this agent online as much as possible 」を選択 登録後にagent.jarがダウンロードできるのでMac miniにセットして、
表示される以下コマンド(Jenkinsが作成してくれる)を実行し、Jenkinsと同期状態(ビルド実行状態が待機中)となれば成功java -jar /Users/jenkins/jobs/agent.jar -jnlpUrl http://xxx.xxx.xxx/computer/mac-mini/slave-agent.jnlp -secret [シークレットコード] -workDir "/Users/jenkins/jobs" -failIfWorkDirIsMissing※ 設定で上記の項目を変更した場合、agent.jarとコマンドも変更になるので都度ダウンロードとコマンドの変更を行う
Jenkinsマスター(EC2インスタンスで起動している)へのポート設定
このJNLPのサービスがJenkinsと通信を行うため、JenkinsのJNLPに使用するポートの固定とEC2インスタンスに設定しているセキュリティグループのポート設定を行う
- ポート固定
- Jenkinsの管理 -> グローバルセキュリティの設定 -> Agents
- -> TCP port for JNLP agents を固定、40790 を指定
- -> Agent protocols で Java Web Start Agent Protocol/4 (TLS encryption) を指定
- セキュリティグループ Mac miniのIPアドレス(インターネット向け)のJNLPのポート(40790)を通すようにする。
タイプ プロトコル ポート範囲 ソース 説明 カスタム TCP ルール TCP 40790 xxx.xxx.xxx.xxx/32 port for iOS build MacMini to Master Jenkins MacMiniの自動起動設定
MacMini起動時にJNLP起動コマンドを自動で実行するようにlaunchctlに登録する
Fastlaneやaws s3コマンドへパスが通っていないと実行できないので、plistへPATHもセットしておく
- 設定ファイル/Users/jenkins/Library/LaunchAgents/com.xxxx.com.ci.plistシステム環境設定 -> 省エネルギー -> スケジュールでMacの自動起動、終了を行うようにする
平日の09:30起動、21:00終了としているメモ
JenkinsでMacMiniの状態がオフラインになることがある。
Jenkinsのビルド実行状態 -> mac-mini がオフラインになり以下のエラーが表示されるコネクションが切断されました。java.nio.channels.ClosedChannelException回避策として
Macのシステム環境設定 -> 省エネルギー -> ディスプレイがオフのときにコンピュータを自動でスリープさせないのチェックを入れているが、またオフラインになる場合があるかもしれないので、
その場合はターミナルで以下、launchctlを再起動する
- launchctlを止める方法
launchctl unload /Users/jenkins/Library/LaunchAgents/com.xxxx.com.ci.plist
- launchctlを起動する方法
launchctl load /Users/jenkins/Library/LaunchAgents/com.xxxx.com.ci.plist
- Jenkins本体が落ちた場合
- Jenkins本体のEC2がスポットインスタンスの価格高騰などで落ちた場合、Jenkins本体が再び上がったら自動的に接続するのでMac mini側は何もしなくてよい
- Jenkins本体が上がってしばらくしてもオフラインの場合は、Mac mini側を再起動してみる。
aws S3へのアップロード
- python3とawscliのインストール
aws s3
のコマンドでビルドしたipaをs3へアップロードする awsアカウントのcredentialをセットしてawsコマンドを使えるようにする(省略)backlogへのリンクのポスト
BacklogのAPIを実行できるRubyのgemをシステムにインストールしてbacklogのチケットへコメントするスクリプトを実装
https://github.com/emsk/backlog_kit$ gem install backlog_kitslack通知
ビルドエラーの際、slackのチャンネルへ通知する
Fastfileのslackアクションにエラー時に通知するように設定できるプロビジョニングが更新されたら
Mac miniのプロビジョニングファイルはfastfileに記載した自動更新オプションにより自動で更新されるはずだが、念の為以下のプロビジョニングファイルを削除しておく。(ビルド時に新しく取得しなおすように)
~/Library/MobileDevice/Provisioning Profiles/
- fastfileのプロビジョニング自動更新オプション
xcargs: "-allowProvisioningUpdates",
- キャッシュが原因でビルドが失敗していると思ったら 以下パス直下のファイルを全て消してみる。
/Users/jenkins/Library/Developer/Xcode/DerivedDatafastfileで以下のようにしてクリアしていれば、毎回のビルドで消えているはずだが、ビルド失敗するようなら自分で消してみる
# delete Xcode Derrived Data clear_derived_data※↑のプロビジョニングファイルも含めて毎回ビルド毎に rm するようにしたらいいかもしれない
fastlaneのアップデート
ひんぱんにアップデートされ、iOSの新バージョンリリース後にビルドがうまくいかなくなった場合は、リリースノートを見ると対応されていることがあったので、バージョンアップ手順は確認しておきたい。
リリースノート
現在のバージョン確認 -> 2.126.0 (commander-fastlaneは何かは知らない)
$ gem list | grep 'fastlane' commander-fastlane (4.4.6) fastlane (2.126.0)
- 以下コマンドでアップデート
$ sudo gem install fastlane
- アップデート後のバージョン確認 -> 2.134.0になった
$ gem list | grep 'fastlane' commander-fastlane (4.4.6) fastlane (2.134.0, 2.126.0)
- 元のバージョンに戻したい場合、以下でいけるはず
$ gem install fastlane -v “2.126.0”
- 投稿日:2020-02-08T17:53:49+09:00
SwiftPMで�Quickを入れたら"No such module 'Quick'" というエラーが出た話
iOSテスト全書を読んでいると、第5章に「BDDによるアプリ開発」というQuick/Nimbleを利用したチュートリアルがあったので、早速やろうとしたらライブラリ導入の時点でつまづきました。
初歩的すぎて呆れる方もいるかもしれませんが、初心者の自分はこれで3時間以上溶かしてしまったので、同じ様な初心者の方にお役に立てれば幸いです。Qiita初投稿のため、内容に誤り等あればコメントでご指摘いただけると助かります。
ちなみに、簡単にSwiftPMの話もしています。
環境
MacOS Mojave 10.14.6
Xcode11.2.1
Swift5まずライブラリを入れる
「iOSテスト全書」内ではCocoaPodsとCarthageでの導入手順が記載されていますが、私はSwiftPMで入れてみました。Xcode11からはGUIでパッケージ管理できるので便利ですね。
まずはXcodeのメニューから[File]→[Swift Packages]→[Add Package Dependency...]を選びます。
すると、githubのリポジトリを検索するウインドウが出てくるので、お目当の「Quick」を検索します。ちなみに、github以外にあるものでもURLを指定すればパッケージを入れることができるそうです(私は未検証)。
バージョンやブランチを聞かれます。今回は特にこだわらないのでそのままNextを押します。
次は、ライブラリをどのターゲットに対して追加するかを聞かれます。デフォルトでは実際のソース(今回はqiitasample)が選ばれていますが、Quickはテストコードで使いたいライブラリなので、忘れずにテストコードの方(qiitasampleTests)を選びましょう。
すると、Xcodeの左側に追加したパッケージと、その依存関係にあるパッケージが追加されます。Quickを入れただけでSwiftPMがNimbleも落としてきてくれたみたいですね。
いきなりエラー
早速Quick/Nimbleを使ってテストコードを書いていきましょう!まずはQuickをimportして...
それらしいものがサジェストされません。もう不穏ですね。
とりあえずQuickと手打ちしてビルド(Controll+B)してみると...
No such module Quick
と言われてしまいました。いや、あるじゃん。
この後プロジェクトをクリーンしてみたり、エラー名でググってみたりしましたが、それらしい解決法に出会えずハマってしまいました...解決法
どうにかできないかと思いXcodeのメニューを見ていると、[Product]の中に[Build for...]という項目を見つけました。よく見ると...
Build For Testingがある!ここでようやく、Controll+Bではテストコードに対するビルドが行われていないことに気付きました。確かにXcode上部のバーにQuickがビルドされているような表示は出ていなかった気がする。
Build For Testingを実施すると、QuickやNimbleにもビルドがかかり、無事エラーも解消されました。まとめ
- テストコードに対してパッケージを追加した時は、Build For Testingを実行する。
- ググってもわからないからといって諦めず、メニューをよく見てIDEの機能をよく確認する。
- 投稿日:2020-02-08T15:56:54+09:00
dyld: Library not loaded: @rpath/Realm.framework/Realm
Xcode 11.3.1 になってからなのか、iOS 13.3.1 になってからなのか不明だが、実機の場合になぜか下記のエラーが出るようになった。(シミュレーターはOK)
dyld: Library not loaded: @rpath/Realm.framework/Realm下記を参考に Podfile を変更して動くようになりましたが、あまり理解できていないのでやる方は自己責任でお願いします。バグなのかは不明ですが、Apple Developer Program に登録していない場合になるみたいです。
https://github.com/Alamofire/Alamofire/issues/3051
- use_frameworks! をコメントアウトして変わりに use_modular_headers! にして update。
# Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'MyApp' do # Comment the next line if you don't want to use dynamic frameworks # use_frameworks! use_modular_headers! # Pods for MyApp pod 'RealmSwift' end
- プロジェクトファイルの Frameworks, Libraries, and Embedded Content にあるフレームワークを変更。
Pods_MyApp.framework ↓↓↓ libPods-MyApp.a
- 投稿日:2020-02-08T15:38:13+09:00
CombineLatest Streamのテスト手法
はじめに
テスト書いてますか?
テストコードが0のアプリにようやくテストコードを入れるようになり、RxTest/RxBlockingを使いはじめました。
CombineLatest
で複数のObservabelをまとめて観測する値がtuple型のときに、テストで詰まったので解決策を残します。環境
- macOS Catalina 10.15.3
- Xcode 11.3.1 (11C504)
- RxSwift 5.0.1
Rxのテスト
Model
テストしたいModelを用意します。
今回はuserStream: Observable<(String, Int)>
の値をテストします。import RxSwift import RxCocoa class UserModel { let name: BehaviorRelay<String> = BehaviorRelay(value: "") let age: BehaviorRelay<Int> = BehaviorRelay(value: 0) // outputStream let userStream: Observable<(String, Int)> init(name: String, age: Int) { self.name.accept(name) self.age.accept(age) userStream = Observable.combineLatest(self.name, self.age) } }テストコード
それではUserModelの
userStream
のテストを書いてみます。import XCTest import RxSwift import RxCocoa import RxBlocking import RxTest @testable import TestApp class TestAppTests: XCTestCase { var userModel: UserModel! var scheduler: TestScheduler! var disposeBag: DisposeBag! override func setUp() { userModel = UserModel(name: "takutoki", age: 5) scheduler = TestScheduler(initialClock: 0) disposeBag = DisposeBag() super.setUp() } func testObservableWithTuple() { // assertする型を定義 let testObserver = scheduler.createObserver((String, Int).self) // テスト対象のbind先をtestObserverにする userModel.userStream .bind(to: testObserver) .disposed(by: disposeBag) // name に流すテストObservable let nameTestObservable = scheduler.createColdObservable([ Recorded.next(10, "ShirasakaKoume"), Recorded.next(20, "KoshimizuSachiko"), Recorded.next(30, "HoshiSyoko") ]) // age に流すテストObservable let ageTestObservable = scheduler.createColdObservable([ Recorded.next(15, 13), Recorded.next(25, 14), Recorded.next(35, 15) ]) nameTestObservable .bind(to: userModel.name) .disposed(by: disposeBag) ageTestObservable .bind(to: userModel.age) .disposed(by: disposeBag) // expect // 0: 初期値 // 10,20,30: nameが変更されたときに流れる // 15,25,35: ageが変更されたときに流れる let expected: [Recorded<Event<(String, Int)>>] = [ Recorded.next(0, ("takutoki", 5)), Recorded.next(10, ("ShirasakaKoume", 5)), Recorded.next(15, ("ShirasakaKoume", 13)), Recorded.next(20, ("KoshimizuSachiko", 13)), Recorded.next(25, ("KoshimizuSachiko", 14)), Recorded.next(30, ("HoshiSyoko", 14)), Recorded.next(35, ("HoshiSyoko", 15)) ] scheduler.start() XCTAssertEqual(testObserver.events, expected) // Global function 'XCTAssertEqual(_:_:file:line:)' requires that '(String, Int)' conform to 'Equatable' } }XCTAssertEqualの行でコンパイル前にエラーが起きます。
余談ですが、
Xcode10.3
ではビルド時
に以下のエラーが発生します。
これだと原因がいまいちはっきりしませんね。
実際の直面したときは以下のエラーだったので、解決法がなかなか見つかりませんでした。Expression type '()' is ambiguous without more contextどうするか
エラーメッセージにある通り
Equatable
に準拠した型同士でassertするようにすれば良いです。
そこでstruct同士でassertを行うようにします。struct AssertStruct: Equatable { var name: String var age: Int public static func == (lhs: AssertStruct, rhs: AssertStruct) -> Bool { return lhs.name == rhs.name && lhs.age == rhs.age } }
Equatable
を準拠したAssertStruct
を作成して==
メソッドを実装します。
テストも書き直します。func testObservableWithTuple() { // assertする型をAssertStructに変更する let testObserver = scheduler.createObserver(AssertStruct.self) // userStreamをAssertStructに変換するoperatorを追加する userModel.userStream .map{ AssertStruct(name: $0.0, age: $0.1) } .bind(to: testObserver) .disposed(by: disposeBag) let nameTestObservable = scheduler.createColdObservable([ Recorded.next(10, "ShirasakaKoume"), Recorded.next(20, "KoshimizuSachiko"), Recorded.next(30, "HoshiSyoko") ]) let ageTestObservable = scheduler.createColdObservable([ Recorded.next(15, 13), Recorded.next(25, 14), Recorded.next(35, 15) ]) nameTestObservable .bind(to: userModel.name) .disposed(by: disposeBag) ageTestObservable .bind(to: userModel.age) .disposed(by: disposeBag) // 期待する値はAssertStructの型に変更する let expected: [Recorded<Event<AssertStruct>>] = [ Recorded.next(0, AssertStruct(name: "takutoki", age: 5)), Recorded.next(10, AssertStruct(name: "ShirasakaKoume", age: 5)), Recorded.next(15, AssertStruct(name: "ShirasakaKoume", age: 13)), Recorded.next(20, AssertStruct(name: "KoshimizuSachiko", age: 13)), Recorded.next(25, AssertStruct(name: "KoshimizuSachiko", age: 14)), Recorded.next(30, AssertStruct(name: "HoshiSyoko", age: 14)), Recorded.next(35, AssertStruct(name: "HoshiSyoko", age: 15)) ] scheduler.start() XCTAssertEqual(testObserver.events, expected) }最後に
上記の例では
こんなんテストする必要あるんか?
と思いますが、
実際の業務ではoperatorを複数噛ませたりdistinctUntilChangedなど入れたりと複雑な要件になりがちです。
テストでoutputを素早く確認できるのは安心ですね。
- 投稿日:2020-02-08T02:06:54+09:00
XCTestのAssertion一覧、テストファイルの使い方について
XCTestのAssertion一覧と使い方について、頭に入りきっていないので自身のためのカンペを兼ねてまとめました。
筆者が初学者のため、噛み砕いた記事になります。誤字脱字、補足、ここおかしいよ!等歓迎します。テストファイルの記入
import XCTest //XCTestのフレームワークをインポート
@testable import モジュール名 //モジュールをテスト用にインポート。モジュールが異なる場合、アクセス識別子がpublic,openでないとアクセスできないが、@testableとすることでinternal(明示的にアクセス識別子が書かれてない場合のデフォルト値)へアクセスできる。
class クラス名: XCTestCase //XCTestCaseを継承
XCTestのAssertion一覧
Assertion名の引数の最後にあるstringは試験失敗時のメッセージとして、文字列を入れることができる。試験の結果が確認しやすいよう状況に応じて使用する。
Boolean Assertions
Assertion名 説明 XCTAssert(expression,string) expressionがtrueであることを確認する XCTAssertTrue(expression,string) expressionがtrueであることを確認する(XCTAssertと同じ) XCTAssertFalse(expression,string) expressionがfalseであることを確認する Nil and Non-nil Assertions
Assertion名 説明 XCTAssertNil(expression,string) expressionがnilであることを確認する XCTAssertNotNil(expression,string) expressionがnilではないことを確認する Comparable Value Assertions
Assertion名 説明 XCTAssertGreaterThan(expresstion1,expression2,string) expression1 > expression2となっていることを確認する XCTAssertGreaterThanOrEqual(expresstion1,expression2,string) expression1 ≧ expression2となっていることを確認する XCTAssertLessThan(expresstion1,expression2,string) expression1 < expression2となっていることを確認する XCTAssertLessThanOrEqual(expresstion1,expression2,string) expression1 ≦ expression2となっていることを確認する Equality and Inequality Assertions
Assertion名 説明 XCTAssertEqual(c,string expression1とexpression2を比較し、一致することを確認する XCTAssertNotEqual(expresstion1,expression2,string) expression1とexpression2を比較し、一致しないことを確認する XCTAssertEqualObjects(c,string expression1とexpression2を比較し同一オブジェクトであることを確認する(Objective-C専用) XCTAssertNotEqualObjects(expresstion1,expression2,string) expression1とexpression2を比較し同一オブジェクトではないことを確認する(Objective-C専用) XCTAssertEqualWithAccuracy(expresstion1,expression2,accuracy,string) expression1とexpression2を比較しaccuracyの範囲以内に差が収まっていることを確認する XCTAssertNotEqualWithAccuracy(expresstion1,expression2,accuracy,string) expression1とexpression2を比較しaccuracyの範囲以内に差が収まっていないことを確認する NSException Assertions
Assertion名 説明 XCTAssertThrows(expression,string) expressionで例外が発生することを確認する XCTAssertNoThrows(expression,string) expressionで例外が発生しないことを確認する XCTAssertThrowsSpecific(expression,exception_class,string) expressionで、特定のクラス(exception_class)で例外が発生することを確認する XCTAssertNoThrowsSpecific(expression,exception_class,string) expressionで、特定のクラス(exception_class)で例外が発生しないことを確認する XCTAssertThrowsSpecificNamed(expression,exception_class,exception_name,string) expressionで、特定のクラス(exception_class)の特定の例外(exception_name)が発生することを確認する XCTAssertNoThrowsSpecificNamed(expression,exception_class,exception_name,string) expressionで、特定のクラス(exception_class)の特定の例外(exception_name)が発生しないことを確認する Failing Unconditionally
Assertion名 説明 XCTFail(string) テストを失敗させる