くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

Rx(Swift)とは?

今回はリアクティブプログラミングについてまとめようと思います

RxSwiftをベースに説明しますが、概念の部分はAndroidや他のプログラミング言語でリアクティブプログラミングを行う場合でも共通すると思います

Rxとは?

Rx(Reactive X)とは、「オブザーバパターン」「イテレータパターン」「関数型プログラミング」の概念を実装している機能です

Rxを導入するメリットは、「値の変化を検知できる」「非同期の処理を簡潔に書ける」です

値の変化というのは変数の変化やUIの変化も含まれます(ボタンのタッチなども含める)

Observerパターン

Rx全般において、処理の流れの基本的な設計として用いられるものの1つに、Observerパターンと呼ばれるデザインパターンがあります

Observerパターンとは、あるオブジェクトで発生したイベントを別のオブジェクト(オブザーバー)へ通知する処理で用いられるデザインパターンの1つです

Rx関連でのイベントとは、画面上で発生するタップイベントの他に、変数の値が変わった・APIから値が取得できたなどプログラム内におけるオブジェクトの状態の変化全てを指します

RxSwiftでは、イベントストリームの中で流れて来るデータの部分をデータストリームと言われています。Observerパターンでデータストリームを扱うクラスには、次の2つのクラスがあります

  • Observable:監視可能なものを表すクラス
  • Observer:Observableからデータストリームを受け取るクラス

Observable監視可能なものを表すクラスで通知を行うことができます。一方、Observerは監視を行う側です。

Observableを「出版」、Observerを「購読」と呼ぶ場合もあります。それは次のような由来があります

出版社が事業を始め、定期的な刊行物を発行します。購読者は、出版社に購読の申し込みをすると定期的に刊行物が届けられます。 購読を解除すると、解除日以降は刊行物は届けられません。 出版者が新聞や雑誌などの定期刊行物を発行し、それを誰でも好きなタイミングで購読したり辞めたりできることと同じように考えるものです。

用語説明

Observable

Observerパターンで説明したObserverはイベントを流せるクラスで、Observableはイベントを受け取れるクラスです

RxSwiftでのイベントは次の3種類があります

イベントの種別 発生するタイミング 渡される値
next(Element) イベントが発生した際に何度でも イベント派生元の値(データストリーム)
error エラーが発生した際に一度だけ エラー情報
completed 処理が完了した際に一度だけ なし

Observableはイベントを受け取れるのですが、イベントを受け取る際にはsubscribe(_:)メソッドを利用します。次のソースコードはRxSwiftの一例です

let disposable = textField.rx.text.asObservable()
.subscribe(
  onNext: { value in
    // 通常イベント発生時の処理
  },
  onError: { error in
    // エラー発生時の処理
  },
  onCompleted: {
    // 完了時の処理
  }
)

textField.rx.text.asObservable()が Observableです。Observableが値が更新されたというイベントを受け取って、Observerはイベントに対して登録された処理を行います(今回だとonNext: { value in }など)

先程のソースコードtextField.rx.text.asObservable().subscribe()まではObservableで、onNext・onError・onCompletedの処理はObserverです(Observerはイベントを流せます)

Operator

RxにはObservableで作ったストリームを操作する機能が提供されています

ストリーム(Observable)を変換・合成・生成などの操作をオペレーターと言います

オペレーターにはmapfilterなど多くのものがあります

いくつかみていこうと思います。まずはmapです。mapは流れてきた値を変換するために使います。 下記例では入力された Int 型の値を 10倍にしています

_ = Observable.of(1,2,3)
      .map { $0 * 10 }
      .subscribe(onNext: {
          print("onNext: ", $0)
      })

// 出力結果
onNext:  10
onNext:  20
onNext:  30

次はfilterです。filterは流れてきた値で条件に合うものだけを残すために使います。 下記例では4で割り切れる数字のみを残しています

 = Observable.of(1, 2, 3, 4, 5)
 .filter { $0 % 4 == 0 }
 .subscribe(onNext: {
  print($0) // => 4
 })

