Home > database >  How to merge dictionary in swiftui?
How to merge dictionary in swiftui?

Time:09-05

I'm working with the response from the API using MVVM swiftui. The program I made runs smoothly if it only displays 1 array of dictionary.

But I'm very confused because I have to display 2 arrays of dictionaries sequentially based on the SAME DATE, how to combine 2 arrays of dictionaries to displayed in 1 view at a time?

This is my struct model

struct JsonData : Codable{
    var errCode : Int
    var dataJson : DataJson
    
    enum CodingKeys: String, CodingKey {
            case errCode = "err_code"
            case dataJson = "data"
        }
}

struct DataJson: Codable{
    var message : String
    var result  : ResultData
}
struct ResultData : Codable {
    var check_in  : [Checkin] = []
    var check_out : [Checkout] = []
}

struct Checkin : Codable, Hashable{
    var created_at : String
    var late : String
    var location : String?
    var place : String
}

struct Checkout : Codable, Hashable{
    var created_at : String
    var early : String
    var location : String?
    var place : String
}

THIS IS MY VIEW

struct ContentView: View {
    @ObservedObject var vm = HistoryManager()
    var body: some View {
        let checkIn  = vm.fetchedResult?.dataJson.result.check_in
        let checkOut = vm.fetchedResult?.dataJson.result.check_out
        VStack(alignment: .leading,spacing: 20){
            ForEach(checkIn ?? [], id:\.created_at){ checkin in
                Text("check_in: \(checkin.created_at)")
                    .font(.headline)
                    .fontWeight(.bold)
                Text("Late: \(checkin.late)")
            }
            
            Button {
                vm.getHistory(nik: "3201020301050007", nip: "3131313164643", tenant: "someTenant")
            } label: {
                Text("GET")
                    .foregroundColor(.white)
                    .font(.headline)
                    .fontWeight(.medium)
            }
            .padding()
            .padding(.horizontal)
            .background(.blue)
            .cornerRadius(10)

        }
    }
}

This is my ViewModel

class HistoryManager : ObservableObject {
    @Published var fetchedResult : JsonData?
    
    
    func getHistory(nik: String, nip: String, tenant: String) {
        
        guard let url = URL(string: historyURL) else { return }
        
        let body : Dictionary<String, Any> = [
            "nik"    : nik,
            "nip"    : nip,
            "tenant" : tenant
        ]
        let finalBody = try? JSONSerialization.data(withJSONObject: body, options: [])
        var request   = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody   = finalBody
        
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        URLSession.shared.dataTask(with: request) { data, response, error in
          
            if let data = data {
                do {
                    let decoder = JSONDecoder()
                    let json = try? decoder.decode(JsonData.self, from: data)
                    print(json as Any)
                    DispatchQueue.main.async {
                        self.fetchedResult = json
                    }
                    
                }
            }
        }.resume()
    }
}

This is Response that i got

{
  "err_code": 0,
  "data": {
    "message": "Success",
    "result": {
      "check_in": [
        {
          "created_at": "2022-09-05 09:30:13",
          "late": "True",
          "location": "location 1",
          "place": "place 1"
        },
         {
          "created_at": "2022-09-02 08:30:11",
          "late": "Tolerance",
          "location": "location 1",
          "place": "place 1"
        }
      ],
      "check_out": [
        {
          "created_at": "2022-09-02 16:42:53",
          "early": "True",
          "location": "location 1",
          "place": "place 1",
        }
      ]
    }
  }
}

This is what i expected so i expecting the dictionary in [CheckOut] merge with dictionary in [CheckIn] based on the Same Date

{
  "err_code": 0,
  "data": {
    "message": "Success",
    "result": {
      "check_in": [
        {
          "created_at": "2022-09-05 09:30:13",
          "late": "True",
          "location": "location 1",
          "place": "place 1"
        },
         {
          "created_at": "2022-09-02 08:30:11",
          "late": "Tolerance",
          "location": "location 2",
          "place": "place 2"
        },
        {
          "created_at": "2022-09-02 16:42:53",
          "early": "True",
          "location": "location 2",
          "place": "place 2",
        }
      ],
      "check_out": []
    }
  }
}

i really don't have any idea how to deal with it @_@

CodePudding user response:

First I would make the check_in and check_out the same type.

And identifiable:

struct CheckInfo : Codable, Hashable,Identifiable{
    var created_at : String
    var late : String
    var location : String?
    var place : String
    
