20201129のSwiftに関する記事は14件です。

TableViewセルの作り方 その1(Prototype Cells)(忘備録)

手順

UIの配置と設定

  • Storyboardで配置したTableViewを選択
  • InspectorのTableViewでPrototype Cellsの数を増やす

スクリーンショット 2020-11-29 22.51.52.png

  • TableViewに追加されたPrototype Cellsに必要なUIを追加して制約を追加
  • コードでUITableViewCellのClassを作成する
  • StoryboardのPrototypeCellを選択してInspectorのCustomClassに作成したClassを指定

スクリーンショット 2020-11-30 0.04.19.png

ViewControllerとUITableViewCellとの接続

ViewController.swift
import UIKit

class ListViewController: UIViewController {

  private let cellId = "cellId"

//TableViewの接続
  @IBOutlet weak var listTableView: UITableView!

  override func viewDidLoad() {
    super.viewDidLoad()

    listTableView.delegate = self
    listTableView.dataSource = self 
  }
}

// MARK: - UITableViewDelegate, UITableViewDataSource
extension listViewController: UITableViewDelegate, UITableViewDataSource{
//〜省略〜
}


// MARK: - UITableViewCell
class ListTableViewCell: UITableViewCell{

//CellのUIをOutlet接続
  @IBOutlet weak var imageView: UIImageView!
  @IBOutlet weak var msgLabel: UILabel!

  //awakeFromNibは「func」のみの方を選択
  //StoryBoardやxibファイルからインスタンス化される場合に呼ばれる
  //Nibファイルが開かれ、使用準備が完了。
  override func awakeFromNib() {
    super.awakeFromNib()

    //UIのプロパティーなどを記述
    imageView.layer.cornerRadius = 35
  }

  //セルの選択された状態を設定し、オプションで状態間の遷移をアニメーション化する
  override func setSelected(_ selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)
  }

StoryboardからTableView本体はViewControllerに接続して
CellのUIはUITableViewCellに接続する。

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

【RxSwiftの勉強メモ】#2「ログインボタンで条件を満たしていたらボタンを活性化する」

この記事の目的

RxSwiftの使い方を学んでいくために簡単なサンプルを作成し記録する。
後から見直して頑張ります。
今回は
@r_chibaさんの
RxSwift入門(ハンズオン)
(https://qiita.com/r_chiba/items/5618a3dd96618222cca8#rx%E5%81%B4%E5%85%A8%E4%BD%93%E5%83%8F)
を参考にさせていただきました。(よりシンプルに、とりあえず動くものを作ろうという方針でコードを書きました。)
↓にサンプルコードを示します。

サンプル

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {

    @IBOutlet weak var mailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var loginButton: UIButton!

    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        mailTextField.rx.text.subscribe { (event) in
            self.changeLoginEnabled()
        }.disposed(by: disposeBag)

        passwordTextField.rx.text.subscribe { (event) in
            self.changeLoginEnabled()
        }.disposed(by: disposeBag)

    }

    private func changeLoginEnabled() {
            if mailTextField.text!.count > 0 && passwordTextField.text!.count > 0 {

                print("ボタン活性!")
                // ボタンの活性状態
                loginButton.isEnabled = true

                loginButton.backgroundColor = UIColor.red

            } else {
                print("ボタン非活性!")
                // ボタンの活性状態
               loginButton.isEnabled = false

               // ボタンの背景色
                loginButton.layer.backgroundColor = UIColor.secondarySystemBackground.cgColor

            }
        }

}

結果

以下のように「mail」と「password」両方のテキストフィールドに文字が
1文字以上入力されるとボタンの背景が赤く変化し活性化します。
画面収録 2020-11-29 22.06.00.gif

まとめ

比較的容易に実装ができたと思います。
まだまだRxSwiftを全貌は把握していませんが、便利なのかな〜
くらいはわかるようになりました!

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

【RxSwiftの勉強メモ】#1「ボタンのタップを検知してラベルの内容を変更する」

この記事の目的

RxSwiftの使い方を学んでいくために簡単なサンプルを作成し記録する。
後から見直して頑張ります。
↓にサンプルコードを示します。

サンプル

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {


    @IBOutlet weak var button: UIButton!
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var badButton: UIButton!
    var numberofGood = 0

    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        button.rx.tap
            .subscribe { (event) in
                self.buttonTapped()
            }
            .disposed(by: disposeBag)

        badButton.rx.tap
            .subscribe { (event) in
                self.badButtonTapped()
            }
            .disposed(by: disposeBag)

        label.text = String(0)
    }

    func buttonTapped() {

        numberofGood += 1
        label.text = String(numberofGood)
    }

    func badButtonTapped() {

        if numberofGood > 0{
            numberofGood -= 1
            label.text = String(numberofGood)
        }
    }
}

スクリーンショット 2020-11-29 20.35.48.png

サンプルコードの内容

サンプルコードは至ってシンプル。
画面左上のいいねボタンがタップされると画面中央の数値がインクリメントされ、
画面右上の悪いねボタンがタップされるとデクリメントされます。
画面収録 2020-11-29 20.13.19.gif

まとめ

RxSwiftを理解している人にとっては容易すぎる内容は承知の上で
引き続きメモ的な位置付けとして投稿していきたいと思います!。

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

UserDefaultsを使ってアプリ起動画面を動的に切り替える

はじめに

アプリインストール後 初めてアプリを開いた場合はサインアップ画面に遷移し、
2回目以降はログイン画面に遷移するような実装をつくりました。

調べた中でUserDefaultsを使うのが一番簡単そうだったので紹介します。

UserDefaultsとは

アプリ起動時にキーと値のペアを自動的に保存してくれる便利な機能。
(アプリを削除するとUserDefaults内のデータも全て消えるため、重要なデータをUserDefaultsのみに持たせるべきではない。
Token等のセキュアな情報はKeychainAccessを使う。)

キーに値を設定する方法

UserDefaults.standard.set(保存したい値, forKey: キー)

値を取得する方法

UserDefaults.standard.bool(forKey: キー)

キーに対応する値が無い場合は、falseが返る。
他にもstringやarrayなど様々パターンがあるが、今回は起動したことがあるか否かの2パターンのためboolを使う。

実装

※前提
ログイン画面のStoryboard名は「Login.Storyboard」、Storyboard IDは「LoginViewController」
サインアップ画面のStoryboard名は「SignUp.Storyboard」、Storyboard IDは「SignUpViewController」と設定しております。

方針

【初めて起動したときの流れ】
①UserDefaults.standard.bool(forKey: "起動履歴あり") でfalseが返る(値が存在していないから)
②サインアップ画面に遷移させる
③UserDefaults.standard.set(true, forKey: "起動履歴あり")で次回以降はtrueを返すように設定する

【2回目以降に起動したときの流れ】
①UserDefaults.standard.bool(forKey: "起動履歴あり") 判定でtrueが返る
②ログイン画面に遷移させる

コード

SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let _ = (scene as? UIWindowScene) else { return }

        // ここから起動チェック
        if UserDefaults.standard.bool(forKey: "起動履歴あり") {
            // 初期画面をログイン画面にする
            self.window?.rootViewController = createViewController(storyboardName: "Login", storyboardId: "LoginViewController")
        } else {
            // 初期画面をサインアップ画面にする
            self.window?.rootViewController = createViewController(storyboardName: "SignUp", storyboardId: "SignUpViewController")
            UserDefaults.standard.set(true, forKey: "起動履歴あり")  // 一度起動したので値をtrueに設定する
        }

        self.window?.makeKeyAndVisible()
    }

    // 画面遷移のメソッド
    func createViewController(storyboardName: String, storyboardId: String) -> UINavigationController {

        let storyboard: UIStoryboard = UIStoryboard(name: storyboardName, bundle: nil)
        let viewController = storyboard.instantiateViewController(withIdentifier: storyboardId)

        let navigationController = UINavigationController()
        navigationController.view.backgroundColor = .white
        navigationController.viewControllers = [viewController]

        return navigationController
    }
}

最後に

UserDefaultsは準備不要でいきなり使えるからかなり手軽で便利でした。
もっとスマートに書く方法があったら教えて下さい。

参考URL

https://qiita.com/uhooi/items/429cac9b798b9c0937ae#%E8%BF%BD%E5%8A%A0%E6%9B%B4%E6%96%B0
ありがとうございました!

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

SwiftUIAppでFirebaseプロジェクトを追加する

はじめに

SwiftUIApp でFirebaseを初期化する方法について共有していきたいと思います。

Podfileを作成する手順までは公式チュートリアルと同じです!

そのためFirebaseを初期化する手順が必要になるかと思います。

実際のコード

SwiftUIAppExample.swift
import SwiftUI
import FirebaseCore

@main
struct SwiftUIAppExample: App{
    init() {
        FirebaseApp.configure()
    }

    var body: some Scene{
        WindowGroup{
            ContentView()
        }
    }
}

このように初期化メソッドをコードに追加してあげることでうまく動作すると思います!

