SwiftUI for MacOS 中的多行 TextField/TextEditor 表单

Posted

技术标签:

【中文标题】SwiftUI for MacOS 中的多行 TextField/TextEditor 表单【英文标题】:Multiline TextField/TextEditor in SwiftUI for MacOS in forms 【发布时间】:2021-12-04 12:09:57 【问题描述】:

我需要一个适用于 MacOS 的 SwiftUI 多行文本输入控件,满足以下要求:

允许像在编辑器中一样进行光标控制(即按 RETURN 会导致换行) 表单中的工作标签

我尝试使用带有 lineLimit() 修饰符的 TextField,它看起来完全符合我的需要,即标签显示正确(包括对齐),但如果它为空且 RETURN 键没有,则它只有 1 行的高度t 做我想做的事(即换行):

struct ContentView: View 
    @State var field1 = ""
    @State var field2 = ""
    @State var notes = ""

    var body: some View 
        Form 
            TextField("Label", text: $field1)
            TextField("Long Label", text: $field2)
            TextField("Notes", text: $notes)
                .lineLimit(10)
        
        .padding()
        .frame(height: 150)
    

然后我尝试了一个 TextEditor,但这缺乏定义标签的能力。标签的位置使得 Form 元素对 MacOS 非常有用,因为它允许标签的正确对齐而没有任何黑客攻击。缺少边框样式只是一个小问题,可以使用边框样式解决:

struct ContentView: View 
    @State var field1 = ""
    @State var field2 = ""
    @State var notes = ""

    var body: some View 
        Form 
            TextField("Label", text: $field1)
            TextField("Long Label", text: $field2)
            TextEditor(text: $notes)
        
        .padding()
        .frame(height: 150)
    

我只对面向未来的干净解决方案感兴趣。如果没有,hack 必须至少非常灵活,即所有标签必须正确对齐。 workingdog 的解决方案不适合我,因为一旦标签文本发生变化,一切都会崩溃。

【问题讨论】:

您可以尝试使用HStack,例如:HStack Text("Note") TextEditor(text: $notes) 为您的文本编辑器获取“标签”。 不幸的是,这并不容易。标签将与其他文本字段左对齐,注释字段将被插入。 “表单”以某种方式识别“真实”文本字段的标签并执行所有对齐操作。但不适用于 HStack 中的自构建标签。 【参考方案1】:

我做了一个“自定义”Form 看起来像一个真实的。

代码:

struct ContentView: View 
    @State private var field1 = ""
    @State private var field2 = ""
    @State private var notes = ""
    @State private var maxLabelWidth: CGFloat?

    var body: some View 
        VStack 
            FormItem("Label", text: $field1)
            FormItem("Long Label", text: $field2)
            FormItem("Notes", text: $notes, kind: .textEditor)
        
        .padding()
        .onPreferenceChange(MaxWidthKey.self)  maxWidth in
            maxLabelWidth = maxWidth
        
        .environment(\.maxLabelWidth, maxLabelWidth)
    

struct MaxWidthKey: PreferenceKey 
    static let defaultValue: CGFloat = 0

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) 
        value = max(value, nextValue())
    

struct MaxLabelWidthKey: EnvironmentKey 
    static let defaultValue: CGFloat? = nil


extension EnvironmentValues 
    var maxLabelWidth: CGFloat? 
        get  self[MaxLabelWidthKey.self] 
        set  self[MaxLabelWidthKey.self] = newValue 
    

struct FormItem: View 
    enum Kind 
        case textEditor
        case textField
    

    @Environment(\.maxLabelWidth) private var maxLabelWidth
    @Binding private var text: String
    private let title: String
    private let kind: Kind

    init(_ title: String, text: Binding<String>, kind: Kind = .textField) 
        _text = text
        self.title = title
        self.kind = kind
    

    var body: some View 
        HStack(alignment: .top) 
            Text(title)
                .foregroundColor(Color(NSColor.labelColor))
                .frame(maxWidth: maxLabelWidth, alignment: .trailing)
                .background(
                    GeometryReader  geo in
                        Color.clear.preference(
                            key: MaxWidthKey.self,
                            value: geo.size.width
                        )
                    
                )
                .padding(.top, 3)

            switch kind 
            case .textEditor:
                TextEditor(text: $text)
                    .font(.system(size: 13))
                    .padding(.top, 3)
            case .textField:
                TextField("", text: $text)
            
        
    

结果:

虽然它没有设置TextEditor 背景,但它可能与您所获得的一样接近。

【讨论】:

这是迄今为止最好的解决方案!非常感谢你做的这些。此外,这可能是未来的证明,因为它不是一种“hacky”方法。【参考方案2】:

这种方法怎么样(根据您的需要调整):

struct ContentView: View 
    @State var field1 = ""
    @State var field2 = ""
    @State var notes = ""
    
    var body: some View 
        VStack (alignment: .leading, spacing: 20) 
            Form 
                TextField("Label", text: $field1)
                TextField("Long Label", text: $field2)
            
            HStack (alignment: .top) 
                Spacer().frame(width: 30)
                Text("Notes")
                TextEditor(text: $notes).frame(height: 200)
            
        
        .padding()
        .frame(height: 400)
    

【讨论】:

我想过这样的黑客攻击,但希望有一个干净的解决方案。这样做的问题是,您必须手动调整标签的宽度,而“表单”会根据所有标签的最大宽度自动调整。如果我使用这样的东西,我可能会完全删除“表单”,这样表单控件的宽度和 HStack 中的控件之间就不会出现差异。【参考方案3】:

我个人更喜欢在这种情况下将相同的视图与overlay 放在一起,如下所示:

Form 
    TextField("Label", text: $field1)
    TextField("Long Label", text: $field2)
    TextEditor(text: $notes)
        .overlay(
            TextEditor(text: .constant("label"))
                .allowsHitTesting(false)
                .opacity(notes.isEmpty ? 1 : 0)
        )

缺点是TextEditor 不像大多数其他 SwiftUI 视图那样工作:它自己绘制默认背景。您可以使用this hack 使光标通过叠加层可见,并自己在主TextEditor 上绘制背景。

【讨论】:

我不明白这种方法。如果我使用此代码,则左侧的“标签”列中没有文本编辑器的标签,但它会覆盖到文本编辑器本身。此外,无法在文本编辑器中输入任何内容。我使用的是针对 MacOS 12 的 XCode 13.1。您是否使用这些版本对其进行了测试? 这些是我正在使用的确切版本。您是否从链接的答案中添加了 NSTextView 扩展名? 好的,通过链接的破解,文本是可编辑的。但是没有表单标签,不太理想。

以上是关于SwiftUI for MacOS 中的多行 TextField/TextEditor 表单的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 中的多行可编辑文本字段

SwiftUI for MacOS 窗口,带圆角,不带标题栏

在 macOS SwiftUI 中模拟按钮点击?

在 SwiftUI for macOS 应用程序中获取点击手势的鼠标/指针位置

Swiftui: MacOS App: Fetching Core Data in ContentView OK,但是如何使用 AppDelegate 的结果呢?

如何在 macOS 上检测 SwiftUI 中的键盘事件?