趣谈 TextField 视图在 SwiftUI 各个版本中的进化(下)

102 阅读3分钟

在这里插入图片描述

概述

在 SwiftUI 众多原生内置视图中,TextField 无疑是其中最常用的一个。它不仅可以显示文本而且可以让用户输入文本,这样可攻可守且可爱的特性怎能让人不爱呢?

在这里插入图片描述

从 SwiftUI 4.0 开始,苹果为 TextField 陆续添加了诸多实用功能,这使得它从功能和易用性上不断逼近和超越 UIKit 中的老大哥 UITextField。

在本篇博文中,您将学到如下内容:

  • 3.1 读取选中文本
  • 3.1 控制文本选中
    1. macOS 15 为 TextField 添加文本输入建议
    1. 源代码

相信看完本篇的小伙伴们会对 SwiftUI 中 TextField 新增的功能更加刮目相看,欲罢不能。

那还等什么呢?Let's go!!!;)


3.1 读取选中文本

上面说过,在 SwiftUI 6.0 里读取 TextField 选中文本的全部秘诀就在于其新增构造器中的 selection 形参,我们可以通过它来读取选中的文本:

@State var desc = "贪吃贪睡的大熊猫侯佩🐼!!!"
@State var textSelection: TextSelection?
@State var selectingText: String = ""

TextField("添加描述", text: $desc, selection: $textSelection)
    .padding()
    .onChange(of: textSelection) {_,new in
        guard let selection = textSelection else { return }
        switch selection.indices {
        case .selection(let range):
            selectingText = String(desc[range])
        default:
            break
        }
    }


if !selectingText.isEmpty {
    Text("当前选中内容:\(Text(selectingText).foregroundStyle(.green).bold())")
}

在这一段示例代码中,我们通过读取 TextSelection.indices 属性的值获到了当前 TextField 选中的文本:

在这里插入图片描述

3.1 控制文本选中

除了读取 TextField 当前选中的文本以外,我们还可以优哉游哉的用代码控制 TextField 内选中的文本。这就是为什么在前面 init(_:text:selection:...) 构造器里 selection 形参是一个绑定的原因了:它擅长双向控制。

@State var desc = "贪吃贪睡的大熊猫侯佩🐼!!!"
@State var textSelection: TextSelection?
@State var selectingText: String = ""
@State var selectStart = 0.0
@State var selectCount = 0.0

@FocusState var descFocus: Bool

private func handleSelecting() {
    // 对应的 TextField 需要激活输入焦点才能控制选中文本    
    guard descFocus else { return }
    
    let startIndex = desc.startIndex
    let selectStartIndex = desc.index(startIndex, offsetBy: Int(selectStart))
    let selectEndIndex = desc.index(startIndex, offsetBy: Int(selectStart + selectCount))
    textSelection = .init(range: selectStartIndex..<selectEndIndex)
}

TextField("添加描述", text: $desc, selection: $textSelection)
    .focused($descFocus)
    .onChange(of: selectStart + selectCount) {_,_ in
        handleSelecting()
    }

if descFocus {
    VStack {
        LabeledContent("选择开始位置") {
            Slider(value: $selectStart, in: 0...Double(desc.count))
        }
        
        LabeledContent("选择字符数量") {
            Stepper("\(Int(selectCount))", value: $selectCount, in: 0...Double(maxSelectingCount))
        }
    }
    .transition(.slide.combined(with: .opacity))
}

在上面的代码中,我们做了以下几件事:

  • 使用 descFocus 状态检测 TextField 当前是否激活输入焦点;
  • 使用 selectStart 和 selectCount 状态分别控制 TextField 当前选中文本的起始位置和选中的字符数;
  • 在上面两个属性变化的同时,调用 handleSelecting() 方法来计算并设置当前 TextField 中实际选中的文本;

编译并在 Xcode 预览中看一下美美哒的成果吧:

在这里插入图片描述

4. macOS 15 为 TextField 添加文本输入建议

除了上面提到的文本选中控制以外,SwiftUI 6.0 还为 TextField 新增了文本输入建议的功能,这是通过 textInputSuggestions() 视图修改器来实现的:

在这里插入图片描述


不过,目前该功能只支持 macOS 并且必须是 15.0 以上的版本。我们希望将来有朝一日,它能够出现在 iOS 或 iPadOS 中。


用 textInputSuggestions() 方法实现 TextField 输入建议内容的精妙之处就在于它的 suggestions 闭包,注意形参前面的 @ViewBuilder 修饰符:

nonisolated public func textInputSuggestions<S>(
	@ViewBuilder _ suggestions: () -> S) -> some View 
	where S : View