最後まで読んでいただきありがとうございました!

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

Convenience Initializerは何がコンビニエンスなのか

クラス特有のイニシャライザ

Swiftには様々なイニシャライザがありますが、今回はクラス特有のイニシャライザである指定イニシャライザ(designated initializer)と簡易イニシャライザ(convenience initializer)について簡単にまとめます。

それぞれの特徴

指定イニシャライザ(designated initializer)

  • クラスを定義する際に使うイニシャライザ。基本的にこれを使う。
  • 必須であり、クラスは最低1つ指定イニシャライザを書く必要がある。
  • 継承している場合、自身のプロパティを初期化後にスーパークラスの指定イニシャライザを呼ぶ。

簡易イニシャライザ(convenience initializer)

  • 指定イニシャライザをサポートするイニシャライザ。
  • あくまで副次的なイニシャライザであり、必須ではない。
  • 最終的に、同一クラスの指定イニシャライザを呼ぶ。

簡易イニシャライザの使い所

私自身指定イニシャライザはよく書くので慣れ親しんでいる一方、簡易イニシャライザに関しては何がコンビニエンスなのかいまいちよく理解しておらず、使い所がピンときていない状態でした。
そこで一度参考としてThe Swift Programming Languageをあたってみました。

そこには

You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer’s parameters set to default values. You can also define a convenience initializer to create an instance of that class for a specific use case or input value type.

You do not have to provide convenience initializers if your class does not require them. Create convenience initializers whenever a shortcut to a common initialization pattern will save time or make initialization of the class clearer in intent.

と書かれてありました。
要約すると
「指定イニシャライザのパラメータにデフォルト値を設定しておきたかったり、特定のユースケース用のインスタンスを作る時に使ってね。でも時間の節約だったり、クラスの初期化の意図を明確にする時のためにあるから、いらないのに無理矢理使う必要はないよ。」
ということでしょうか。
やはりピンとこないので、実際に書いてみました。

実践

簡易イニシャライザ使用
class Man {

    let attack: Int
    let defense: Int

    //指定イニシャライザ(必須)
    init(attack: Int, defense: Int) {
        self.attack = attack
        self.defense = defense
    }

    //簡易イニシャライザ(攻守0のザコを作りたい時用)
    convenience init() {
        self.init(attack: 0, defense: 0)
    }

    //簡易イニシャライザ(守0のザコを作りたい時用)
    convenience init(attack: Int) {
        self.init(attack: attack, defense: 0)
    }

    //簡易イニシャライザ(攻0のザコを作りたい時用)
    convenience init(defense: Int) {
        self.init(attack: 0, defense: defense)
    }

}

以上のように指定イニシャライザをラップすることによって、更に別の初期化処理を書くことができます。
しかし、これだけならわざわざ簡易イニシャライザを使う必要はありません。
以下のように書けば簡易イニシャライザを使わないでも同じことができます。

簡易イニシャライザ未使用
class Man {
    let attack: Int
    let defense: Int

    init(attack: Int, defense: Int) {
        self.attack = attack
        self.defense = defense
    }

    //convenienceを付けなくても以下のようにすれば先程と同じことができる
    init() {
        self.attack = 0
        self.defense = 0
    }

    init(attack: Int) {
        self.attack = attack
        self.defense = 0
    }

    init(defense: Int) {
        self.attack = 0
        self.defense = defense
    }
}

簡易イニシャライザの真価

上で書いたようなことをしたいだけならわざわざ簡易イニシャライザを使う必要はありません。
ではいつ簡易イニシャライザが真価を発揮するかというと、クラスの継承の時です。
というのも、Swiftのイニシャライザには自動継承という特徴があります。かっこいいですね。
自動継承とは
「ある条件下でスーパークラスのイニシャライザが自動的にサブクラスに継承される = サブクラスで使えるようになる」ということです。
The Swift Programming Languageには、その条件が以下のように書かれています。

Rule 1
If your subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers.

Rule 2
If your subclass provides an implementation of all of its superclass designated initializers—either by inheriting them as per rule 1, or by providing a custom implementation as part of its definition—then it automatically inherits all of the superclass convenience initializers.

ここで大切なのは簡易イニシャライザに関係するRule2の方です。
要約すると、サブクラスがスーパークラスの指定イニシャライザを全て実装している場合、そのサブクラスはスーパークラスに定義されている全ての簡易イニシャライザを自動的に継承するということです。

例えば、Manクラスを継承し、速さも兼ね備えたSuperManクラスを実装するとします。
比較として、まずは継承元のManクラスで簡易イニシャライザを使わず、全て指定イニシャライザで書いてみます。そうすると以下のようになります。

簡易イニシャライザ未使用
class Man {

    let attack: Int
    let defense: Int

    init(attack: Int, defense: Int) {
        self.attack = attack
        self.defense = defense
    }

    init() {
        self.attack = 0
        self.defense = 0
    }

    init(attack: Int) {
        self.attack = attack
        self.defense = 0
    }

    init(defense: Int) {
        self.attack = 0
        self.defense = defense
    }

}

class SuperMan: Man {

    let speed: Int

    init(attack: Int, defense: Int, speed: Int) {
        self.speed = speed
        super.init(attack: attack, defense: defense)
    }

}

こんな感じです。
この場合SuperManクラスをインスタンス化しようとすると、イニシャライザは以下のようになります。
スクリーンショット 2020-11-29 18.12.00.png

Manクラスのイニシャライザは全て指定イニシャライザで実装したので、SuperManクラスで攻0や守0のザコを作りたい場合、以下のように手作業でManクラスの指定イニシャライザ全パターンをoverrideさせなければいけません。めんどくさいですね。

//スーパークラス(Manクラス)で実装した指定イニシャライザ全パターンをもう一度書かなければいけない
    convenience override init() {
        self.init(attack: 0, defense: 0, speed: 0)
    }

    ~以下略~
    ***

では今度は継承元のManクラスに簡易イニシャライザを使って書いてみます。

簡易イニシャライザ使用
class Man {

    let attack: Int
    let defense: Int

    init(attack: Int, defense: Int) {
        self.attack = attack
        self.defense = defense
    }

    convenience init() {
        self.init(attack: 0, defense: 0)
    }

    convenience init(attack: Int) {
        self.init(attack: attack, defense: 0)
    }

    convenience init(defense: Int) {
        self.init(attack: 0, defense: defense)
    }

}

class SuperMan: Man {

    let speed: Int

    init(attack: Int, defense: Int, speed: Int) {
        self.speed = speed
        super.init(attack: attack, defense: defense)
    }

    convenience override init(attack: Int, defense: Int) {
        self.init(attack: attack, defense: defense, speed: 0)
    }

}

この場合、自動継承の条件である
サブクラスがスーパークラスの指定イニシャライザを全て実装している場合、そのサブクラスはスーパークラスに定義されている全ての簡易イニシャライザを自動的に継承する

override init(attack: Int, defense: Int) {
}

で満たしているので、SuperManクラスをインスタンス化しようとすると...
スクリーンショット 2020-11-29 18.36.29.png
このように、継承元のManクラスで実装した簡易イニシャライザ全パターンが自動で継承されます。
Manクラスで簡易イニシャライザを使わずに書いた場合に比べ、コードがスッキリし、その名の通りコンビニエンスになりました。
これこそが簡易イニシャライザがコンビニエンスたる所以なのですね(?)

まとめ

簡易イニシャライザは継承時に真価を発揮することが分かりました。
私自身実際に使ったことがなかったので、機会があれば簡易イニシャライザのコンビニエンスさを生で実感してみたいです。
もし間違いなどあれば指摘していただけると幸いです。よろしくお願い致します。

引用

The Swift Programming Language

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

Swiftで使われる演算式には何がある?

演算式とは

演算式とは、演算子の種類に応じた演算を行い、演算結果の値を返す式です。
演算子とは、少しかっこよく言ってますが、+ , - , × , ÷ などです。
ちなみにプログラミングでは、+ , - , * , / で表します。

こちらが演算式の例になります。

var a = 12 + 20   // 32 (+演算子)
var b = 12 - 10   // 2  (-演算子)
var c = 12 * 2    // 24 (*演算子)
var d = 12 / 3    // 4  (/演算子)

少しレベルの上がった演算式

先ほどまでの演算式は全て整数同士の演算式でした。
ここからは少しややこしい演算式をご紹介します。

演算式には、それぞれの演算に対応した型が定義されています。

例えば、乗算を行う*演算子は、
整数を表すInt型や、浮動少数点数を表すDouble型に対応していますが、
文字列を表すString型には対応していません。

String型に対応してる演算子なんかないだろ!と思うかもしれませんが、
実は+演算子はString型に対応しています。

var a = 2 + 2   // 4
var b = "こんに" + "ちは"   // こんにちは

なので、ただ整数のみを扱うのもという認識ではなく、
小数や文字列の計算もできると言うことを頭の隅に置いておいてください。

