Swift UI - 动态列表、TextField 焦点和删除
Swift UI - Dynamic List, TextField focus and deletion
我在 Mac OS 上对 Swift UI 有一个非常奇怪的行为。
这个想法是我有一个可编辑元素的动态列表(我可以添加、编辑和删除)。
如果我不关注 TextField,我可以毫无问题地添加/删除元素。
但是,如果我开始将焦点放在 TextField 上并使用 tab
浏览我的列表 TextField,它最终会使我的应用程序崩溃。
为了说明这个问题,我创建了一个小游乐场。
import Foundation
import SwiftUI
import PlaygroundSupport
struct Container {
var lines: [Line]
}
struct Line: Identifiable {
var id = UUID()
var field1: String
var field2: String
var field3: Double = 0
var field4: Double = 0
}
struct ContainerEditor: View {
@State var hidden = false
@State var myContainer = Container(lines: [
Line(field1: "Line1.1", field2: "Line1.2"),
Line(field1: "Line2.1", field2: "Line2.2"),
Line(field1: "Line3.1", field2: "Line3.2"),
Line(field1: "Line4.1", field2: "Line4.2"),
])
var body: some View {
if !hidden {
ContainerView(container: $myContainer) { line in
print("Removing:")
print(line)
myContainer.lines.removeAll(where: { [=12=].id == line.id })
}
Button("Add line", action: { myContainer.lines.append(Line(field1: "New1", field2: "New2"))})
.buttonStyle(.bordered)
}
Button("Toggle hidden", action: { hidden = !hidden })
}
}
struct ContainerView: View {
var container: Binding<Container>
var onRemove: (_ line: Line) -> Void
var body: some View {
ForEach(container.lines) { line in
LineView(line: line) {
onRemove(line.wrappedValue)
}
}
}
}
struct LineView: View {
var line: Binding<Line>
var onRemove: () -> Void
private var numberFormatter: NumberFormatter {
get {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 4
return formatter
}
}
var body : some View {
HStack {
TextField("field1", text: line.field1).textFieldStyle(.roundedBorder)
TextField("field2", text: line.field2).textFieldStyle(.roundedBorder)
TextField("field3", value: line.field3, formatter: numberFormatter).textFieldStyle(.roundedBorder)
TextField("field4", value: line.field4, formatter: numberFormatter).textFieldStyle(.roundedBorder)
Button("remove") {
print("Remove insider")
onRemove()
}.buttonStyle(.bordered)
}.frame(maxWidth: 300)
}
}
PlaygroundPage.current.setLiveView(ContainerEditor())
示例以 4 行开头。
例如,如果我使用按钮删除第二行,聚焦第一行的第一个 TextField 并开始使用 tab
导航,当焦点到达最后一个字段时应用程序崩溃。
尝试删除行和焦点以及在焦点时删除,还有其他方法可以使 playground 崩溃。
我还怀疑 TextField
和 NumberFormatter
有关系。
我做错了什么吗?从其他与动态列表相关的线程来看,我觉得还不错。
在我的应用程序中,当它崩溃时,我没有得到任何问题的痕迹。
就一个 Swift/ContiguousArrayBuffer.swift:575: Fatal error: Index out of range
指示此行:
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
欢迎任何帮助!
更新
所以我尝试了一种不同的方法,即使用 NSViewRepresentable
和带有协调器的常规 NSTextField
。
这里是Xcodemacos游乐场
import Foundation
import SwiftUI
import AppKit
import PlaygroundSupport
struct Container {
var lines: [Line]
}
struct Line: Identifiable {
var id = UUID()
var field1: String
var field2: String
var field3: Double = 0
var field4: Double = 0
}
struct ContainerEditor: View {
@State var hidden = false
@State var myContainer = Container(lines: [
Line(field1: "Line1.1", field2: "Line1.2"),
Line(field1: "Line2.1", field2: "Line2.2"),
Line(field1: "Line3.1", field2: "Line3.2"),
Line(field1: "Line4.1", field2: "Line4.2"),
])
var body: some View {
if !hidden {
ContainerView(container: $myContainer) { line in
print("Removing:")
print(line)
myContainer.lines.removeAll(where: { [=14=].id == line.id })
}
Button("Add line", action: { myContainer.lines.append(Line(field1: "New1", field2: "New2"))})
.buttonStyle(.bordered)
}
Button("Toggle hidden", action: { hidden = !hidden })
}
}
struct ContainerView: View {
@Binding var container: Container
var onRemove: (_ line: Line) -> Void
var body: some View {
ForEach($container.lines) { line in
LineView(line: line) {
onRemove(line.wrappedValue)
}
}
}
}
struct LineView: View {
@Binding var line: Line
var onRemove: () -> Void
private var numberFormatter: NumberFormatter {
get {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 4
return formatter
}
}
var body : some View {
HStack {
TextNumberField(value: $line.field3)
TextNumberField(value: $line.field4)
Button("remove") {
print("Remove insider")
onRemove()
}.buttonStyle(.bordered)
}.frame(width: 400)
}
}
struct TextNumberField: NSViewRepresentable {
@Binding var value: Double
var font = NSFont.systemFont(ofSize: 12, weight: .medium)
var onEnter: (() -> Void)? = nil
var initialize: ((NSTextField) -> Void)? = nil
func makeNSView(context: Context) -> NSTextField {
let view = NSTextField()
view.delegate = context.coordinator
view.isEditable = true
let formatter = NumberFormatter()
formatter.hasThousandSeparators = false
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 4
view.formatter = formatter
return view
}
func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.doubleValue = value
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, NSTextFieldDelegate {
var parent: TextNumberField
init(_ parent: TextNumberField) {
self.parent = parent
}
func controlTextDidChange(_ obj: Notification) {
guard let textView = obj.object as? NSTextField else {
return
}
self.parent.value = textView.doubleValue
}
}
}
PlaygroundPage.current.setLiveView(ContainerEditor())
只要我不删除任何一行,它就可以工作。如果我这样做,并尝试在删除的行之后编辑一行,应用程序会在该行崩溃并显示 Index out of range
:
self.parent.value = textView.doubleValue
就像协调器不再与视图同步一样。
确实感觉去掉line的时候循环有问题
我可以复制你的步骤,我相信这是一个错误。
您可以使用“新”format
和 .number
而不是 formatter
来规避此问题
TextField("field3", value: $line.field3, format: .number).textFieldStyle(.roundedBorder)
TextField("field4", value: $line.field4, format: .number).textFieldStyle(.roundedBorder)
您应该提交错误报告
工作代码
struct ContainerView: View {
@Binding var container: Container
var onRemove: (_ line: Line) -> Void
var body: some View {
ForEach($container.lines) { $line in
LineView(line: $line) {
onRemove(line)
}
}
}
}
struct LineView: View {
@Binding var line:Line
var onRemove: () -> Void
var body : some View {
HStack {
TextField("field1", text: $line.field1)
TextField("field2", text: $line.field2)
TextField("field3", value: $line.field3, format: .number)
TextField("field4", value: $line.field4, format: .number)
Button("remove") {
print("Remove insider")
onRemove()
}.buttonStyle(.bordered)
}.frame(maxWidth: 300)
.textFieldStyle(.roundedBorder)
}
}
一行崩溃
struct LineView: View {
@Binding var line:Line
var onRemove: () -> Void
var body : some View {
HStack {
TextField("field1", text: $line.field1)
TextField("field2", text: $line.field2)
TextField("field3", value:
Binding(get: {
line.field3 //**Crash at this line**
}, set: { new in
line.field3 = new
})
, formatter: .numberFormatter)
TextField("field4", value:
Binding(get: {
line.field4
}, set: { new in
line.field4 = new
}), formatter: .numberFormatter )
Button("remove") {
print("Remove insider")
onRemove()
}.buttonStyle(.bordered)
}.frame(maxWidth: 300)
.textFieldStyle(.roundedBorder)
}
}
extension Formatter{
static var numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 4
return formatter
}()
}
解决方法
这是目前的解决方法。它会影响性能,因为它会强制完全重绘 View
,你不会看到像这样的简单 View
太多,但如果你的视图变得更长和更复杂,它会减慢一切。
将.id(myContainer.lines.count)
添加到ContainerView
struct ContainerEditor: View {
@State var hidden = false
@State var myContainer = Container(lines: [
Line(field1: "Line1.1", field2: "Line1.2"),
Line(field1: "Line2.1", field2: "Line2.2"),
Line(field1: "Line3.1", field2: "Line3.2"),
Line(field1: "Line4.1", field2: "Line4.2"),
])
var body: some View {
if !hidden {
ContainerView(container: $myContainer) { line in
print("Removing:")
print(line)
myContainer.lines.removeAll(where: { [=13=].id == line.id })
}.id(myContainer.lines.count)
}
Button("Toggle hidden", action: { hidden = !hidden })
}
}
我在 Mac OS 上对 Swift UI 有一个非常奇怪的行为。
这个想法是我有一个可编辑元素的动态列表(我可以添加、编辑和删除)。
如果我不关注 TextField,我可以毫无问题地添加/删除元素。
但是,如果我开始将焦点放在 TextField 上并使用 tab
浏览我的列表 TextField,它最终会使我的应用程序崩溃。
为了说明这个问题,我创建了一个小游乐场。
import Foundation
import SwiftUI
import PlaygroundSupport
struct Container {
var lines: [Line]
}
struct Line: Identifiable {
var id = UUID()
var field1: String
var field2: String
var field3: Double = 0
var field4: Double = 0
}
struct ContainerEditor: View {
@State var hidden = false
@State var myContainer = Container(lines: [
Line(field1: "Line1.1", field2: "Line1.2"),
Line(field1: "Line2.1", field2: "Line2.2"),
Line(field1: "Line3.1", field2: "Line3.2"),
Line(field1: "Line4.1", field2: "Line4.2"),
])
var body: some View {
if !hidden {
ContainerView(container: $myContainer) { line in
print("Removing:")
print(line)
myContainer.lines.removeAll(where: { [=12=].id == line.id })
}
Button("Add line", action: { myContainer.lines.append(Line(field1: "New1", field2: "New2"))})
.buttonStyle(.bordered)
}
Button("Toggle hidden", action: { hidden = !hidden })
}
}
struct ContainerView: View {
var container: Binding<Container>
var onRemove: (_ line: Line) -> Void
var body: some View {
ForEach(container.lines) { line in
LineView(line: line) {
onRemove(line.wrappedValue)
}
}
}
}
struct LineView: View {
var line: Binding<Line>
var onRemove: () -> Void
private var numberFormatter: NumberFormatter {
get {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 4
return formatter
}
}
var body : some View {
HStack {
TextField("field1", text: line.field1).textFieldStyle(.roundedBorder)
TextField("field2", text: line.field2).textFieldStyle(.roundedBorder)
TextField("field3", value: line.field3, formatter: numberFormatter).textFieldStyle(.roundedBorder)
TextField("field4", value: line.field4, formatter: numberFormatter).textFieldStyle(.roundedBorder)
Button("remove") {
print("Remove insider")
onRemove()
}.buttonStyle(.bordered)
}.frame(maxWidth: 300)
}
}
PlaygroundPage.current.setLiveView(ContainerEditor())
示例以 4 行开头。
例如,如果我使用按钮删除第二行,聚焦第一行的第一个 TextField 并开始使用 tab
导航,当焦点到达最后一个字段时应用程序崩溃。
尝试删除行和焦点以及在焦点时删除,还有其他方法可以使 playground 崩溃。
我还怀疑 TextField
和 NumberFormatter
有关系。
我做错了什么吗?从其他与动态列表相关的线程来看,我觉得还不错。
在我的应用程序中,当它崩溃时,我没有得到任何问题的痕迹。
就一个 Swift/ContiguousArrayBuffer.swift:575: Fatal error: Index out of range
指示此行:
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
欢迎任何帮助!
更新
所以我尝试了一种不同的方法,即使用 NSViewRepresentable
和带有协调器的常规 NSTextField
。
这里是Xcodemacos游乐场
import Foundation
import SwiftUI
import AppKit
import PlaygroundSupport
struct Container {
var lines: [Line]
}
struct Line: Identifiable {
var id = UUID()
var field1: String
var field2: String
var field3: Double = 0
var field4: Double = 0
}
struct ContainerEditor: View {
@State var hidden = false
@State var myContainer = Container(lines: [
Line(field1: "Line1.1", field2: "Line1.2"),
Line(field1: "Line2.1", field2: "Line2.2"),
Line(field1: "Line3.1", field2: "Line3.2"),
Line(field1: "Line4.1", field2: "Line4.2"),
])
var body: some View {
if !hidden {
ContainerView(container: $myContainer) { line in
print("Removing:")
print(line)
myContainer.lines.removeAll(where: { [=14=].id == line.id })
}
Button("Add line", action: { myContainer.lines.append(Line(field1: "New1", field2: "New2"))})
.buttonStyle(.bordered)
}
Button("Toggle hidden", action: { hidden = !hidden })
}
}
struct ContainerView: View {
@Binding var container: Container
var onRemove: (_ line: Line) -> Void
var body: some View {
ForEach($container.lines) { line in
LineView(line: line) {
onRemove(line.wrappedValue)
}
}
}
}
struct LineView: View {
@Binding var line: Line
var onRemove: () -> Void
private var numberFormatter: NumberFormatter {
get {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 4
return formatter
}
}
var body : some View {
HStack {
TextNumberField(value: $line.field3)
TextNumberField(value: $line.field4)
Button("remove") {
print("Remove insider")
onRemove()
}.buttonStyle(.bordered)
}.frame(width: 400)
}
}
struct TextNumberField: NSViewRepresentable {
@Binding var value: Double
var font = NSFont.systemFont(ofSize: 12, weight: .medium)
var onEnter: (() -> Void)? = nil
var initialize: ((NSTextField) -> Void)? = nil
func makeNSView(context: Context) -> NSTextField {
let view = NSTextField()
view.delegate = context.coordinator
view.isEditable = true
let formatter = NumberFormatter()
formatter.hasThousandSeparators = false
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 4
view.formatter = formatter
return view
}
func updateNSView(_ nsView: NSTextField, context: Context) {
nsView.doubleValue = value
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, NSTextFieldDelegate {
var parent: TextNumberField
init(_ parent: TextNumberField) {
self.parent = parent
}
func controlTextDidChange(_ obj: Notification) {
guard let textView = obj.object as? NSTextField else {
return
}
self.parent.value = textView.doubleValue
}
}
}
PlaygroundPage.current.setLiveView(ContainerEditor())
只要我不删除任何一行,它就可以工作。如果我这样做,并尝试在删除的行之后编辑一行,应用程序会在该行崩溃并显示 Index out of range
:
self.parent.value = textView.doubleValue
就像协调器不再与视图同步一样。
确实感觉去掉line的时候循环有问题
我可以复制你的步骤,我相信这是一个错误。
您可以使用“新”format
和 .number
而不是 formatter
TextField("field3", value: $line.field3, format: .number).textFieldStyle(.roundedBorder)
TextField("field4", value: $line.field4, format: .number).textFieldStyle(.roundedBorder)
您应该提交错误报告
工作代码
struct ContainerView: View {
@Binding var container: Container
var onRemove: (_ line: Line) -> Void
var body: some View {
ForEach($container.lines) { $line in
LineView(line: $line) {
onRemove(line)
}
}
}
}
struct LineView: View {
@Binding var line:Line
var onRemove: () -> Void
var body : some View {
HStack {
TextField("field1", text: $line.field1)
TextField("field2", text: $line.field2)
TextField("field3", value: $line.field3, format: .number)
TextField("field4", value: $line.field4, format: .number)
Button("remove") {
print("Remove insider")
onRemove()
}.buttonStyle(.bordered)
}.frame(maxWidth: 300)
.textFieldStyle(.roundedBorder)
}
}
一行崩溃
struct LineView: View {
@Binding var line:Line
var onRemove: () -> Void
var body : some View {
HStack {
TextField("field1", text: $line.field1)
TextField("field2", text: $line.field2)
TextField("field3", value:
Binding(get: {
line.field3 //**Crash at this line**
}, set: { new in
line.field3 = new
})
, formatter: .numberFormatter)
TextField("field4", value:
Binding(get: {
line.field4
}, set: { new in
line.field4 = new
}), formatter: .numberFormatter )
Button("remove") {
print("Remove insider")
onRemove()
}.buttonStyle(.bordered)
}.frame(maxWidth: 300)
.textFieldStyle(.roundedBorder)
}
}
extension Formatter{
static var numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 4
return formatter
}()
}
解决方法
这是目前的解决方法。它会影响性能,因为它会强制完全重绘 View
,你不会看到像这样的简单 View
太多,但如果你的视图变得更长和更复杂,它会减慢一切。
将.id(myContainer.lines.count)
添加到ContainerView
struct ContainerEditor: View {
@State var hidden = false
@State var myContainer = Container(lines: [
Line(field1: "Line1.1", field2: "Line1.2"),
Line(field1: "Line2.1", field2: "Line2.2"),
Line(field1: "Line3.1", field2: "Line3.2"),
Line(field1: "Line4.1", field2: "Line4.2"),
])
var body: some View {
if !hidden {
ContainerView(container: $myContainer) { line in
print("Removing:")
print(line)
myContainer.lines.removeAll(where: { [=13=].id == line.id })
}.id(myContainer.lines.count)
}
Button("Toggle hidden", action: { hidden = !hidden })
}
}