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