また、演算するもの同士の型にも注意が必要です。
基本的に計算するものは全て同じ型で計算する必要があります。

Int型 + Int型や、String型 + String型などは見てきました。
では、Int型 + String型や、Double型 + Int型はどうなのか見てみます。

var a = 10   // Int型
var b = "こんにちは"   //String型
var c = a + b   // 10こんにちは ではなく、コンパイルエラー

var d = 10.0   // Double型
var e = 1   // Int型
var f = d + e   // 11.0 ではなく、コンパイルエラー

このような結果になります。

そして実際に出るエラー内容が、
var c = a + b のコンパイルエラー内容
Binary operator '+' cannot be applied to operands of type 'Int' and 'String'
和訳:二項演算子「+」は、タイプ「整数」および「文字列」のオペランドには適用できません。

var f = d + e のコンパイルエラー内容
Binary operator '+' cannot be applied to operands of type 'Double' and 'Int'
和訳:二項演算子「+」は、タイプ「Double」および「Int」のオペランドには適用できません。

これを見て分かったかと思いますが、
型が違うもの同士の計算は基本的に出来ません。

ですが、Int + String はまだしも、
Int + Double の計算でしたら今後使う可能性は高いと思います。

こういう時の解決方法は、Int型をDouble型に変える方法です。

Double + Int がダメなら Double + Double にしてしまえ!ということですね。

var a = 10.0   // Double型
var b = 1   // Int型
var c = a + Double(b)   // 11.0  Double型

今回は、Int型ではなくDouble型同士の計算にしたいので、
Double型のイニシャライザを使いInt型をDouble型にしてます。
var c = a + Double(b)

これを行うことによりDouble型同士の計算になりコンパイルエラーは発生しません。

では、String型もInt型やDouble型に変換してから計算すればいいのでは!?
と思う方もいるかもしれません。(私は単純なのでそう思いました・・・。)

var a = "こんにちは"    // String型
var b = Int(a)   // nil

予想通りといえば予想通りですが、Int型にはなりませんでした(笑)
ただ、コンパイルエラーにはなりませんでした。
おそらく変換できない値が入っていると nil になるように作られているのかもしれません。

そして、これはたまたま見つけたのですが、
下記のような場合だと自動でDouble型にしてくれるっぽいです。

var a = 10.0 + 1   //11.0  Double型

つまりどういうことかと言うと、
一度宣言された変数を使う場合はイニシャライザで変換してから型を合わせないといけない。
変数を使わず数字同士の計算をする場合は自動的に変換してくれる。

ということかと思います。

もし間違っていたらすみません。

次に最後なのですが、否定演算子というものも存在します。

否定演算子とは"!"のことで真理値を反転する演算子になります。
真理値とは、真偽を表すtrue や false のことで、型はBool型になります。

!演算子を使うとこの結果を逆にできます。
true は false に
false は true になります。

var a = true   // Bool型
var b = !a   //false   

!なんか使う機会あるの?と思うかもしれませんが、
if文という記法で結構使用するらしいので覚えておいて損はないと思います。

以上、長くなってしまいましたが最後まで読んでくださりありがとうございました。

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

アプリを起動したときに読み込まれるStoryBoardを設定する(忘備録)

手順

  • Storyboardファイルを作成する
  • StoryboardのInspectorを設定する
    • Custom ClassのClassを指定して紐付け
    • IdentityのStoryboard IDを設定(Class名と同じでOK)
    • Use Storybord IDにチェックを入れる
  • 以下の要領でコードをSceneDelegateのwillConnectTo sessionに追記
SceneDelegate.swift
  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    //初期の画面を設定することができる(ここに記載された方が優先される)
    let window = UIWindow(windowScene:  scene as! UIWindowScene)
    self.window = window
    //レシーバを前面に表示してキーボード等のイベントを受け付けるようにする
    window.makeKeyAndVisible()

    //対象のStoryBoardをファイル名で指定
    let storyboard = UIStoryboard(name: "StoryBoardのファイル名", bundle: nil)
    //表示させるviewControllerをInspectorsで設定したStoryboardIDで指定
    let viewController = storyboard.instantiateViewController(identifier: "設定したStoryBoardID")
    //navigationBarがある場合
    let nav = UINavigationController(rootViewController: viewController)
    //UIWindowのrootViewControllerに設定 
    window.rootViewController = nav

    guard let _ = (scene as? UIWindowScene) else { return }
  }

以上で、初めに読み込まれるStoryboardを設定することができる。

Xcode Version 12.2

[参考にさせていただきました]
【Swift】 LINE風Chatアプリ作成(Ep1) - 概要の説明とChatList画面の作成を解説
https://www.youtube.com/watch?v=XandgrGiV-8&t=564s

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

Big SurのMultiColorのAccent Colorに対応する。

macOS Big Surから導入されたアプリごとにAccent Colorを設定できるMultiColorに関する記事がなかったので備忘録として

スクリーンショット 2020-11-29 13.51.20.png

Accent ColorをAsset Catalogで定義する。(名前はなんでもいい)

スクリーンショット 2020-11-29 13.52.48.png

Build SettingsのAsset Catalog Compiler - Optionsから
Global Accent Color Nameに先ほど作った色の名前を書く

スクリーンショット 2020-11-29 13.53.44.png

適応された。

スクリーンショット 2020-11-29 13.56.07.png

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

Xcode12でArchiveビルドがとても長くなった時の調査方法

起こったこと

  • Xcode11.7 -> Xcode12(12.1や12.2でも同様だった)にアップデートしたら、アーカイブビルドがとても長くなった。
    • 具体的には、Xcode11.7で15分程度だったのが、Xcode12.2で3時間程度かかるようになった。
  • bitriseだと60分でタイムアウトするし、これだと運用が厳しいので対策するべく原因調査した。

原因、対策

後述する通りいろいろと調べたところ、原因は下記の通りだった。

  • Xcode12にバンドルされているSwift5.3のコンパイラのOptimization LevelOptimize for SpeedOptimize for Sizeのとき(アーカイブビルドのときに指定されている)のswift コンパイルの挙動が変わっていたため
  • 上記Optimization Levelのときに、コンパイルが特に遅いSwiftファイルが1ファイルあった。
    • 対策としては、このSwiftファイルをコンパイルが速くなるように書き換えた。

この記事のメインテーマは、どうやって上記の原因までたどり着いたかのところで、それを下記します。
もしかしたら、当たり前なことなのかもしれないが、わたしはとても苦戦しました。

調べたこと

アーカイブ時のビルド時のログ出力確認

最初、普通にビルドログ見てたが、それだと、Build Phaseの Compile Source(swiftファイルをコンパイルするフェーズ)の直前のshell script実行のところで止まっていて、そのscript自体は単体ではすぐ終わる処理(単体で実行して確認済み)だったので、ログがちゃんと出ていないように思った。
20201129_1.png

SwiftコンパイラのOptimization Level

デバッグビルドでは問題なかったのと、以前の問題があったので、Archive時のSwiftコンパイラのOptimization Level(Optimize for Speed)が問題なのではと思って、デバッグビルドのOptimization Levelを Optimize for Speed にしたらビルドが遅くなったので、SwiftコンパイラのOptimization Levelが問題であることに気付いた。
※デバッグビルドの方がビルドログが細かく出力されるので、これ以降はデバッグビルドのOptimization Levelを Optimize for Speed にして調査しています。

また、compiler optimization level = Optimize for Size にしたらビルド速くなったら嬉しいなーと思ってやってみたけど、変わらなかった。。。

Swift Other Flag追加してログ出力確認

結果的に今回の調査では役立ったわけではないが、もうちょっとちゃんとビルドログ出力させたいと思って Other Swift Flagsに下記を追加。これでビルドログが詳細に出るようになった。

  • -Xfrontend -warn-long-expression-type-checking=300
    • 型推論に300ms以上かかったらwarning
  • -Xfrontend -warn-long-function-bodies=300
    • コンパイルに100ms以上かかるメソッドをwarning

この辺りは下記の記事を見ながら色々やってみていた。
https://dev.classmethod.jp/articles/investigated_compiletime_swift/
https://techlife.cookpad.com/entry/2017/12/08/124532

ビルドが遅くなっているところを確認

何度やっても下記の赤枠のswiftファイルのコンパイルが遅いので、多分swiftファイルの書き方の問題なんじゃ無いかと思ってきた。
試しに、簡単に直せそうなswiftファイル(10行未満のswiftファイルが含まれた)を全部コメントアウトしても、速くならなかったので、多分この中でも特定のどれかが遅いのだと思ってきた。
qiita20201129-2.png

※このとき、遅くなっているswift ファイルのコンパイル直後に各frameworkのlinkが走っていたので、実はそこが遅いんじゃ無いかと思った(具体的にはBridging-Header.hに @import をいくつか書いてたのでそれが問題なのかもと思った)が、違った。。。

コンパイルが遅くなっているswift ファイルがどれかを特定

