Add owners screen

This commit is contained in:
Selim Mustafaev 2020-06-02 00:24:08 +03:00
parent ee48f01a5f
commit a51ae611e4
7 changed files with 115 additions and 18 deletions

View File

@ -54,6 +54,7 @@
7A6DD90A24329541009DE740 /* RoadNumbers2.0.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */; }; 7A6DD90A24329541009DE740 /* RoadNumbers2.0.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */; };
7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90B24335A6D009DE740 /* FlagLayer.swift */; }; 7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90B24335A6D009DE740 /* FlagLayer.swift */; };
7A6DD90E24337930009DE740 /* PlateNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90D24337930009DE740 /* PlateNumber.swift */; }; 7A6DD90E24337930009DE740 /* PlateNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90D24337930009DE740 /* PlateNumber.swift */; };
7A6E03282485951700DB22ED /* OwnersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6E03272485951700DB22ED /* OwnersController.swift */; };
7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DB2403180A004E8406 /* SectionHeader.swift */; }; 7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DB2403180A004E8406 /* SectionHeader.swift */; };
7A7547DE2403180A004E8406 /* SectionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7A7547DC2403180A004E8406 /* SectionHeader.xib */; }; 7A7547DE2403180A004E8406 /* SectionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7A7547DC2403180A004E8406 /* SectionHeader.xib */; };
7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */; }; 7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */; };
@ -111,6 +112,7 @@
7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers2.0.otf; sourceTree = "<group>"; }; 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers2.0.otf; sourceTree = "<group>"; };
7A6DD90B24335A6D009DE740 /* FlagLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagLayer.swift; sourceTree = "<group>"; }; 7A6DD90B24335A6D009DE740 /* FlagLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagLayer.swift; sourceTree = "<group>"; };
7A6DD90D24337930009DE740 /* PlateNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlateNumber.swift; sourceTree = "<group>"; }; 7A6DD90D24337930009DE740 /* PlateNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlateNumber.swift; sourceTree = "<group>"; };
7A6E03272485951700DB22ED /* OwnersController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersController.swift; sourceTree = "<group>"; };
7A7547DB2403180A004E8406 /* SectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeader.swift; sourceTree = "<group>"; }; 7A7547DB2403180A004E8406 /* SectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeader.swift; sourceTree = "<group>"; };
7A7547DC2403180A004E8406 /* SectionHeader.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SectionHeader.xib; sourceTree = "<group>"; }; 7A7547DC2403180A004E8406 /* SectionHeader.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SectionHeader.xib; sourceTree = "<group>"; };
7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclePhotoCell.swift; sourceTree = "<group>"; }; 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclePhotoCell.swift; sourceTree = "<group>"; };
@ -202,6 +204,7 @@
7AEFE727240455E200910EB7 /* SettingsController.swift */, 7AEFE727240455E200910EB7 /* SettingsController.swift */,
7A3F07AC2436350B00E59687 /* SearchController.swift */, 7A3F07AC2436350B00E59687 /* SearchController.swift */,
7A96AE2C246B2B7400297C33 /* GoogleSignInController.swift */, 7A96AE2C246B2B7400297C33 /* GoogleSignInController.swift */,
7A6E03272485951700DB22ED /* OwnersController.swift */,
); );
path = Controllers; path = Controllers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -438,6 +441,7 @@
7A64AE7F2469E16100ABE48E /* IndefiniteAnimatedView.swift in Sources */, 7A64AE7F2469E16100ABE48E /* IndefiniteAnimatedView.swift in Sources */,
7A11471A23FE839000B424AF /* AuthController.swift in Sources */, 7A11471A23FE839000B424AF /* AuthController.swift in Sources */,
7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */, 7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */,
7A6E03282485951700DB22ED /* OwnersController.swift in Sources */,
7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */, 7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */,
7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */, 7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */,
7AF58D58240309CA00CE01A0 /* VehicleTextParamCell.swift in Sources */, 7AF58D58240309CA00CE01A0 /* VehicleTextParamCell.swift in Sources */,
@ -603,7 +607,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8; CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_TEAM = 46DTTB8X4S; DEVELOPMENT_TEAM = 46DTTB8X4S;
INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_FILE = AutoCat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
@ -625,7 +629,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8; CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_TEAM = 46DTTB8X4S; DEVELOPMENT_TEAM = 46DTTB8X4S;
INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_FILE = AutoCat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;

View File