そして、flatMapについてです。flatMapは今まで触れたmapやfilterとは違いがあります。それは、新たなObservableを発行するかどうかです

flatMapは元のObservableのイベント毎に、Observableに変換して、そのObservableが発行するイベントを1つに集約します

_ = Observable.of(1,2,3)
                .flatMap { number in
                    // mapとは違い新たにObservableを作成する
                    Observable.of("\(number)" + "a", "\(number)" + "b", "\(number)" + "c")
                }
                .subscribe(onNext: {
                    print("onNext: ", $0)
                })


// 出力結果(非同期なので毎回この順番という保証はありません)
onNext:  1a
onNext:  1b
onNext:  2a <- 非同期に処理が行われる
onNext:  1c
onNext:  2b
onNext:  3a
onNext:  2c
onNext:  3b
onNext:  3c

上記の例をみるとわかるようにflatMapでは新たにObservableを作成しています。マーブルダイアグラムでは次のようになります

flatMapは非同期で受け取る値の順番は毎回変わる可能性があります。APIの結果を受け取る場合にはflatMapLatestという常に一番新しいObservableを実行するオペレーターを使うようにしましょう

Subject

RxSwiftでのSubject は、ObservableとObserver両方の機能を持っています(イベントを流すこともでき、イベントを受け取ることもできる)

Observableでもsubscribeすると、onNext・onError・onCompletedの処理が行われますが、流れてくる値を受け取るための手段でObservable自らイベントを発せさせるわけではありません

Subjectは自身でイベントを流すことができます。次の例をみてください

let subject = PublishSubject<String>()
let _ = subject.subscribe(onNext: {
    print("onNext:", $0)
}, onCompleted: {
})

// イベントを流す
subject.onNext("A")
subject.onNext("B")
subject.onNext("C")
subject.onNext("D")
subject.onCompleted()

Observableクラスではsubject.onNext("A")などイベントを流すことはできませんが、Subjectだと可能です

Subjectでよく使うのは次の種類があります

種類 動作
PublishSubject キャッシュせず、来たイベントをそのまま通知する
ReplaySubject 指定した値だけキャッシュを持ち、subscribe時に直近のキャッシュしたものを通知する
BehaviorSubject 初期値を持つことができ、1つだけキャッシュを持ち、subscribe時に直近のキャッシュしたものを通知する(初期値をsubscribeで受け取れる)

HotとCold

ObservableはHotColdの2種類の性質があります

ColdなObservableはsubscribeされるまで動作せず、複数subscribeした場合にはそれぞれに対してObservableが複製されてしまいます。 なので、同じ値が複数回流れることになります。ほとんどのOpertorがColdなObservableです

Observable.of("event")
  .map { text -> String in
      print(text)
      return text
  }

// subscribeしていないので、何も出力されない

HotなObservableはsubscribeされなくても動作し、複数subscribeしているのにも関わらず、1つのObservableで同じ値がそれぞれに流れます

SubjectはHotで、mapなどのオペレーターは内部でObservableを返しますがColdなObservableを返すので、ColdなObservableです

Rxのストリームは基本的にSubscribeされた瞬間に各オペレータの動作が始まるようになっています。 ですがHot Observableをストリームの途中に挟むことで、Subscribeを実行するより前にストリームを稼働させることができます。

また、Hot Observableはストリームを分岐することができます。そして、分岐したストリームそれぞれに同一の値を渡します

最後に

大まかな説明になってしまいましたが、概念を理解することでCombineなどに活かせると思います。 また、iOS以外でのプログラミングでもリアクティブプログラミングという考え方は使うので知っておくとで他の場面で活用できる可能性もあるのでぜひ学習してみてください!

参照

github.com

open8tech.hatenablog.com

qiita.com

github.com

speakerdeck.com

github.com

qiita.com

CompositionalLayoutsを使ってみる

今回はCompositionalLayoutsについてです

CompositionalLayoutsはiOS13から登場したCollectionViewのレイアウト構築方法です

