不幸的是,似乎没有一个“纯粹的”SwiftUI 解决方案。准确实现您所描述的内容的唯一选择似乎是在 UIKit 中实现整个视图并将其集成到应用程序的其余部分中UIViewRepresentable
.
不过,既然您询问了 SwiftUI,我可以向您展示仅使用它可以走多远。
获取光标位置
这里你有两个选择。首先,您可以使用内省 https://github.com/siteline/SwiftUI-Introspect包裹。它允许您访问用于实现某些 SwiftUI 的底层 UIKit 元素View
s。这看起来像这样:
import SwiftUI
import Introspect
struct ContentView: View {
@State var items: [String] = ["Apples", "Oranges", "Bananas", "Pears", "Mangos", "Grapefruit","Apples", "Oranges", "Bananas", "Pears", "Mangos", "Grapefruit"]
@State private var newItemText : String = ""
@State private var uiTextView: UITextView?
@State private var cursorPosition: Int = 0
var body: some View {
ScrollViewReader { (proxy: ScrollViewProxy) in
List{
ForEach(items, id: \.self) {
Text("\($0)")
}
TextEditor(text: $newItemText)
.introspectTextView { uiTextView in
self.uiTextView = uiTextView
}
.onChange(of: newItemText) { newValue in
if let textView = uiTextView {
if let range = textView.selectedTextRange {
let cursorPosition = textView.offset(from: textView.beginningOfDocument, to: range.start)
self.cursorPosition = cursorPosition
}
}
}.id("New Item TextField")
}
}
}
}
但请注意,这种方法可能会在 SwiftUI 的未来版本中失效,因为 Introspect 包依赖于内部 API,并且可能不再能够提取UITextView
from a TextEditor
.
另一种解决方案可能是只比较newValue
of newItemText
每次更改时都与旧索引相匹配,并从末尾开始比较每个索引的字符,直到发现不匹配。请注意,如果用户决定输入一长串相同的字符,则这是有问题的。例如。你不知道在哪里a
比较时插入"aaaaaaa"
and "aaaaaa"
.
滚动
我们现在知道光标在文本中的位置。但是,由于我们不知道空格和换行符在哪里,因此我们不知道光标的高度。
我发现解决这个问题的唯一方法 - 我知道这听起来有多混乱 - 是渲染一个版本TextEditor
其中包含的文本在光标位置被截断并使用GeometryReader
:
struct ContentView: View {
@State var items: [String] = ["Apples", "Oranges", "Bananas", "Pears", "Mangos", "Grapefruit","Apples", "Oranges", "Bananas", "Pears", "Mangos", "Grapefruit"]
@State private var newItemText : String = ""
@Namespace var cursorPositionId
@State private var cursorPosition: Int = 0
@State private var uiTextView: UITextView?
@State private var renderedTextSize: CGSize?
@State private var renderedCutTextSize: CGSize?
var anchorVertical: CGFloat {
if let renderedCutTextSize = renderedCutTextSize, let renderedTextSize = renderedTextSize, renderedTextSize.height > 0 {
return renderedCutTextSize.height / renderedTextSize.height
}
return 1
}
var body: some View {
ZStack {
List {
VStack {
Text(newItemText)
.background(GeometryReader {
Color.clear.preference(key: TextSizePreferenceKey.self,
value: $0.size)
})
Text(String(newItemText[..<(newItemText.index(newItemText.startIndex, offsetBy: cursorPosition, limitedBy: newItemText.endIndex) ?? newItemText.endIndex)]))
.background(GeometryReader {
Color.clear.preference(key: CutTextSizePreferenceKey.self,
value: $0.size)
})
}
}
ScrollViewReader { (proxy: ScrollViewProxy) in
// ...
}
}
.onChange(of: newItemText) { newValue in
// ...
}
.onPreferenceChange(CutTextSizePreferenceKey.self) { size in
self.renderedCutTextSize = size
}
.onPreferenceChange(TextSizePreferenceKey.self) { size in
self.renderedTextSize = size
}
}
}
private struct TextSizePreferenceKey: PreferenceKey {
static let defaultValue = CGSize.zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
let newVal = nextValue()
value = CGSize(width: value.width + newVal.width, height: value.height + newVal.height)
}
}
private struct CutTextSizePreferenceKey: PreferenceKey {
static let defaultValue = CGSize.zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
let newVal = nextValue()
value = CGSize(width: value.width + newVal.width, height: value.height + newVal.height)
}
}
该物业anchorVertical
提供了光标相对于光标所在高度的良好估计TextEditor
s 总高度。理论上我们现在应该能够滚动到这个百分比TextEditor
的身高使用ScrollViewProxy
:
ScrollViewReader { (proxy: ScrollViewProxy) in
List{
ForEach(items, id: \.self) {
Text("\($0)")
}
TextEditor(text: $newItemText)
.introspectTextView { uiTextView in
self.uiTextView = uiTextView
}
.id(cursorPositionId)
}
.onChange(of: cursorPosition) { pos in
proxy.scrollTo(cursorPositionId, anchor: UnitPoint.init(x: 0, y: anchorVertical))
}
}
不幸的是,这不起作用。UnitPoint
有一个自定义坐标的初始值设定项,但是,scrollTo(_:anchor:)
对于非整数值,其行为不符合预期。 IE。UnitPoint.top.height = 0.0
and UnitPoint.bottom.height = 1.0
工作正常,但无论我在中间尝试什么数字(例如0.3
or 0.9
),它总是滚动到文本中相同的随机位置。
这要么是 SwiftUI 中的一个错误,要么scrollTo(_:anchor:)
从未打算与自定义/小数一起使用UnitPoint
s.
因此,我认为没有办法使用 SwiftUI API 让它滚动到正确的位置。
合理的解决方案
我认为您有两个选择:要么在 UIKit 中实现整个子视图,要么减少一点要求:
你可以比较一下cursorPosition
to the newValue
的长度为.onChange(of: newItemText)
并且仅当光标位置位于/接近字符串末尾时才滚动到底部。