UIPickerView:コンポーネント内の表示内容を更新

今回はUIPickerViewを使用して、以下のことを実装していきます。

※今回はUIPickerViewを3つ使用して実装していきます。
下記はわかりにくいですが、実装画像になります。

f:id:SumJun-Blog:20220213154749p:plain:w200f:id:SumJun-Blog:20220213154951p:plain:w200

環境

Xcode:13.1
Swift:5.5.1

全体

まず全体のコードをお見せします。
Storyboardは使用せず、コードのみの実装になります。

//ViewController.swift:メイン画面

import UIKit

class ViewController: UIViewController {
    
    //MARK: - Property
    
    ///西暦の配列
    private let yearArray = ["2017", "2018", "2019", "2020", "2021"]
    ///月の配列
    private let monthArray = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
    ///日数の配列
    private var dayArray = [String]()
    ///西暦のピッカー
    private var yearPickerView = UIPickerView()
    ///月のピッカー
    private var monthPickerView = UIPickerView()
    ///日数のピッカー
    private var dayPickerView = UIPickerView()
    
    //MARK: - Initialization
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        setPicker()
        setCreateDayArray31()
    }
    ///ピッカーの初期化
    private func setPicker() {
        /*
         PickerView初期化
         各PickerViewのデリゲート指定
         */
        yearPickerView.delegate = self
        yearPickerView.dataSource = self
        monthPickerView.delegate = self
        monthPickerView.dataSource = self
        dayPickerView.delegate = self
        dayPickerView.dataSource = self
        //ピッカーの表示、配置
        let stackView = UIStackView(arrangedSubviews: [yearPickerView, monthPickerView, dayPickerView])
        view.addSubview(stackView)
        yearPickerView.anchor(width: UIScreen.main.bounds.size.width / 3, height: 450)
        stackView.axis = .horizontal
        stackView.distribution = .fillEqually
        stackView.spacing = 0
        stackView.anchor(centerY: view.centerYAnchor, centerX: view.centerXAnchor)
    }
    
    //MARK: - Method
    
    /*
     ■行数(要素数)の切り替えメソッド
     各月で異なる日数に対応するため(2月:28日(29日)、1月:31日など)
     ①配列内の日数を全て削除
     理由:例えば2月は28日分を生成して、3月に切り替えたとき、3月は31日分をそのまま生成してしまうと。28日のあとに31日分が追加で生成されてしまうため。
     ②最初の要素を用意。
     理由:for文は要素がある状態での処理が前提になるため。
     ③新たに生成した配列をピッカーに反映するために更新する。
     */
    ///31日分生成
    private func setCreateDayArray31() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 30 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }
    ///30日分生成
    private func setCreateDayArray30() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 29 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }
    ///28日分生成
    private func setCreateDayArray28() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 27 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }
}

//MARK: - UIPickerViewDelegate, UIPickerViewDataSource
extension ViewController: UIPickerViewDelegate, UIPickerViewDataSource {
    
    ///各Pickerのコンポーネントの数(列数)
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    ///各コンポーネント(列)に並べる要素数(行数)
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        if pickerView == yearPickerView {
            return yearArray.count
        } else if pickerView == monthPickerView {
            return monthArray.count
        } else {
            return dayArray.count
        }
    }
    
    /*
     補足:ただ表示させるだけなら、デリメソtitleForRowでも良い
     ここでは、選択された行のみUI表示を変える処理に対応するために、デリメソviewForRowを使用している
     */
    ///各コンポーネントの表示設定
    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        //コンポーネントに表示するUI設定
        let componentLabel = UILabel()
        componentLabel.textColor = .gray
        componentLabel.backgroundColor = .clear
        componentLabel.textAlignment = .center
        componentLabel.font = .systemFont(ofSize: 25, weight: .regular)
        //各コンポーネントをUILabelを使用して表示
        if pickerView == yearPickerView {
            componentLabel.text = yearArray[row]
            return componentLabel
        } else if pickerView == monthPickerView {
            componentLabel.text = monthArray[row]
            return componentLabel
        } else {
            componentLabel.text = dayArray[row]
            return componentLabel
        }
    }
    
    ///各コンポーネントの行が選択された時の処理(ドラムロールが止まったとき)
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        //各コンポーネントの選択された行の表示をカスタム
        let selectLabel = pickerView.view(forRow: row, forComponent: component) as? UILabel ?? UILabel()
        selectLabel.textColor = .systemBlue
        selectLabel.backgroundColor = .clear
        selectLabel.textAlignment = .center
        selectLabel.font = .systemFont(ofSize: 25, weight: .regular)
        
        //選択した月毎に選択できる日数を変える
        if pickerView == monthPickerView {
            switch monthArray[row] {
            case "2": //2月
                setCreateDayArray28()
            case "4", "6", "9", "11": //4、6、9、11月
                setCreateDayArray30()
            default: //上記以外の月
                setCreateDayArray31()
            }
        }
    }
}
//View_Extension.swift:各UIの配置設定

