20200619のSwiftに関する記事は7件です。

ウォークスルー後の通知許可とフォアグラウンド 通知を両立しようとしたら,うまくいかなかった件について

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.swift
import 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])
    }
}

と記述する.

最後に

なんでこんな仕様なんだろう・・・
原理や「お前の勘違いや」,「もっとこうすべき」などのご指摘がございましたらコメントにてよろしくお願いいたします.

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

Swiftで自作ライブラリを公開する手順

はじめに

先日,自作のSwift用のN次元行列演算ライブラリMatftを公開しました(記事github).それにあたって,Matftのような自作ライブラリをXcodeで作成し,公開するまでの一連の流れをまとめたいと思います.

1. 作業用ディレクトリ

1.1 作業用ディレクトリの作成

Xcodeを開き,File > New > Swift Packageでライブラリ用のディレクトリを作成します.既存のディレクトリをライブラリとして公開するには,後述のPackage.swiftファイルを自分で作成すれば良いです.

image.png

今回は例として,Animalという名のライブラリを作成します.

image.png

すると以下のような画面になり,Animalディレクトリが作成されると思います.

image.png

1.2 構成とその役割

↑でAnimalディレクトリが作成されると,その直下のディレクトリの構成はこのようになります.

test
├── Package.swift
├── README.md
├── Sources
│   └── Animal
│       └── Animal.swift
└── Tests
    ├── LinuxMain.swift
    └── AnimalTests
        ├── XCTestManifests.swift
        └── AnimalTests.swift

Package.swift

公式のライブラリ管理ツールSwift Package Managerに対応させるためのファイルです.
ここに,ライブラリの基本情報,依存関係,ソース・テストディレクトリの指定等の情報を記述します.

  • ライブラリの基本情報

.libraryにあるnameで名前,targetsでexecutableなディレクトリ(≒ソースディレクトリ)を指定します.

