IBDesignable を使ったカスタムビュー使用時に発生するエラー原因を調べる

検証環境:
Xcode 11.3.1
Swift 5.1.3

Storyboard上で IBDesignable を使ったカスタムビューを貼り付けたところ、
error: IB Designables: Failed to render and update auto layout status for UIViewController (nyp-0u-XJD): The agent crashed
というエラーが出ました。
しかしこのメッセージだけではエラーの原因がわからないのでクラッシュログからエラー詳細を調べる方法をメモしておきます。

  • まずMac標準のコンソール.app(console.app)を起動します。
f:id:xyk:20200327114001p:plain
  • 検索窓にIBDesignablesAgentを入力してフィルタしておきます。

  • 対象の Storyboard を開き、メニューの Editor -> Refresh All Views を実行します。

f:id:xyk:20200327114348p:plain
  • するとコンソール.appのログに以下のようなクラッシュログが出力されます。

Saved crash report for IBDesignablesAgent-iOS[48380] version 11.3.1 (15706) to IBDesignablesAgent-iOS_2020-03-27-114733_xyk-mbp.crash

  • クラッシュレポートは ~/Library/Logs/DiagnosticReports/ ディレクトリに保存されるので、今回の場合なら
    ~/Library/Logs/DiagnosticReports/IBDesignablesAgent-iOS_2020-03-27-114733_xyk-mbp.crash
    ファイルを開きます。

  • このエラーログを見たところクラッシュの原因が特定できました。
    ログには
    Fatal error: Use of unimplemented initializer 'init(frame:)' for class 'MyButton'
    と出力されていました。
    IBDesignable を使ってる MyButtonクラスのコードを確認してみると、以下の coder が引数の初期化メソッドのみ定義していました。

required init?(coder: NSCoder) {
    super.init(coder: coder)
}

frame が引数の初期化メソッドを追加することでエラーが解消されました。

override init(frame: CGRect) {
    super.init(frame: frame)
}

required init?(coder: NSCoder) {
    super.init(coder: coder)
}

ちなみに両方とも書いてない場合はエラーになりません。
おそらく プレビュー時はランタイム時と違って coder ではなく frame 側の初期化メソッドのみ呼ばれるのが関係している気がします。
これにはちょっとハマってしましました。。

参考:

blog.kaltoun.cz

IBDesignable を使ったカスタムビューのプレビュー時に呼ばれるメソッドについて

検証環境:
Xcode 11.3.1
Swift 5.1.3

IBDesignable や IBInspectable を使うことで Storyboard や Xib 上でカスタムの属性の設定ができ、変更がリアルタイムに反映されてプレビューできるようになります。
ところで、IBOutlet で接続したビューに対して初期設定を行う場合には接続完了後のタイミングで呼ばれる UIView.awakeFromNib() 内でやるのがセオリーかと思いますが、 Storyboard や Xib 上のプレビュー時にはこのメソッドが呼ばれません。
その代わりに UIView.prepareForInterfaceBuilder() が呼ばれるのでこちらにも同様の初期設定のコードを書いておくと実行時と同じような処理結果にすることができます。

プレビュー時とランタイム(実行)時で呼ばれるメソッドの違いについて以下の検証用コードを作って試してみました。

検証用コード

import UIKit

@IBDesignable
open class MyCustomView: UIView {
    
    var items: [String] = []
    
    @IBInspectable
    open var prop1: String? {
        didSet {
            items.append("IBInspectable prop1")
        }
    }
    
    @IBInspectable
    open var prop2: Int = 0 {
        didSet {
            items.append("IBInspectable prop2")
        }
    }
    
    override public init(frame: CGRect) {
        super.init(frame: frame)
        items.append("init(frame:)")
    }
    
    required public init?(coder: NSCoder) {
        super.init(coder: coder)
        items.append("init?(coder:)")
    }
    
    override public func awakeFromNib() {
        super.awakeFromNib()
        items.append("awakeFromNib")
    }
    
    override public func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        items.append("prepareForInterfaceBuilder")
    }
    
    override open func updateConstraints() {
        super.updateConstraints()
        items.append("updateConstraints")
    }

    override public var intrinsicContentSize: CGSize {
        items.append("intrinsicContentSize")
        return super.intrinsicContentSize
    }
    
    override open func layoutSubviews() {
        super.layoutSubviews()
        items.append("layoutSubviews")
        configure()
    }
    
    private func configure() {
        guard subviews.count == 0 else { return }
        let textView = UITextView()
        textView.isEditable = false
        addSubview(textView)
        textView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            textView.topAnchor.constraint(equalTo: topAnchor),
            textView.bottomAnchor.constraint(equalTo: bottomAnchor),
            textView.leadingAnchor.constraint(equalTo: leadingAnchor),
            textView.trailingAnchor.constraint(equalTo: trailingAnchor),
        ])
        let result = items.enumerated().map { "\($0 + 1). \($1)" }.joined(separator: "\n")
        print(result)
        textView.text = result
    }
}