import UIKit

extension UIView {
    ///Viewの配置設定
    /// - Parameters:
    ///   - top:基準となる上方向の対象物を設定
    ///   - bottom:基準となる下方向の対象物を設定
    ///   - left:基準となる左方向の対象物を設定
    ///   - right:基準となる右方向の対象物を設定
    ///   - centerY:指定した対象物内におけるY軸による位置を設定
    ///   - centerX:指定した対象物内におけるX軸による位置を設定
    ///   - width:幅の設定
    ///   - height:高さの設定
    ///   - topPadding:基準となる上方向の対象物との距離を設定
    ///   - bottomPadding:基準となる下方向の対象物との距離を設定
    ///   - leftPadding:基準となる左方向の対象物との距離を設定
    ///   - rightPadding:基準となる右方向の対象物との距離を設定
    func anchor(top: NSLayoutYAxisAnchor? = nil,
                bottom: NSLayoutYAxisAnchor? = nil,
                left: NSLayoutXAxisAnchor? = nil,
                right: NSLayoutXAxisAnchor? = nil,
                centerY: NSLayoutYAxisAnchor? = nil,
                centerX: NSLayoutXAxisAnchor? = nil,
                width: CGFloat? = nil,
                height: CGFloat? = nil,
                topPadding: CGFloat = 0,
                bottomPadding: CGFloat = 0,
                leftPadding: CGFloat = 0,
                rightPadding: CGFloat = 0) {
        
        self.translatesAutoresizingMaskIntoConstraints = false
        
        if let top = top {
            self.topAnchor.constraint(equalTo: top, constant: topPadding).isActive = true
        }
        
        if let bottom = bottom {
            self.bottomAnchor.constraint(equalTo: bottom, constant: -bottomPadding).isActive = true
        }
        
        if let left = left {
            self.leftAnchor.constraint(equalTo: left, constant: leftPadding).isActive = true
        }
        
        if let right = right {
            self.rightAnchor.constraint(equalTo: right, constant: -rightPadding).isActive = true
        }
        
        if let centerY = centerY {
            self.centerYAnchor.constraint(equalTo: centerY).isActive = true
        }
        
        if let centerX = centerX {
            self.centerXAnchor.constraint(equalTo: centerX).isActive = true
        }
        
        if let width = width {
            self.widthAnchor.constraint(equalToConstant: width).isActive = true
        }
        
        if let height = height {
            self.heightAnchor.constraint(equalToConstant: height).isActive = true
        }
    }
}


コンポーネントで選択された行だけUI表示を変える

選択された行だけ表示内容を変えるには、コンポーネントに含まれる全ての行(要素)をUIViewで表示させる必要があるので、まずはデリメソのviewForRowメソッドを使用してコンポーネントに含まれる要素をUILabelで表示する処理を書きます。

///各コンポーネントの表示設定
    func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
        //コンポーネントに表示するUI設定
        let componentLabel = UILabel()
        componentLabel.textColor = .gray
        componentLabel.backgroundColor = .clear
        componentLabel.textAlignment = .center
        componentLabel.font = .systemFont(ofSize: 25, weight: .regular)
        //各コンポーネントをUILabelを使用して表示
        if pickerView == yearPickerView {
            componentLabel.text = yearArray[row]
            return componentLabel
        } else if pickerView == monthPickerView {
            componentLabel.text = monthArray[row]
            return componentLabel
        } else {
            componentLabel.text = dayArray[row]
            return componentLabel
        }
    }

UILabelの設定をしたら、各ピッカーに反映させるために、ピッカーの場合分けで処理を書いていきます。
次はデリメソdidSelectRow内で「pickerView.view(forRow: row, forComponent: component)」を使用して、デリメソviewForRowで実装したUILabelと連動させます。あとはお好みでのカスタムになります。

