SwiftUI - 是否可以将 Stack 或 Group 声明为 属性?

SwiftUI - Is it possible to declare a Stack or Group as a property?

我首先要解析一个大文本文件并收集所有必要的视图,然后再最终显示它们。我让它与 AnyView 数组一起工作,但它真的不好,因为它会擦除所有类型。

基本上,我只是想要一个容器来收集里面的视图,直到我最终显示它们。

所以我想知道我是否可以做这样的事情:

class Demo {
    var content = VStack()

    private func mapInput() {
    // ...
    }

    private func parse() {
        for word in mappedInput { // mappedInput is the collection of tags & words that is done before
            switch previous {
            case "i":
                content.add(Text(word).italic())
            case "h":
                content.add(Text(word).foregroundColor(.green))
            case "img":
               content.add(Image(word))
            }
        }
    }
}

然后用 VStack 做一些事。但是我收到以下错误:

Error: Generic parameter 'Content' could not be inferred
Explicitly specify the generic arguments to fix this issue

Error: Missing argument for parameter 'content' in call
Insert ', content: <#() -> _#>'


编辑:

我尝试用普通的 ViewBuilder 来代替。这里的问题是现在所有的 Text 看起来都不像一个文本。

struct ViewBuilderDemo: View {
    private let exampleInputString =
        """
        <i>Welcome.</i><h>Resistance deepens the negative thoughts, acceptance</h><f>This will be bold</f><h>higlight reel</h><f>myappisgood</f>lets go my friend tag parsin in SwiftUI xcode 13 on Mac<img>xcode</img>Mini<f>2020</f><eh>One is beating oneself up, <img>picture</img>the other for looking opportunities. <h>One is a disempowering question, while the other empowers you.</h> Unfortunately, what often comes with the first type of questions is a defensive mindset. You start thinking of others as rivals; you have to ‘fight’ for something so they can't have it, because if one of them gets it then you automatically lose it.
        """
    private var mappedInput: [String]
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                ForEach(Array(zip(mappedInput.indices, mappedInput)), id: \.0) { index, word in
                    if index > 0 {
                        if !isTag(tag: word) {
                            let previous = mappedInput[index - 1]
                            
                            switch previous {
                            case "i":
                                Text("\(word) ")
                                    .italic()
                                    .foregroundColor(.gray)
                            case "h":
                                Text("\(word) ")
                                    .foregroundColor(.green)
                                    .bold()
                            case "f":
                                Text("\(word) ")
                                    .bold()
                            case "eh":
                                Divider()
                                    .frame(maxWidth: 200)
                                    .padding(.top, 24)
                                    .padding(.bottom, 24)
                            case "img":
                                Image(word)
                                    .resizable()
                                    .scaledToFit()
                                    .frame(width: UIScreen.main.bounds.width * 0.7, height: 150)
                            default:
                                Text("\(word) ")
                            }
                        }
                    }
                }
            }
            .padding()
        }
    }
    
    init() {
        let separators = CharacterSet(charactersIn: "<>")
        mappedInput = exampleInputString.components(separatedBy: separators).filter{[=12=] != ""}
    }
    
    private func isTag(tag currentTag: String) -> Bool {
        for tag in Tags.allCases {
            if tag.rawValue == currentTag {
                return true
            }
        }
        return false
    }
    
    enum Tags: String, CaseIterable {
        case h = "h"
        case hEnd = "/h"
        case b = "f"
        case bEnd = "/f"
        case i = "i"
        case iEnd = "/i"
        case eh = "eh"
        case img = "img"
        case imgEnd = "/img"
    }
}

所以似乎不可能,GroupVStack或类似的东西不应该那样存储。相反,必须以其他方式存储内容,然后在 ViewBuilder(或只是 body)中动态重新创建它。


这就是我最终的做法:

解析器分开 class,它使用两个单独的数组。一个用于跟踪内容类型和顺序(例如 1. 文本、2. 图像、3. 分隔符等),然后一个用于 Texts,因此可以保存所有降价格式以备后用ViewBuilder.

class TextParserTest {
    var inputText: String
    var contentStructure: [(contentType: ContentTag, content: String)] = []
    var separatedTextObjects = [Text("")]
    private var activeTextArrayIndex = 0
    private var lastElementWasText = false
    private var mappedInput: [String]
    
    init(inputString: String) {
        self.inputText = inputString
        
        let newString = inputString.replacingOccurrences(of: "<eh>", with: "<eh>.</eh>")
        let separators = CharacterSet(charactersIn: "<>")
        mappedInput = newString.components(separatedBy: separators)
        parse()
    }
    