Package.swift
products: [
        // 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.swift
dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
  • ソースディレクトリ・テストディレクトリの指定

targets.targetでbuild用のソースディレクトリを,.testTargetでbuild用のテストディレクトリを指定します.

Package.swift
targets: [
        // 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関数にもアクセスできません.ユーザーがどちらも使えるようにするには,openpublicをつけるようにしないといけません.

ライブラリ内
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.swift
open 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.swiftXCTestManifests.swiftはiOS以外でテストを実行するためのファイルらしいので,削除します.そして,AnimalTests.swiftにテストしたい内容を記述していきます.何をしているかはコメントを参照して下さい.

Tests/AnimalTests/AnimalTests.swift
import 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でテストを実行できます.

テスト成功例

成功すると,緑のチェックがつきます.

image.png

テスト失敗例

問答無用で名前を"pochi"にするバグがあったとすると,

Animal.swift
open class Dog{
    public var name: String
    internal var _id: Int
    public init(_ name: String){
        self.name = "pochi"
        self._id = IDManager.shared.grant_id()
    }
}

以下のように赤のバツで示されます.

image.png

テスト関数一覧

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.swift
extension 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.swift
extension 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{}の使用例
AnimalTests
func 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では,有名どころのライセンスを記述したライセンスファイルを自動で作成することができます.独自のライセンス形態を組む必要がない方は以下の手順で,ライセンスファイルを作成できます.

image.png

”LICENSE”と打ち込むと右側に”Choose a lisence template”というボタンが出てくるので,それを押します.

image.png

有名どころのテンプレートが出てくるので,選べば自動で作成してくれます.

image.png

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入れて,適宜選びます

Build Setting
select

  • アップデート

update

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にします.

scheme

  • (ライブラリ直下で)ビルドします
cd {your library path}
carthage build --no-skip-current

インストール方法(ユーザー側)

これだけです.簡単ですね!

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とは別に随時更新する必要があります.

{ライブラリ名}.podspec
Pod::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に記述します.
Podfile
target '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 両方に対応させる

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

SwiftでiOS開発している時に、Timerを利用したコードを、モダンでいい感じにUnitTestする方法

0. 初めに

画面タップイベントや、APIアクセスといった処理は、(ユーザーが連打するなどの場合を除けば)多くの場合一度きりの処理であることが多く、これらのロジックをUnitTestでテストするのは、そこまで大変ではないケースが多くを占めると思います。

しかしながら、一度きりではない処理を実装しないといけない(画面上にカウントダウンを表示させる場合など)ケースがあるのもまた事実かと思います。

いざTimerを使ったコードを書いてみると、実際に動かす場合はまぁよいにしろ、UnitTestを書くところで、

「どうやってテストしたらいいんだろう? :thinking: まさか待つわけにはいかないしな...」

といった感じで、思った以上にTimer部分のテストを書くのが難しく、Timer部分のUnitTestをスキップしてしまうことがあったり・・・するのではないでしょうか???

この記事では、そんなTimerのUnitTestが難しいなと思っている皆さんに向けて、僕はこんなふうにやったよという感じで、僕なりのテスト方法を提案しようと思います。

大まかに分けて下記のような流れで進めていきます。

  1. ApplicationTimerProviderProtocol を作る
  2. Timer そのものではなく、 ApplicationTimerProviderProtocol に依存するようにする(差し替え可能にする)
  3. UnitTest用の FakeApplcationiTimerProvider を作る
  4. 実際のテストを書いてみる(ここでは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の書き方がわからず、悩んでおりました。

僕なりのテスト方法ではありますが皆様のお力になれれば幸いです。

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

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")
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIでOptional型のBindingの変数をunwrapする方法

SwiftUIでToDoアプリを作っていて、CoreDataに保存した日付データを持ってきて、表示しようと思いました。
この時の場合、日付をセットしない場合のことも考えて日付はOptional型で保存していました。
そして、日付がnilだった時(日付を設定してない時)に「日付をセットする」ボタンのようなものをつけたいと思いました。
そこで、最初に書いたコードが次のコードです。(body変数の中で、taskはCoreDataから取ってきたデータの入った変数です。task.dateBinding<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)!)
}

これを実行してみると、目的通りに動きました。

最後に

何度か検索キーワードを変えてやっと出てきたので、また検索して時に出てこないかもしれないと思い、自分用のメモのような意味で書きました。
もっと良い方法や、間違い、改善点などがありましたら、コメントや編集リクエストなどをご気軽にお願いします。

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

[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 -i

matchの初期設定

fastlane match init

fastlnae/Matchファイルが作成されます

Matchfileを編集する

Matchfile
 app_identifier(["tools.fastlane.app", "tools.fastlane.app2"])
 username("user@fastlane.tools") # 作成したAppleID

app_identifierは単数でも複数でもOK
単数の場合は,app_identifier("tools.fastlane.app")

証明書を管理するGIthubのリポジトリを用意する

Tips. 証明書を管理する場所として,s3なども選択できますがgithubがオススメです

作成するリポジトリは,必ずprivateリポジトリにしましょう
証明書などのセンシティブなファイルはパブリックにするべきではありません

Note. Githubとの接続方法は,sshにしてください.httpsでは後で上手くいきません

作成

develop

fastlane match development

adhoc

fastlane match adhoc

store

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 --readonly

adhoc

fastlane match adhoc --readonly

store

fastlane match appstore --readonly

Tips. --readonlyは読み取り専用を意味します.証明書を作成する方以外は,--readonlyを付けて実行しましょう

削除

development

fastlane match nuke development

adhoc 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の名前にする
         }
     })
 end

Tips. ENVは環境変数です. 証明書を追加・更新・取得するためのパスワードとセットで,設定しておきましょう

ここまでできて,ようやくCI上でも動作させる環境が整いました!
この記事では,fastlaneの詳しい設定方法やCIのセットアップはご紹介しません

プロダクトへの導入段階

  1. チームで意思決定をする
    各種パスワードの管理方法や,導入スケジュールをすり合わせておきましょう

  2. developからスタートする
    最も影響範囲が小さいです.Matchfileは出来上がりますが,adhocやstore版に影響がありません.
    また,Gymfileを含めたfastlane周りのファイルを変更する必要がありません
    小さく始めましょう

  3. adhocとstoreも移行する
    ここが最高に緊張感が高いです.
    なぜなら,adhoc配信が止まるので,QAメンバーが困る + 最悪次回リリースが止まります.
    大きく深呼吸して望みましょう

Tips. enterprise版が利用できるプロダクトは,他開発メンバーとQAメンバーにenterprise版を利用するようにアナウンスしましょう.また,導入スケジュールを事前にアナウンスして混乱を最小限にしましょう