//各コンポーネントの選択された行の表示をカスタム
        let selectLabel = pickerView.view(forRow: row, forComponent: component) as? UILabel ?? UILabel()
        selectLabel.textColor = .systemBlue
        selectLabel.backgroundColor = .clear
        selectLabel.textAlignment = .center
        selectLabel.font = .systemFont(ofSize: 25, weight: .regular)


コンポーネント(ピッカー)の選択された行(表示内容)で、別コンポーネント(別のピッカー)の表示内容を切り替える

同じくデリメソdidSelectRow内に処理を書いていきます。

//選択した月毎に選択できる日数を変える
        if pickerView == monthPickerView {
            switch monthArray[row] {
            case "2": //2月
                setCreateDayArray28()
            case "4", "6", "9", "11": //4、6、9、11月
                setCreateDayArray30()
            default: //上記以外の月
                setCreateDayArray31()
            }
        }

まずはピッカーのif文で月のUIPickerViewを指定し、その中でswitch文による「月」の場合分けをし、各月ごとにsetCreateDayArray〜〜メソッド(dayArray配列に1ヶ月分の日数を生成する処理)で日数を生成し、日数のピッカーに反映させます。
ここで以下のことがポイントになります。

  • ①新たに配列要素を生成する際は、既存の要素を削除しないと「既存要素+新要素」になってしまうこと
  • ②reloadAllComponentsをし忘れると、配列の変更が反映されないこと
    ①に関しては、今回は配列内の要素を全て削除して新たに要素を生成する処理を書いておりますが、要素を指定して削除したり、付け足したりする処理の方が楽かもしれません。
///31日分生成
    private func setCreateDayArray31() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 30 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }
    ///30日分生成
    private func setCreateDayArray30() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 29 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }
    ///28日分生成
    private func setCreateDayArray28() {
        dayArray.removeAll()
        dayArray.append("1")
        for day in 0 ..< 27 {
            let newDay = Int(dayArray[day])! + 1
            dayArray.append(String(newDay))
        }
        dayPickerView.reloadAllComponents()
    }


これで完了です。

未解決な部分

下記画像のように月の切替後の日数のピッカーを確認すると、reloadAllComponentsで再初期化されたためか、選択されてもその行のみのUI表示が反映されておりません。UIPickerViewの初期化状態で選択されている行は、選択されていない認識になっております。ここの解決方法に関しては今のところ見つかっておりませんので、他で調べてみてください。

f:id:SumJun-Blog:20220213162620p:plain:w250
以上になります。最後まで読んでいただきありがとうございます。

watchOS:App Store Connect申請時のリジェクト「ITMS-90496」

iOSとwatchOSが連動したアプリをApp Store Connectで申請した際に、下記内容でリジェクトされてしまった。

ITMS-90496: Invalid Executable - The executable 'プロジェクト名.app/Watch/プロジェクト名 WatchOS.app/PlugIns/プロジェクト名 WatchOS WatchKit Extension.appex/プロジェクト名 WatchOS WatchKit Extension' does not contain bitcode.

WatchOS WatchKit Extensionのビットコードが含まれていないと言われているので、含める方法をご紹介します。

環境

Xcode:13.1
Swift:5.5.1

解決方法

TargetでWatchOS WatchKit Extensionを選択→Build Setting→ENABLE_BITCODEの赤線箇所をNO→YESにするだけです。

f:id:SumJun-Blog:20220212153955p:plain:w400

以上になります。読んでいただきありがとうございます。

UITableViewCell:セルの重複を防ぐ

UITableViewをセットしてスクロールしてみると、セルの重複が発生してしまう。 その場合の解決方法を備忘録として残します。お役に立てれば幸いです。

f:id:SumJun-Blog:20220212091840p:plain:w200

環境

Xcode:13.1
Swift:5.5.1

全体

先に全体をお見せします。今回はStoryboardを使用せず、コードのみとなります。

//ViewController.swift:画面に表示させるクラス

import UIKit

class ViewController: UIViewController {
    
    ///セルに表示させるタイトルの配列
    let array = ["オオカミ", "イヌ", "ネコ", "ウマ", "ウシ", "ブタ", "ゴリラ", "オランウータン", "サル", "カバ", "シカ", "ライオン", "ヒョウ", "トラ", "クマ", "パンダ", "ハト", "ツル", "ニワトリ", "タカ", "ワシ", "カピバラ", "ゾウ", "キリン", "ラクダ", "カメレオン", "コウモリ", "ネズミ", "ハムスター", "ヘビ", "サメ", "イルカ", "シャチ", "クジラ", "クラゲ", "サケ", "サンマ", "ブリ", "イカ", "タコ", "ウナギ", "アンコウ"]
    