Storyboard プレビュー時

1. init(frame:)
2. IBInspectable prop1
3. IBInspectable prop2
4. prepareForInterfaceBuilder
5. updateConstraints
6. layoutSubviews

まずinit?(coder:)ではなく、init(frame:)が呼ばれます。
そしてawakeFromNibは呼ばれず、代わりにprepareForInterfaceBuilderが呼ばれます。
AutoLayout を設定してますが、intrinsicContentSizeは呼ばれないようです。

ランタイム(実行)時

こちらは上記StoryBoardで設定したものが実際に実行された時の結果になります。

1. init?(coder:)
2. IBInspectable prop1
3. IBInspectable prop2
4. awakeFromNib
5. intrinsicContentSize
6. updateConstraints
7. layoutSubviews

コードからカスタムビュー生成(Frame指定)

以下はおまけで同じカスタムビューをコードで生成した場合も試してみました。

let myCustomView = MyCustomView(frame: .init(origin: .zero, size: .init(width: 150, height: 150)))
myCustomView.prop1 = "Hello"
myCustomView.prop2 = 100
view.addSubview(myCustomView)

結果

1. init(frame:)
2. IBInspectable prop1
3. IBInspectable prop2
4. updateConstraints
5. layoutSubviews

コードからカスタムビュー生成(AutoLayout指定)

let myCustomView = MyCustomView()
myCustomView.prop1 = "Hello"
myCustomView.prop2 = 100
view.addSubview(myCustomView)
myCustomView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    myCustomView.topAnchor.constraint(equalTo: view.topAnchor),
    myCustomView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
    myCustomView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
    myCustomView.heightAnchor.constraint(equalToConstant: 150),
])

結果

1. init(frame:)
2. IBInspectable prop1
3. IBInspectable prop2
4. intrinsicContentSize
5. updateConstraints
6. layoutSubviews

Xcode の Swift Package Manager で Repository を追加しようとしたらエラーになる

検証環境:
Xcode 11.3.1
Swift 5.1.3

Swift Package Manager でリポジトリを追加しようとしたところ以下のようなエラーが出た。

f:id:xyk:20200221110253p:plain

f:id:xyk:20200221110303p:plain

Couldn’t communicate with a helper application.
Try your operation again. If that fails, quit and relaunch the application and try again.

ググるXcode の Preferences > Source Control > Git から Author を設定すればよいというのが出てくるけどそれで解決しなかった。
自分の場合はどうやら git の Conditional includes 機能を使ったために設定ファイルに追加された includeIf セクションがあることが原因のようだった。
とりあえずのワークアラウンドとして~/.gitconfigからこの設定箇所をコメントアウトしたら追加できた。

#[includeIf "gitdir:~/.ghq/gitlab.com/xyk/"]
#   path = ~/.gitconfig-gitlab-xyk

【Swift5】数値を3桁毎にカンマ区切りにした文字列にする

検証環境:
Xcode 11.3
Swift 5.1.3

数値や金額の表記で3桁毎にカンマ区切りにした文字列に変換する方法についてです。
String.localizedStringWithFormatメソッドを使う、またはNumberFormatterを使うと言った方法があります。

let million: Int = 1_000_000

do {
    // 1
    let result = String.localizedStringWithFormat("%d", million)
    print(result) // 1,000,000
}
do {
    // 2
    let f = NumberFormatter()
    f.numberStyle = .decimal
    f.groupingSeparator = "," // 区切り文字を指定
    f.groupingSize = 3 // 何桁ごとに区切り文字を入れるか指定
    
    let result = f.string(from: NSNumber(integerLiteral: million)) ?? "\(million)"
    print(result) // 1,000,000
}
do {
    // 3
    let f = NumberFormatter()
    f.numberStyle = .currency // 先頭に通貨記号が付与される。ロケールが日本なら¥記号
    f.groupingSeparator = ","
    f.groupingSize = 3
    
    let result = f.string(from: NSNumber(integerLiteral: million)) ?? "\(million)"
    print(result) // ¥1,000,000
}

Extension として追加

