In this section you need to post information about:
- Author
- Application Version
Input data
{
"citiesFrom" : [ ], //массив пунктов отправления
"citiesTo" : [ ] //массив пунктов назначения
}
City object
{
"countryTitle" : "Россия", //название страны
"point" : { //координаты города
"longitude" : 50.64357376098633,
"latitude" : 55.37233352661133
},
"districtTitle" : "Чистопольский район", //название района
"cityId" : 4454, //идентификатор города
"cityTitle" : "Чистополь", //название города
"regionTitle" : "Республика Татарстан", //название региона
"stations" : [...] //массив станций
}
Station object
{
"countryTitle" : "Россия", //название страны (денормализация данных, дубль из города)
"point" : { //координаты станции (в общем случае отличаются от координат города)
"longitude" : 50.64357376098633,
"latitude" : 55.37233352661133
},
"districtTitle" : "Чистопольский район", //название района
"cityId" : 4454, //идентификатор города
"cityTitle" : "город Чистополь", //название города
"regionTitle" : "Республика Татарстан", //название региона
"stationId" : 9362, //идентификатор станции
"stationTitle" : "Чистополь" //полное название станции
}
Parsing JSON
First of all we need to extract data from JSON file and convert this JSON to objects. Parsing JSON can be long term task, so we must do such tasks in background using.
DispatchQueue.global(qos: .userInitiated).async
Also for more clarity that this task can be long we add pause for 3 seconds with
sleep(3)
Let's create service for parsing JSON. This service will be Singleton.
import Foundation
class JSONParsingService {
// 1
typealias ScheduleCompletion = (Schedule) -> Void
typealias JSONDictionary = [String: Any]
// 2
static let instance = JSONParsingService()
// 3
func getSchedule(completion: @escaping ScheduleCompletion) {
// 4
DispatchQueue.global(qos: .userInitiated).async { [weak self] in // 5
// 6
guard let strongSelf = self else { return }
sleep(3) // 7
// 8
if let jsonObject = strongSelf.parseJSONData(from: "allStations") {
// 9
let schedule = Schedule(dictionary: jsonObject)
completion(schedule)
}
}
}
// 10
private func parseJSONData(from file: String) -> JSONDictionary? {
// 11
guard let jsonUrl = Bundle.main.url(
forResource: file, withExtension: "json") else { return nil }
// 12
do {
let jsonData = try Data(contentsOf: jsonUrl)
if let jsonObject = try JSONSerialization.jsonObject(with: jsonData,
options: []) as? JSONDictionary {
return jsonObject
}
} catch let error {
print("Error happened when parsing json: \(error.localizedDescription)")
}
// 13
return nil
}
}
There are following main things in JSONParsingService:
- Using typealias for more clear and short names
- Service supposed to be singleton, so instance creation as static property
- Public API for Service, getting Schedule object in completion handler
- Using background task for long term tasks, such as extracting JSON data from file and convert it to objects
- Avoiding retain cycle with weak self
- Checking self for nil and return in case of nil
- Simulate long term task - making 3 seconds pause
- Get JSON data from file named "allStations"
- Creating Schedule object from JSON data
- Get JSON dictionary from file and return Optional Value because file could not exist or has wrong data
- Checking that path to file is correct
- Trying to create Data from content of file
- Return nil by default
Creating the Model
Schedule
struct Schedule {
typealias JSONDictionary = Dictionary<String, Any>
typealias JSONArray = Array<JSONDictionary>
// 1
private(set) var citiesFrom = [City]()
private(set) var citiesTo = [City]()
// 2
init(dictionary: JSONDictionary = JSONDictionary()) {
if let citiesFromDict = dictionary["citiesFrom"] as? JSONArray {
for cityFromDict in citiesFromDict {
let city = City(dictionary: cityFromDict)
citiesFrom += [city]
}
}
if let citiesToDict = dictionary["citiesTo"] as? JSONArray {
for cityToDict in citiesToDict {
let city = City(dictionary: cityToDict)
citiesTo += [city]
}
}
}
}
- Init properties with default values - empty arrays
- Init object from JSON dictionary
City
import Foundation
extension String {
static var unknownValue: String {
// return unknown
return "неизвестно"
}
}
extension Int {
static var unknownValue: Int {
return -1
}
}
struct City {
typealias JSONDictionary = Dictionary<String, Any>
typealias JSONArray = Array<JSONDictionary>
private(set) var countryTitle: String
private(set) var districtTitle: String
private(set) var cityId: Int
private(set) var cityTitle: String
private(set) var regionTitle: String
private(set) var point: PointCoordinates
private(set) var stations = [Station]()
init(dictionary: JSONDictionary) {
// 1
if let countryTitle = dictionary["countryTitle"] as? String {
self.countryTitle = countryTitle
// 2
} else {
countryTitle = String.unknownValue
}
if let districtTitle = dictionary["districtTitle"] as? String {
self.districtTitle = districtTitle
} else {
self.districtTitle = String.unknownValue
}
if let cityId = dictionary["cityId"] as? Int {
self.cityId = cityId
} else {
self.cityId = Int.unknownValue
}
if let cityTitle = dictionary["cityTitle"] as? String {
self.cityTitle = cityTitle
} else {
self.cityTitle = String.unknownValue
}
if let regionTitle = dictionary["regionTitle"] as? String {
self.regionTitle = regionTitle
} else {
self.regionTitle = String.unknownValue
}
if let pointDict = dictionary["point"] as? JSONDictionary {
self.point = PointCoordinates(dictionary: pointDict)
} else {
self.point = PointCoordinates()
}
if let stationsArray = dictionary["stations"] as? JSONArray {
for stationItemDict in stationsArray {
let station = Station(dictionary: stationItemDict)
stations += [station]
}
}
}
}
// 3
extension City: CustomStringConvertible {
var description: String {
return "City(id: \(cityId), title: \(cityTitle), region: \(regionTitle), "
+ "country: \(countryTitle), stations count: \(stations.count), "
+ "point: (\(point.longitude, point.latitude)))"
}
}
- Trying to get value from JSON dictionary
- If dictionary has no value then set default value to property
- Make City type conform to CustomStringConvertible protocol for handy printing City object to console
Station
struct Station {
typealias JSONDictionary = Dictionary<String, Any>
private(set) var countryTitle: String
private(set) var districtTitle: String
private(set) var cityId: Int
private(set) var cityTitle: String
private(set) var regionTitle: String
private(set) var stationId: String
private(set) var stationTitle: String
private(set) var point: PointCoordinates
init(dictionary: JSONDictionary) {
if let countryTitle = dictionary["countryTitle"] as? String {
self.countryTitle = countryTitle
} else {
self.countryTitle = String.unknownValue
}
if let districtTitle = dictionary["districtTitle"] as? String {
self.districtTitle = districtTitle
} else {
self.districtTitle = String.unknownValue
}
if let cityId = dictionary["cityId"] as? Int {
self.cityId = cityId
} else {
self.cityId = Int.unknownValue
}
if let cityTitle = dictionary["cityTitle"] as? String {
self.cityTitle = cityTitle
} else {
self.cityTitle = String.unknownValue
}
if let regionTitle = dictionary["regionTitle"] as? String {
self.regionTitle = regionTitle
} else {
self.regionTitle = String.unknownValue
}
if let stationId = dictionary["stationId"] as? String {
self.stationId = stationId
} else {
self.stationId = String.unknownValue
}
if let stationTitle = dictionary["stationTitle"] as? String {
self.stationTitle = stationTitle
} else {
self.stationTitle = String.unknownValue
}
if let pointDict = dictionary["point"] as? JSONDictionary {
self.point = PointCoordinates(dictionary: pointDict)
} else {
self.point = PointCoordinates()
}
}
}
extension Station: CustomStringConvertible {
var description: String {
return "Station: \(stationTitle), with id: \(stationId), country: \(countryTitle), city: \(cityTitle), "
+ "region: \(regionTitle), district: \(districtTitle), "
+ "point: \(point.longitude, point.latitude)"
}
}
PointCoordinates
struct PointCoordinates {
typealias JSONDictionary = Dictionary<String, Any>
private(set) var longitude: Double
private(set) var latitude: Double
// 1
init(longitude: Double = 0.0, latitude: Double = 0.0) {
self.longitude = longitude
self.latitude = latitude
}
init(dictionary: JSONDictionary) {
var longitude = 0.0
var latitude = 0.0
if let longitudeVal = dictionary["longitude"] as? Double {
longitude = longitudeVal
}
if let latitudeVal = dictionary["latitude"] as? Double {
latitude = latitudeVal
}
self.init(longitude: longitude, latitude: latitude)
}
}
- Init with default values
Creating User Interface
Let's describe UI that we want to implement.
Navigation Controller is the root view controller. The main View Controller is CityListViewController. It has Segmented Control with two options: "From" and "To" for selecting type of cities to display and Table View for displaying list of cities. After data will be loaded, in Segmented Control will be displayed count of cities of each type. Table View is used for displaying list of cities. In viewDidLoad method we start getting Schedule object via JSONParsingService. Initially Table View is hidden. When loading is started Activity Indicator become visible and animated. When data is fully loaded Table View will become visible and Activity Indicator become hidden. When we switch between "From" an "To" items in Segmented Control, according list of cities will be displayed. Also, because we use one Table View for two different lists of cities we need to save scroll position for each list.
View Controllers in Stroyboard
Source Code of CityListViewController
import UIKit
class CityListViewController: UIViewController {
// MARK: - Inner Types
// 1
enum CityType {
case from
case to
}
// MARK: - Properties
// 2
private var isLoading = false
private let cellId = "StationCell"
// 3
private var cityList = [City]()
// 4
private var currentCitiesFromIndexPath = IndexPath(item: 0, section: 0)
private var currentCitiesToIndexPath = IndexPath(item: 0, section: 0)
// 5
private var schedule = Schedule() {
didSet {
reloadSegmentedControl(schedule: schedule)
reloadTableViewData(schedule: schedule)
}
}
// 6
private var selectedCityType: CityType = .from {
didSet {
reloadTableViewData(schedule: schedule)
}
}
// MARK: - IBOutlets
@IBOutlet weak var tableView: UITableView! {
didSet {
tableView.dataSource = self
tableView.delegate = self
tableView.alpha = 0.0
}
}
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var segmentedControl: UISegmentedControl!
// MARK: - IBActions
// 7
@IBAction func segmentedControlChanged(_ sender: UISegmentedControl) {
if sender.selectedSegmentIndex == 0 {
selectedCityType = .from
} else if sender.selectedSegmentIndex == 1 {
selectedCityType = .to
}
}
// MARK: - View Life Cycle
// 8
override func viewDidLoad() {
super.viewDidLoad()
getSchedule()
}
// MARK: - Main Logic
// 9
private func reloadSegmentedControl(schedule: Schedule) {
guard !isLoading else { return }
DispatchQueue.main.async {
let citiesFromCount = schedule.citiesFrom.count
if let fromTitle = self.segmentedControl.titleForSegment(at: 0),
citiesFromCount > 0 {
self.segmentedControl.setTitle("\(fromTitle) \(citiesFromCount)", forSegmentAt: 0)
}
let citiesToCount = schedule.citiesTo.count
if let toTitle = self.segmentedControl.titleForSegment(at: 1),
citiesToCount > 0 {
self.segmentedControl.setTitle("\(toTitle) \(citiesToCount)", forSegmentAt: 1)
}
}
}
// 10
private func reloadTableViewData(schedule: Schedule) {
guard !isLoading else { return }
// 11
DispatchQueue.main.async {
let isFrom = self.selectedCityType == .from
self.cityList = isFrom ? schedule.citiesFrom : schedule.citiesTo
self.tableView.reloadData()
// 12
self.tableView.scrollToRow(at: isFrom ?
self.currentCitiesFromIndexPath :
self.currentCitiesToIndexPath, at: .top, animated: true)
}
}
// 13
private func getSchedule() {
isLoading = true
activityIndicator.startAnimating()
JSONParsingService.instance.getSchedule { [unowned self] schedule in
self.isLoading = false
self.schedule = schedule
// 14
DispatchQueue.main.async {
UIView.animate(withDuration: 0.6, animations: {
self.tableView.alpha = 1.0
})
self.activityIndicator.stopAnimating()
}
}
}
}
// MARK: - UITableViewDataSource
extension CityListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cityList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
let city = cityList[indexPath.row]
cell.textLabel?.text = "\(indexPath.row + 1) \(city.cityTitle)"
cell.detailTextLabel?.text = city.countryTitle
return cell
}
}
// MARK: - UITableViewDelegate
extension CityListViewController: UITableViewDelegate {
// 15
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let indexPath = tableView.indexPathsForVisibleRows?.first {
if selectedCityType == .from {
currentCitiesFromIndexPath = indexPath
} else {
currentCitiesToIndexPath = indexPath
}
}
}
}
Let's go through the code and explore what happens in it:
- Enum for switching between "cities from" and "cities to"
- Flag to identify that loading (JSON parsing) in progress
- Array of cities to display in table
- Index Path to remember for auto scrolling to position
- Schedule property init with default init
- When switching between tabs in segmented control
- When switching between tabs in segmented control update property
- Start loading schedule with Service when view did load
- Update titles in segmented control when data is loaded. Show count of cities of each type: from and to
- Reload table view with new data
- Update UI on main queue
- Scrolling to last remembered index path
- Start loading Schedule via JSONParsingService
- Update UI on main queue when schedule is loaded
- Remember last showing position in table view for selected city type
Results
This is how it looks on device
Source Code
Source code for this part of tutorial can be found here on
GitHub
Your article is one of its kind which explained every bit of Custom Build Website. looking for further valuable articles from you
ReplyDeleteReally amazing information you have shared i was looking for this kind of unique information please do share that kind of unique information more as you can.
ReplyDelete-Web Development Services
Really great information you've offered; I've been seeking for stuff like this, so please continue to share it as much as you can. Best Custom Websites
ReplyDeleteThank you very much for sharing this informational blog.I could not find such kind of information in any other site.I'll come back for more fantastic material.
ReplyDeleteBest wishes, and good luck.
Custom Website