その他のTips

  • 取得コマンド等は,Makefiileでまとめておくと楽です
  • 本番を想定して,近しい環境で予め試しておくことが大切です

実際に導入してみて

個人開発や新規開発では試行錯誤しながら導入できますが,既に稼働している大規模プロダクトではそうもいきません.念入りな準備と覚悟が必要です

[iOS] Bitrise + fastlane + App Distributionで配布環境を作成 の延長戦のような気分で実践しましたが,緊張感が個人開発とは天と地の差がありました

ただそれでも,掲げた課題を解決することができてよかったです

参考文献

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

【プログラミング初心者】Swift基礎~継承~

はじめに

今回はオブジェクト指向の考え方の1つである継承の使い方について紹介します。
継承がどういうものかはこちらで紹介しているので参照してください。

継承

定義の仕方

クラスの継承の構文以下となります。

class 子クラス: 親クラス {

}

具体的な実装方法を紹介します。
犬クラス、猫クラスを子クラスとし、親クラスとして動物クラスを継承しているとします。
この場合以下の実装となります。

Animal.swift
class Animal {

}
Dog.swift
class Dog: Animal {

}
Cat.swift
class Cat: Animal {

}

継承していてもインスタンスの生成方法は変わりません。

let dog = Dog()
let cat = Cat()

メソッドを継承する

次にAnimalクラスにeat()メソッドを定義します。

Animal.swift
class Animal {
    func eat() {
        print("もぐもぐ")
    }
}

するとAnimalを継承したDogCatはメソッドを定義しなくてもeat()を呼び出すことができます。

let dog = Dog()
let cat = Cat()

dog.eat()
cat.eat()
実行結果
もぐもぐ
もぐもぐ

このように継承することで処理を共通化することができます。

またクラス内で呼び出す場合は自分自身のメソッドとして扱えるのでself.eat()で呼び出せます。

個別の処理を実装するにはそれぞれのクラスで実装します。
DogsitDown()メソッドを定義します。

Dog.swift
class 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.swift
class Animal {
    var name: String

    init(name: String) {
        self.name = name
    }
}
Dog.swift
class Dog: Animal {

}
Cat.swift
class Cat: Animal {

}
let dog = Dog(name: "ポチ")
let cat = Cat(name: "ミケ")

print(dog.name)
print(cat.name)
実行結果
ポチ
ミケ

プロパティもメソッド同様それぞれの子クラスで定義したプロパティはそのクラスのオブジェクトしか持ちません。

オーバーライド

継承したメソッドは子クラスで上書きすることができます。
これをオーバーライドと言います。

オーバーライドの構文は以下となります。

override func 上書きするメソッド() {

}

メソッドの頭にoverrideを付けるだけです。

Animaleat()メソッドをオーバーライドしてみます。

Animal.swift
class Animal {
    func eat() {
        print("もぐもぐ")
    }
}
Dog.swift
class Dog: Animal {
    override func eat() {
        print("ドッグフードをモグモグ")
    }
}
Cat.swift
class Cat: Animal {

}

それぞれの子クラスからeat()を呼び出してみましょう。

let dog = Dog()
let cat = Cat()

dog.eat()
cat.eat()
実行結果
ドッグフードをモグモグ
もぐもぐ

dog.eat()ではオーバライドした処理が、cat.eat()ではAnimalで定義した処理が実行されていることがわかります。
このようにメソッドを書き換えることができます。

ですがこれではあまり共通化した意味がないように思えます。
よくあるのはオーバーライドし、共通処理+クラスの独自処理というような実装です。

Animalを継承したHumanクラスを作成します。
Humaneat()では食べる前に「いただきます」、食べ終わったら「ごちそうさまでした」というようにします。

Human.swift
class Human: Animal {
    override func eat() {
        print("いただきます")
        super.eat()
        print("ごちそうさまでした")
    }
}
let human = Human()
human.eat()
実行結果
いただきます
もぐもぐ
ごちそうさまでした

super.eat()で親クラスであるAnimaleat()メソッドを呼び出しています。
自分自身を使うときは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)

これは勝手にSuperClassinit()が呼ばれます。

次は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を指します。
クラス定義としてはSuperClassSubClassはそれぞれ別クラスですが、インスタンスは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()

今度はSubClasstestMethod2()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

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