上記の赤枠のファイルを選択して、コピペすると、下記のようなテキストがペーストされた。

Showing Recent Messages
CompileSwift normal arm64 /プロジェクトのパス/〜〜〜〜〜/〜〜〜〜〜〜〜.swift (in target '〜〜〜〜〜' from project '〜〜〜〜〜')
    cd /プロジェクトのパス
    /Applications/Xcode12_2.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -c -filelist /var/folders/5t/〜〜〜〜〜〜〜〜〜〜〜〜〜〜/T/sources-4f43a5 -primary-file /プロジェクトのパス/〜〜〜〜〜/〜〜〜〜〜〜〜.swift -emit-module-path /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/Objects-normal/arm64/swiftファイル名\~partial.swiftmodule -emit-module-doc-path /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/Objects-normal/arm64/swiftファイル名\~partial.swiftdoc -emit-module-source-info-path /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/Objects-normal/arm64/swiftファイル名\~partial.swiftsourceinfo -serialize-diagnostics-path /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/Objects-normal/arm64/swiftファイル名.dia -emit-dependencies-path /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/Objects-normal/arm64/swiftファイル名.d -emit-reference-dependencies-path /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/Objects-normal/arm64/swiftファイル名.swiftdeps -target arm64-apple-ios11.0 -Xllvm -aarch64-use-tbi -enable-objc-interop -sdk /Applications/Xcode12_2.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.2.sdk -I /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/APIKit -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/Alamofire -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/AlamofireImage -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/AppAuth -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/BoringSSL-GRPC -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/Cartography -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/Differ -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/GoogleDataTransport -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/JWTDecode -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/MBProgressHUD -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/RxCocoa -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/SwiftGifOrigin -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/YoutubePlayer-in-WKWebView -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜 -F /プロジェクトのパス/Pods/AppsFlyerFramework/iOS -F /プロジェクトのパス/Pods/FirebaseAnalytics/Frameworks -F /プロジェクトのパス/Pods/〜〜〜〜〜/Frameworks -F /プロジェクトのパス/Pods/〜〜〜〜〜/Frameworks -F /プロジェクトのパス/Pods/〜〜〜〜〜/Frameworks -F /プロジェクトのパス/Pods/〜〜〜〜〜/〜〜〜〜〜.embeddedframework -F /プロジェクトのパス/Pods/〜〜〜〜〜 -F /プロジェクトのパス/Pods/〜〜〜〜〜/Build/iOS -F /プロジェクトのパス/Pods/〜〜〜〜〜/Build/iOS -F /プロジェクトのパス -F /プロジェクトのパス/〜〜〜〜〜 -F /プロジェクトのパス/ライブラリ/Frameworks-build -enable-testing -g -module-cache-path /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/ModuleCache.noindex -swift-version 5 -enforce-exclusivity\=checked -O -D DEBUG -D COCOAPODS -D COCOAPODS -enable-swift3-objc-inference -warn-swift3-objc-inference-minimal -warn-long-function-bodies\=300 -warn-long-expression-type-checking\=300 -serialize-debugging-options -Xcc -working-directory -Xcc /プロジェクトのパス -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/swift-overrides.hmap -Xcc -iquote -Xcc /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/〜〜〜〜〜-generated-files.hmap -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/〜〜〜〜〜-own-target-headers.hmap -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/〜〜〜〜〜-all-non-framework-target-headers.hmap -Xcc -ivfsoverlay -Xcc /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/all-product-headers.yaml -Xcc -iquote -Xcc /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/〜〜〜〜〜-project-headers.hmap -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/include -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜/〜〜〜〜〜.framework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜/〜〜〜〜〜.framework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜/〜〜〜〜〜.framework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/BoringSSL-GRPC/openssl_grpc.framework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/〜〜〜〜〜/〜〜〜〜〜.framework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/MK2Router/MK2Router.framework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/YoutubePlayer-in-WKWebView/YoutubePlayer_in_WKWebView.framework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/gRPC-Core/grpc.framework/Headers -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Products/Debug-iphoneos/swiftライブラリframework/Headers -Xcc -I/プロジェクトのパス/Pods/Headers/Public -Xcc -I/プロジェクトのパス/Pods/Headers/Public/ライブラリ -Xcc -I/プロジェクトのパス/Pods/Headers/Public/ライブラリ -Xcc -I/プロジェクトのパス/Pods/Firebase/CoreOnly/Sources -Xcc -I/Sources/FBLPromises/include -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/DerivedSources-normal/arm64 -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/DerivedSources/arm64 -Xcc -I/ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/DerivedSources -Xcc -DDEBUG\=1 -Xcc -DCOCOAPODS\=1 -Xcc -DDEBUG\=1 -Xcc -DGPB_USE_PROTOBUF_FRAMEWORK_IMPORTS\=1 -Xcc -DDEBUG\=1 -Xcc -DPB_FIELD_32BIT\=1 -Xcc -DPB_NO_PACKED_STRUCTS\=1 -Xcc -DPB_ENABLE_MALLOC\=1 -target-sdk-version 14.2 -import-objc-header /プロジェクトのパス/〜〜〜〜〜/Bridging-Header.h -pch-output-dir /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/PrecompiledHeaders -pch-disable-validation -module-name 〜〜〜〜〜 -o /ユーザーフォルダのパス/Library/Developer/Xcode/DerivedData/〜〜〜〜〜-ejcjafpdqhgmythgozbispsdrwky/Build/Intermediates.noindex/〜〜〜〜〜.build/Debug-iphoneos/〜〜〜〜〜.build/Objects-normal/arm64/swiftファイル名.o -embed-bitcode-marker


CompileSwift normal arm64 〜〜〜〜が選択したswiftファイル分続く。

上記の、

cd /プロジェクトのパス 
/Applications/Xcode12_2.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend ~~~~~

のところをそのままshell(ターミナル)で実行したら普通に実行できた(1ファイルのswiftコンパイルができる)ので、それで赤枠の各swiftファイルのコンパイルを試したところ、ある1ファイルだけ、すごく時間がかかったのでそれでどのファイルがコンパイルが遅いのかがわかった。

swiftファイル内のどの処理が遅いか特定

やり方は色々ありそうだがそれほど苦労しなかった。
実際にやったやり方は、一旦問題のswiftファイルを全部コメントアウトしてみてから、上記のshell上でのswiftコンパイルを実行し、少しずつコメントアウトを復活させながら、shell上でのswiftコンパイルの実行繰り返して特定した。
その結果、あるenum(かなり大きめでいろいろなViewControllerを参照したAssociated Values を持つenum。このswiftファイルとは別のswiffファイル内で定義)を引数で参照している箇所が遅いことが発覚したので、それのenumを参照しないようにすることで問題解決した。

最後に

上記対応後、結果的に多分、2分程度はXcode12にしてからアーカイブが速くなったような気がします。
最初、この問題に遭遇した時は、解決できるのだろうか(できればXcode12がアップデートされたら勝手に解決されないかなとおもった)、、、と思ったが、諦めずに1つ1つの問題の原因に向き合っていけば、解決に近づいていける、、、そう信じたいと思った。解決できてよかった。

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

テスト

どういうこと

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

ARKit+Vision+iOS14 で らくがき のジオメトリ化③【テクスチャ貼り付け】

前回のつづきでジオメトリにテクスチャを貼り付けてみる。
※3回に分けて書いた記事の最後です。
<完成イメージ>
demo.png demo.gif

テクスチャの貼り付け手順

カメラからキャプチャした画像をテクスチャにして輪郭から生成した3Dモデルに貼り付ける。
正攻法としては輪郭のパス情報をもとに SCNShape で作成したジオメトリについて、そのテクスチャの座標を設定すること(個々のポリゴンの頂点座標とキャプチャ画像内の座標の整合をとること)ではあるが、これは大変面倒。すでに輪郭検出範囲の四隅のワールド座標がわかっているので、この四隅の情報で平面ジオメトリ を新たに作成し、それにキャプチャ画像を設定&SCNShapeで作ったジオメトリ にくっつける、という方法で実装した。

SCNShape の中身を実際に確認してみると、表面・側面・背面のそれぞれの SCNGeometryElement を取得することができ、表面のポリゴンに対応する テクスチャ座標SCNGeometrySourceもとれるので、テクスチャ座標を計算してそれに差し替えたカスタムジオメトリ を作ることは可能。

手順は次の通り。
⑦を除く①〜⑧は前回前々回の記事と同じなので、そちらを参照ください。

①キャプチャ画像からスクリーンに表示されている範囲を切り出す
②輪郭検出の前に①の画像を加工し輪郭検出しやすくする
③輪郭を検出する
④画像にある輪郭は複数検出されるので、着目したい輪郭のみ選択する
⑤④を表示する
⑥CGPathの輪郭情報をUIBezierPathに変換
⑦⑥の情報からSCNNodeを作成    ←今回はこの部分を変更
⑧⑦をシーンに追加

以下、詳細を説明します。