    ///tableView
    let tableView = UITableView()
    ///セル登録の際に用いるID
    let cellId = "cellId"
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //背景
        view.backgroundColor = .white
        //デリゲート指定
        tableView.delegate = self
        tableView.dataSource = self
        //背景
        tableView.backgroundColor = .white
        //セルの登録(ID付与)
        tableView.register(TableViewCell.self, forCellReuseIdentifier: cellId)
        //何も表記されていない余計なセルを削除するため、UIViewを設置
        tableView.tableFooterView = UIView(frame: .zero)
        //TableViewの表示
        view.addSubview(tableView)
        //TableViewの配置
        tableView.anchor(top: view.topAnchor, bottom: view.bottomAnchor, left: view.leftAnchor, right: view.rightAnchor)
        //↑bottom: view.bottomAnchorの指定を忘れないように。
    }
}

//MARK: - UITableViewDelegate, UITableViewDataSource
extension ViewController: UITableViewDelegate, UITableViewDataSource {
    
    //セルの数
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        //セルに表示させるタイトルの配列の数を指定
        return array.count
    }
    
    //セルの中身
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        //TableViewCellインスタンスの生成
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId) as! TableViewCell
        //セル内のUI設定
        cell.setCustomCell(title: array[indexPath.row])
        //設定したセルを返す(TableViewに反映する)
        return cell
    }
    
    //セルの高さ
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 80
    }
}
//TableViewCell.swift:セルとその中身のUIを設定するクラス。

import UIKit

class TableViewCell: UITableViewCell {
    
    ///セル内のUI設定
    func setCustomCell(title: String) {
        /*
         サブビューの削除
         ①セルの重複が起きないように、一旦セル内のサブビュー(セル内に設置したUIたち)を削除。
         理由:UITableViewのデリメソcellForRowAtは、生成したセルを再利用する仕組みがある。
         そのため、ある一定までスクロールしていくと、既に生成したセルの再利用(再表示)と新たに生成されるセルが重複して表示されてしまう。
         これを防ぐために、再利用されるセルを削除する必要がある。
         
         ②さらにタグで指定したサブビューだけを削除する理由は、
         全てのサブビューを削除してしまうと、UITableViewCellのサブビューであるタップを感知するUIResponderまで削除してしまうため。
         */
        self.subviews.forEach { subView in
            if subView.tag == 1 {
                subView.removeFromSuperview()
            }
        }
        //新たにセル内のUI(サブビュー)をセット
        //タイトル
        let titleLabel = UILabel()
        titleLabel.text = title
        titleLabel.textColor = .gray
        titleLabel.font = .systemFont(ofSize: 20, weight: .regular)
        titleLabel.textAlignment = .center
        //タグ指定→理由:削除するものを指定するために
        titleLabel.tag = 1
        //表示
        self.addSubview(titleLabel)
        //配置
        titleLabel.anchor(centerY: self.centerYAnchor, centerX: self.centerXAnchor, width: 200)
    }
}
//View+Extesion.swift:各UIを配置する処理

import UIKit