これまではUICollectionViewFlowLayoutクラスを使用していましたが、次のような課題がありました

  • Boilerplate code: ボイラープレートコード(定型的なコードで冗長になりがち)
  • Performance considerations: パフォーマンスへの配慮
  • Supplementary and decoration view challenges: 補足・装飾表示の課題
  • Self-sizing challenges: セルフサイジングの課題

その解決策として登場したのが、CompositionalLayoutsです

CompositionalLayoutsは次のように説明されています

  • Composing small layout groups together:小さなレイアウトグループをまとめて合成
  • Layout groups are line-based:レイアウトグループはラインベース
  • Composition instead of subclassing:サブクラス化ではなくコンポジション

CompositionalLayoutsの構造

CompositionalLayoutsは次のように4つの構成があります

Item -> Group -> Section -> Layoutという階層構造になっています。まずは上記図のような構成を再現するようにします

NSCollectionLayoutItem

Compositional Layoutsの階層構造の最小部品のitemは生成時にNSCollectionLayoutSizeを設定する必要があります。 NSCollectionLayoutSizeというのは大まかにいうとセルのサイズです

NSCollectionLayoutSizeでサイズを指定するには次の設定値があります

  • fractionalWidth、fractionalHeight: 画面サイズに対しての比率の割合を指定
  • absolute: 指定した値にサイズを固定
  • estimated: 値の指定はするものの優先される制約がある場合は値は変化する

次のようにサイズをして、itemは生成します

let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                              heightDimension: .fractionalHeight(2/3))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

上記の場合、横幅は画面一杯、縦幅は画面に対して2/3のサイズを指定しています。 absoluteやestimatedは.absolute(44)などのように指定します

NSCollectionLayoutGroup

Groupはレイアウトの基本単位を構成します

以下3つの形で定義することが可能です

  • horizontal(縦方向に並べる)
  • vertical(横方向に並べる)
  • custom(並べる方式をカスタマイズ)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                               heightDimension: .fractionalHeight(2/3))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)

上記の場合にはデータを1つずつ縦方向に並べるという実装になります

NSCollectionLayoutSection

NSCollectionLayoutSectionはCollectionViewにおけるSectionと同じ要素です。 Groupを持ち、CompositionalLayoutsにおけるレイアウトの単位になります

NSCollectionLayoutSectionでは、次のようにGroupをSectionに追加します

let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging

また、section.orthogonalScrollingBehaviorでスクロールする形式を設定することができます。 .pagingで横スクロールを設定します

Layouts

Layoutsはここまでで作成したセクション単位のレイアウトを並べます

UIcollectionViewのcollectionViewLayoutに作成するレイアウトを設定します

enum SectionType: CaseIterable {
    case main
    case sub
 }

collectionViewLayout = UICollectionViewCompositionalLayout { sectionIndex,_ -> NSCollectionLayoutSection? in
        let sectionLayoutKind = SectionType.allCases[sectionIndex]
        // sectionによって作成するセクションを変更する
        switch sectionLayoutKind {
        case .main:
            return self.generateApplianceHorizontalLayout()
        case .sub:
            return self.generateArticleLayout()
        }
    }

ここまで説明したものをまとめたソースコードは以下になります

import UIKit

class CompositionalLayoutsCollectionView: UICollectionView {
    enum SectionType: CaseIterable {
        case main
        case sub
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        registerNib()
        setupLayouts()
    }
    
 // セルを登録する処理を行う
    private func registerNib() {
    }
    
    private func setupLayouts() {
        collectionViewLayout = UICollectionViewCompositionalLayout { sectionIndex,_ -> NSCollectionLayoutSection? in
            let sectionLayoutKind = SectionType.allCases[sectionIndex]
            switch sectionLayoutKind {
            case .main:
                return self.generateApplianceHorizontalLayout()
            case .sub:
                return self.generateArticleLayout()
            }
        }
    }
}

