如何在SwiftUI中动态创建混合文本与TextField的视图?
解决方案
核心问题拆解
你当前代码存在两个关键问题:
- 排版不符合预期:
LazyVGrid是网格布局,无法实现自然文本流的换行效果,会把每个单词/输入框当成独立网格项排列。 - 多输入框绑定冲突:所有占位符共享同一个
answerText变量,无法分别捕获每个空白的输入内容。
实现步骤
- 解析文本结构:将原始文本拆分为静态文本段和占位符段,方便后续动态生成视图。
- 多绑定处理:用数组存储每个占位符的输入值,每个输入框绑定数组对应的索引。
- 流式布局实现:自定义
Layout协议实现流式布局,让文本和输入框像自然句子一样自动换行。
完整代码
import SwiftUI // 定义文本组件类型:静态文本或占位符 enum TextComponent { case staticText(String) case placeholder(index: Int) } // 自定义流式布局,实现文本+输入框的自然换行 struct FlowLayout: Layout { let spacing: CGFloat = 4 // 元素间间距,可调整 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { guard !subviews.isEmpty else { return .zero } var totalWidth = 0.0 var totalHeight = 0.0 var currentLineWidth = 0.0 var currentLineHeight = 0.0 for subview in subviews { let subviewSize = subview.sizeThatFits(proposal) // 如果当前行放不下,换行 if currentLineWidth + subviewSize.width + spacing > (proposal.width ?? .infinity) { totalWidth = max(totalWidth, currentLineWidth) totalHeight += currentLineHeight + spacing currentLineWidth = subviewSize.width currentLineHeight = subviewSize.height } else { currentLineWidth += subviewSize.width + spacing currentLineHeight = max(currentLineHeight, subviewSize.height) } } // 加上最后一行的尺寸 totalWidth = max(totalWidth, currentLineWidth) totalHeight += currentLineHeight return CGSize(width: totalWidth, height: totalHeight) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { guard !subviews.isEmpty else { return } var x = bounds.minX var y = bounds.minY var currentLineHeight = 0.0 for subview in subviews { let subviewSize = subview.sizeThatFits(proposal) // 换行判断 if x + subviewSize.width > bounds.maxX { x = bounds.minX y += currentLineHeight + spacing currentLineHeight = 0.0 } // 放置当前元素 subview.place(at: CGPoint(x: x, y: y), proposal: proposal) x += subviewSize.width + spacing currentLineHeight = max(currentLineHeight, subviewSize.height) } } } struct MultiText: View { @State var text = "There was once a {-} that only had {-} in it. How did they get in? {-}" @State private var answers: [String] = [] // 存储每个占位符的输入值 // 解析原始文本为TextComponent数组 private var parsedComponents: [TextComponent] { let parts = text.components(separatedBy: "{-}") var components = [TextComponent]() for (index, part) in parts.enumerated() { if !part.isEmpty { components.append(.staticText(part)) } // 最后一段后不需要添加占位符 if index < parts.count - 1 { components.append(.placeholder(index: index)) } } return components } var body: some View { FlowLayout() { ForEach(parsedComponents.indices, id: \.self) { index in switch parsedComponents[index] { case .staticText(let content): Text(content) .font(.body) case .placeholder(let answerIndex): TextField("", text: $answers[answerIndex]) .font(.body) .frame(minWidth: 80, height: 24) // 输入框最小宽度,可调整 .border(Color.gray.opacity(0.5)) // 添加边框区分输入区域 } } } .padding() .onAppear { // 根据占位符数量初始化answers数组 let placeholderCount = text.components(separatedBy: "{-}").count - 1 answers = Array(repeating: "", count: placeholderCount) } } } struct MultiText_Previews: PreviewProvider { static var previews: some View { MultiText() } }
效果说明
- 文本和输入框会像自然句子一样自动换行,保持正常的阅读排版。
- 每个占位符对应独立的输入值,可通过
answers数组获取所有用户输入内容。 - 输入框样式可根据需求调整(比如边框颜色、最小宽度等)。
内容的提问来源于stack exchange,提问作者Andrew Donnelly
相关产品推荐
相关产品推荐

