0

I have a multi-level dictionary that I need use to build navigation links. Now I have a 2-level depth dictionary:

let multiDimDict: [String: [[String: [String]]]]
= ["A": [["A1": ["A11", "A12"]], ["A2": ["A21", "A22"]]],
   "B": [["B1": ["B11", "B12"]], ["B2": ["B21", "B22"]]]
    ]

I can build the navigation view with all navigation links. However, I feel that I am somehow repeating some code in building navigation links. The question is, if I have a many-level dictionary, say 10-level, I don't think it's wise to build 10 different child-view generators in order to build the complete navigation view. Any idea how to reduce the code?

struct PlayingWithMultiDimNavLink: View {

    let multiDimDict: [String: [[String: [String]]]]
    = ["A": [["A1": ["A11", "A12"]], ["A2": ["A21", "A22"]]],
       "B": [["B1": ["B11", "B12"]], ["B2": ["B21", "B22"]]]
        ]




    var body: some View {

        NavigationView{
            List {
                ForEach(multiDimDict.keys.sorted(), id: \.self) { key in

                    NavigationLink(destination: GenerateChildView(key: key, dict: self.multiDimDict)){

                        Text(key)
                    }

                }
            }
        }

    }
}

This is the :first child view generator:

struct GenerateChildView: View {

    var key: String
    var dict: [String: [[String: [String]]]]
    var infoList: [[String: [String]]]

    init(key: String, dict: [String: [[String: [String]]]]  ){
        self.key = key
        self.dict = dict
        self.infoList = dict[key]!
    }


    var body: some View {
        List{
            ForEach(infoList, id: \.self){ info in
                self.getSomeView(infoList: self.infoList)

            }
        }

}


    func getSomeView(infoList: [[String: [String]]] )-> AnyView{

        let dictToUse = infoList[0]
        return AnyView(
            ForEach(dictToUse.keys.sorted(), id: \.self){ key2 in

                    NavigationLink(destination: GenerateChildView2(infoList: dictToUse[key2]!)){
                        Text(key2)
                    }
                }


        )

    }

The last child-view generator:

struct GenerateChildView2: View {

    var infoList: [String]

    init(infoList: [String]){
        self.infoList = infoList
    }

    var body: some View {

        List{

            ForEach(infoList, id:\.self){ content in

                Text(content)
            }

        }


    }
}
mic
  • 155
  • 2
  • 9

2 Answers2

0

I would start by abandoning the use of a dictionary and use a simple recursive struct that provides a more useful model:

struct Item: Identifiable {
    let id = UUID()
    var title: String
    var children: [Item] = []
}

Then you can define a recursive View to display this model:

struct NavView: View { 
    var item: Item

    var body: some View {
        NavigationView {
            List {
                ForEach(item.children) { child in
                    if child.children.isEmpty {
                        Text(child.title)
                    } else {  
                        NavigationLink(destination: NavView(item:child)) {
                            Text(child.title)
                        }
                    }
                }
            }.navigationBarTitle(item.title)
        }
    }
}

Then you can create your hierarchy as required and pass the root to your view:

let root = Item(title:"",children:
    [Item(title:"A",children:
        [Item(title:"A1", children:
            [Item(title:"A11"),Item(title:"A12")]
            )]),
          Item(title:"B",children:
            [Item(title:"B1"),Item(title:"B2")]
        )]
)

NavView(item:root))
Paulw11
  • 108,386
  • 14
  • 159
  • 186
  • the idea of using a struct to present the nested dict is great! Thanks! – mic Jun 10 '20 at 14:10
  • if you click on A then A1, there will be two back buttons stacking above one another in the navigation view. I just removed NavigationView {} from NavView, and put it my view. – mic Jun 10 '20 at 14:19
  • Yes, sorry, I realised that there should be a different root view that ha s the navigation view. – Paulw11 Jun 10 '20 at 20:55
0

It is not clear from provided snapshot how list of dictionaries should be presented, and, actually, why is it used a dictionary as base model at all, but irrelative to uncertainties here is possible direction (not final but tested & can be considered)

struct PlayingWithMultiDimNavLink2: View {

    let multiDimDict: [String: [[String: [String]]]]
    = ["A": [["A1": ["A11", "A12"]], ["A2": ["A21", "A22"]]],
       "B": [["B1": ["B11", "B12"]], ["B2": ["B21", "B22"]]]
        ]

    var body: some View {
       NavigationView{
            GenericChildView(model: multiDimDict)
        }
    }
}

struct GenericChildView: View {
    let model: [String: Any]

    var body: some View {
        List{
            ForEach(model.keys.sorted(), id: \.self) { key in
                self.nextLevelView(for: key)
            }
        }
    }

    private func nextLevelView(for key: String) -> some View {
        let next = model[key]
        return Group {
            if next as? String != nil {
                Text(next as! String)
            } else if next as? [Any] != nil {
                NavigationLink(destination: GenerateChildListView(infoList: next as! [Any]))
                    { Text(key) }
            } else if next as? [String: Any] != nil {
                NavigationLink(destination: GenericChildView(model: next as! [String: Any]))
                    { Text(key) }
            }
        }
    }
}

struct GenerateChildListView: View {

    var infoList: [Any]

    init(infoList: [Any]){
        self.infoList = infoList
    }

    var body: some View {
        Group {
            if infoList.count == 1 {
                self.singleContentView()
            }
            if infoList.count > 1 {
                List{
                    self.contentView()
                }
            }
        }
    }

    private func singleContentView() -> some View {
        return Group {
            if infoList as? [String] != nil {
                Text(infoList[0] as! String)
            } else if infoList as? [[String: Any]] != nil {
                GenericChildView(model: self.infoList[0] as! [String: Any])
            }
        }
    }

    private func contentView() -> some View {
        return Group {
            if infoList as? [String] != nil {
                ForEach(infoList as! [String], id:\.self) { content in
                    Text(content)
                }
            } else if infoList as? [[String: Any]] != nil {
                ForEach((infoList as! [[String: Any]]).indices, id: \.self) { i in
                    GenericChildView(model: self.infoList[i] as! [String: Any])
                }
            }
        }
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thanks for sharing the code. Well, let's say the input of of nested dictionaries comes with different variety of depth-level. I do not have prior knowledge about the structure of the nested dictionary. Check [this](https://stackoverflow.com/questions/26695091/multidimensional-dictionaries-possible-in-swift) out. If I extract the car data from db using a different time period filter each time, the result will differ. However, really appreciate your code here. I never thought that I could present nested dictionaries using customized struct, it would be easier. – mic Jun 10 '20 at 13:23