extension CompositionalLayoutsCollectionView {
    private func generateApplianceHorizontalLayout() -> NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                              heightDimension: .fractionalHeight(2/3))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                               heightDimension: .fractionalHeight(2/3))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
        
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .paging
        
        return section
    }
    
    private func generateArticleLayout() -> NSCollectionLayoutSection {
        let fullArticleItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                         heightDimension: .fractionalHeight(1/4))
        let fullArticleItem = NSCollectionLayoutItem(layoutSize: fullArticleItemSize)
        
        let pairArticleItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/2),
                                                         heightDimension: .fractionalHeight(1))
        let pairArticleItem = NSCollectionLayoutItem(layoutSize: pairArticleItemSize)
        let pairGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                   heightDimension: .fractionalHeight(1/4))
        let pairGroup = NSCollectionLayoutGroup.horizontal(layoutSize: pairGroupSize, subitem: pairArticleItem, count: 2)
        
        let nestedGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                                     heightDimension: .fractionalHeight(1))
        let nestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: nestedGroupSize, subitems: [pairGroup, fullArticleItem])
        
        let section = NSCollectionLayoutSection(group: nestedGroup)
        
        return section
    }
}

実際にビルドしてみると次のような画面になります。最初に表示していた作成イメージと構成が一致しているのがわかると思います

実装後の表示画面

アイテム間のスペース

アイテム間のスペースを開けたい場合があると思います。そんなときに設定するプロパティを少し紹介します

interGroupSpacing

interGroupSpacingはSectionに設定するプロパティで、セクション内でのグループ間のスペースを設定します。 上下左右一律に設定した値のスペースができます

let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 10

interItemSpacing

interItemSpacingはGroupに設定するプロパティで、グループ内でのアイテム間のスペースを設定します。 上下左右一律に設定した値のスペースができます

let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
group.interItemSpacing = .fixed(10.0)

contentInsets

contentInsetsはGroupやSectionなど限定なしに設定できるプロパティで、スペースを設定します。 上下左右に設定した値のスペースができます

let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 10, trailing: 10)

最後に

ここまで説明しましたが、まだ完全に説明できているわけではありません。 気がついたら更新しようと思いますが、公式ドキュメントやWWWDCの動画を見たりして補いながら学習できるので、試してください!!

参照

developer.apple.com

muchan611.hatenablog.com

future-architect.github.io

blog.shota-ito.com

qiita.com

iOSの証明書について

今回はiOSで使用する証明書についてです

アプリをビルドしたり、リリースしたりする際に証明書関連の対応手順は理解していても、それぞれどんなことをしているのかがあまり理解できていなかったので、調べてまとめます

まずは全体像を下記に載せます

ビルドやアーカイブを行うために必要な手順を図にしました。それぞれの手順をもう少し詳しく説明します

証明(署名)とは?

ビルドやアーカイブを行うために必要な手順を説明する前に、そもそも証明とは一体なんでしょうか?

証明(署名)は、iOSが信頼できるアプリしかインストールしないようにするための仕組みです

信頼できるアプリとは開発者を特定できるというのと開発者が署名してからアプリが改変されていないという2つの条件を満たすものを指しています

信頼できないアプリは適当な人が作った適当なアプリや、ちゃんとした開発者が作り、その後別の誰かによって改変されたアプリをインストールすることが可能ということになります

これを防ぐため、iOSでは署名されたアプリしか実行できないようになっており、開発者はアプリに署名をする必要になります

証明(署名)の大まかな流れ

アプリの開発者は、公開鍵/秘密鍵のペアを用意します

公開鍵はデジタル証明書の形でインストール先のiOSに渡され、秘密鍵は鍵ペアを生成したマシンに保持され、署名に使われます

アプリをアーカイブする際、あらかじめ決められた手順でアプリからハッシュ値を計算し、これを秘密鍵で暗号化してアプリに同梱します。このプロセスを署名と呼びます

アプリをインストールしたiOSは、同じ手順でアプリからハッシュ値を計算します

一方で、アーカイブ時に計算されて暗号化されたハッシュ値を証明書に含まれている開発者の公開鍵で復号する。

これら2つの方法で得られたハッシュ値が一致することを確認します

ここで、まず暗号化されたハッシュ値が正しく復号できたことで、開発者の特定ができます