@ -19,7 +19,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let config = Realm.Configuration( let config = Realm.Configuration(
schemaVersion: 5, schemaVersion: 6,
migrationBlock: { migration, oldSchemaVersion in migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion <= 3 { if oldSchemaVersion <= 3 {
var numbers: [String] = [] var numbers: [String] = []

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pme-aR-UNJ"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pme-aR-UNJ">
<device id="retina4_7" orientation="portrait" appearance="dark"/> <device id="retina4_7" orientation="portrait" appearance="dark"/>
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
@ -180,6 +180,21 @@
</objects> </objects>
<point key="canvasLocation" x="844" y="948.57571214392806"/> <point key="canvasLocation" x="844" y="948.57571214392806"/>
</scene> </scene>
<!--Owners Controller-->
<scene sceneID="0bv-cp-2uj">
<objects>
<viewController storyboardIdentifier="OwnersController" id="73d-bt-c62" customClass="OwnersController" customModule="AutoCat" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Eae-Lq-vht">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="bmk-bz-ZBr"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="URC-NW-y2j" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1729" y="949"/>
</scene>
<!--Search Controller--> <!--Search Controller-->
<scene sceneID="3Md-yW-a0R"> <scene sceneID="3Md-yW-a0R">
<objects> <objects>
@ -193,14 +208,14 @@
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes> <prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="VehicleCell" id="VEP-QD-i6y" customClass="VehicleCell" customModule="AutoCat" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="VehicleCell" id="VEP-QD-i6y" customClass="VehicleCell" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="85.5"/> <rect key="frame" x="0.0" y="28" width="375" height="85"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VEP-QD-i6y" id="8hH-8I-XLB"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VEP-QD-i6y" id="8hH-8I-XLB">
<rect key="frame" x="0.0" y="0.0" width="375" height="85.5"/> <rect key="frame" x="0.0" y="0.0" width="375" height="85"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Kia (JF) Optima" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AQY-7N-q8D"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Kia (JF) Optima" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AQY-7N-q8D">
<rect key="frame" x="8" y="8" width="124" height="21.5"/> <rect key="frame" x="8" y="8" width="124" height="21"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/> <fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/> <nil key="textColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@ -212,7 +227,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cvf-vM-QnT" customClass="PlateView" customModule="AutoCat" customModuleProvider="target"> <view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cvf-vM-QnT" customClass="PlateView" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="8" y="37.5" width="317" height="40"/> <rect key="frame" x="8" y="37" width="317" height="40"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="40" id="Xoz-Iw-PCU"/> <constraint firstAttribute="height" constant="40" id="Xoz-Iw-PCU"/>

View File

@ -0,0 +1,40 @@
import UIKit
import Eureka
class OwnersController: FormViewController {
public var owners: [VehicleOwnershipPeriod] = []
private var formatter = DateFormatter()
override func viewDidLoad() {
super.viewDidLoad()
self.formatter.dateStyle = .long
self.formatter.timeStyle = .none
self.title = "\(self.owners.count) owner(s)"
for (index, owner) in self.owners.enumerated() {
form +++ Section(header: "", footer: owner.lastOperation)
<<< LabelRow("Owner\(index)") { row in
row.title = "Owner type"
row.value = owner.ownerType
}
<<< LabelRow("From\(index)") { row in
row.title = "From"
let date = Date(timeIntervalSince1970: TimeInterval(owner.from/1000))
row.value = self.formatter.string(from: date)
}
<<< LabelRow("To\(index)") { row in
row.title = "To"
if owner.to == 0 {
row.value = "now"
} else {
let date = Date(timeIntervalSince1970: TimeInterval(owner.to/1000))
row.value = self.formatter.string(from: date)
}
}
}
}
}

View File

@ -22,16 +22,20 @@ enum ReportSection: Int, CaseIterable, CustomStringConvertible {
enum ReportGeneralSection: Int, CaseIterable, CustomStringConvertible { enum ReportGeneralSection: Int, CaseIterable, CustomStringConvertible {
case year = 0 case year = 0
case category = 1 case color = 1
case wheelPosition = 2 case category = 2
case japanese = 3 case wheelPosition = 3
case japanese = 4
case owners = 5
var description: String { var description: String {
switch self { switch self {
case .year: return "Year" case .year: return "Year"
case .color: return "Color"
case .category: return "Category" case .category: return "Category"
case .wheelPosition: return "Steering wheel position" case .wheelPosition: return "Steering wheel position"
case .japanese: return "Japanese" case .japanese: return "Japanese"
case .owners: return "Owners (from PTS)"
} }
} }
} }
@ -145,6 +149,9 @@ class ReportController: UIViewController, UICollectionViewDataSource, UICollecti
case .year: case .year:
cell?.configure(param: generalSection.description, value: String(vehicle.year)) cell?.configure(param: generalSection.description, value: String(vehicle.year))
break break
case .color:
cell?.configure(param: generalSection.description, value: vehicle.color ?? "<unknown>")
break
case .category: case .category:
cell?.configure(param: generalSection.description, value: vehicle.category ?? "<unknown>") cell?.configure(param: generalSection.description, value: vehicle.category ?? "<unknown>")
break break
@ -153,6 +160,10 @@ class ReportController: UIViewController, UICollectionViewDataSource, UICollecti
break break
case .japanese: case .japanese:
cell?.configure(param: generalSection.description, value: vehicle.isJapanese ? "Yes" : "No") cell?.configure(param: generalSection.description, value: vehicle.isJapanese ? "Yes" : "No")
break
case .owners:
cell?.configure(param: generalSection.description, value: String(vehicle.ownershipPeriods.count))
break
} }
} }
return cell ?? UICollectionViewCell() return cell ?? UICollectionViewCell()
@ -217,14 +228,23 @@ class ReportController: UIViewController, UICollectionViewDataSource, UICollecti
} }
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard indexPath.section == ReportSection.photos.rawValue else { return } if indexPath.section == ReportSection.photos.rawValue {
let mediaBrowser = MediaBrowserViewController(index: indexPath.item, dataSource: self, delegate: self) let mediaBrowser = MediaBrowserViewController(index: indexPath.item, dataSource: self, delegate: self)
mediaBrowser.shouldShowTitle = true mediaBrowser.shouldShowTitle = true
mediaBrowser.title = self.vehicle?.photos[indexPath.item].description mediaBrowser.title = self.vehicle?.photos[indexPath.item].description
present(mediaBrowser, animated: true, completion: nil) present(mediaBrowser, animated: true, completion: nil)
} }
if indexPath.section == ReportSection.general.rawValue {
if indexPath.row == ReportGeneralSection.owners.rawValue {
let sb = UIStoryboard(name: "Main", bundle: nil)
let controller = sb.instantiateViewController(identifier: "OwnersController") as OwnersController
controller.owners = self.vehicle?.ownershipPeriods.toArray() ?? []
self.navigationController?.pushViewController(controller, animated: true)
}
}
}
// MARK: - UICollectionViewDelegateMagazineLayout // MARK: - UICollectionViewDelegateMagazineLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeModeForItemAt indexPath: IndexPath) -> MagazineLayoutItemSizeMode func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeModeForItemAt indexPath: IndexPath) -> MagazineLayoutItemSizeMode