我们在 suggestions 闭包中可以任意添加所需的建议内容,并用 textInputCompletion() 修改器来映射实际输入的文本:

@State var favHeroName = ""

#if os(macOS)
    Section("文本输入建议") {
        TextField("最欣赏的英雄", text: $favHeroName)
            .textInputSuggestions {
                Label("贪吃贪睡路痴未秃码农", systemImage: "01.circle")
                    .textInputCompletion("大熊猫侯佩")
                Label("72般变化 + 金箍棒", systemImage: "02.circle")
                    .textInputCompletion("孙悟空")
                Label("宇宙计划生育办公室主任", systemImage: "03.circle")
                    .textInputCompletion("灭霸")
            }
        
    }
#endif

将项目运行目标改为 macOS 系统,然后我们就可以在 Xcode 预览中一窥究竟了:

在这里插入图片描述

平心而论,苹果在 SwiftUI 新版本中对 TextField 所做的功能更新的确解决了长期以来秃头码农们的燃眉之急和掉发之苦。输入视图的重要性在任何系统中怎么强调都不为过,希望苹果能够再接再厉,把 TextField 打造成未来 SwiftUI 框架中的石室金鐀、玉圭金臬,棒棒哒!💯

5. 源代码

本系列博文全部全代码在此:

import SwiftUI

struct ContentView: View {
    @State var name = ""
    @State var desc = "贪吃贪睡的大熊猫侯佩🐼!!!"
    @State var textSelection: TextSelection?
    @State var selectingText: String = ""
    @State var selectStart = 0.0
    @State var selectCount = 0.0
    @State var favHeroName = ""
    
    @FocusState var descFocus: Bool
    
    private var maxSelectingCount: Int {
        desc.count - Int(selectStart)
        
    }
    
    private func handleSelecting() {
        
        guard descFocus else { return }
        
        let startIndex = desc.startIndex
        let selectStartIndex = desc.index(startIndex, offsetBy: Int(selectStart))
        let selectEndIndex = desc.index(startIndex, offsetBy: Int(selectStart + selectCount))
        textSelection = .init(range: selectStartIndex..<selectEndIndex)
    }
    
    var body: some View {
        NavigationStack {
            List {
                
                Section("横竖轴自适应") {
                    
                    LabeledContent("横轴") {
                        TextField("大熊猫侯佩", text: $name, axis: .horizontal)
                            .frame(width: 200)
                    }
                    
                    LabeledContent("竖轴") {
                        TextField("大熊猫侯佩", text: $name, axis: .vertical)
                            .frame(width: 200)
                    }
                }
                .animation(.bouncy, value: name)
                
                
                Section("文本选择的读取与设置") {
                    TextField("添加描述", text: $desc, selection: $textSelection)
                        .focused($descFocus)
                        .padding()
                        .onChange(of: textSelection) {_,new in
                            guard let selection = textSelection else { return }
                            switch selection.indices {
                            case .selection(let range):
                                selectingText = String(desc[range])
                            default:
                                break
                            }
                        }
                    
                    
                    if !selectingText.isEmpty {
                        Text("当前选中内容:\(Text(selectingText).foregroundStyle(.green).bold())")
                    }
                    
                    if descFocus {
                        VStack {
                            LabeledContent("选择开始位置") {
                                Slider(value: $selectStart, in: 0...Double(desc.count))
                            }
                            
                            LabeledContent("选择字符数量") {
                                Stepper("\(Int(selectCount))", value: $selectCount, in: 0...Double(maxSelectingCount))
                            }
                        }
                        .transition(.slide.combined(with: .opacity))
                    }
                }
                
#if os(macOS)
                Section("文本输入建议") {
                    TextField("最欣赏的英雄", text: $favHeroName)
                        .textInputSuggestions {
                            Label("贪吃贪睡路痴未秃码农", systemImage: "01.circle")
                                .textInputCompletion("大熊猫侯佩")
                            Label("72般变化 + 金箍棒", systemImage: "02.circle")
                                .textInputCompletion("孙悟空")
                            Label("宇宙计划生育办公室主任", systemImage: "03.circle")
                                .textInputCompletion("灭霸")
                        }
                    
                }
#endif
            }
            .navigationTitle("TextField 进化功能演示")
            .listStyle(.plain)
            .animation(.bouncy, value: descFocus)
            .font(.title2)
            .padding()
            .onChange(of: selectStart + selectCount) {_,_ in
                handleSelecting()
            }
        }
    }
}

#Preview {
    ContentView()
}

总结

在本篇博文中,我们介绍了 SwiftUI 6.0 中 TextField 新增的文本选中控制特性,并随后讨论了如何在 macOS 15+ 的系统中实现 TextField 的输入建议功能。

感谢观赏,再会啦!8-)