ハッシュ値が公開鍵で復号できるということはそれを暗号化したのは開発者の秘密鍵であり、秘密鍵を持っているのは開発者本人しかいないからです

次に、2つのハッシュ値が一致したということから、開発者が署名した後アプリが改変されていないことの確認ができます

もし第三者がアプリの内容をいじっていたらiOSで計算されるハッシュ値が変わってしまい一致しません

ハッシュ値の方も一緒に変えられた場合には、第三者は開発者の秘密鍵を持っておらずハッシュ値の暗号化ができないため、改変したハッシュ値を差し替えることができないです

適当な秘密鍵で暗号化することは可能ですが、そうすると開発者の公開鍵では復号できず、そこでiOSでの検証に引っかかります

CSRファイル作成

まずは図の①のCSR(証明書署名要求)についてです

CSRは証明書署名リクエストです。公開鍵に開発者情報などを付加したもので、これををAppleに渡すことで、Appleから証明書をもらうことができます

ローカルのキーチェーンで作成し、証明書を作成するのに使います

前の項に書いたように、コード署名というのは大雑把に言うとこのとき生成された秘密鍵で開発者がアプリに署名をし、それをインストール先のiOSが公開鍵で検証するというものなので、ここで生成される鍵ペアは非常に重要な役割を持ちます。

作成時に、秘密鍵と公開鍵も同時に作られます

作成方法はキーチェーンアクセスから「証明書アシスタント」>「認証局に証明書を要求」を選択します。すると、鍵ペア情報が表示されるので、鍵のサイズは2048ビット、アルゴリズムRSAを選択して作成します

作成したファイルはApp Developer Programで登録(アップロード)します

.cerファイル作成

ここでは図の②、③について説明します

まずは、作成したCSRファイルをApp Developer Programで登録します

登録する際には開発用か本番用かを選択したり、プッシュ通知を導入するかなどの選択を行います

ここではユーザーの公開鍵含まれていて、認証局秘密鍵で暗号化します

設定が完了してCSRファイルをアップロードしたら、拡張子が.cerのファイルをダウンロードできると思います

一般的に.cerファイルと呼ばれているこのファイルは開発者個人を特定するための証明書、またはAd Hoc用やリリース用の証明書です

.cerファイルをダウンロードして取り込む(ファイルをダブルクリック)ことで、ローカルマシン内のキーチェーンアクセス上で証明書と秘密鍵が紐付きます(登録されます)

.cerファイルを取り込んだけれども、キーチェーンアクセス上で証明書しか表示されないという場合、CSRファイルを作ったローカルマシンと.cerファイルを取り込んだマシンが一致してない可能性があります

p12ファイル

例えば、PCが壊れたので別のPCを使うとなった場合に、CSRファイル作成からやり直さなければいけないでしょうか?

やり直さなくていいように.p12ファイルを取得するという方法があります

.p12ファイルとは証明書の他に秘密鍵を含めたファイルです。ビルドを行うPCを変更する場合にも.cerファイルを取り込むのと同じようにできます

キーチェーンアクセス上で取り込んだ証明書を右クリックで「○○○○○を書き出す」を選択します

そして、フォーマットで.p12を指定して保存ボタンを押すと.p12ファイルが保存されます

別のPCに変更する場合には.p12ファイルをダブルクリックで取り込めば証明書を取り込んだことになります

Provision Profile登録

次にProvision Profileを取り込む必要があります

Provision ProfileとはアプリのIDや証明書、開発用のデバイス情報を内包したファイルです

これにより、開発用にビルドしたアプリが指定した端末以外で立ち上げられないようになります

また、これに保存されている証明書と、前々節で説明したキーチェーンに登録した証明書が一致した時のみビルドが許される仕組みとなっています

Provision Profileを取り込むためにはApp Developer Programで設定が必要になります

App Developer Programでの追加設定

Provision Profileを作成するためにAppIDの登録と実行デバイス情報の登録を行います

まずはAppIDの登録ですが、アプリのBundleIDを登録します

配布アプリが端末内で動作する条件の中にアプリのBundleIDが、AppIDと一致するというものがあります

