Search code examples
iosswiftswiftuistatebind

Declaring TextField and Toggle in a SwiftUI dynamic form


I went back to swift programming after 4 years and everything changed. I learn everything from scratch. I'm trying to solve a variable binding problem. I want to build a dynamic form with fields that will come back from API. Code:

import SwiftUI


struct Filter: Codable, Identifiable {
      var id: Int
      var name: String
      var type: String
      var defaultValue: Int
init(_ dictionary: [String: Any]) {
      self.id = dictionary["id"] as? Int ?? 0
      self.name = dictionary["name"] as? String ?? ""
      self.type = dictionary["type"] as? String ?? ""
      self.defaultValue = dictionary["defaultValue"] as? Int ?? 0
    }
}

struct ContentView: View {
    
    @State var filters:Array<Filter> = []
    
    func load(){
        guard let url = URL(string: "https://api.kierklosebastian.pl/") else {return}
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
        guard let dataResponse = data,
                  error == nil else {
                  print(error?.localizedDescription ?? "Response Error")
                  return }
            do {
                let decoder = JSONDecoder()
                let model = try decoder.decode([Filter].self, from:
                             dataResponse)
                self.filters = model;
            } catch let parsingError {
                print("Error", parsingError)
            }
        }
        task.resume()
    }
    
    var body: some View {
        VStack{
            Button(action: self.load, label: {
                Text("Get json")
            })
            List{
                ForEach(self.filters) { filter in
                    HStack{
                        Text("NAME: \(filter.name)")
                        Text("TYPE: \(filter.type)")
           
                        if filter.type == "textField" {
//                            TextField("", ???????)
                        }
                        
                        if filter.type == "checkbox" {
//                            Toggle(isOn: ????????) {
//                                ""
//                            }
                        }
                    }
                }
            }
        }
    }
    
}

How do I declare the TextFiled and Toggle variables?


Solution

  • Toggle needs to have a boolean binding property that determines whether the toggle is on or off. So you need to define a boolean property somehwere, for ex, for Filter.

    struct Filter: Codable, Identifiable {
        var id: Int
        var name: String
        var type: String
        var defaultValue: Int
        var isOn: Bool = false
        init(_ dictionary: [String: Any]) {
            self.id = dictionary["id"] as? Int ?? 0
            self.name = dictionary["name"] as? String ?? ""
            self.type = dictionary["type"] as? String ?? ""
            self.defaultValue = dictionary["defaultValue"] as? Int ?? 0
            self.isOn = (dictionary["type"] as? String ?? "") ==  "checkbox"
        }
    }
    

    Create a view model that confirms to ObservableObject.

    https://developer.apple.com/documentation/combine/observableobject

    class FilterViewModel: ObservableObject {
        @Published var filters:Array<Filter> = []
        
        func load(){
            guard let url = URL(string: "https://api.kierklosebastian.pl/") else {return}
            let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
                guard let dataResponse = data,
                      error == nil else {
                    print(error?.localizedDescription ?? "Response Error")
                    return }
                do {
                    let decoder = JSONDecoder()
                    let model = try decoder.decode([Filter].self, from:
                                                    dataResponse)
                    self.filters = model;
                } catch let parsingError {
                    print("Error", parsingError)
                }
            }
            task.resume()
        }
    }
    

    And use it instead of @State var filters:Array<Filter> = [] in ContentView.

    struct FilterView: View {
        
        @ObservedObject var viewModel = FilterViewModel()
       
        -----
    }
    
    

    Create a new view for the content of the ForEach and pass viewModel and filter to it. In the new view, find index of the current filter.

    struct FilterDetailView: View {
        
        @ObservedObject var viewModel: FilterViewModel
        var filter: Filter
        
        
        var indexOfFilter: Int {
            self.viewModel.filters.firstIndex { $0 == filter}!
        }
        var body: some View {
            
            HStack{
                Text("NAME: \(filter.name)")
                Text("TYPE: \(filter.type)")
                
                if filter.type == "textField" {
                    TextField("TextField", text: self.$viewModel.filters[indexOfFilter].name)
                }
                
                if filter.type == "checkbox" {
                    Toggle(isOn: self.$viewModel.filters[indexOfFilter].isOn) {
                        Text("Toggle")
                    }
                }
            }
        }
    }
    
    

    You need to confirm to Equatable too. Oterwise, you can not use firstIndex { $0 == filter}!

    struct Filter: Codable, Identifiable, Equatable {
    }
    

    Complete code

    
    struct Filter: Codable, Identifiable, Equatable {
        var id: Int
        var name: String
        var type: String
        var defaultValue: Int
        var isOn: Bool = false
        init(_ dictionary: [String: Any]) {
            self.id = dictionary["id"] as? Int ?? 0
            self.name = dictionary["name"] as? String ?? ""
            self.type = dictionary["type"] as? String ?? ""
            self.defaultValue = dictionary["defaultValue"] as? Int ?? 0
            self.isOn = (dictionary["type"] as? String ?? "") ==  "checkbox"
        }
    }
    
    
    class FilterViewModel: ObservableObject {
        @Published var filters:Array<Filter> = []
        
        func load(){
            guard let url = URL(string: "https://api.kierklosebastian.pl/") else {return}
            let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
                guard let dataResponse = data,
                      error == nil else {
                    print(error?.localizedDescription ?? "Response Error")
                    return }
                do {
                    let decoder = JSONDecoder()
                    let model = try decoder.decode([Filter].self, from:
                                                    dataResponse)
                    self.filters = model;
                } catch let parsingError {
                    print("Error", parsingError)
                }
            }
            task.resume()
        }
    }
    
    struct FilterView: View {
        
        @ObservedObject var viewModel = FilterViewModel()
        
        var body: some View {
            VStack{
                Button(action: self.viewModel.load, label: {
                    Text("Get json")
                })
                List{
                    ForEach(self.viewModel.filters) { filter in
                        FilterDetailView(viewModel: viewModel, filter: filter)
                    }
                }
            }
        }
        
    }
    
    
    struct FilterDetailView: View {
        
        @ObservedObject var viewModel: FilterViewModel
        var filter: Filter
        
        
        var indexOfFilter: Int {
            self.viewModel.filters.firstIndex { $0 == filter}!
        }
        var body: some View {
            
            HStack{
                Text("NAME: \(filter.name)")
                Text("TYPE: \(filter.type)")
                
                if filter.type == "textField" {
                    TextField("TextField", text: self.$viewModel.filters[indexOfFilter].name)
                }
                
                if filter.type == "checkbox" {
                    Toggle(isOn: self.$viewModel.filters[indexOfFilter].isOn) {
                        Text("Toggle")
                    }
                }
            }
        }
    }
    
    

    iOS 15+

    ForEach(self.viewModel.filters) { $filter in
      -----
    }
    
    Toggle(isOn: $filter.isOn) {
       Text("Toggle")
    }
    
    

    If you are interested, watch What's new in SwiftUI - WWDC21 (08:00) and follow Apple's SwiftUI Tutorials