    private func isTag(word: String) -> Bool {
        for tag in RawTags.allCases {
            if tag.rawValue == word {
                return true
            }
        }
        return false
    }
    
    private func parse() {
        for (index, word) in mappedInput.enumerated() {
            if index > 0 {
                if !isTag(word: word) {
                    let tag = mappedInput[index - 1]
                    applyStyle(tag: tag, word: word)
                }
            }
            else if (index == 0 && !isTag(word: word)) {
                var text = separatedTextObjects[activeTextArrayIndex]
                    .foregroundColor(.black)
                text = text + Text("\(word) ")
                separatedTextObjects[activeTextArrayIndex] = text
            }
        }
        
        if (lastElementWasText) {
            contentStructure.append((contentType: ContentTag.text, content: "\(activeTextArrayIndex)"))
        }
    }
    
    private func applyStyle(tag: String, word: String) {
        var text = separatedTextObjects[activeTextArrayIndex]
            .foregroundColor(.black)
        
        switch tag {
        case "i":
            text = text +
            Text("\(word)")
                .italic()
                .foregroundColor(.gray)
            separatedTextObjects[activeTextArrayIndex] = text
            lastElementWasText = true
        case "h":
            text = text +
            Text("\(word)")
                .foregroundColor(.accentColor)
                .bold()
                .kerning(2)
            separatedTextObjects[activeTextArrayIndex] = text
            lastElementWasText = true
        case "f":
            text = text +
            Text("\(word)")
                .bold()
            separatedTextObjects[activeTextArrayIndex] = text
            lastElementWasText = true
        case "eh":
            contentStructure.append((contentType: ContentTag.text, content: "\(activeTextArrayIndex)"))
            contentStructure.append((contentType: ContentTag.divider, content: ""))
            separatedTextObjects.append(Text(""))
            activeTextArrayIndex += 1
            lastElementWasText = false
        case "img":
            contentStructure.append((contentType: ContentTag.text, content: "\(activeTextArrayIndex)"))
            contentStructure.append((contentType: ContentTag.image, content: "\(word)"))
            separatedTextObjects.append(Text(""))
            activeTextArrayIndex += 1
            lastElementWasText = false
        default:
            text = text +
            Text("\(word)")
            separatedTextObjects[activeTextArrayIndex] = text
            lastElementWasText = true
        }
    }
    
    private enum RawTags: String, CaseIterable {
        case h = "h"
        case hEnd = "/h"
        case b = "f"
        case bEnd = "/f"
        case i = "i"
        case iEnd = "/i"
        case eh = "eh"
        case ehEnd = "/eh"
        case img = "img"
        case imgEnd = "/img"
    }
}

enum ContentTag: String {
    case text = "text"
    case image = "image"
    case divider = "divider"
}

然后像这样与 SwiftUI 一起使用它:

struct ViewBuilderDemo2: View {
    private let exampleInputString =
        """
        <i>Welcome.</i><h>Resistance deepens the negative thoughts, acceptance</h><f>This will be bold</f><h>higlight reel</h><f>myappisgood</f>lets go my friend tag parsin in SwiftUI xcode 13 on Mac<img>xcode</img>Mini<f>2020</f><eh>One is beating oneself up, <img>picture</img>the other for looking opportunities. <h>One is a disempowering question, while the other empowers you.</h> Unfortunately, what often comes with the first type of questions is a defensive mindset. You start thinking of others as rivals; you have to ‘fight’ for something so they can't have it, because if one of them gets it then you <eh><f>automatically</f> lose it. Kelb kelb text lorem ipsum and more.
        """
    var parser: TextParserTest
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                ForEach(parser.contentStructure.indices, id: \.self) { i in
                    let contentType = parser.contentStructure[i].contentType
                    let content = parser.contentStructure[i].content
                    
                    switch contentType {
                    case ContentTag.divider:
                        Divider()
                            .frame(maxWidth: 200)
                            .padding(.top, 24)
                            .padding(.bottom, 24)
                    case ContentTag.image:
                        Image(content)
                            .resizable()
                            .scaledToFit()
                            .frame(width: UIScreen.main.bounds.width * 0.7, height: 150)
                            .padding(.top, 24)
                            .padding(.bottom, 24)
                    case ContentTag.text:
                        let textIndex = Int(content) ?? 0
                        parser.separatedTextObjects[textIndex]
                    }
                }
            }
            .padding()
        }
    }
    
    init() {
        parser = TextParserTest(inputString: exampleInputString)
    }
}