🦋
カスタムしたNSTextViewをSwiftUIで使う
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
で包みます。(また、.drawsBackground
がtrue
だと文字列がある行の背景しか色が塗られず変な感じになるので、直接利用するなら幅や高さが固定の時限定になると思います。) -
NSTextView(frame:textContainer:)
のイニシャライザは使わないこと。使う場合はtextContainer
を自前で用意して管理しなければならず面倒です。もし使う場合は、NSTextStorage
、NSLayoutManager
、NSTextContainer
を作ってそれぞれつなぎます。
Discussion