0

I first want to parse a large text file and collect all the necessary views before I finally display them. I have it working with an array of AnyView but it's really not nice since it erases all types.

Basically, I just want a container to collect views inside until I finally display them.

So I was wondering if I could do something like this:

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))
            }
        }
    }
}

And then do something with the VStack later. But I get the following errors:

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: <#() -> _#>'


Edit:

I have attempted to do it with the normal ViewBuilder instead. The problem here is that it's all separate Texts now that don't look like one 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{$0 != ""}
    }
    
    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"
    }
}
Big_Chair
  • 2,781
  • 3
  • 31
  • 58
  • It does not work that way, parse and prepare model, then construct views in `body` conditionally depending on kind of model. – Asperi Nov 05 '21 at 12:32
  • 1
    And, judging from what you put up there, you will want to use `AttributedString' in your model where you can control the strings appearance for when you use it later, at least for the text portion you are showing. See [AttributedString documentation](https://developer.apple.com/documentation/foundation/attributedstring). – Yrb Nov 05 '21 at 12:38
  • @Yrb Yeah the problem is that in-between the text can be dividers and images, so I sometimes have to split the text into parts, which makes it nearly impossible with `ViewBuilder`. So I have been using an array of `AnyView` type to collect all the stuff in the necessary order and then display it. I really don't know how to do it better. – Big_Chair Nov 05 '21 at 13:47
  • It would help if you showed the part of the model you want to display. You really haven't asked the question you want answered. I would start a new question with a, as best as you can, [Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example). – Yrb Nov 05 '21 at 14:00
  • @Yrb I actually asked about it here: https://stackoverflow.com/questions/69800340. So this question here is like a follow-up after trying multiple different things. – Big_Chair Nov 05 '21 at 14:26
  • Again, you haven't shown your model or a MRE. We aren't going to be able to help until you do. – Yrb Nov 05 '21 at 14:36
  • @Yrb I added a MRE – Big_Chair Nov 05 '21 at 15:46
  • I see the problem. You need to do the parsing work in a Model. You are trying to do everything in your view, and make one view do EVERYTHING. The model then sends the parsed data to the view. You gave one example of the data; is it all in the same basic format, or can it vary? Also, how do you want the view to look in the end? – Yrb Nov 05 '21 at 15:54
  • @Yrb The texts come from an SQLite db and are all written approximately like the `exampleInputString`. – Big_Chair Nov 05 '21 at 16:08
  • It needs to be parsed in a model, and handled there. – Yrb Nov 05 '21 at 16:54
  • @Yrb I solved it now, see below if you're interested. – Big_Chair Nov 07 '21 at 11:12

1 Answers1

0

So it seems that it is not possible, Group, VStack or similar are not meant to be stored like that. Instead, one has to store the content in some other way and then dynamically recreate it in a ViewBuilder (or just body).


This is how I ended up doing it:

Separate class for the parser, which uses two separate arrays. One to keep track of the content type and order (e.g. 1. text, 2. image, 3. divider etc.) and then one for the Texts only, so all the markdown formatting can be saved for later use in the 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"
}

And then use it with SwiftUI like this:

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)
    }
}
Big_Chair
  • 2,781
  • 3
  • 31
  • 58