private let formatter: NumberFormatter = {
    let f = NumberFormatter()
    f.numberStyle = .decimal
    f.groupingSeparator = ","
    f.groupingSize = 3
    return f
}()

extension Int {
    var withComma: String {
        return formatter.string(from: NSNumber(integerLiteral: self)) ?? "\(self)"
    }
}

あるいはこちらの記事で紹介されているように予め style と locale を指定した Extension を用意するのもあり。

private let formatter = NumberFormatter()

extension Int {

    private func formattedString(style: NumberFormatter.Style, localeIdentifier: String) -> String {
        formatter.numberStyle = style
        formatter.locale = Locale(identifier: localeIdentifier)
        return formatter.string(from: NSNumber(integerLiteral: self)) ?? "\(self)"
    }

    // カンマ区切り
    var formattedJPString: String {
        return formattedString(style: .decimal, localeIdentifier: "ja_JP")
    }

    // 日本円表記
    var JPYString: String {
        return formattedString(style: .currency, localeIdentifier: "ja_JP")
    }
}

let million: Int = 1_000_000
million.formattedJPString   // 1,000,000
million.JPYString           // ¥1,000,000

はてなブログのMarkdownエディタで画像のサイズを変更する

ブログに挿入した画像のサイズを小さくする方法について。
div style で囲んで width を指定する。

<div style="width: 200px;">
[f:id:xyk:20200203150754p:plain]
</div>

【Swift5】UITextViewの任意の文字列をタップ可能なリンクにする

検証環境:
Xcode 11.3
Swift 5.1.3

UITextView の任意の文字列のタップ可能なリンクにする方法についてです。

f:id:xyk:20200130122003p:plain

リンク化する文字列のみ、色を変えてアンダーラインを追加します。
UITextView のサブクラスして実装し、IBDesignable と IBInspectable を使って Storyboard から設定できるようにしました。
UITextView の text プロパティに全文を設定し、リンク化したい部分を linkText に設定します。

import UIKit

@IBDesignable
class LinkTextView: UITextView, UITextViewDelegate {
    
    @IBInspectable
    var fontSize: CGFloat = 14
    
    @IBInspectable
    var isBold: Bool = false
    
    @IBInspectable
    var linkText: String = ""
    
    @IBInspectable
    var linkColor: UIColor = .red
    
    @IBInspectable
    var linkURL: String = ""

    override func awakeFromNib() {
        super.awakeFromNib()
        configure()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        configure()
    }
    
    private func configure() {
        
        let systemFont: UIFont = isBold ? .boldSystemFont(ofSize: fontSize) : .systemFont(ofSize: fontSize)
        let attributedString = NSMutableAttributedString(string: text)
        let attrs: [NSAttributedString.Key: Any] =  [
            .font: systemFont,
            .foregroundColor: textColor ?? .black
        ]
        attributedString.setAttributes(attrs, range: NSString(string: text).range(of: text))
        let linkAttrs: [NSAttributedString.Key: Any] =  [
            .font: systemFont,
            .link: linkURL // リンク先URL
        ]
        attributedString.setAttributes(linkAttrs, range: NSString(string: text).range(of: linkText))
        attributedText = attributedString
        linkTextAttributes = [
            .foregroundColor: linkColor, // リンクの色
            .underlineStyle: NSUnderlineStyle.single.rawValue // アンダーラインを追加
        ]
        
        backgroundColor = .clear
        isSelectable = true
        isEditable = false
        isScrollEnabled = false
        delegate = self
    }
    
    // MARK: - UITextViewDelegate
    
    func textView(_ textView: UITextView,
                  shouldInteractWith URL: URL,
                  in characterRange: NSRange,
                  interaction: UITextItemInteraction) -> Bool {
        
        UIApplication.shared.open(URL)
        return false
    }
}

以前の記事

xyk.hatenablog.com

CGRect 構造体で使える便利なメソッド

offsetBy

origin の移動

// 右下に移動
let newRect = rect.offsetBy(dx: 10, dy: 10)

insetBy

center は変えずに size の変更

let rect = CGRect(x: 20, y: 20, width: 100, height: 100)

//  縮小する -> (x: 40, y: 40, width: 60, height: 60)
let smallerRect = rect.insetBy(dx: 20, dy: 20)

//  拡大する -> (x: 0, y: 0, width: 140, height: 140)
let largerRect = rect.insetBy(dx: -20, dy: -20)

まとめ記事
www.hackingwithswift.com

わかりやすい画像
f:id:xyk:20200124162159j:plain