⑦⑥の情報からSCNNodeを作成

テクスチャの貼り付けは次のステップで行う。

1) キャプチャ画像からパス部分の画像を切り抜く
2) 輪郭検出範囲に一致する平面ジオメトリ を作り、1)を設定
3) 輪郭から作成した厚みのある3Dオブジェクトに 2)を設定

⑦-1) キャプチャ画像からパス部分の画像を切り抜く

キャプチャ画像から輪郭パスの内側のみを切り抜く。
o2.png
このパス&キャプチャ画像を例にとって切り抜く過程を見ていきましょう。

まず、CGPathはVisionの座標系である左下(0, 0)、右上(1, 1)となっているので、これを左上(0, 0)、右下(320, 320)に変換する。

var transform = CGAffineTransform(scaleX: detectSize, y: -detectSize)
transform = transform.concatenating(CGAffineTransform(translationX: 0, y: detectSize))
guard let transPath = normalizedPath.copy(using: &transform) else { return nil }

この部分の説明は前々回の記事の『⑤④を表示する』を参照ください。

次に、変換したパスをオフスクリーンに描画する。

// パス描画のスケールを端末の種類によらず '1px/pt' に固定する。
let format = UIGraphicsImageRendererFormat()
format.scale = 1
// オフスクリーンでパスを描画する(白で塗りつぶす)
let pathFillImage = UIGraphicsImageRenderer(size: CGSize(width: self.detectSize, height: self.detectSize), format: format).image { context in
    context.cgContext.setFillColor(UIColor.white.cgColor)
    context.cgContext.addPath(transPath)
    context.cgContext.fillPath()
}

あとで画面キャプチャ画像と乗算合成する際に用いるため白色で塗りつぶす。
g2.png
次に、キャプチャした画像をCGImage経由でCIImageに変換する。

let ciContext = CIContext(options: nil)
guard let captureCGImage = ciContext.createCGImage(captureImage, from: captureImage.extent) else { return nil }
let captureCIImage = CIImage(cgImage: captureCGImage)

もともとキャプチャ画像はCIImageなのになぜ、CGImage経由で変換するのかというと、このあとの乗算合成で問題が起きるから。下は加工前のキャプチャ画像のCIImageで、サイズは320x320となっているが、originが(0,0)ではない。
captureImage.png

originが(0, 0)でない場合、CIFilterによる合成結果がずれてしまうので、CGImage経由でCIImageに変換している。

最後に白色パス画像とキャプチャ画像を乗算合成する。

let filter = CIFilter.multiplyCompositing()
filter.inputImage = captureCIImage
filter.backgroundImage = maskCIImage
guard let texture = filter.outputImage else { return nil }

画像合成は CIMultiplyCompositing を使う。このフィルターは2つの画像を乗算合成する。乗算なので画像Aに真っ白い部分があれば、画像Bの対応する部分はそのまま色が残り(白は '1'なので 1 × b = b )、画像Aに真っ黒な部分があれば、画像Bの対応する部分は黒となる(黒は '0'なので 0 × b = 0 )。

■合成結果。パスの内側のキャプチャ画像を切り出せている。
g3.png

⑦-2) 輪郭検出範囲に一致する平面ジオメトリ を作り、1)を設定

作成したテクスチャを貼る平面ジオメトリ を作成する。
前回の記事で、CGPath→UIBezierPathへの変換の際に、輪郭検出範囲の四隅について、画面の中央を(0, 0, 0)としたときのワールド座標上の位置(画面中央からの相対位置なのでローカルというべきか?)は取得できているのでこれを使ってポリゴンを定義する。

// 四隅の座標をワールド座標の中心を基準にした座標に変換
let worldCenter = (leftTopWorldPosition + rightTopWorldPosition + leftBottomWorldPosition + rightBottomWorldPosition) / 4
self.leftTop = leftTopWorldPosition - worldCenter
self.rightTop = rightTopWorldPosition - worldCenter
self.leftBottom = leftBottomWorldPosition - worldCenter
self.rightBottom = rightBottomWorldPosition - worldCenter

上の四隅の座標を頂点座標の配列にする。

guard let lt = leftTop, let rt = rightTop, let lb = leftBottom, let rb = rightBottom else { return nil }
let vertices = [ lt, rt, lb, rb ]

次に頂点座標に対応するテクスチャ座標の配列を作る。

// テクスチャ座標
private let texcoords: [CGPoint] = [
    CGPoint(x: 0.0, y: 0.0),    // 左上
    CGPoint(x: 1.0, y: 0.0),    // 右上
    CGPoint(x: 0.0, y: 1.0),    // 左下
    CGPoint(x: 1.0, y: 1.0),    // 右下
]

最後にポリゴンの3角形の順番の配列を作る(四角形なので2つだけ)。

// テクスチャを貼るノードのポリンゴンのインデックス
private let indices: [Int32] = [
    0, 2, 1,    // 左上、左下、右上の三角形
    1, 2, 3,    // 右上、左下、右下の三角形
]

ここまでに作成した頂点座標配列、テクスチャ座標配列、座標のインデックス配列をもとにして、次のようにジオメトリ を作成する。

let verticeSource = SCNGeometrySource(vertices: vertices)
let texcoordSource = SCNGeometrySource(textureCoordinates: self.texcoords)
let geometryElement = SCNGeometryElement(indices: self.indices, primitiveType: .triangles)
let geometry = SCNGeometry(sources: [verticeSource, texcoordSource], elements: [geometryElement])

作成したジオメトリ に、⑦-1) で作成したテクスチャを設定してジオメトリ は完成。

let context = CIContext(options: nil)
let cgImage = context.createCGImage(texture, from: texture.extent)
let matrial = SCNMaterial()
matrial.diffuse.contents = cgImage
geometry.materials = [matrial]

ここでのCIImageはテクスチャとして認識されないのでCGImageに変換してからマテリアルに設定している。

⑦-3) 輪郭から作成した厚みのある3Dオブジェクトに 2)を設定

用意するSCNNodeは全部で3つになる。

  • パスから作ったノード。厚みがある
  • テクスチャを貼っただけの平面ノード。
  • 上記2つを組み合わせるノード。これに物理判定を設定する。
// パスの厚みを持つノードと表面のテクスチャが貼られた平面ノードの親ノードを作成
let node = SCNNode()
node.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0)

// ベジェ曲線から3Dモデルを作成
let pathShapeNode = makePathShapeNode(geometryPath: geometryPath)
pathShapeNode.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
node.addChildNode(pathShapeNode)

// 3Dモデルの表面ノードを作成
guard let shapeFaceNode = makeShapeFaceNode(from: normalizedPath, captureImage: captureImage) else { return nil }
shapeFaceNode.eulerAngles = SCNVector3(x: Float.pi/2, y: 0, z: 0)
shapeFaceNode.position = SCNVector3(0, 0.0, 0.0051) // 表面の位置になるように座標を調整
node.addChildNode(shapeFaceNode)

// ノードに物理判定情報を設定
node.physicsBody = makeShapePhysicsBody(from: pathShapeNode.geometry)

ポイント(?)は shapeFaceNode.position = SCNVector3(0, 0.0, 0.0051) の部分で、テクスチャがちょうどパスの3Dモデルの表面部分になるように座標調整しているところ。

ここで気になるのが、『パスから作った3Dモデルと、輪郭検出範囲の四隅の情報から作ったテクスチャモデルがぴったり合うかどうか?』。結果を見ると、、、合っている!
z1.png
3Dモデルは、VNContourから得られるCGPathパスを『重心座標系での線形補間』を使ってワールド座標スケールに変換している。かたや、テクスチャは四隅の座標だけでスケールしている。どちらも四隅のワールド座標をもとに計算はしているが『重心座標系での線形補間』の正しらしさが不明だったので、ずれるかも、、と思っていたが、それなりに見えるように変換できていたようだ。

説明は以上です。

全体ソースコード

ViewController.swift
import ARKit
import Vision
import CoreImage.CIFilterBuiltins
import SwiftUI
import UIKit

class ViewController: UIViewController, ARSessionDelegate, ARSCNViewDelegate {