View File

@ -40,6 +40,18 @@ class VehiclePhoto: Object, Decodable {
} }
} }
enum OwnerType: String {
case legal
case individual
}
class VehicleOwnershipPeriod: Object, Decodable {
@objc dynamic var lastOperation: String
@objc dynamic var ownerType: String
@objc dynamic var from: Int64
@objc dynamic var to: Int64
}
class Vehicle: Object, Decodable, IdentifiableType { class Vehicle: Object, Decodable, IdentifiableType {
@objc dynamic var brand: VehicleBrand? @objc dynamic var brand: VehicleBrand?
@objc dynamic var model: VehicleModel? @objc dynamic var model: VehicleModel?
@ -57,6 +69,7 @@ class Vehicle: Object, Decodable, IdentifiableType {
@objc dynamic var addedDate: TimeInterval = 0 @objc dynamic var addedDate: TimeInterval = 0
@objc dynamic var addedBy: String = "" @objc dynamic var addedBy: String = ""
let photos = List<VehiclePhoto>() let photos = List<VehiclePhoto>()
let ownershipPeriods = List<VehicleOwnershipPeriod>()
var identity: String { number } var identity: String { number }
@ -77,6 +90,7 @@ class Vehicle: Object, Decodable, IdentifiableType {
case addedDate case addedDate
case addedBy case addedBy
case photos case photos
case ownershipPeriods
} }
required init(from decoder: Decoder) throws { required init(from decoder: Decoder) throws {
@ -100,6 +114,10 @@ class Vehicle: Object, Decodable, IdentifiableType {
if let photosArray = try container.decodeIfPresent([VehiclePhoto].self, forKey: .photos) { if let photosArray = try container.decodeIfPresent([VehiclePhoto].self, forKey: .photos) {
self.photos.append(objectsIn: photosArray) self.photos.append(objectsIn: photosArray)
} }
if let ownersipsArray = try container.decodeIfPresent([VehicleOwnershipPeriod].self, forKey: .ownershipPeriods) {
self.ownershipPeriods.append(objectsIn: ownersipsArray)
}
} }
required init() { required init() {

View File

@ -7,7 +7,7 @@ class Api {
} }
private static func createRequest<T>(api: String, method: String, body: [String: T]? = nil) -> URLRequest? where T: LosslessStringConvertible { private static func createRequest<T>(api: String, method: String, body: [String: T]? = nil) -> URLRequest? where T: LosslessStringConvertible {
guard var urlComponents = URLComponents(string: Constants.debugBaseUrl + api) else { return nil } guard var urlComponents = URLComponents(string: Constants.baseUrl + api) else { return nil }
if let body = body, method.uppercased() == "GET" { if let body = body, method.uppercased() == "GET" {
urlComponents.queryItems = body.map { URLQueryItem(name: $0, value: String($1)) } urlComponents.queryItems = body.map { URLQueryItem(name: $0, value: String($1)) }