🦋

カスタムしたNSTextViewをSwiftUIで使う

2022/11/17に公開

NSTextViewをSwiftUIで利用するためにNSViewRepresentable対応をしようと思ったらかなりハマったのでメモを残しておきます。

例えば、フォントサイズを変えられるカスタムなNSTextViewを定義します。

import AppKit

class MyTextView: NSTextView {
    func setFontSize(_ fontSize: Double) {
        self.font = NSFont(name: "HiraKakuProN-W3", size: fontSize)
        ?? NSFont.systemFont(ofSize: fontSize, weight: .regular)
    }
}

これをNSViewRepresentableで包む場合は以下のようにします。

import SwiftUI

struct WrappedMyTextView: NSViewRepresentable {
    typealias NSViewType = NSScrollView

    @Binding private var text: String
    @Binding private var fontSize: Double

    init(text: Binding<String>, fontSize: Binding<Double>) {
        _text = text
        _fontSize = fontSize
    }

    func makeNSView(context: Context) -> NSScrollView {
        let scrollView = MyTextView.scrollableTextView()
        let textView = scrollView.documentView as! MyTextView
        textView.delegate = context.coordinator
        textView.setFontSize(fontSize)
        return scrollView
    }

    func updateNSView(_ nsView: NSScrollView, context: Context) {
        let textView = nsView.documentView as! MyTextView
        textView.setFontSize(fontSize)
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text)
    }

    class Coordinator: NSObject, NSTextViewDelegate {
        @Binding var text: String

        init(text: Binding<String>) {
            _text = text
        }

        func textDidChange(_ notification: Notification) {
            if let textView = notification.object as? MyTextView {
                text = textView.string
            }
        }
    }
}
利用例
struct ContentView: View {
    @State var text: String = ""
    @State var fontSize: Double = 15

    var body: some View {
        WrappedMyTextView(text: $text, fontSize: $fontSize)
            .frame(minWidth: 400,
                   idealWidth: 500,
                   maxWidth: .infinity,
                   minHeight: 80,
                   idealHeight: 100,
                   maxHeight: .infinity)
            .padding(8)
    }
}

ポイント

  • NSTextViewを直接NSViewRepresentableで包むと行数が増えて表示高さを超えたときに内容を読めなくなるので、NSScrollViewで包みます。(また、.drawsBackgroundtrueだと文字列がある行の背景しか色が塗られず変な感じになるので、直接利用するなら幅や高さが固定の時限定になると思います。)
  • NSTextView(frame:textContainer:)のイニシャライザは使わないこと。使う場合はtextContainerを自前で用意して管理しなければならず面倒です。もし使う場合は、NSTextStorageNSLayoutManagerNSTextContainerを作ってそれぞれつなぎます。

Discussion