    var id: String{
        (location ?? "")   (created_at ?? "")
    }
}

After that you merged not a dictionary but an array. You can do it with several methods. One is just setting it:

let decoder = JSONDecoder()
if var jsonobject = try? decoder.decode(JsonData.self, from: json.data(using: .utf8)!){
    
    
    jsonobject.dataJson.result.check_in = jsonobject.dataJson.result.check_in   jsonobject.dataJson.result.check_out.filter{ jsonobject.dataJson.result.check_in.contains($0) == false }
    
    jsonobject.dataJson.result.check_out = []
    
    print(jsonobject.dataJson.result)
    
}

The best approach here would be to implement a custom codeable on your result data.

See Apple docs

I would do something like that:

extension ResultData {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let checkOutData = try values.decode([CheckInfo].self,forKey: .check_out)
        let checkInData = try values.decode([CheckInfo].self,forKey: .check_in)
        
        check_out = []
        
        check_in = checkInData   checkOutData.filter{ checkInData.contains($0) == false }
        
    }
}

CodePudding user response:

...I have to display 2 arrays of dictionaries sequentially based on the SAME DATE...

You could try this example code to achieve what you want to display. It uses two ForEach loops and a filter for the same date.

 ForEach(checkIn ?? [], id:\.created_at) { checkin in
     VStack {
         Text("check_in: \(checkin.created_at)").font(.headline).fontWeight(.bold)
         Text("Late: \(checkin.late)")
         ForEach(checkOut?.filter({ $0.created_at == checkin.created_at }) ?? [], id:\.created_at) { checkout in
             Text("check_out: \(checkout.created_at)").foregroundColor(.blue)
             Text("Early: \(checkout.early)")
         }
     }
 }
 

Note, dates in the code are compared using String. You may want to convert those Strings to Date and use those, to compare/filter dates based, for example, on year, month, day, hours but not minutes and seconds.

CodePudding user response:

First of all, I think you should change your coding keys using description and you can combine both of your check_in and check_out into one is CommonCheckInfo too.

struct JsonData : Codable{
    var errCode : Int
    var dataJson : DataJson

    enum CodingKeys: String, CodingKey {
        case errCode
        case dataJson
        var description: String {
            switch self {
                case .errCode:
                    return "err_code"
            case .dataJson:
                    return "data"
            }
        }
    }
}

struct DataJson: Codable{
    var message : String
    var result  : ResultData
}

struct ResultData : Codable {
    var check_in  : [CommonCheckInfo] = []
    var check_out : [CommonCheckInfo] = []
}

struct CommonCheckInfo : Codable, Hashable{
    var created_at : String
    var early : String?
    var late : String?
    var location : String?
    var place : String
}

And for your question, after decode json we will get a list of check_in and check_out array.

I don't see you mention if the list of check_in array has different days or not. I assume it has different days and if the check_out array is one of days like check_in it will combine into one.

First I need function which convert your Date into date formate which not have hours, minutes, seconds

extension String  {
    func getDate() -> String? {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
        dateFormatter.timeZone = TimeZone.current
        dateFormatter.locale = Locale.current
        let date = dateFormatter.date(from: "2015-04-01T11:42:00")
        // replace Date String
        if date == nil {
            return nil
        }
        dateFormatter.dateFormat = "yyyy-MM-dd"
        return dateFormatter.string(from: date!)
    }
}

Then I have a func to combine which date create in check_out is the same as one of day in check_in

func combineIfSameDay(_ checkIn: [CommonCheckInfo],_ checkOut: [CommonCheckInfo]) -> [CommonCheckInfo] {
    // list check in array
    var result = checkIn
    var listCheckInDate : [String] = []
    for checkInfo in checkIn {
        let checkInDate = checkInfo.created_at.getDate() ?? ""
        if (!checkInDate.isEmpty) {
            listCheckInDate.append(checkInDate)
        }
    }

    // check if date is as same as one of day in listCheckInDate
    for checkInfo in checkOut {
        let checkoutDate = checkInfo.created_at.getDate() ?? ""
        if (!checkoutDate.isEmpty) {
            if listCheckInDate.contains(checkoutDate) {
                result.append(checkInfo)
            }
        }
    }

    return result
}

After all, you will get the result

let result = combineIfSameDay((json?.dataJson.result.check_in)!, (json?.dataJson.result.check_out)!) // the list result after combine here
  • Related