extension UIView {
    ///Viewの配置設定
    /// - Parameters:
    ///   - top:基準となる上方向の対象物を設定
    ///   - bottom:基準となる下方向の対象物を設定
    ///   - left:基準となる左方向の対象物を設定
    ///   - right:基準となる右方向の対象物を設定
    ///   - centerY:指定した対象物内におけるY軸による位置を設定
    ///   - centerX:指定した対象物内におけるX軸による位置を設定
    ///   - width:幅の設定
    ///   - height:高さの設定
    ///   - topPadding:基準となる上方向の対象物との距離を設定
    ///   - bottomPadding:基準となる下方向の対象物との距離を設定
    ///   - leftPadding:基準となる左方向の対象物との距離を設定
    ///   - rightPadding:基準となる右方向の対象物との距離を設定
    func anchor(top: NSLayoutYAxisAnchor? = nil,
                bottom: NSLayoutYAxisAnchor? = nil,
                left: NSLayoutXAxisAnchor? = nil,
                right: NSLayoutXAxisAnchor? = nil,
                centerY: NSLayoutYAxisAnchor? = nil,
                centerX: NSLayoutXAxisAnchor? = nil,
                width: CGFloat? = nil,
                height: CGFloat? = nil,
                topPadding: CGFloat = 0,
                bottomPadding: CGFloat = 0,
                leftPadding: CGFloat = 0,
                rightPadding: CGFloat = 0) {
        
        self.translatesAutoresizingMaskIntoConstraints = false
        
        if let top = top {
            self.topAnchor.constraint(equalTo: top, constant: topPadding).isActive = true
        }
        
        if let bottom = bottom {
            self.bottomAnchor.constraint(equalTo: bottom, constant: -bottomPadding).isActive = true
        }
        
        if let left = left {
            self.leftAnchor.constraint(equalTo: left, constant: leftPadding).isActive = true
        }
        
        if let right = right {
            self.rightAnchor.constraint(equalTo: right, constant: -rightPadding).isActive = true
        }
        
        if let centerY = centerY {
            self.centerYAnchor.constraint(equalTo: centerY).isActive = true
        }
        
        if let centerX = centerX {
            self.centerXAnchor.constraint(equalTo: centerX).isActive = true
        }
        
        if let width = width {
            self.widthAnchor.constraint(equalToConstant: width).isActive = true
        }
        
        if let height = height {
            self.heightAnchor.constraint(equalToConstant: height).isActive = true
        }
    }
}

解決方法

TableViewCellクラスのメソッド「setCustomCell」に記述した下記で解決できます。

self.subviews.forEach { subView in
            if subView.tag == 1 {
                subView.removeFromSuperview()
            }
        }

UITableViewCellのサブビューを指定し、さらにその中でタグ番号が1番のサブビューを削除する処理をしております。

何故タグで指定したサブビューのみを削除するのか

UITableViewCellの階層内でサブビューに該当するものの中に、UIResponderというユーザーのタップを感知(受信)する機能を持ったサブビューが存在するためです。これごと削除してしまうと、セルのタップ(押下)ができなくなってしまいます。そのためタグで指定したサブビューのみを削除する処理にしております。
下記はセル内にセットするUILabel(サブビュー)にタグ付けする記述です。

//タイトル
        let titleLabel = UILabel()
        titleLabel.text = title
        titleLabel.textColor = .gray
        titleLabel.font = .systemFont(ofSize: 20, weight: .regular)
        titleLabel.textAlignment = .center
        //タグ指定→理由:削除するものを指定するために
        titleLabel.tag = 1

結果

画像なのでわかりづらいですが、重複はなくなりました。

f:id:SumJun-Blog:20220212092308p:plain:w200

以上になります。最後まで読んでいただきましてありがとうございます。

CocoaPods:正常にライブラリのアップデートができない

Xcode13への切り替えにあたり、既存プロジェクトで使用しているライブラリをアップデートする必要がありました。CocoaPodsによるアップデートをしようとしたら、以下のようなエラーが発生して正常にアップデートできなかったので、その際の解決方法をご紹介します。

[!] Unable to find a specification for `ライブラリ名`
You have either:
 * mistyped the name or version.
 * not added the source repo that hosts the Podspec to your Podfile.

環境

Xcode:13.1
CocoaPods:1.11.2

解決方法

pod repo update

一度Pod内のライブラリを削除して、再度インストールし直すことで解決できました。 これはいくつかある解決方法の一つに過ぎません。私はこれで済みましたが、これでも解決できない場合は今回参考にさせていただいた記事をご確認ください。

参考記事

CocoaPodsが正常にアップデートできない時の対処方法 | ニートに憧れるプログラム日記

Xcode:バージョン切替時のCommand Line Toolsの設定

昨年9月頃、Apple Developerサイトで今後のAppStoreへのApp提出についての告知があり、それに伴いXcode13へのアップグレードをしました。そこで毎回忘れてしまうCommand Line Toolsの設定について、自分への備忘録として残します。

ダウンロード

DeveloperサイトよりアップグレードしたいバージョンのXcodeをダウンロードします。 https://developer.apple.com/download/all/

f:id:SumJun-Blog:20220105221411p:plain:w400

※このサイトに入るにはデベロッパーアカウントを作成している必要があります。

ダウンロード後、Finderのダウンロード階層の先ほどダウンロードしたxipファイルを解凍します。 この解凍には暫く時間が掛かります。

Xcode起動