よって、開発環境や本番環境など複数の環境のProvision Profileを作成する場合には複数のBundleIDが必要になります(Provision Profileは環境ごと必要)

そして、使用する端末のUDIDを登録する必要があります。登録した端末のみでビルドできるように制限するためです

App Developer ProgramでAppIDや使用する端末情報を登録するとProvision Profileがダウンロードできると思います

ダウンロードしたProvision Profileをダブルクリックして取り込んだら、Xcodeの対象のプロジェクトでBundle IDやSigning(署名)の設定を行うとビルドやアーカイブができるようになるはずです

参照

mmorley.hatenablog.com

blue-red.ddo.jp

scrapbox.io

技術書典13にサークル参加します

告知になります

明日9/10(土)からの技術書典13にサークル参加します!!(公式サイトはこちら)

今回はオンライン・オフライン両方で実施されます

今回オンラインのみで本を出します!(紙の本用の表紙と印刷が間に合わなさそうだったので...)

8月にブログを更新できていなかったのは準備と執筆を行っていたためです

どんな本を出すのかというと、Androidの基礎的な部分を解説した本です。初めてAndroidに触れる方やまだ経験が浅い方でも読んでもらえるような本になっています

気になった方は手にとっていただければ幸いです

また、読んでいただいた方は感想をお持ちしております

↓↓ココにあります↓↓

techbookfest.org

Xcodeで例外発生箇所を特定する

今回はXcodeで例外発生箇所を特定する方法についてです。(小ネタですが最後まで見てね)

クラッシュしたときに原因となった場所が表示される場合もあれば、次のようにわかりづらい箇所が表示されたり、AppDelegateで止まったりすることがあります

そんなときにクラッシュの原因はどこなのかを突き止めるためにException Breakpointを使います

まずは次のようにBreakpointアイコンのボタンをクリックして、Breakpoint Navigatorを開きます。そして、Exception Breakpointを選択します

すると、Breakpointを設定するツールチップのようなものが表示されます

ExceptionはAll、Objective-CC++を選択できます。今回はデフォルトのAllを設定しています

このようにBreakpointを設定した状態で、アプリを実行すると次のようにクラッシュした場所で止まります

また、Actionにpo $arg1を設定することでクラッシュの原因のログが表示されます

このようにクラッシュ箇所を探すのに時間をかけず、クラッシュ解決のために多くの時間を使えるようにしましょう!!

参照

dev.classmethod.jp

PencilKitを使ってみる

今回はPencilKitについてです

PencilKitとはApple Pencilやユーザーの指からの入力を受け取り、iPadOS、iOSmacOSで表示する画像に変換するiOSアプリケーション用の描画できるSDKです

PencilKitには、線を作成したり、消したり、選択したりするツールが付属しています

簡単な実装で実現できるのでどのように実現させるのかみていきましょう

レイアウト作成

まずはレイアウトを作成します。StoryBoardは次のようにしています

PKCanvasViewを使用して入力をキャプチャします。このオブジェクトは、Apple Pencilや指によるタッチをキャプチャすることができます。

Viewのクラス名をPKCanvasViewに設定するとPKCanvasViewとして認識されます。 また、検索するとPKCanvasViewをStoryBoardで設定せずに、ソースコードで直接PKCanvasViewを生成している記事が多いですが、それでも大丈夫です

imageViewを配置しているのは画像の上で入力できるようにするためです

キャンバスとパレットの設定

レイアウトを作成したら入力できるように設定します

PKCanvasViewはレイアウトの設定を行ったので、入力エリアは設定できています。 あとは、PKToolPickerという色やペンの種類を変更するためのパレットを表示させる設定を行います

class PencilKitViewController: UIViewController {
    @IBOutlet private weak var pencilKit: PKCanvasView!
    
