滚动到 SwiftUI 中的 TextEditor 光标位置

2024-02-22

是否可以滚动到TextEditor的当前光标位置?

我有一个List with a TextEditor作为最后一行。我正在滚动到最后一行List以确保TextEditor位于键盘上方。

问题是因为我将滚动锚设置为.bottom,在编辑长文本时,由于滚动视图滚动到行的底部,因此顶部的文本不可见。

您应该能够使用以下代码重新创建问题。只需输入一些长文本,然后尝试在顶部编辑该文本。


import SwiftUI


struct ContentView: View {

    @State var items: [String] = ["Apples", "Oranges", "Bananas", "Pears", "Mangos", "Grapefruit","Apples", "Oranges", "Bananas", "Pears", "Mangos", "Grapefruit"]

    @State private var newItemText : String = ""


    var body: some View {
        ScrollViewReader { (proxy: ScrollViewProxy) in
                List{
                     ForEach(items, id: \.self) {
                         Text("\($0)")
                     }
                     TextEditor(text: $newItemText)
                        .onChange(of: newItemText) { newValue in
                            proxy.scrollTo("New Item TextField", anchor: .bottom)
                        }.id("New Item TextField")
                }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

不幸的是,似乎没有一个“纯粹的”SwiftUI 解决方案。准确实现您所描述的内容的唯一选择似乎是在 UIKit 中实现整个视图并将其集成到应用程序的其余部分中UIViewRepresentable.

不过,既然您询问了 SwiftUI,我可以向您展示仅使用它可以走多远。

获取光标位置

这里你有两个选择。首先,您可以使用内省 https://github.com/siteline/SwiftUI-Introspect包裹。它允许您访问用于实现某些 SwiftUI 的底层 UIKit 元素Views。这看起来像这样:

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提供了光标相对于光标所在高度的良好估计TextEditors 总高度。理论上我们现在应该能够滚动到这个百分比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:)从未打算与自定义/小数一起使用UnitPoints.

因此,我认为没有办法使用 SwiftUI API 让它滚动到正确的位置。

合理的解决方案

我认为您有两个选择:要么在 UIKit 中实现整个子视图,要么减少一点要求:

你可以比较一下cursorPosition to the newValue的长度为.onChange(of: newItemText)并且仅当光标位置位于/接近字符串末尾时才滚动到底部。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

滚动到 SwiftUI 中的 TextEditor 光标位置 的相关文章

随机推荐

  • QDockWidget导致qt崩溃

    我有 ubuntu 11 10 中内置的 Qt 版本 我正在尝试使用QDockWidget实际上无法停靠 基本上 我只想要一个浮动的窗口 我不想只使视图成为顶级视图 因为那样我就会将操作系统窗口栏放在那里 这是我不想要的 并且如果我隐藏它
  • 在封闭网络中安装powershell模块

    我有一个简单的问题 但我找不到任何答案 我正在使用 powershell 5 我在封闭环境中工作 无法连接到互联网 我想在我的机器上手动安装模块 基本上是任何可下载的模块 例如 posh ssh 能做到吗 可以说在这里保存模块并安装吗 由于
  • 使用 Join、Group By 和having 进行更新

    select 语句执行时没有错误或警告 更新语句抛出错误 关键字 group 附近的语法不正确 select sSVsys textUniqueWordCount count as actCount from docSVsys as sSV
  • 使用 -jdkinternals 时 jdeps 没有输出

    我正在尝试使用Java依赖分析工具 jdeps https wiki openjdk java net display JDK8 Java Dependency Analysis Tool 首先 我尝试在没有参数的情况下执行此操作 如下所示
  • Rails 3 的 Bundler“bundle install --deployment”到底是做什么的?

    正式文档中描述的东西有点复杂 它是否只是添加以下行 bundle config BUNDLE PATH vendor bundle 然后执行bundle install 就这样 安装后会将所有宝石安装到vendor bundle 然后当应用
  • 生成 EC Diffie-Hellman 公钥和私钥对

    我需要生成 EC Diffie Hellman 密钥对 我正在使用名为 curve 的 secp256r1 和 OpenSSL 到目前为止 这就是我所拥有的 unsigned char ecdh size t secret len EVP
  • 如何使用 .Net 读取 .cds 数据库 (TClientDataSet)

    我有一个来自用 Delphi 编写的应用程序的旧数据库文件 我的任务是提取数据并将其移至 SQL 数据库 我知道它是 cds 格式 它使用 Delphi 的 TClientDataSet Class 问题是 我没有 Delphi 没有文档
  • React-Redux 中“@@INIT”操作的目的是什么?

    只是注意到它始终是打开页面时调度的第一个操作 它是否用于使用减速器的默认状态来初始化存储 我想这会回答你的问题 When a store is created an INIT action is dispatched so that eve
  • Java 正则表达式:分割逗号分隔值但忽略引号中的逗号

    我的文字如下 text 1 more more text 3 谁能告诉我必须使用哪些正则表达式分隔符才能获得以下内容 text 1 more more text 3 我正在阅读 Sun 教程here http docs oracle com
  • 使用新标签微调模型的分类器层

    我想使用仅包含 1 个模型之前未见过的附加标签的新数据集来微调已经微调的 BertForSequenceClassification 模型 这样 我想向模型当前能够正确分类的标签集添加 1 个新标签 此外 我不希望随机初始化分类器权重 我想
  • php对对象的属性进行排序

    我想对对象的属性进行排序 以便可以按定义的顺序循环遍历它们 例如 我有一个对象 book 具有以下属性 id title author date 现在我想像这样循环访问这些属性 foreach book as prop gt val do
  • 将字符串严格格式化为大写字母,然后将数字分成两半[重复]

    这个问题在这里已经有答案了 我有几个格式的字符串 AA11 AAAAAA1111111 AA1111111 我需要分离字符串的字母和数字部分 如果它们都是一系列字母 后跟一系列数字 没有非字母数字字符 那么sscanf http www p
  • 在 while 循环中使用文本作为条件

    我在使用文本作为 while 循环的条件时遇到一些问题 目前的基本编码是 result struct val yes while result val yes result val input more digits end 正如您所看到的
  • Canvas 中同一动画 GIF 的多个实例 (Java)

    所以我正在制作一款游戏 你可以在角色的位置放置炸弹 当炸弹显示并最终爆炸时 每个炸弹都与一个 GIF 图像相关联 想想炸弹人 问题是 当我尝试在屏幕上绘制多个炸弹时 它是从 GIF 的最后一帧绘制的 经过调查 我找到了 image flus
  • CSS DOM 遍历优于 JavaScript DOM 遍历?

    JavaScript and CSS两者在遍历 HTML 元素时都使用自己的 DOM 树 In JavaScript 我们可以使用它自己的DOM遍历方法比如 element parentNode element nextSibling 然而
  • 是否可以将 JavaScript 变量保存为文件? [复制]

    这个问题在这里已经有答案了 有没有办法将 JavaScript 变量中的字符串转换为用户可以在单击按钮时下载的可下载文件 感谢您的任何建议 div Lorem Ipsum div br
  • 如何在 C# 中动态添加字段到类中

    有什么办法可以添加Field or FieldInfo 也许这与运行时的类是一样的 您无法在运行时更改类定义 但是 您可以创建一个继承自原始类的新类 如果它不是sealed 并声明该字段 您可以通过使用发出适当的 IL 代码来做到这一点Sy
  • 有没有办法使用 glom 库将字典列表合并为单个字典? [复制]

    这个问题在这里已经有答案了 我在用着glom https github com mahmoud glom项目 有没有办法转换 id 1 name foo id 2 name bar to 1 foo 2 bar New glom版本 19
  • Nextjs getStaticProps 将对象作为 prop 而不是纯字符串传递时出错

    我希望你一切都好 在使用新的内部化 NextJS 功能时 我发现了一个我根本没有预料到的错误 如果我传递一个纯字符串作为 prop 从getStaticProps to the Page Component一切正常default local
  • 滚动到 SwiftUI 中的 TextEditor 光标位置

    是否可以滚动到TextEditor的当前光标位置 我有一个List with a TextEditor作为最后一行 我正在滚动到最后一行List以确保TextEditor位于键盘上方 问题是因为我将滚动锚设置为 bottom 在编辑长文本时