- 投稿日:2020-06-19T20:12:45+09:00
ウォークスルー後の通知許可とフォアグラウンド 通知を両立しようとしたら,うまくいかなかった件について
TL;DR
- よくあるチュートリアルの後に通知許可の処理をしたらアプリ内通知が動かなかった
- これをするときは
appDelegateにフォアグラウンド処理を書いてもダメ- 通知許可の
viewControllerにフォアグラウンド処理を書こう何があったのか(前提条件)
- appDelegateの
application(_ application: UIApplication, didFinishLaunchingWithOptions)に通知許可の処理を書かない -> アプリ立ち上げでは通知許可は出さない- ウォークスルー終了後のメイン画面で通知許可を記述.
- フォアグラウンド通知のための
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification:〜以下略)はappDelegateに書いていた.実行環境
機材 バージョン Xcode 11.5 iOS 13.5.1 Swift 5.2.4
解決方法
上に書いてあるとおり,appDelegateには通知系の処理を書かず,通知許可を取りたい画面のコードに,
MainViewController.swiftimport UIKit import UserNotifications class MainViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() //通知許可 let center = UNUserNotificationCenter.current() center.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] (granted, _) in if granted { let center = UNUserNotificationCenter.current() center.delegate = self } } //~~~~~~ (中略) //~~~~~~ } extension MainViewController: UNUserNotificationCenterDelegate { //フォアグラウンド通知処理 func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { //通知とサウンドのみ completionHandler([.alert, .sound]) } }と記述する.
最後に
なんでこんな仕様なんだろう・・・
原理や「お前の勘違いや」,「もっとこうすべき」などのご指摘がございましたらコメントにてよろしくお願いいたします.
- 投稿日:2020-06-19T17:41:24+09:00
Swiftで自作ライブラリを公開する手順
はじめに
先日,自作のSwift用のN次元行列演算ライブラリMatftを公開しました(記事,github).それにあたって,Matftのような自作ライブラリをXcodeで作成し,公開するまでの一連の流れをまとめたいと思います.
1. 作業用ディレクトリ
1.1 作業用ディレクトリの作成
Xcodeを開き,File > New > Swift Packageでライブラリ用のディレクトリを作成します.既存のディレクトリをライブラリとして公開するには,後述の
Package.swiftファイルを自分で作成すれば良いです.今回は例として,
Animalという名のライブラリを作成します.すると以下のような画面になり,
Animalディレクトリが作成されると思います.1.2 構成とその役割
↑で
Animalディレクトリが作成されると,その直下のディレクトリの構成はこのようになります.test├── Package.swift ├── README.md ├── Sources │ └── Animal │ └── Animal.swift └── Tests ├── LinuxMain.swift └── AnimalTests ├── XCTestManifests.swift └── AnimalTests.swiftPackage.swift
公式のライブラリ管理ツール
Swift Package Managerに対応させるためのファイルです.
ここに,ライブラリの基本情報,依存関係,ソース・テストディレクトリの指定等の情報を記述します.
- ライブラリの基本情報
.libraryにあるnameで名前,targetsでexecutableなディレクトリ(≒ソースディレクトリ)を指定します.Package.swiftproducts: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "Animal", targets: ["Animal"]), ]
- 依存関係
.packageをコメントアウトして,他パッケージのgithubのアカウントとバージョンを指定します.Package.swiftdependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), ],
- ソースディレクトリ・テストディレクトリの指定
targetsの.targetでbuild用のソースディレクトリを,.testTargetでbuild用のテストディレクトリを指定します.Package.swifttargets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "Animal", dependencies: []), .testTarget( name: "AnimalTests", dependencies: ["Animal"]), ]README.md
ご存知でしょうので割愛します.わかりやすく書きましょう.
Sources
ソースディレクトリです.ここに,クラスや関数を定義していきます.ソースの書き方は後述します.
Tests
テストディレクトリです.ライブラリを公開する場合は,テストは必須だと思います.面倒なので,省略したいところですが,バグを防ぐためにも必ずテストコードを書いてください.テストの書き方も後述します.
2. コードを書く
あとは,Sourcesで設定したディレクトリ(デフォルトでは
Sourcesディレクトリ)にコードを書いていくのですが,アクセス修飾子に注意する必要があります.2.1 アクセス修飾子
Swiftにはアクセス修飾子が5つあり,以下のようになっています.([Swift]アクセス修飾子より)
アクセス修飾子 説明 open 別モジュールから呼び出せる。継承やオーバーライドが可能 public 別モジュールから呼び出せるが継承やオーバーライドが不可能 internal 同モジュール内からのみ呼び出せる fileprivate 同ファイル内からのみ呼び出せる private 同スコープ内からのみ呼び出せる ここで注意したいのが,このアクセス修飾子を省略した場合は
internalとしてクラスや関数が定義されることです.つまり,クラスや関数の定義でアクセス修飾子を普段のクセで省略してしまうと,ライブラリのユーザーは,その省略されたクラスや関数を使うことができなくなります.例えば,
AnimalライブラリがDogクラスと2匹のDogの名前をつなげてprintする関数の実装が以下のようになされているとします.ご覧のようにアクセス修飾子は特に明示されていません.ライブラリ内class Dog{ var name: String } func get_2dogs_name(_ a: Dog, _ b: Dog){ print("\(a.name) and \(b.name)") }ユーザーlet pochi = Dog(name: "pochi") //これも let inu = Dog(name: "inu") //これも get_2dogs_name(pochi, inu) //これもコンパイラに怒られるこの場合は,
AnimalライブラリのユーザーはDogクラスにもget_2dogs_name関数にもアクセスできません.ユーザーがどちらも使えるようにするには,openやpublicをつけるようにしないといけません.ライブラリ内open class Dog{ public var name: String } public func get_2dogs_name(_ a: Dog, _ b: Dog){ print("\(a.name) and \(b.name)") }なので,ユーザーが悲しい思いをしないようにするためにも,例え
internalだったとしても,普段からアクセス修飾子は明示的につけるように癖づけておいた方が良さそうですね.2.2 テスト
テストの行い方
ある程度ソースコードを書いたら,適宜テストをしましょう.ありがたいことに,Xcodeにはテスト機能がしっかりと整備されています.これを使わない手はないので使いましょう.
#Swiftやライブラリの作成に限ったことではないですが,テストは本当に大事だと思います.
#もし,print文などでデバッグ・テストしている方は絶対にこの方法に変えた方が良いと断言します!以下のような実装を例(ソースディレクトリ)として,テストの仕方をまとめていきます.
Sources/Animal/Animal.swiftopen class Dog{ public var name: String internal var _id: Int public init(_ name: String){ self.name = name self._id = IDManager.shared.grant_id() } } internal class IDManager{ static let shared = IDManager() private var _id_num = -1 internal func grant_id() -> Int{ self._id_num += 1 return self._id_num } }まず,
LinuxMain.swiftとXCTestManifests.swiftはiOS以外でテストを実行するためのファイルらしいので,削除します.そして,AnimalTests.swiftにテストしたい内容を記述していきます.何をしているかはコメントを参照して下さい.Tests/AnimalTests/AnimalTests.swiftimport XCTest //テスト用のモジュールをインポート @testable import Animal //ソースをインポート.@testableをつけることによって,internal修飾子にもアクセスできるようになります. final class AnimalTests: XCTestCase {//XCTestCaseを継承したクラスを作成 func testInitializer() { //Prefixに"test"をつけた関数を実行します. do{ let pochi = Dog("pochi") XCTAssertEqual(pochi.name, "pochi") //pochi.nameが"pochi"と等しいか確認 XCTAssertEqual(pochi._id, 0) //pochi._idが0と等しいか確認 } do{ let shiro = Dog("shiro") XCTAssertEqual(shiro.name, "shiro") XCTAssertEqual(shiro._id, 1) } } }大事な点は,3点あります.
@testable付きimport
@testable import Animalで@testableをつけると,このファイル内では上述のinternal修飾子のクラスや関数にもアクセスできるようになります.
→Dogクラスのinternal var _idプロパティにアクセスできていることが分かりますね.
test***メソッド
XCTestCaseを継承したクラスでtest***メソッドをテスト関数として実行します.
→***に入るのはなんでもよく,テストしたいものに合わせて名前をつけると良いと思います.
XCTestの関数でテスト
XCTestの関数(例:XCTAssertEqual)を使って,テストします.
→例では,XCTAssertEqualで等しいかどうかをテストしていますが,他にもいろいろあり,よく使うものを次項にまとめています.詳しくは公式にまとめられているので,そちらを参照された方が良いと思います.恐らく名前でだいたい何をテストするかはわかると思います.テストの実行
テストコードが完成したら,
Cmd+Uでテストを実行できます.テスト成功例
成功すると,緑のチェックがつきます.
テスト失敗例
問答無用で名前を
"pochi"にするバグがあったとすると,Animal.swiftopen class Dog{ public var name: String internal var _id: Int public init(_ name: String){ self.name = "pochi" self._id = IDManager.shared.grant_id() } }以下のように赤のバツで示されます.
テスト関数一覧
Boolean判定系
関数名 テストする内容 XCTAssert 与えられたexpression引数がtrueを返すとOK. XCTAssertTrue 未確認ですが↑と同じだと思います. XCTAssertFalse 与えられたexpression引数がfalseを返すとOK. Nil判定系
関数名 テストする内容 XCTAssertNil 与えられたexpression引数がnilを返すとOK. XCTAssertNotNil 与えられたexpression引数がnilでないとOK. XCTUnwrap 与えられたexpression引数がnilでないかつ値をもつとOK. 同値判定系※
関数名 テストする内容 XCTAssertEqual 与えられた2つの引数(expression1とexpression2)のexpression1がexpression2と同じであるとOK. XCTAssertNotEqual 与えられた2つの引数(expression1とexpression2)のexpression1がexpression2と違うとOK. ※自前のクラスに対して,同値判定を行う場合は,そのクラスが
Equatableである必要があります.つまり,例のDogクラスを用いるのであれば,以下のようにBoolを返す関数が実装されていないといけません.Animal.swiftextension Dog: Equatable{ public static func == (lhs: Dog, rhs: Dog) -> Bool { return lhs.name == rhs.name } }比較判定系※
関数名 テストする内容 XCTAssertGreaterThan 与えられた2つの引数(expression1とexpression2)のexpression1がexpression2より大きければOK. XCTAssertGreaterOrThan 与えられた2つの引数(expression1とexpression2)のexpression1がexpression2以上であればOK. XCTAssertLessThan 与えられた2つの引数(expression1とexpression2)のexpression1がexpression2より小さければOK. XCTAssertLessOrThan 与えられた2つの引数(expression1とexpression2)のexpression1がexpression2以下であればOK. ※同値判定同様,自前のクラスである場合は,
Comparableである必要があります.
例:Animal.swiftextension Dog: Comparable{ public static func < (lhs: Dog, rhs: Dog) -> Bool { return lhs._id < rhs._id } }エラーハンドリング系
関数名 テストする内容 XCTAssertThrowsError 与えられたexpression引数がエラーを吐けばOK. XCTAssertNoThrow 与えられたexpression引数がエラーを吐かなければOK.
XCTestCaseのメソッド
関数名 内容 measure{} クロージャになっていて,この中に書かれた処理時間を計算してくれます.(下の例参照) setUp() test***メソッドを実行する前に呼び出される関数.
overrideして,事前に読み込んだデータなどをtest***メソッドに渡す場合などに使うと良さそう.tearDown() test***メソッドを実行した後に呼び出される関数.
具体的な使用例は使ったことがないので分かりません.
self.measure{}の使用例AnimalTestsfunc testInitializerPerformance(){ self.measure { for _ in 0..<10000{ let kuro = Dog("kuro") } } }consoleの出力Animal/Tests/AnimalTests/AnimalTests.swift:20: Test Case '-[AnimalTests.AnimalTests testInitializerPerformance]' measured [Time, seconds] average: 0.008, relative standard deviation: 13.806%, values: [0.010634, 0.007030, 0.006645, 0.007511, 0.007080, 0.007037, 0.007481, 0.008026, 0.007512, 0.007762], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100※デフォルトでは,debugモードでの実行時間を計測するので,より厳密な実行時間を計測するにはreleaseモードで行った方が良いと思います.
3. 公開
テストも無事完了したら,いよいよ公開です.Swiftでは,ライブラリの管理ツールがいくつかあり,ユーザーに使ってもらうためにも代表的なものには対応させたいところです.代表的なライブラリ管理ツールは以下の3つで,
- Swift Package Manager
- Carthage
- CocoaPods
今回はユーザーがこれらのツール経由でインストールできるように自作のライブラリを対応させたいと思います.
※基本的にはGithubでプログラムを管理しているものとして話を進めていきます.3.1 ライセンスファイルの作成
管理ツールに対応させる前に,ライセンスを決めます.Githubでは,有名どころのライセンスを記述したライセンスファイルを自動で作成することができます.独自のライセンス形態を組む必要がない方は以下の手順で,ライセンスファイルを作成できます.
”LICENSE”と打ち込むと右側に”Choose a lisence template”というボタンが出てくるので,それを押します.
有名どころのテンプレートが出てくるので,選べば自動で作成してくれます.
3.2 Swift Package Manager
Package.swiftファイルを作成すれば良いです.書き方はPackage.swiftを参照してください.
対応方法(開発者側)
- バージョンができたら,commitにタグをつけ,pushするだけで良いです.
git tag -a x.x.x -m "メッセージ" {commit id} # 例 # git tag -a 0.1.2 -m "猫追加" 3dqse1aaa9 git push --tags※x.x.xの形でないといけない
※ブランチだとたぶんうまくいかない
- 誤ってタグをつけた場合(タグの削除)
git tag --delete {tagname} # 例 # git tag --delete 0.1.2 git push origin :{tagname} # 例 # git push origin :0.1.2インストール方法(ユーザー側)
めちゃくちゃ簡単ですね!
- Project > Build Setting > + でgitのURL入れて,適宜選びます
- アップデート
3.3 Carthageへの対応
対応方法(開発者側)
- Carthageをインストールします.
brew update brew install carthage
- dynamic frameworkの用意(
〜.xcodeprojの作成)します.swift package generate-xcodeproj
作成された
〜.xcodeprojを開き,File > New Target > iOS > frameworkでframeworkを作成します.(既にtargetがある場合は省略)Product > Scheme > Manage SchemesからSchemeを以下のようにSharedにします.
- (ライブラリ直下で)ビルドします
cd {your library path} carthage build --no-skip-current
- 問題なければ,3.1 Swift Package Manager同様gitでタグ付けすればOKです.
インストール方法(ユーザー側)
これだけです.簡単ですね!
echo 'github "{ライブラリのOwnerのユーザー名}/{repository名}"' > Cartfile carthage update ###or append '--platform ios'3.4 CocoaPodsへの対応
対応方法(開発者側)
- cocoapodsをインストールします.
sudo gem install cocoapods
- podspecファイルを作成します.
{ライブラリ名}.podspecファイルが作成されます.cd {your library path} pod spec create {ライブラリ名}
- 適宜編集します.(コメントを参照してください)
※
spec.versionはgitのtagとは別に随時更新する必要があります.{ライブラリ名}.podspecPod::Spec.new do |spec| spec.name = "Matft" #ライブラリ名 spec.version = "0.1.1" #バージョン番号 spec.summary = "Numpy-like matrix operation library in swift" #要約 spec.homepage = "https://github.com/jjjkkkjjj/Matft" #ライブラリ名 spec.license = { :type => 'BSD-3-Clause', :file => 'LICENSE' } #ライセンス spec.author = "jjjkkkjjj" #作者名 spec.platform = :ios, "10.0" #プラットフォーム spec.swift_versions = "4.0" #Swiftのバージョン spec.pod_target_xcconfig = { 'SWIFT_VERSION' => '4.0' } #Swiftのバージョン.上との違いがわかっていません. spec.source = { :git => "https://github.com/jjjkkkjjj/Matft.git", :tag => "#{spec.version}" } #ソース情報を指定 spec.source_files = "Sources/**/*" #ソースファイルを正規表現で指定 end
{ライブラリ名}.podspecファイルの文法を確認します.pod lib lint※warningは無視することができます.
- cocoapodsに開発者情報を登録します.(初回のみ)
pod trunk register ~@mail 'name' # 例 # pod trunk register hoge@fuga.com 'hugaga'→メールが届くので,認証してください.
- cocoapodsへライブラリを登録(ライブラリの更新)
pod trunk push {ライブラリ名}.podspec #--allow-warnings※--allow-warningsはwarningを無視します.
何もなければおめでとう?的なメッセージが出力され,cocoapodsへの登録(更新)が完了します.
- 誤って登録(更新)した場合
pod trunk delete {ライブラリ名} x.x.x(version number) pod cache clean --allインストール方法(ユーザー側)
Podfileを作成します. (既にある場合は省略)pod init
pod '{ライブラリ名}'をPodfileに記述します.Podfiletarget 'your project' do pod 'Matft' end
- インストールします.
pod installおわりに
以上がライブラリの作成〜公開の手順です.お疲れ様でした.
参考
[Swift]アクセス修飾子
iOS アプリの Unit Test - Swift 編
自作ライブラリのSwift Package Manager(SwiftPM)対応
【iOS】オープンソースSwiftライブラリのつくり方
iOSアプリ開発にSwift Package Managerを使おう
自作ライブラリを CocoaPods と Carthage 両方に対応させる
- 投稿日:2020-06-19T16:39:36+09:00
SwiftでiOS開発している時に、Timerを利用したコードを、モダンでいい感じにUnitTestする方法
0. 初めに
画面タップイベントや、APIアクセスといった処理は、(ユーザーが連打するなどの場合を除けば)多くの場合一度きりの処理であることが多く、これらのロジックをUnitTestでテストするのは、そこまで大変ではないケースが多くを占めると思います。
しかしながら、一度きりではない処理を実装しないといけない(画面上にカウントダウンを表示させる場合など)ケースがあるのもまた事実かと思います。
いざTimerを使ったコードを書いてみると、実際に動かす場合はまぁよいにしろ、UnitTestを書くところで、
「どうやってテストしたらいいんだろう?
まさか待つわけにはいかないしな...」
といった感じで、思った以上にTimer部分のテストを書くのが難しく、Timer部分のUnitTestをスキップしてしまうことがあったり・・・するのではないでしょうか???
この記事では、そんなTimerのUnitTestが難しいなと思っている皆さんに向けて、僕はこんなふうにやったよという感じで、僕なりのテスト方法を提案しようと思います。
大まかに分けて下記のような流れで進めていきます。
ApplicationTimerProviderProtocolを作るTimerそのものではなく、ApplicationTimerProviderProtocolに依存するようにする(差し替え可能にする)- UnitTest用の
FakeApplcationiTimerProviderを作る- 実際のテストを書いてみる(ここではQuick/Nimbleを利用したコードを載せます)
1.
ApplicationTimerProviderProtocolを作るUnitTestが書きやすいプログラムとは外部から差し替え可能なクラスに依存しているクラスである、と僕は思います。
Timerを使おう〜!と思って下記のようにTimerを使ってしまっても実際の動きとしては良いのですが、Testコードを書こうとすると、TimeInterval分待つしか無くなってしまい、綺麗なUnitTestが書けないことになります。
import Foundation final class TimerClass { var timer: Timer? func check() { timer = Timer.scheduledTimer( withTimeInterval: 1.0, repeats: false, block: { timer in // do something... }) } }「実際にTimeInterval分待たなくてはならない」問題は、上記のプログラムが
Timer自体に依存しているため発生しています。そのため、下記のような
ApplicationTimerProviderProtocolを作成し、Timerへの依存を外しましょう。protocol ApplicationTimerProviderProtocol { func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer } struct ApplicationTimerProvider { } extension ApplicationTimerProvider: ApplicationTimerProviderProtocol { func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer { return Timer.scheduledTimer(withTimeInterval: interval, repeats: repeats, block: block) } }2.
ApplicationTimerProviderProtocolに依存させる作成した、
ApplicationTimerProviderProtocolを利用することで、UnitTestの時に差し替えを行うことが可能になります。UnitTestの時に差し替えを行うため、作成した
ApplicationTimerProviderを利用するようにTimerClassのコードを下記のように修正します、final class TimerClass { let timerProvider: ApplicationTimerProviderProtocol var timer: Timer? init(timerProvider: ApplicationTimerProviderProtocol) { self.timerProvider = timerProvider } func check() { timer = Timer.scheduledTimer( withTimeInterval: 1.0, repeats: false, block: { timer in // do something... }) } }3. UnitTest用の
FakeApplcationiTimerProviderを作る下記のようにUnitTestの際に利用する
FakeApplicationTimerProviderを作成します。class FakeApplcationiTimerProvider: ApplcationiTimerProviderProtocol { final class DummyTimer: Timer { override func invalidate() { } } var blocks = [(Timer) -> Void]() var scheduledTimer_callCount = 0 func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer { scheduledTimer_callCount += 1 blocks.append(block) return DummyTimer() } var fire_callCount = 0 func fire() { fire_callCount += 1 guard let first = blocks.first else { return } first(DummyTimer()) _ = blocks.removeFirst() } }4. 実際のテストを書いてみる
ここまでくれば下記のように、UnitTestを書くことができるようになっていると思います。
class TimerClassSpec: QuickSpec { override func spec() { var fakeTimerProvider: FakeApplcationiTimerProvider! var subject: TimerClass! beforeEach { fakeTimerProvider = FakeOrderAppliTimerProvider() subject = TimerClass(timerProvider: fakeTimerProvider) } it("check") { subject.check() fakeTimerProvider.fire() // ここまでくればクロージャーが発火しているのでテストをすることができる } } }終わりに
Timerを利用したことは何度かあったのですが、上手なUnitTestの書き方がわからず、悩んでおりました。
僕なりのテスト方法ではありますが皆様のお力になれれば幸いです。
- 投稿日:2020-06-19T16:30:09+09:00
Swiftで、UIButtonがタップされた時に呼ばれる関数に、パラメーターを渡す方法
.addTargetにカスタムパラメーターを渡すことは出来ない。
ボタンのtagプロパティに値をセットすることで、カスタムパラメーターのような事ができる。button.tag = 5 button.addTarget( self, action: #selector("buttonCliccked:"), forControlEvents: UIControlEvents.TouchUpInside ) @objc func buttonClicked(sender:UIButton) { if(sender.tag == 5){ var abc = "argOne" } print("hello") }
- 投稿日:2020-06-19T14:28:54+09:00
SwiftUIでOptional型のBindingの変数をunwrapする方法
SwiftUIでToDoアプリを作っていて、CoreDataに保存した日付データを持ってきて、表示しようと思いました。
この時の場合、日付をセットしない場合のことも考えて日付はOptional型で保存していました。
そして、日付がnilだった時(日付を設定してない時)に「日付をセットする」ボタンのようなものをつけたいと思いました。
そこで、最初に書いたコードが次のコードです。(body変数の中で、taskはCoreDataから取ってきたデータの入った変数です。task.dateはBinding<Date?>となっていました。)if task.date == nil { Button(action: { self.task.date = Date() }) { Text("Set Date") } } else { DatePicker("Date", selection: $task.date!) }すると次のようなエラーが出てしまいました。
Cannot convert value of type 'Binding<Date?>' to expected argument type 'Binding<Date>'そこで色々と検索して、とあるStackOverflowの質問を見ると、
Binding($task.date)!で同じようなことができるとわかりました。if task.date == nil { Button(action: { self.task.date = Date() }) { Text("Set Date") } } else { DatePicker("Date", selection: Binding($task.date)!) }これを実行してみると、目的通りに動きました。
最後に
何度か検索キーワードを変えてやっと出てきたので、また検索して時に出てこないかもしれないと思い、自分用のメモのような意味で書きました。
もっと良い方法や、間違い、改善点などがありましたら、コメントや編集リクエストなどをご気軽にお願いします。
- 投稿日:2020-06-19T14:14:22+09:00
[iOS]大規模プロダクトにfastlane matchを導入した話
モチベーション
iOSエンジニアにおいて,証明書は避けて通れない壁だと考えております.
今回はこの壁について,大規模プロダクトへ導入する際の課題・解決方法・導入手順をお話できればと思います.本記事の立ち位置としては,新卒研修時に取り組んだ内容 の延長戦といった形です.
課題
証明書を手動管理している
1. 1年に1度,証明書を手動で更新する必要がある
2. 開発メンバーや開発PCを追加する度に,Certificatesを作りProvisioningProfilesを更新する必要がある
3. 新規検証端末を追加する度に,ProvisioningProfilesを更新する必要がある上記による課題は何か
1. 手動で更新する時間と手間がかかる.また,ヒューマンエラーが起こる可能性がある
2. オンボーディングに時間がかかってしまう.特にインターン生にとっては,本質的ではない箇所に時間を割く必要がある
3. 新しい端末を追加するたびに,更新して再アップロードする必要がある課題の解決方法
fastalne matchを導入して解決する
導入後は,上記の課題が下記のように解決される
1. 指定のコマンドを叩くだけであり,再アップロード等は必要ない
2. 指定のコマンドを叩いてDLするだけであり,短時間で導入と更新が完了する
3. 指定のコマンドを叩して端末を追加する.追加後は,指定のコマンドを叩くようチームにアナウンスするmatchの導入手順
ローカルPC上で動作させる
開発メンバーが共通して使用できるAppleIDを作成する
Tips. 特定のメンバーのIDではなく,別途作成することにより,メンバーが変わった後も問題なく使用できる.
作成したAppleIDに対して,App Managerの役割を割り当てるApp Store Connect >「ユーザーとアクセス」> App > 該当アプリケーション
デベロッパリソース > Certificates, Identifiers & Profiles(証明書、ID、プロファイル)へのアクセスhttps://help.apple.com/app-store-connect/?lang=ja-jp#/deve5f9a89d7
「Certificates, Identifiers & Profile (証明書、ID & プロファイル)」へのアクセスは、組織のチームのメンバーである、App Manager または Developer の役割を持つユーザに追加で割り当てることができる特権です。この権限が追加されたユーザは、すべての App に関連付けられた証明書、ID、およびプロファイルを表示できます。
初めてアプリを作成する場合は, App IDの確保を確保する
fastlane produce -a bundleidentifder -i-iはStoreでの作成をスキップ
例えば,fastlane produce -a com.hoghoge.test -imatchの初期設定
fastlane match initfastlnae/Matchファイルが作成されます
Matchfileを編集する
Matchfile app_identifier(["tools.fastlane.app", "tools.fastlane.app2"]) username("user@fastlane.tools") # 作成したAppleIDapp_identifierは単数でも複数でもOK
単数の場合は,app_identifier("tools.fastlane.app")証明書を管理するGIthubのリポジトリを用意する
Tips. 証明書を管理する場所として,s3なども選択できますがgithubがオススメです
作成するリポジトリは,必ずprivateリポジトリにしましょう
証明書などのセンシティブなファイルはパブリックにするべきではありませんNote. Githubとの接続方法は,sshにしてください.httpsでは後で上手くいきません
作成
develop
fastlane match developmentadhoc
fastlane match adhocstore
fastlane match appstore初めて作成する時は,作成するPCのパスワードを求められます
2回目以降は下記を参照しているはずです
/Users/username/Library/Keychains/login.keychain-db証明書の管理場所として,先程作成したリポジトリのURLを入力します.
このとき,必ずssh版のURLにしてください.
その後,証明書を管理するパスワードを入力します.
ここでいうパスワードとは,証明書を追加・更新・取得するためのパスワードですTips. ここで入力したパスワードの管理方法は予めチームで決めておいて下さい.
Note. matchでは,Certificatesを1種類につき1つしか持てません.なので,既にCertificatesがある方は,削除する必要があります.そして,Certificatesが削除された瞬間に,該当のProvisioningProfilesは失効します.覚悟してください.
ただし,失効してもStoreでの配信が直ちに停止するわけではありません.https://developer.apple.com/jp/support/certificates
iOS配布用証明書(App Store)
Apple Developer Programのメンバーシップが有効な場合は、App Storeで公開されている既存のAppに影響が及ぶことはありません。ただし、期限切れまたは無効になっている証明書を使用して署名した新しいAppやアップデート版は、App Storeに提出できなくなります。
iOS配布用証明書(社内用App)
この証明書を使用して署名されたAppは、今後実行できなくなります。新しいバージョンのAppは、新しい証明書で署名して配信する必要があります。取得
develop
fastlane match development --readonlyadhoc
fastlane match adhoc --readonlystore
fastlane match appstore --readonlyTips.
--readonlyは読み取り専用を意味します.証明書を作成する方以外は,--readonlyを付けて実行しましょう削除
development
fastlane match nuke developmentadhoc and store
fastlane match nuke distribution削除できたら,再度作ります
Tips. 特定のファイルだけを消すことはできません.初めて導入する時にのみ実行することをオススメします更新
証明書が切れると下記のエラーが出ます
[!] Your certificate 'XXXXXXXXXX.cer' is not valid, please check end date and renew it if necessary
更新方法は大きく分けて2つあります
1. nukeコマンドを叩いた後に,再度作成する
2. Githubのリポジトリをクローンして該当のファイルを削除して,pushするTips. 特定のファイルだけを消すことはできません.初めて導入する時にのみ実行することをオススメします
の理由から,2. Githubのリポジトリをクローンして該当のファイルを削除して,pushする をオススメします検証端末の追加
fastlane run register_device name:"<name>" udid:"<udid>"追加後は再取得する
CI上で動作させる
Fastfile, Matchfile, Gymfileを編集する
CI上で動作させるには,上記に加えてもう少し作業が必要です
下記は
XcodeでDevelop/Staging/Release環境を上手に切り分ける方法
を参考に,development, adhoc, storeで環境を分けていますまた,下記の例はadhocを想定していますが,store版も同様に必要です
# Fastfile lane :adhoc_archive do // 署名用のkychain作成とmatchを追記する if is_ci? create_keychain( name: ENV['MATCH_KEYCHAIN_NAME'], password: ENV['MATCH_KEYCHAIN_PASSWORD'], default_keychain: true, timeout: 1800, ) end match gym // build_ios_appと同義# Matchfile for_lane :adhoc_archive do app_identifier "com.hoghoge.test" type "adhoc" readonly true end# Gymfile for_lane :adhoc_archive do scheme "mtchtests" configuration 'Release_Adhoc' export_method "ad-hoc" export_options({ provisioningProfiles: { "com.hoghoge.test" => "match AdHoc com.hoghoge.test" // ここを出来上がったprovisioningProfilesの名前にする } }) endTips. ENVは環境変数です. 証明書を追加・更新・取得するためのパスワードとセットで,設定しておきましょう
ここまでできて,ようやくCI上でも動作させる環境が整いました!
この記事では,fastlaneの詳しい設定方法やCIのセットアップはご紹介しませんプロダクトへの導入段階
チームで意思決定をする
各種パスワードの管理方法や,導入スケジュールをすり合わせておきましょうdevelopからスタートする
最も影響範囲が小さいです.Matchfileは出来上がりますが,adhocやstore版に影響がありません.
また,Gymfileを含めたfastlane周りのファイルを変更する必要がありません
小さく始めましょうadhocとstoreも移行する
ここが最高に緊張感が高いです.
なぜなら,adhoc配信が止まるので,QAメンバーが困る + 最悪次回リリースが止まります.
大きく深呼吸して望みましょうTips. enterprise版が利用できるプロダクトは,他開発メンバーとQAメンバーにenterprise版を利用するようにアナウンスしましょう.また,導入スケジュールを事前にアナウンスして混乱を最小限にしましょう
その他のTips
- 取得コマンド等は,Makefiileでまとめておくと楽です
- 本番を想定して,近しい環境で予め試しておくことが大切です
実際に導入してみて
個人開発や新規開発では試行錯誤しながら導入できますが,既に稼働している大規模プロダクトではそうもいきません.念入りな準備と覚悟が必要です
[iOS] Bitrise + fastlane + App Distributionで配布環境を作成 の延長戦のような気分で実践しましたが,緊張感が個人開発とは天と地の差がありました
ただそれでも,掲げた課題を解決することができてよかったです
参考文献
- 投稿日:2020-06-19T01:23:41+09:00
【プログラミング初心者】Swift基礎~継承~
はじめに
今回はオブジェクト指向の考え方の1つである継承の使い方について紹介します。
継承がどういうものかはこちらで紹介しているので参照してください。継承
定義の仕方
クラスの継承の構文以下となります。
class 子クラス: 親クラス { }具体的な実装方法を紹介します。
犬クラス、猫クラスを子クラスとし、親クラスとして動物クラスを継承しているとします。
この場合以下の実装となります。Animal.swiftclass Animal { }Dog.swiftclass Dog: Animal { }Cat.swiftclass Cat: Animal { }継承していてもインスタンスの生成方法は変わりません。
let dog = Dog() let cat = Cat()メソッドを継承する
次に
Animalクラスにeat()メソッドを定義します。Animal.swiftclass Animal { func eat() { print("もぐもぐ") } }すると
Animalを継承したDog、Catはメソッドを定義しなくてもeat()を呼び出すことができます。let dog = Dog() let cat = Cat() dog.eat() cat.eat()実行結果もぐもぐ もぐもぐこのように継承することで処理を共通化することができます。
またクラス内で呼び出す場合は自分自身のメソッドとして扱えるので
self.eat()で呼び出せます。個別の処理を実装するにはそれぞれのクラスで実装します。
DogにsitDown()メソッドを定義します。Dog.swiftclass Dog: Animal { func sitDown() { print("おすわり") } }let dog = Dog() dog.sitDown()実行結果おすわりこれは
Dogクラスで定義されたメソッドなので当然Catでは呼び出せない処理になります。let cat = Cat() cat.sitDown() // error: Value of type 'Cat' has no member 'sitDown'プロパティを継承する
プロパティもメソッド同様に継承されます。
Animal.swiftclass Animal { var name: String init(name: String) { self.name = name } }Dog.swiftclass Dog: Animal { }Cat.swiftclass Cat: Animal { }let dog = Dog(name: "ポチ") let cat = Cat(name: "ミケ") print(dog.name) print(cat.name)実行結果ポチ ミケプロパティもメソッド同様それぞれの子クラスで定義したプロパティはそのクラスのオブジェクトしか持ちません。
オーバーライド
継承したメソッドは子クラスで上書きすることができます。
これをオーバーライドと言います。オーバーライドの構文は以下となります。
override func 上書きするメソッド() { }メソッドの頭に
overrideを付けるだけです。
Animalのeat()メソッドをオーバーライドしてみます。Animal.swiftclass Animal { func eat() { print("もぐもぐ") } }Dog.swiftclass Dog: Animal { override func eat() { print("ドッグフードをモグモグ") } }Cat.swiftclass Cat: Animal { }それぞれの子クラスから
eat()を呼び出してみましょう。let dog = Dog() let cat = Cat() dog.eat() cat.eat()実行結果ドッグフードをモグモグ もぐもぐ
dog.eat()ではオーバライドした処理が、cat.eat()ではAnimalで定義した処理が実行されていることがわかります。
このようにメソッドを書き換えることができます。ですがこれではあまり共通化した意味がないように思えます。
よくあるのはオーバーライドし、共通処理+クラスの独自処理というような実装です。
Animalを継承したHumanクラスを作成します。
Humanのeat()では食べる前に「いただきます」、食べ終わったら「ごちそうさまでした」というようにします。Human.swiftclass Human: Animal { override func eat() { print("いただきます") super.eat() print("ごちそうさまでした") } }let human = Human() human.eat()実行結果いただきます もぐもぐ ごちそうさまでした
super.eat()で親クラスであるAnimalのeat()メソッドを呼び出しています。
自分自身を使うときはselfを使います。
一方でsuperは親クラスを表します。このように共通処理に独自の処理を追加し、クラスを拡張していくことで効率的にコーディングしていきます。
イニシャライザ
継承した時のイニシャライザ(
init)の定義の仕方は少し注意が必要です。
こういうものだと覚えてください。継承していないときのイニシャライザは以下のように定義しますね。
class SuperClass { var property: Int init(property: Int) { self.property = property } } let object = SuperClass(property: 0)
SuperClassクラスを継承したSubClassをプロパティなしで定義します。
この場合SubClassにイニシャライザが不要なので特に意識することはありません。class SubClass: SuperClass { } let subObject = SubClass(property: 0)これは勝手に
SuperClassのinit()が呼ばれます。次は
SubClassに新しくプロパティを追加します。class SubClass: SuperClass { var subProperty: Int }すると「Class 'SubClass' has no initializers」とイニシャライザがいないですよと怒られます。
これは継承していなくてもプロパティの初期値が決まっていないので発生するエラーです。
イニシャライザを追加してあげます。class SubClass: SuperClass { var subProperty: Int init(subProperty: Int) { self.subProperty = subProperty } }これでいいかと思いきや「'super.init' isn't called on all paths before returning from initializer」とまだ怒られます。
イニシャライザの中でsuper.initが呼ばれていないと言われています。
親クラスのプロパティが初期化されていないため怒られています。
継承した場合、子クラスのイニシャライザの中で親クラスのイニシャライザも呼ばなければいけません。子クラスのイニシャライズを以下のように修正します。
class SubClass: SuperClass { var subProperty: Int init(property: Int, subProperty: Int) { self.subProperty = subProperty super.init(property: property) } } let subObject = SubClass(property: 0, subProperty: 0)これでエラーが発生しなくなりました。
ここで注意が必要なのですが、super.initは子クラスのプロパティ全ての初期化が終わった後に呼び出してください。
先に親クラスをのイニシャライザを呼び出すとまた怒られます。・プロパティは何かしらの値で初期化する必要がある
・子クラスのイニシャライザの最後に親クラスのイニシャライザを呼び出す必要がある現状はこの2点を覚えておけば大丈夫です。
親クラスのselfでのメソッド呼び出し
以下のようにクラス定義します。
class SuperClass { func testMethod1() { print("SuperClass testMethod1 is called") self.testMethod2() } func testMethod2() { print("SuperClass testMethod2 is called") } }このクラスの
testMethod1()を呼び出した結果はどうなるでしょう。let object = SuperClass() object.testMethod1()実行結果SuperClass testMethod1 is called SuperClass testMethod2 is called
testMethod1()の中でself.testMethod2()が呼ばれているのでそれぞれのメソッドが実行されていますね。
これは当然普段通りです。では以下の場合はどうでしょうか。
class SuperClass { func testMethod1() { print("SuperClass testMethod1 is called") self.testMethod2() } func testMethod2() { print("SuperClass testMethod2 is called") } } class SubClass: SuperClass { override func testMethod1() { super.testMethod1() print("SubClass testMethod1 is called") } } let subClass = SubClass() subClass.testMethod1()子クラスで
testMethod1()をオーバーライドし親クラスのtestMethod1()を呼んでいるので以下の結果となります。実行結果SuperClass testMethod1 is called SuperClass testMethod2 is called SubClass testMethod1 is calledこれもここまで説明してきた内容です。
では以下のように
testMethod2()をオーバーライドするとどうなるでしょうか。class SuperClass { func testMethod1() { print("SuperClass testMethod1 is called") self.testMethod2() } func testMethod2() { print("SuperClass testMethod2 is called") } } class SubClass: SuperClass { override func testMethod1() { super.testMethod1() print("SubClass testMethod1 is called") } override func testMethod2() { print("SubClass testMethod2 is called") } } let subClass = SubClass() subClass.testMethod1()これを実行すると以下のように出力されます。
実行結果SuperClass testMethod1 is called SubClass testMethod2 is called SubClass testMethod1 is called親クラスで呼んでいる
self.testMethod2()は子クラスのtestMethod2()を実行していることがわかります。
selfは自分自身のクラスではなくインスタンスを意味します。
つまりlet subClass = SubClass()で作成したsubClassを指します。
クラス定義としてはSuperClassとSubClassはそれぞれ別クラスですが、インスタンスはsubClassただ1つです。
親クラスでselfとしていてもそれはsubClassインスタンスを指します。
testMethod2()はオーバーライドされているので子クラスのtestMethod2()の処理を実行するため「SubClass testMethod2 is called」が表示されているというわけです。以下のパターンも試してみます。
class SuperClass { func testMethod1() { print("SuperClass testMethod1 is called") self.testMethod2() } func testMethod2() { print("SuperClass testMethod2 is called") } } class SubClass: SuperClass { override func testMethod1() { super.testMethod1() print("SubClass testMethod1 is called") } override func testMethod2() { super.testMethod2() print("SubClass testMethod2 is called") } } let subClass = SubClass() subClass.testMethod1()今度は
SubClassのtestMethod2()でsuper.testMethod2()を呼んでいるので親クラスのtestMethod2()も呼ばれます。実行結果SuperClass testMethod1 is called SuperClass testMethod2 is called SubClass testMethod2 is called SubClass testMethod1 is calledこのように意図しない挙動をする可能性ががあるのでメソッドがどのような順序で呼ばれるのかに注意した上で実装してください。
とはいえ最初のうちはそこまで気にすることはないと思うので頭に留めて置く程度でもいいかと思います。最後に
今回はSwiftの継承の実装方法について紹介しました。
ですが少し難しい部分があったり、そもそもいつ継承を使うかなどはある程度自分で実装していき経験を積まなければ判断できないかと思います。iOSアプリ開発では画面オブジェクトは
UIViewControllerを継承し実装します。
そのため継承という考え方自体は押さえておく必要はありますが、UIViewControllerを親クラスとして使用するだけです。自分で親クラスを作るわけではありません。
このあたりはある程度パターン化されているので形として覚え、なんとなくこんなことをしてるのかなー程度がわかればいいかと思います。今回の内容は以上です。
本記事とは別でプログラミング未経験からiOSアプリ開発が行えるようになることを目的とした記事を連載しています。
連載は以下にまとめていますのでそちらも是非もご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684