解凍後はXcode.appが展開されますので、Finderのアプリケーション階層に移動させましょう。 ※既にある別バージョンのXcode.appを削除せず残す場合は、互いに被らないよう名前を変更しましょう。

f:id:SumJun-Blog:20220105223147p:plain:w200



Xcodeを起動させます。 新しいXcodeバージョンの起動に伴い、その他で必要なインストールもあるため「Install addtional required components?」というダイアログが表示されますので、「Install」を押下します。暫くするとXcodeが起動するはずです。

Command Line Toolsの設定

Xcodeが無事起動したら完了!、、、ではありません。Command Line Toolsの設定が必要です。 Xcodeを開いた状態でMacのメニューバー(画面上のバー)で「Xcode」→「Preferences」でダイアログが表示されます。その中の「Location」を選択します。

f:id:SumJun-Blog:20220105224443p:plain:w400

command Line Tools項目のプルダウンリストの中から、新しくダウンロードしたXcodeバージョンを指定します。

f:id:SumJun-Blog:20220105224711p:plain:w400

ターミナルでも設定できますがこちらの方が簡単だと思います。

これでXcodeのバージョン切替は完了です。 やることは簡単ですが、忘れてしまうと余計に時間を取られてしまいます。

設定し忘れると、、、

例えばCocoapodsをプロジェクトにインストールしたり、Pods(外部ライブラリ)をアップデートしたりするときに以下のようなエラーに遭遇します。

f:id:SumJun-Blog:20220105225243p:plain:w400

これはCommand Line Toolsが設定されていないために、ターミナルでCocoapodsのインストールの記述をしても対応できない状態になっております。この他にもコマンド入力をしても反応せずエラーになります。

その他

Xcodeのバージョンを上げると、その周りにあるライブラリのバージョンも確認する必要があります。 サポートされない状態になり、プロジェクトがビルドできないこともあります。

XCFramework:実機・シミュレーター両方に対応したフレームワークの作成・使用方法

記事が少なかったので、自分用の備忘録として残します。参考になれば幸いです。


実機用とシミュレーター用のバイナリ(またはアーキテクチャ)を含み、それぞれに対応できるユニバーサルフレームワーク(またはファットフレームワーク)があります。拡張子は「.framework」。

しかしこのフレームワークはXcode12あたりから弾かれる仕様になり、ビルドしようとしても下記エラーなどが表示されます。

Building for iOS Simulator, but the linked and embedded framework 'プロジェクト名.framework' was built for iOS + iOS Simulator.

そこでXcode11から利用開始になった「XCFramework」(拡張子は「.xcframework」)を使用することで、実機用とシミュレーター用の両方に対応したフレームワークを作成できます。


環境

Xcode:12.4

Swift :5.3.2


フレームワークの作成

まずフレームワークを作成するため、「Framework」を選択してプロジェクトを立ち上げます。

f:id:SumJun-Blog:20211031022943p:plain:w400
f:id:SumJun-Blog:20211031023016p:plain:w400

フレームワークの中身(したい処理)を実装するため、Swiftファイルを追加して処理内容を記述していきます。

f:id:SumJun-Blog:20211031023052p:plain:w400
f:id:SumJun-Blog:20211031023109p:plain:w400

各々でしたい処理を記述してください。

今回はコンソールにログを出力する処理を記述しました。

ここに記述されたこと(処理)を外部からアクセスするため、クラスやメソッドなどの修飾子は必ず「public」にします。

デフォルトは「internal」のため、このままだとアクセスできません。

f:id:SumJun-Blog:20211031023132p:plain

処理を実装したところで、次はいよいよ実機用とシミュレーター用の両方に対応したXCFrameworkを作成していきます。

上部バーの「File」>「New」>「Target」>「Aggregate」を選択して新たにターゲットを立ち上げます。

f:id:SumJun-Blog:20211031023147p:plain:w400
f:id:SumJun-Blog:20211031023204p:plain:w400
f:id:SumJun-Blog:20211031023221p:plain:w400

立ち上げたらTARGETSで新たに立ち上げたAggregateマークを選択>「Build Phases」>「+」で「New Run Script Phase」を選択します。

f:id:SumJun-Blog:20211031023236p:plain:w400

下記画像で選択された部分にXCFrameworkを作成するコードを記述するのですが、今回はその下のコードをコピペします。

f:id:SumJun-Blog:20211031023334p:plain
# 出力先ディレクトリ(プロジェクトの直下)
OUTPUT_DIR=${PROJECT_DIR}/Output