    @IBOutlet weak var scnView: ARSCNView!
    // 輪郭描画用
    private var contourPathLayer: CAShapeLayer?
    // キャプチャ画像上の輪郭検出範囲
    private let detectSize: CGFloat = 320.0
    // 3次元化ボタンが押下状態
    private var isButtonPressed = false
    // 床の厚さ(m)
    private let floorThickness: CGFloat = 1.0
    // 床のローカル座標。床の厚さ分、Y座標を下げる
    private lazy var floorLocalPosition = SCNVector3(0.0, -self.floorThickness/2, 0.0)
    // SCNShapeの仮の拡大率。SCNShapeに小さいジオメトリ を与えるとジオメトリが崩れるので拡大する
    private let tempGeometryScale: CGFloat = 10.0
    // 検出領域の四隅のシーン内の位置を示すマーカーノード
    private var cornerMarker1: SCNNode!
    private var cornerMarker2: SCNNode!
    private var cornerMarker3: SCNNode!
    private var cornerMarker4: SCNNode!
    // 輪郭検出範囲
    private var leftTop: SCNVector3?
    private var rightTop: SCNVector3?
    private var leftBottom: SCNVector3?
    private var rightBottom: SCNVector3?
    // テクスチャを貼るノードのポリンゴンのインデックス
    private let indices: [Int32] = [
        0, 2, 1,    // 左上、左下、右上の三角形
        1, 2, 3,    // 右上、左下、右下の三角形
    ]
    // テクスチャ座標
    private let texcoords: [CGPoint] = [
        CGPoint(x: 0.0, y: 0.0),    // 左上
        CGPoint(x: 1.0, y: 0.0),    // 右上
        CGPoint(x: 0.0, y: 1.0),    // 左下
        CGPoint(x: 1.0, y: 1.0),    // 右下
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        // シーンの設定
        self.setupScene()
        // AR Session 開始
        self.scnView.delegate = self
        self.scnView.session.delegate = self
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal]
        self.scnView.session.run(configuration, options: [.removeExistingAnchors, .resetTracking])
    }

    // アンカーが追加された
    func renderer(_: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard anchor is ARPlaneAnchor else { return }

        // 床ノードを追加
        let floorNode = makeFloorNode()
        DispatchQueue.main.async {
            node.addChildNode(floorNode)
        }
    }

    // アンカーが更新された
    func renderer(_: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard anchor is ARPlaneAnchor else { return }

        if let childNode = node.childNodes.first {
            DispatchQueue.main.async {
                // 床ノードの位置を再設定
                childNode.position = self.floorLocalPosition
            }
        }
    }

    // ARフレームが更新された
    func session(_ session: ARSession, didUpdate frame: ARFrame) {
        // キャプチャ画像をスクリーンで見える範囲に切り抜く
        let screenImage = cropScreenImageFromCapturedImage(frame: frame)
        // 一番外側の輪郭を取得
        guard let contour = getFirstOutsideContour(screenImage: screenImage) else { return }
        // UIKitの座標系のCGPathを取得
        guard let path = getCGPathInUIKitSpace(contour: contour) else { return }

        DispatchQueue.main.async {
            // 輪郭(2D)を描画
            self.drawContourPath(path)
            // 輪郭(3D)を描画
            let croppedImage = screenImage.cropped(to: CGRect(x: screenImage.extent.width/2 - self.detectSize/2,
                                                              y: screenImage.extent.height/2 - self.detectSize/2,
                                                              width: self.detectSize,
                                                              height: self.detectSize))
            if  self.isButtonPressed {
                self.isButtonPressed = false
                self.drawContour3DModel(normalizedPath: contour.normalizedPath, captureImage: croppedImage)
            }
        }
    }

    private func setupScene() {
        // ディレクショナルライト追加
        let directionalLightNode = SCNNode()
        directionalLightNode.light = SCNLight()
        directionalLightNode.light?.type = .directional
        directionalLightNode.light?.castsShadow = true  // 影が出るライトにする
        directionalLightNode.light?.shadowMapSize = CGSize(width: 2048, height: 2048)   // シャドーマップを大きくしてジャギーが目立たないようにする
        directionalLightNode.light?.shadowSampleCount = 2   // 影の境界を若干柔らかくする
        directionalLightNode.light?.shadowColor = UIColor.lightGray.withAlphaComponent(0.8) // 影の色は明るめ
        directionalLightNode.position = SCNVector3(x: 0, y: 3, z: 0)
        directionalLightNode.eulerAngles = SCNVector3(x: -Float.pi/3, y: 0, z: -Float.pi/3)
        self.scnView.scene.rootNode.addChildNode(directionalLightNode)
        // 暗いので環境光を追加
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light?.type = .ambient
        directionalLightNode.position = SCNVector3(x: 0, y: 0, z: 0)
        self.scnView.scene.rootNode.addChildNode(ambientLightNode)
        // 検出領域の四隅のシーン内のマーカーノード
        self.cornerMarker1 = makeMarkerNode()
        self.cornerMarker1.isHidden = true
        self.scnView.scene.rootNode.addChildNode(self.cornerMarker1)
        self.cornerMarker2 = makeMarkerNode()
        self.cornerMarker2.isHidden = true
        self.scnView.scene.rootNode.addChildNode(self.cornerMarker2)
        self.cornerMarker3 = makeMarkerNode()
        self.cornerMarker3.isHidden = true
        self.scnView.scene.rootNode.addChildNode(self.cornerMarker3)
        self.cornerMarker4 = makeMarkerNode()
        self.cornerMarker4.isHidden = true
        self.scnView.scene.rootNode.addChildNode(self.cornerMarker4)
    }

    // ジオメトリ化ボタンが押された
    @IBAction func pressButton(_ sender: Any) {
        isButtonPressed = true
    }
}

// MARK: - 輪郭検出関連

extension ViewController {

    private func getFirstOutsideContour(screenImage: CIImage) -> VNContour? {
        // 輪郭検出しやすいように画像処理を行う
        guard let preprocessedImage = preprocessForDetectContour(screenImage: screenImage) else { return nil }
        // 輪郭検出
        let handler = VNImageRequestHandler(ciImage: preprocessedImage)
        let contourRequest = VNDetectContoursRequest.init()
        contourRequest.maximumImageDimension = Int(self.detectSize) // 検出画像サイズはクリップした画像と同じにする。デフォルトは512。
        contourRequest.detectsDarkOnLight = true                    // 明るい背景で暗いオブジェクトを検出
        try? handler.perform([contourRequest])
        // 検出結果取得
        guard let observation = contourRequest.results?.first as? VNContoursObservation else { return nil }
        // トップレベルの輪郭のうち、輪郭の座標数が一番多いパスを見つける
        let outSideContour = observation.topLevelContours.max(by: { $0.normalizedPoints.count < $1.normalizedPoints.count })
        if let contour = outSideContour {
            return contour
        } else {
            return nil
        }
    }

    private func cropScreenImageFromCapturedImage(frame: ARFrame) -> CIImage {

        let imageBuffer = frame.capturedImage
        // カメラキャプチャ画像をスクリーンサイズに変換
        // 参考 : https://stackoverflow.com/questions/58809070/transforming-arframecapturedimage-to-view-size
        let imageSize = CGSize(width: CVPixelBufferGetWidth(imageBuffer), height: CVPixelBufferGetHeight(imageBuffer))
        let viewPortSize = self.scnView.bounds.size
        let interfaceOrientation  = self.scnView.window!.windowScene!.interfaceOrientation
        let image = CIImage(cvImageBuffer: imageBuffer)
        // 1) キャプチャ画像を 0.0〜1.0 の座標に変換
        let normalizeTransform = CGAffineTransform(scaleX: 1.0/imageSize.width, y: 1.0/imageSize.height)
        // 2) 「Flip the Y axis (for some mysterious reason this is only necessary in portrait mode)」とのことでポートレートの場合に座標変換。
        //     Y軸だけでなくX軸も反転が必要。
        var flipTransform = CGAffineTransform.identity
        if interfaceOrientation.isPortrait {
            // X軸Y軸共に反転
            flipTransform = CGAffineTransform(scaleX: -1, y: -1)
            // X軸Y軸共にマイナス側に移動してしまうのでプラス側に移動
            flipTransform = flipTransform.concatenating(CGAffineTransform(translationX: 1, y: 1))
        }
        // 3) キャプチャ画像上でのスクリーンの向き・位置に移動
        // 参考 : https://developer.apple.com/documentation/arkit/arframe/2923543-displaytransform
        let displayTransform = frame.displayTransform(for: interfaceOrientation, viewportSize: viewPortSize)
        // 4) 0.0〜1.0 の座標系からスクリーンの座標系に変換
        let toViewPortTransform = CGAffineTransform(scaleX: viewPortSize.width, y: viewPortSize.height)
        // 5) 1〜4までの変換を行い、変換後の画像をスクリーンサイズでクリップ
        let transformedImage = image.transformed(by: normalizeTransform.concatenating(flipTransform).concatenating(displayTransform).concatenating(toViewPortTransform)).cropped(to: self.scnView.bounds)
        return transformedImage
    }

