SwiftPamphletApp
SwiftPamphletApp copied to clipboard
TextEditor

对应的代码如下:
import SwiftUI
import CodeEditorView
struct PlayTextEditorView: View {
// for TextEditor
@State private var txt: String = "一段可编辑文字...\n"
@State private var count: Int = 0
// for CodeEditorView
@Environment(\.colorScheme) private var colorScheme: ColorScheme
@State private var codeMessages: Set<Located<Message>> = Set ()
@SceneStorage("editLocation") private var editLocation: CodeEditor.Location = CodeEditor.Location()
var body: some View {
// 使用 SwiftUI 自带 TextEditor
TextEditor(text: $txt)
.font(.title)
.lineSpacing(10)
.disableAutocorrection(true)
.padding()
.onChange(of: txt) { newValue in
count = txt.count
}
Text("字数:\(count)")
.foregroundColor(.secondary)
.font(.footnote)
// 使用的 CodeEditorView 显示和编辑代码高亮的代码,还有 minimap
CodeEditor(text: .constant("""
static func number() {
// Int
let i1 = 100
let i2 = 22
print(i1 / i2) // 向下取整得 4
// Float
let f1: Float = 100.0
let f2: Float = 22.0
print(f1 / f2) // 4.5454545
let f4: Float32 = 5.0
let f5: Float64 = 5.0
print(f4, f5) // 5.0 5.0 5.0
// Double
let d1: Double = 100.0
let d2: Double = 22.0
print(d1 / d2) // 4.545454545454546
// 字面量
print(Int(0b10101)) // 0b 开头是二进制
print(Int(0x00afff)) // 0x 开头是十六进制
print(2.5e4) // 2.5x10^4 十进制用 e
print(0xAp2) // 10*2^2 十六进制用 p
print(2_000_000) // 2000000
// isMultiple(of:) 方法检查一个数字是否是另一个数字的倍数
let i3 = 36
print(i3.isMultiple(of: 9)) // true
}
"""),
messages: $codeMessages,
language: .swift,
layout: CodeEditor.LayoutConfiguration(showMinimap: true)
)
.environment(\.codeEditorTheme, colorScheme == .dark ? Theme.defaultDark : Theme.defaultLight)
// 包装的 NSTextView
HSplitView {
PNSTextView(text: .constant("左边写...\n"), onDidChange: { (s, i) in
print("Typing \(i) times.")
})
.padding()
PNSTextView(text: .constant("右边写...\n"))
.padding()
} // end HSplitView
} // end body
}
// MARK: - 自己包装 NSTextView
struct PNSTextView: NSViewRepresentable {
@Binding var text: String
var onBeginEditing: () -> Void = {}
var onCommit: () -> Void = {}
var onDidChange: (String, Int) -> Void = { _,_ in }
// 返回要包装的 NSView
func makeNSView(context: Context) -> PNSTextConfiguredView {
let t = PNSTextConfiguredView(text: text)
t.delegate = context.coordinator
return t
}
func updateNSView(_ view: PNSTextConfiguredView, context: Context) {
view.text = text
view.selectedRanges = context.coordinator.sRanges
}
// 回调
func makeCoordinator() -> TextViewDelegate {
TextViewDelegate(self)
}
}
// 处理 delegate 回调
extension PNSTextView {
class TextViewDelegate: NSObject, NSTextViewDelegate {
var tView: PNSTextView
var sRanges: [NSValue] = []
var typeCount: Int = 0
init(_ v: PNSTextView) {
self.tView = v
}
// 开始编辑
func textDidBeginEditing(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.tView.text = textView.string
self.tView.onBeginEditing()
}
// 每次敲字
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
typeCount += 1
self.tView.text = textView.string
self.sRanges = textView.selectedRanges
self.tView.onDidChange(textView.string, typeCount)
}
// 提交
func textDidEndEditing(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.tView.text = textView.string
self.tView.onCommit()
}
}
}
// 配置 NSTextView
final class PNSTextConfiguredView: NSView {
weak var delegate: NSTextViewDelegate?
private lazy var tv: NSTextView = {
let contentSize = sv.contentSize
let textStorage = NSTextStorage()
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(containerSize: sv.frame.size)
textContainer.widthTracksTextView = true
textContainer.containerSize = NSSize(
width: contentSize.width,
height: CGFloat.greatestFiniteMagnitude
)
layoutManager.addTextContainer(textContainer)
let t = NSTextView(frame: .zero, textContainer: textContainer)
t.delegate = self.delegate
t.isEditable = true
t.allowsUndo = true
t.font = .systemFont(ofSize: 24)
t.textColor = NSColor.labelColor
t.drawsBackground = true
t.backgroundColor = NSColor.textBackgroundColor
t.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
t.minSize = NSSize(width: 0, height: contentSize.height)
t.autoresizingMask = .width
t.isHorizontallyResizable = false
t.isVerticallyResizable = true
return t
}()
private lazy var sv: NSScrollView = {
let s = NSScrollView()
s.drawsBackground = true
s.borderType = .noBorder
s.hasVerticalScroller = true
s.hasHorizontalRuler = false
s.translatesAutoresizingMaskIntoConstraints = false
s.autoresizingMask = [.width, .height]
return s
}()
var text: String {
didSet {
tv.string = text
}
}
var selectedRanges: [NSValue] = [] {
didSet {
guard selectedRanges.count > 0 else {
return
}
tv.selectedRanges = selectedRanges
}
}
required init?(coder: NSCoder) {
fatalError("Error coder")
}
init(text: String) {
self.text = text
super.init(frame: .zero)
}
override func viewWillDraw() {
super.viewWillDraw()
sv.translatesAutoresizingMaskIntoConstraints = false
addSubview(sv)
NSLayoutConstraint.activate([
sv.topAnchor.constraint(equalTo: topAnchor),
sv.trailingAnchor.constraint(equalTo: trailingAnchor),
sv.bottomAnchor.constraint(equalTo: bottomAnchor),
sv.leadingAnchor.constraint(equalTo: leadingAnchor)
])
sv.documentView = tv
} // end viewWillDraw
}
SwiftUI 中用 NSView,可以通过 NSViewRepresentable 来包装视图,这个协议主要是实现 makeNSView、updateNSView 和 makeCoordinator 三个方法。makeNSView 要求返回需要包装的 NSView。每当 SwiftUI 的状态变化时触发 updateNSView 方法的调用。为了实现 NSView 里的 delegate 和 SwiftUI 通信,就要用 makeCoordinator 返回一个用于处理 delegate 的实例。