# 中間ファイルの出力先ディレクトリ
DERIVED_DIR=${OUTPUT_DIR}/${CONFIGURATION}-derived

# archiveの出力先ディレクトリ
ARCHIVE_DIR=${OUTPUT_DIR}/${CONFIGURATION}-archive

# xcframeworkの出力先ディレクトリ
XCFRAMEWORK_DIR=${OUTPUT_DIR}/${CONFIGURATION}-xcframework

# 出力先ディレクトリ削除
rm -rf ${OUTPUT_DIR}

# 中間ファイルの出力先ディレクトリ作成
mkdir -p ${DERIVED_DIR}

# アーカイブファイルの出力先ディレクトリ作成
mkdir -p ${ARCHIVE_DIR}

# xcframeworkの出力先ディレクトリ作成
mkdir -p ${XCFRAMEWORK_DIR}

# iOS実機用のarchiveファイル
ARCHIVE_FILE_IOS=${ARCHIVE_DIR}/ios.xcarchive
echo "ARCHIVE_FILE_IOS:${ARCHIVE_FILE_IOS}"

# iOSシミュレータ用のarchiveファイル
ARCHIVE_FILE_IOS_SIMULATOR=${ARCHIVE_DIR}/iossimulator.xcarchive
echo "ARCHIVE_FILE_IOS_SIMULATOR:${ARCHIVE_FILE_IOS_SIMULATOR}"

# iOS実機用のarchiveファイル作成
xcodebuild archive -scheme ${PROJECT_NAME} -destination="iOS" -archivePath $ARCHIVE_FILE_IOS -derivedDataPath $DERIVED_DIR -sdk iphoneos SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES

# iOSシミュレータ用のarchiveファイル作成
xcodebuild archive -scheme ${PROJECT_NAME} -destination="iOS Simulator" -archivePath $ARCHIVE_FILE_IOS_SIMULATOR -derivedDataPath $DERIVED_DIR -sdk iphonesimulator SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES

# xcframework作成
xcodebuild -create-xcframework -framework $ARCHIVE_FILE_IOS/Products/Library/Frameworks/${PROJECT_NAME}.framework -framework $ARCHIVE_FILE_IOS_SIMULATOR/Products/Library/Frameworks/${PROJECT_NAME}.framework -output $XCFRAMEWORK_DIR/${PROJECT_NAME}.xcframework

# xcframeworkの出力ディレクトリをFinderで開く
open ${XCFRAMEWORK_DIR}

下記はコピペ後になります。

f:id:SumJun-Blog:20211031023349p:plain:w400

XCFrameworkを作成する処理を実装できたので、最後はビルドになります。

スキームをAggregateアイコン、端末を「Any iOS Device」に選択してビルドします。

f:id:SumJun-Blog:20211031023441p:plain

ビルドに成功すると、FinderでXCFrameworkが作成されたディレクトリが表示されます。

実機用とシミュレーター用のバイナリが作成されているのが確認できます。

f:id:SumJun-Blog:20211031023501p:plain

これでフレームワークは作成できたので、次はそのフレームワークを使用していきます。


フレームワークの使用

フレームワークを使うプロジェクトを別で立ち上げます。

f:id:SumJun-Blog:20211031024402p:plain:w400
f:id:SumJun-Blog:20211031024437p:plain:w400

Finderから「.xcframework」をドラッグ&ドロップでプロジェクトに追加します。

f:id:SumJun-Blog:20211031024458p:plain

追加する際に下記画像のようなダイアログが表示されますので、「Copy items if needed」にチェックを入れてください。

チェックしないと、このプロジェクトでビルド・実行する度に先ほどの参照元の「.xcframework」にアクセスすることになります。 もしこの参照元の「.xcframework」が消えてしまった場合、このプロジェクトでフレームワークを使用することができなくなってしまいます。 そのため、チェックすることで「.xcframework」のコピーを作成しプロジェクトに追加します。参照元が消えてしまってもプロジェクトには影響がありません。

f:id:SumJun-Blog:20211031024616p:plain

追加しただけでは機能しないので、「TARGETS」を選択>「General」>「Frameworks, Libraries, and Embedded Content」フレームワークが追加されているので、「Embedded」項目を「Embedded&Sign」にします。

f:id:SumJun-Blog:20211031024640p:plain:w400

フレームワークをインポートして、フレームワークの処理を実装します。

f:id:SumJun-Blog:20211031024655p:plain