    private func preprocessForDetectContour(screenImage: CIImage) -> CIImage? {
        // 画像の暗い部分を広げて細い線を太くする。
        // WWDC2020(https://developer.apple.com/videos/play/wwdc2020/10673/)
        // 04:06あたりで紹介されているCIMorphologyMinimumを利用。
        let blurFilter = CIFilter.morphologyMinimum()
        blurFilter.inputImage = screenImage
        blurFilter.radius = 5
        guard let blurImage = blurFilter.outputImage else { return nil }
        // ペンの線を強調。RGB各々について閾値より明るい色は 1.0 にする。
        let thresholdFilter = CIFilter.colorThreshold()
        thresholdFilter.inputImage = blurImage
        thresholdFilter.threshold = 0.1
        guard let thresholdImage = thresholdFilter.outputImage else { return nil }
        // 検出範囲を画面の中心部分に限定する
        let screenImageSize = screenImage.extent    // CIMorphologyMinimumフィルタにより画像サイズと位置が変わってしまうので、オリジナル画像のサイズ・位置を基準にする
        let croppedImage = thresholdImage.cropped(to: CGRect(x: screenImageSize.width/2 - detectSize/2,
                                                             y: screenImageSize.height/2 - detectSize/2,
                                                             width: detectSize,
                                                             height: detectSize))
        return croppedImage
    }
}
// MARK: - パス描画(2D)

extension ViewController {

    private func getCGPathInUIKitSpace(contour: VNContour) -> CGPath? {
        // UIKitで使うため、クリップしたときのサイズに拡大し、上下の座標を反転後、左上が (0,0)になるようにする
        let path = contour.normalizedPath
        var transform = CGAffineTransform(scaleX: detectSize, y: -detectSize)
        transform = transform.concatenating(CGAffineTransform(translationX: 0, y: detectSize))
        let transPath = path.copy(using: &transform)
        return transPath
    }

    private func drawContourPath(_ path: CGPath) {
        // 表示中のパスは消す
        if let layer = self.contourPathLayer {
            layer.removeFromSuperlayer()
            self.contourPathLayer = nil
        }
        // 輪郭を描画
        let pathLayer = CAShapeLayer()
        var frame = self.view.bounds
        frame.origin.x = frame.width/2 - detectSize/2
        frame.origin.y = frame.height/2 - detectSize/2
        frame.size.width = detectSize
        frame.size.height = detectSize
        pathLayer.frame = frame
        pathLayer.path = path
        pathLayer.strokeColor = UIColor.blue.cgColor
        pathLayer.lineWidth = 10
        pathLayer.fillColor = UIColor.clear.cgColor
        self.view.layer.addSublayer(pathLayer)
        self.contourPathLayer = pathLayer
    }
}
// MARK: - パス描画(3D)

extension ViewController {

    private func drawContour3DModel(normalizedPath: CGPath, captureImage: CIImage) {
        // ベジェパスをもとにノードを生成
        guard let node = makeNode(from: normalizedPath, captureImage: captureImage) else { return }

        // 画面中央上の20cm上から落とす
        let screenCenter = CGPoint(x: self.view.bounds.width/2, y: self.view.bounds.height/2 - 150)
        guard var position = self.getWorldPosition(from: screenCenter) else { return }
        position.y += 0.2
        node.worldPosition = position
        self.scnView.scene.rootNode.addChildNode(node)
    }

    // レイキャストでワールド座標を取得
    private func getWorldPosition(from: CGPoint) -> SCNVector3? {

        guard let query = self.scnView.raycastQuery(from: from, allowing: .existingPlaneGeometry, alignment: .horizontal),
              let result = self.scnView.session.raycast(query).first else {
            return nil
        }
        let p = result.worldTransform.columns.3
        return SCNVector3(p.x, p.y, p.z)
    }

    private func makeFloorNode() -> SCNNode {
        // 落ちてくるノードを受け止めるためアンカーに大きめなSCNBoxを設定する。
        let geometry = SCNBox(width: 3.0, height: 3.0, length: self.floorThickness, chamferRadius: 0.0)
        let material = SCNMaterial()
        material.lightingModel = .shadowOnly    // 平面の色は影だけになるように指定
        geometry.materials = [material]
        let node = SCNNode(geometry: geometry)
        node.position = self.floorLocalPosition
        node.castsShadow = false                // これがないとplaneNodeがチラつくことがある
        node.transform = SCNMatrix4MakeRotation(-Float.pi / 2, 1, 0, 0)
        node.physicsBody = SCNPhysicsBody.static()
        node.physicsBody?.friction = 1.0        // この辺りのプロパティはモデルの物理運動を抑止するためのもの
        node.physicsBody?.restitution = 0.0
        node.physicsBody?.rollingFriction = 1.0
        node.physicsBody?.angularDamping = 1.0
        node.physicsBody?.linearRestingThreshold = 1.0
        node.physicsBody?.angularRestingThreshold = 1.0

        return node
    }

    private func makeMarkerNode() -> SCNNode {

        let sphere = SCNSphere(radius: 0.001)
        let material = SCNMaterial()
        material.diffuse.contents = UIColor.red
        sphere.materials = [material]
        return SCNNode(geometry: sphere)
    }

    private func convertPath(from normalizedPath: CGPath) -> UIBezierPath? {
        // 検出領域の四隅のワールド座標を取得
        let origin = CGPoint(x: self.view.bounds.width/2 - self.detectSize/2,
                             y: self.view.bounds.height/2 - self.detectSize/2)
        guard let leftTopWorldPosition = self.getWorldPosition(from: origin),
              let rightTopWorldPosition = self.getWorldPosition(from: CGPoint(x: origin.x + self.detectSize,
                                                                              y: origin.y)),
              let leftBottomWorldPosition = self.getWorldPosition(from: CGPoint(x: origin.x,
                                                                                y: origin.y + self.detectSize)),
              let rightBottomWorldPosition = self.getWorldPosition(from: CGPoint(x: origin.x + self.detectSize,
                                                                                 y: origin.y + self.detectSize)) else {
            print("検出領域の四隅のワールド座標が取れない。iPhoneを前後左右に動かしてください。")
            return nil
        }
        // 検出した座標にワールド座標位置確認用の赤い球を配置
        self.cornerMarker1.worldPosition = leftTopWorldPosition
        self.cornerMarker1.isHidden = false
        self.cornerMarker2.worldPosition = rightTopWorldPosition
        self.cornerMarker2.isHidden = false
        self.cornerMarker3.worldPosition = leftBottomWorldPosition
        self.cornerMarker3.isHidden = false
        self.cornerMarker4.worldPosition = rightBottomWorldPosition
        self.cornerMarker4.isHidden = false
        // 四隅の座標をワールド座標の中心を基準にした座標に変換
        let worldCenter = (leftTopWorldPosition + rightTopWorldPosition + leftBottomWorldPosition + rightBottomWorldPosition) / 4
        self.leftTop = leftTopWorldPosition - worldCenter
        self.rightTop = rightTopWorldPosition - worldCenter
        self.leftBottom = leftBottomWorldPosition - worldCenter
        self.rightBottom = rightBottomWorldPosition - worldCenter
        // 2次元のCGPathを3次元の座標系に変換
        let geometryPath = UIBezierPath()
        let path = Path(normalizedPath)
        var elementCount = 0
        path.forEach { element in
            switch element {
            case .move(to: let to):
                geometryPath.move(to: convertPathPoint(to))
            case .line(to: let to):
                geometryPath.addLine(to: convertPathPoint(to))
            case .quadCurve(to: let to, control: _):
                geometryPath.addLine(to: convertPathPoint(to))
            case .curve(to: let to, control1: _, control2: _):
                geometryPath.addLine(to: convertPathPoint(to))
            case .closeSubpath:
                geometryPath.close()
                break
            }
            elementCount += 1
        }
        print("path element count[\(elementCount)]")
        return geometryPath
    }

    private func convertPathPoint(_ from: CGPoint) -> CGPoint {

        guard let leftTop = self.leftTop,
              let rightTop = self.rightTop,
              let leftBottom = self.leftBottom,
              let rightBottom = self.rightBottom else {
            return CGPoint.zero
        }
        // パスの各座標について三角形の重心座標系でワールド座標を導出
        var point = CGPoint.zero
        let pl: CGFloat = 1.0     // CGPathの一辺の長さ。VNContourの返す輪郭は(0,0)〜(1,1)の範囲
        if from.y > from.x {
            // 四角形の上側の三角形
            let t: CGFloat = pl * pl / 2    // 四角形の上側の三角形の面積
            let t2 = pl * (pl - from.y) / 2   // t2の面積
            let t3 = pl * from.x / 2          // t3の面積
            let t1 = t - t2 - t3            // t1の面積

            let ltRatio = t1 / t    // 左上座標の割合
            let rtRatio = t3 / t    // 右上座標の割合
            let lbRatio = t2 / t    // 左下座標の割合

            // 各頂点の重みに応じてワールド座標を算出
            let p = leftTop * ltRatio + rightTop * rtRatio + leftBottom * lbRatio
            point.x = p.x.cg
            point.y = p.z.cg * -1
        } else {
            // 四角形の下側の三角形
            let t: CGFloat = pl * pl / 2    // 四角形の下側の三角形の面積
            let t5 = pl * from.y / 2          // t5の面積
            let t6 = pl * (pl - from.x) / 2   // t6の面積
            let t4 = t - t5 - t6            // t4の面積

            let rtRatio = t5 / t    // 右上座標の割合
            let lbRatio = t6 / t    // 左下座標の割合
            let rbRatio = t4 / t    // 右下座標の割合

            // 各頂点の重みに応じてワールド座標を算出
            let p = rightTop * rtRatio + leftBottom * lbRatio + rightBottom * rbRatio
            point.x = p.x.cg
            point.y = p.z.cg * -1
        }
        // 後でSCNShapeに与える座標となるが、SCNShapeに小さい座標を与えると正しく表示されないのでいったん、拡大しておく。
        return point * self.tempGeometryScale
    }

