You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在SwiftUI中动态创建混合文本与TextField的视图?

解决方案

核心问题拆解

你当前代码存在两个关键问题:

  • 排版不符合预期:LazyVGrid是网格布局,无法实现自然文本流的换行效果,会把每个单词/输入框当成独立网格项排列。
  • 多输入框绑定冲突:所有占位符共享同一个answerText变量,无法分别捕获每个空白的输入内容。

实现步骤

  1. 解析文本结构:将原始文本拆分为静态文本段和占位符段,方便后续动态生成视图。
  2. 多绑定处理:用数组存储每个占位符的输入值,每个输入框绑定数组对应的索引。
  3. 流式布局实现:自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 20:50:38