最後に実機とシミュレーターでそれぞれ実行してみます。

今回はコンソールにログ出力をする処理をフレームワークに実装したので、コンソールに出力されれば成功です。

f:id:SumJun-Blog:20211031024727p:plain:w400
f:id:SumJun-Blog:20211031024801p:plain:w400

以上になります!

ここまでのご視聴ありがとうございました😃


参考サイト

xcframeworkを作成する(第1回) - Qiita

xcframeworkを作成する(第2回) - Qiita

[iOS][Xcode] error: Building for iOS Simulator, but the linked framework ‘Hoge.framework’ was built for iOS.というエラーが出たときの対処法 – Web-y.dev

Breaking Changes with Xcode 12 and XCFramework

Xcode12時代のCarthageで起こった問題とXCFrameworkへの移行 - クックパッド開発者ブログ

XCFrameworkって何?どうやって作成するの? - Qiita

Swift:Storyboardを使わないで、コーディングのみで画面を立ち上げる(SceneDelegate使用しない)

今回はStoryboard無しで、コーディングで画面を立ち上げていきたいと思います。

ここではいつでも立ち上げられるように備忘録として残していきます。

 

■Storyboard無しのメリット/デメリット

*少し長くなるので、やり方をすぐに知りたい方は飛ばしてください。

個人的に感じたことを書いていこうと思います。機能性に関することはわかりませんので、もし詳しく知りたい方は他の方の記事等をお読みください。

 

【メリット】

・分散しない、統一性がある。

Storyboardを使用しているとUI部品など(Button、Label、TextField、ImageView等)はOutlet接続してプロパティ宣言はできるものの、その中の設定はStoryboardでされており、ソースコード(エディタエリア)からは把握できないので、Storyboardのユーティリティエリアを確認する手間があります。

設定の例:ボタンの色、ラベルの文字列の色、AutoLayoutなど

 

コーディングのみだと、例えば「名前と色が違うけど、あとは同じ設定のボタン」とうような場合、extensionやクラスで骨格だけ作成して使い回すことができます。また初心者だと難しく見える「引数」の勉強にもなります。

またAutoLayoutは一度骨格を作成すれば数値を入れるだけなので、他のXcodeプロジェクトでも使い回すことができ、非常に便利だと感じました。

僕自身StoryboardでのAutoLayout設定は正直苦手でしたが、コーディングでスムーズに配置設定ができるようになりました。

 

【デメリット】

・パッと見でわからない。

Storyboardがあれば、どんなアプリなのか?の全体感が直ぐに把握できるのですが、Storyboardが無いと、ソースコードを読んでから大凡の想像をしていく必要があります。

 

■画面の立ち上げ

それではStoryboard無しでの立ち上げをしてきます。

 

まず使わないMain.Storyboardを削除します。

f:id:SumJun-Blog:20210524114004p:plain

今回はSceneDelegateも使用しないので、全てコメントアウトします。

f:id:SumJun-Blog:20210524120635p:plain

続いて、AppDelegateを編集します。

下記画像の赤線と四角い赤線内の記述をします。

*window?.rootViewController = 「起動して最初に表示させたいViewControllerのインスタンス」を記述することで画面表示させることができます。

*window?.makeKeyAndVisible()この記述が無いと起動しても画面は真っ暗のままです。

そして丸く囲んである赤線内は使用しないので、コメントアウトします。

f:id:SumJun-Blog:20210524120701p:plain

次に、「TERGETS」の「Main Interface」に記載されている「Main」を削除します。これがあるとMain.Storyboadから始める設定になってしまいますので。

f:id:SumJun-Blog:20210524120737p:plain

残るは、「info.plist」の「Application Scene Manifest」を削除します。

この中にある「Storyboard Name」を削除するだけでも大丈夫です。

f:id:SumJun-Blog:20210524114029p:plain

あとはViewControllerをお好みで編集します。今回は背景色を設定します。

これでシミュレータ実行すると無事、画面が表示されました!

f:id:SumJun-Blog:20210524120800p:plain

とりあえず画面を立ち上げることができました。😃

また今回はSceneDelegateをコメントアウトしましたが、次回ではSceneDelegate に記述をして画面を立ち上げる方法を書いていきたいと思います。

理由としましては、Xcodeプロジェクト内で扱う内容によって今回の方法で立ち上がらない場合もあったためです。😅

 

以上、ここまで見ていただきありがとうございました!