    private func makeNode(from normalizedPath: CGPath, captureImage: CIImage) -> SCNNode? {
        // 輪郭(CGPath)をワールド座標のUIBezierPathに変換
        guard let geometryPath = convertPath(from: normalizedPath) else { return nil }

        // パスの厚みを持つノードと表面のテクスチャが貼られた平面ノードの親ノードを作成
        let node = SCNNode()
        node.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0)

        // ベジェ曲線から3Dモデルを作成
        let pathShapeNode = makePathShapeNode(geometryPath: geometryPath)
        pathShapeNode.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
        node.addChildNode(pathShapeNode)

        // 3Dモデルの表面ノードを作成
        guard let shapeFaceNode = makeShapeFaceNode(from: normalizedPath, captureImage: captureImage) else { return nil }
        shapeFaceNode.eulerAngles = SCNVector3(x: Float.pi/2, y: 0, z: 0)
        shapeFaceNode.position = SCNVector3(0, 0.0, 0.0051) // 表面の位置になるように座標を調整
        node.addChildNode(shapeFaceNode)

        // ノードに物理判定情報を設定
        node.physicsBody = makeShapePhysicsBody(from: pathShapeNode.geometry)

        return node
    }

    private func makePathShapeNode(geometryPath: UIBezierPath) -> SCNNode {

        let geometry = SCNShape(path: geometryPath, extrusionDepth: 0.01 * self.tempGeometryScale)
        let node = SCNNode(geometry: geometry)
        // ベジェパスの座標計算時にいったん、拡大していたので縮小する
        node.scale = SCNVector3(1/self.tempGeometryScale, 1/self.tempGeometryScale, 1/self.tempGeometryScale)
        node.castsShadow = true // ノードの影をつける
        let material = SCNMaterial()
        material.diffuse.contents = UIColor.lightGray
        geometry.materials = [material]
        node.geometry = geometry

        return node
    }

    private func makeShapeFaceNode(from normalizedPath: CGPath, captureImage: CIImage) -> SCNNode? {
        // パスを塗りつぶす画像を生成
        var transform = CGAffineTransform(scaleX: detectSize, y: -detectSize)
        transform = transform.concatenating(CGAffineTransform(translationX: 0, y: detectSize))
        guard let transPath = normalizedPath.copy(using: &transform) else { return nil }

        // パス描画のスケールを端末の種類によらず '1px/pt' に固定する。
        let format = UIGraphicsImageRendererFormat()
        format.scale = 1
        // オフスクリーンでパスを描画する(白で塗りつぶす)
        let pathFillImage = UIGraphicsImageRenderer(size: CGSize(width: self.detectSize, height: self.detectSize), format: format).image { context in
            context.cgContext.setFillColor(UIColor.white.cgColor)
            context.cgContext.addPath(transPath)
            context.cgContext.fillPath()
        }
        // 描いたパスをCGImage経由でCIImageに変換
        guard let maskCGImage = pathFillImage.cgImage else { return nil }
        let maskCIImage = CIImage(cgImage: maskCGImage)

        // キャプチャした画像CIImageをCGImageに変換して再度、CIImageに戻す。
        // テクスチャ用のCIImageはCropしているせいだと思われるが、Filterをかけると、CIImage内部に持っている画像のオフセットが無視されて思ったようなフィルタをかけられない。
        let ciContext = CIContext(options: nil)
        guard let captureCGImage = ciContext.createCGImage(captureImage, from: captureImage.extent) else { return nil }
        let captureCIImage = CIImage(cgImage: captureCGImage)

        // パスの内側だけキャプチャした画像を切り抜く
        let filter = CIFilter.multiplyCompositing()
        filter.inputImage = captureCIImage
        filter.backgroundImage = maskCIImage
        guard let texture = filter.outputImage else { return nil }

        // テクスチャを貼るだけのノードを作る
        let textureNode = SCNNode()
        textureNode.geometry = makeShapeFaceGeometory(texture: texture)

        return textureNode
    }

    private func makeShapeFaceGeometory(texture: CIImage) -> SCNGeometry? {
        // パス検出範囲が四隅となる平面ジオメトリ を作成
        guard let lt = leftTop, let rt = rightTop, let lb = leftBottom, let rb = rightBottom else { return nil }
        let vertices = [ lt, rt, lb, rb ]

        let verticeSource = SCNGeometrySource(vertices: vertices)
        let texcoordSource = SCNGeometrySource(textureCoordinates: self.texcoords)
        let geometryElement = SCNGeometryElement(indices: self.indices, primitiveType: .triangles)
        let geometry = SCNGeometry(sources: [verticeSource, texcoordSource], elements: [geometryElement])

        // マテリアルにテクスチャを設定
        let context = CIContext(options: nil)
        let cgImage = context.createCGImage(texture, from: texture.extent)
        let matrial = SCNMaterial()
        matrial.diffuse.contents = cgImage
        geometry.materials = [matrial]

        return geometry
    }

    private func makeShapePhysicsBody(from: SCNGeometry?) -> SCNPhysicsBody? {

        guard let geometry = from else { return nil }
        let bodyMax = geometry.boundingBox.max
        let bodyMin = geometry.boundingBox.min
        let bodyGeometry = SCNBox(width: (bodyMax.x - bodyMin.x).cg * 1/self.tempGeometryScale,
                                  height: (bodyMax.y - bodyMin.y).cg * 1/self.tempGeometryScale,
                                  length: (bodyMax.z - bodyMin.z).cg * 1/self.tempGeometryScale,
                                  chamferRadius: 0.0)
        let bodyShape = SCNPhysicsShape(geometry: bodyGeometry, options: nil)
        let physicsBody = SCNPhysicsBody(type: .dynamic, shape: bodyShape)
        physicsBody.friction = 1.0
        physicsBody.restitution = 0.0
        physicsBody.rollingFriction = 1.0
        physicsBody.angularDamping = 1.0
        physicsBody.linearRestingThreshold = 1.0
        physicsBody.angularRestingThreshold = 1.0

        return physicsBody
    }
}

extension SCNVector3 {
    static func + (lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3{
        return SCNVector3(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z)
    }

    static func - (lhs: SCNVector3, rhs: SCNVector3) -> SCNVector3{
        return SCNVector3(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z)
    }

    static func * (lhs: SCNVector3, rhs: CGFloat) -> SCNVector3{
        return SCNVector3(lhs.x * Float(rhs), lhs.y * Float(rhs), lhs.z * Float(rhs))
    }

    static func / (lhs: SCNVector3, rhs: Float) -> SCNVector3{
        return SCNVector3(lhs.x / rhs, lhs.y / rhs, lhs.z / rhs)
    }
}

extension CGPoint {
    static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint{
        return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
    }
}

extension Float {
    var cg: CGFloat { CGFloat(self) }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

guard文まとめてみた

guard文の役割

想定外の状況が起こった時にその処理から抜け出す役割をします。

書き方

guard 条件 else {
   条件を満たさなかったときの処理
   break //returnでも良い
} 条件を満たした時の処理

使ってみた

number2を割る数として割る数に0が入った時、{}内の処理が行われます。
条件式 number2 != 0の時{}外の処理
number2 == 0の時{}内の処理です。

            guard number2 != 0 else{
                answerLabel.text = "割る数には0以外の数字を入れてください"
                return
            }
            kekka = number1 / number2

参考文献

詳解Swift 第5版

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

swich文について初歩的なことをかく

スクリーンショット 2020-11-29 8.07.16.png

swich文の書き方

    let number1 = Double(number1TextField.text!) ?? 0
    let number2 = Double(number2TextField.text!) ?? 0
    var kekka :Double = 0
switch calculateSegmentedControl.selectedSegmentIndex{
    case 0:
        kekka = number1 + number2
    case 1:
        kekka = number1 - number2
    case 2:
        kekka = number1 * number2
    case 3:
        guard number2 != 0 else{
            answerLabel.text = "割る数には0以外の数字を入れてください"
            return
        }
        kekka = number1 / number2
      default:
        print("該当なし")
    }

メモ程度なので自分が使用したコードをそのまま載せます。
swich 式 {
case ラベル1:
    文...
case ラベル2:
    文...
default:
文...
}
こんな感じで書く。
式にはcaseで条件分けしたい式を書きます。この場合だとsegmentedControlのIndexを式に入れることで、ラベルに0番目、1番目と指定してそれぞれの処理を書いていきます。

segmentedControler
スクリーンショット 2020-11-29 8.07.16.png
スクリーンショット 2020-11-29 8.07.16.png

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