Home > Net >  How to render resursively with SwiftUI?
How to render resursively with SwiftUI?

Time:09-09

I got this long error:

Function opaque return type was inferred as '_ConditionalContent<_ConditionalContent<some View, VStack<ForEach<[Dictionary<String, JSON>.Keys.Element], Dictionary<String, JSON>.Keys.Element, TupleView<(some View, some View)>?>>>, some View>' (aka '_ConditionalContent<_ConditionalContent<some View, VStack<ForEach<Array<String>, String, Optional<TupleView<(some View, some View)>>>>>, some View>'), which defines the opaque type in terms of itself

I think the issue is the "resursive" character of the rendering as the error log itself says also:

which defines the opaque type in terms of itself

If I replace in the second if-else-statement, as in comment you see, and return only a text, then I got no error.

@ViewBuilder
func showNode(json: JSON) -> some View {
    if let v = json.get() as? String {
        Text("\(v)").padding()
    } else if let d = json.get() as? [String: JSON] {
        VStack {
            ForEach(d.keys.sorted(), id: \.self) { k in
                if let v = d[k] {
                    Text("\(k)").padding()
                    showNode(json: v)
                }
            }
        }
        // Text("").padding()
    } else {
        Text("").padding()
    }
}

Method is a recursive method, which should render a json tree.

And the JSON

public enum JSON {
    case string(String)
    case number(Float)
    case object([String:JSON])
    case array([JSON])
    case bool(Bool)
    
    func get() -> Any {
        switch self {
        case .string(let v):
            return v
        case .number(let v):
            return v
        case .object(let v):
            return v
        case .array(let v):
            return v
        case .bool(let v):
            return v
        }
    }
}

And all I do render it on screen:

struct ContentView: View {
    var data: JSON? = nil

        init() {
        updateValue()
        return
    }
    
    mutating func updateValue() {
        do {
            if let jsonURL = Bundle.main.url(forResource: "user", withExtension: "json") {
                let jsonData = try Data(contentsOf: jsonURL)
                guard let d2 = try JSONSerialization.jsonObject(with: jsonData, options: .mutableLeaves) as? JSON else {
                    print("Can not convert to d2")
                    return
                }
                data = d2
            }
        } catch  {
            
        }
    }

    var body: some View {
        VStack {
            if let data = self.data {
                showNode(json: data)
            }
        }
    }

CodePudding user response:

some View is just a language feature that allows you to not write out the whole return type of the method.

If you actually try to write out the whole return type of the method by meticulously following all the language rules, you will find that you run into a problem. Because you need to know what the return type of showNode is, in order to know the return type of showNode! This is partly due to its recursive nature, and partly due to @ViewBuilder.

If you are wondering what on earth is _ConditionalContent, those come from buildEither, which is what your if statements translate into when put inside a @ViewBuilder. TupleView comes from buildBlock, and all their type parameters are determined by the types of the expressions you put inside, one of them being the showNode call, whose type we are in the middle of figuring out.

You can fix this by either using AnyView, the type-erased view:

func showNode(json: JSON, depth: Int = 1) -> AnyView {
    if let v = json.get() as? String {
        return AnyView(Text("\(v)").padding())
    } else if let d = json.get() as? [String: JSON] {
        return AnyView(VStack {
            ForEach(d.keys.sorted(), id: \.self) { k in
                if let v = d[k] {
                    Text("\(k)").padding()
                    showNode(json: v)
                }
            }
        })
    } else {
        return AnyView(Text("").padding())
    }
}

Or make a new View type to stop the infinite recursion:

struct NodeView: View {
    let json: JSON
    
    var body: some View {
        if let v = json.get() as? String {
            Text("\(v)").padding()
        } else if let d = json.get() as? [String: JSON] {
            VStack {
                ForEach(d.keys.sorted(), id: \.self) { k in
                    if let v = d[k] {
                        Text("\(k)").padding()
                        NodeView(json: v)
                    }
                }
            }
        } else {
            Text("").padding()
        }
    }
}

A few additional notes:

  • The way that you are parsing the JSON right now is incorrect. JSONSerialization won't give you your own JSON type. You should probably use a custom Codable implementation instead, but exactly how to do that belongs to another question.
  • The view that this code draws doesn't actually show the "levels" of the JSON. Not sure if that is intended or not
  • if let v = json.get() as? String { doesn't handle Bools or Floats or arrays. If you want to handle those, you need to write checks for them as well.
  • Related