    private let pkToolPicker = PKToolPicker()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // CanvasViewで入力ができるようにする       
        pencilKit.tool = PKInkingTool(.pen, color: .black, width: 30)
        if #available(iOS 14.0, *) {
            pencilKit.drawingPolicy = .anyInput
        }
        
        // CanvasViewの下の画像が表示できるようにCanvasViewを透明にする
        pencilKit.isOpaque = true
        pencilKit.backgroundColor = .clear

        pencilKit.becomeFirstResponder()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        if let pencilKit = pencilKit {
            pkToolPicker.setVisible(true, forFirstResponder: pencilKit)
            pkToolPicker.addObserver(pencilKit)
        }
    }
}

pencilKit.drawingPolicy = .anyInputApple Pencilだけでなく手でも入力できるようにしています。drawingPolicyはiOS14以降で設定できるため注意してください

基本的には上記ソースコードにコメントを記載しています。ただ、「CanvasViewの下の画像が表示できるようにCanvasViewを透明にする」というのは画像が下にあって入力できるようにするために必要なので、 お絵描きをするためだけであればなくてもいい処理になります

画像の上にCanvasViewを表示させる

また、ツールパレットを表示させる処理はviewDidAppearで行わないと表示されないので注意が必要です

入力したデータを変換する

PKCanvasViewで入力したデータをUIImageに変換することができます

次のように、PKDrawing.imageでUIImageを取得します

let image = pencilKit.drawing.image(from: pencilKit.bounds, scale: 1.0)

UIImage として取得したいPKCanvasViewの範囲をfromに指定して、画像の拡大率をscaleに指定します

上記の方法でUIImageを取得できます

最後に

お絵描きアプリなど作成する場合には湯買うことになると思いますが、短いコードで実現できるので導入しやすいと思います

細かい情報はWWDCなどの映像や公式ドキュメントで確認できるので調べてみるのをおススメします!

参照

developer.apple.com

developer.apple.com

frameとboundsの違いについて

今回はViewの配置やサイズ指定で使うframeboundsについて違いを調べていこうと思います

違いを知らずに表示の不具合を起こす可能性があるので要注意です(まさに自分が体験しました...)

frameについて

公式ドキュメントにあるように親Viewから見た相対的な座標・大きさを返すプロパティです

つまり、親Viewの左上を原点とし、右へx,下へyへ移動した位置であるoriginと、width, heightからなるサイズであるsizeで構成される情報です

実際に見てみましょう。

まずは下記のようなimageViewを表示させる画面があります

そして次のように赤い背景色のViewを追加します。

@IBOutlet private weak var imageView: UIImageView!

override func viewDidLoad() {
       super.viewDidLoad()
        
       let view = UIView()
       view.backgroundColor = .red
       view.frame = imageView.frame
        
       self.view.addSubview(view)
}

追加するViewにimageViewのframeを指定して位置を決めています

このときのframeの情報をログで確認すると(0.0, 74.0, 414.0, 788.0)となっています

右へ0,下へ74(safeArea + 30)へ移動した位置の幅414、高さ788のviewになります

右へ0,下へ74は親Viewの左上を原点としたもので、今回の場合にはViewControllerのViewになります

frameの値は回転すると変わってしまいます

バイスを回転させる場合、ウィンドウは回転せず、ビュー コントローラーのビューが回転します

ウィンドウの視点からは、すべてが実質的にポートレートモードとなります

このため、subViewのframeの値が変わります

次のようにViewを回転させるとframeの値は変わります

@IBOutlet private weak var imageView: UIImageView!

override func viewDidLoad() {
       super.viewDidLoad()
        
       let view = UIView()

       var transRotate = CGAffineTransform()
       let angle = 35 * CGFloat.pi / 180
       transRotate = CGAffineTransform(rotationAngle: CGFloat(angle));
       view.transform = transRotate
       
       view.backgroundColor = .red
       view.frame = imageView.frame
        
       self.view.addSubview(view)
}

回転したときのframeの情報をログで確認すると(-188.55358909013347, 26.52377222547068, 791.1071781802668, 882.9524555490586)となっています。

boundsについて

boundsは自身を左上を原点とし、右へx,下へyへ移動した位置であるoriginと、width, heightからなるサイズであるsizeで構成される情報です。

frameとの違いは、自身を起点としているので、回転してもboundsは変わりません。

次のように設定します

@IBOutlet private weak var imageView: UIImageView!

override func viewDidLoad() {
       super.viewDidLoad()
        
       let view = UIView()
       view.backgroundColor = .red
       view.bounds = imageView.bounds
        
       self.view.addSubview(view)
}

このときのboundsの情報をログで確認すると(0.0, 74.0, 414.0, 788.0)となっています

ここまではframeと同じ結果です。

次に回転させたときを確認します。

@IBOutlet private weak var imageView: UIImageView!

override func viewDidLoad() {
       super.viewDidLoad()
        
       let view = UIView()

       var transRotate = CGAffineTransform()
       let angle = 35 * CGFloat.pi / 180
       transRotate = CGAffineTransform(rotationAngle: CGFloat(angle));
       view.transform = transRotate
       
       view.backgroundColor = .red
       view.bounds = imageView.bounds
        
       self.view.addSubview(view)
}

このときのboundsの情報をログで確認すると(0.0, 74.0, 414.0, 788.0)となっていて、回転させても値は変わりません

結論

ここまでframeとboundsの違いを説明しましたが、結局どちらを使った方がいいのでしょうか?

結果的にorigin(左上の位置)やwidthやheightを使う場合には回転したり、拡大・縮小などで変わってしまう可能性があるので、boundsを使うことを優先的に考えるのがいいと思います

例えば、画像と画像を合成するときに2つの画像の起点を合わせないと変な画像になるなど、想定外のことが発生する可能性は多少なりともあるのでframeを使う場合には気をつけないといけないです

参照

developer.apple.com

developer.apple.com

Swiftのvar、letについての個人的疑問を解消する

今回はSwiftのvar、letについてです。

たまにvar、letを定義したときに思わぬワーニングが表示されることがあり、個人的に疑問に思う場面があったので疑問を解消する記事にしようと思います

基本的なvar、letの解説は割愛しますが、varは変数でletは定数です。なので、letで初期値を定義すると後から値を変更しようとするとエラーが発生します

また、下記クラスを使うことを前提として進めます

class Test {
    var name: String?
    var tall: Int?
}

疑問点①

まずはクラスをインスタンス化して変数を変えるのにクラスをletで定義するべきというワーニングが発生するという点です

少しわかりにくいと思うので、下記にコードを載せます

// letに変えるべきというワーニングが発生!!
var test = Test()
test.name = "テスト"

個人的感覚かもしれませんが、値を変更するのでvarにするのではと思ってしまいます

これは、letが正しいです。なぜなら、初期化中にtestインスタンスへのポインタが割り当てられ、別の値が割り当てられることはなく定数扱いだからです

すべてのクラスインスタンスが実際にヒープ領域でバックアップされたクラスへのポインターなのです

Testクラスのnameが変わっても割り当てられたポインタが変わらないのでテストインスタンスがnullからnullではない値に変わったりする場合には変数扱いになるかもしれないです

疑問点②

2つ目の疑問は下記コードを見てください

let tests = [Test]()

let test = Test()
test.name = "テスト"
 
// varに変更しないとエラーが発生
tests.append(test)

testsは定数だからletでいい気がするのですが、appendする場合にはvarに変更しないとエラーになります

これはドキュメントに記載がありました

If you create an array, a set, or a dictionary, and assign it to a variable, the collection that’s created will be mutable. This means that you can change (or mutate) the collection after it’s created by adding, removing, or changing items in the collection. If you assign an array, a set, or a dictionary to a constant, that collection is immutable, and its size and contents can’t be changed.

どういうことかというとimmutable(不変)の配列やDictionaryなどはサイズや中身を変更できないのでletで定義しているとappendできないことになります。appendする場合にはvarに変更する必要がありそうです

疑問点を調べてみて

まだまだ学ぶことが多くあるなあと感じました。あと、ワーニングやエラーが発生してボタンを押すと勝手にFixする場合があると思います。そのときに自動で修正されて終わりではなく、どういう原因だったのかなど深掘りしたほうが理解が深まりそうです

参照

www.web-dev-qa-db-ja.com

docs.swift.org