[Swift Project] Smoking/Nonsmoking Area
1. Requirements
Project Objectives
Produce an iOS application that display 🚬smoking areas for smokers and 🚭 non-smoking areas for non-smokers in South Korea.
Specification
version 1.0.0
- Mark smoking areas in the map. (YongSan-Gu)
- Mark non-smoking areas in the map.
- The map screen can be zoomed.
- Can move to the current position on the map screen.
Next version
- Precise way to mark ‘near’ positioin in non-smoking area. (current: same administrative & locality)
- Mark other localities’ smoking area API. (current: smoking area 1, non-smoking area: all South Korea except API error area.)
- Show information of the selected area.
- Optional: people density
2. Structure/Function
version 1.0.0
: one view, one controller, one model.
(1) View(main storyboard)
- Apple MapView
- two switch(smoking area, nonsmoking area on/off) with each label → stack view
- stepper(zoom)
- button(current location)
(2) Main Controller
moveToCurrentLocation(): move to current location.getCoordinateToAddress(): transfer coordinate(latitude & longitude) to Korean address.addAnnotations(): add/remove multiple Annotations(markers) in the Apple MapView.mapScaleStepper(): zoom in/out.callJsonApi(): call public API in JSON format.callXmlApi(): get public API in XML format.smokingAreaSwitchAction(): smoking area annotations on/off.nonsmokingAreaSwitchAction(): nonsmoking area annotations on/off.
(3) API Model
- nonsmoking area API model
- smoking area API model
- address model
Next version
- Applying design patterns.
3. Implementation: Focus on the Problem-Solving
: Among the implementations, parts that take a long time or still have a room for improvement are summarized with the source.
version 1.0.0
In version 1.0.0, there are main 4 features.
➀ Move to current location. ➁ Zoom in/out map. ➂ Display the non-smoking area data near the user on the map. ➃ Display the smoking area data of Yonsan-Gu on the map.
The codes below are the main functions needed to implement these four features.
Main problems
- call XML/JSON API
- synchronize with/without loop
Functions
➀ Move to current locations
- basic settings in
viewDidLoad()
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
moveToCurrentLocation()
currentDisplayCenter = mapView.centerCoordinate
mapView.showsUserLocation = true
self.mapView.delegate = selfmoveToCurrentLocation()
private func moveToCurrentLocation() {
if let userLocation = locationManager.location?.coordinate {
let viewRegion = MKCoordinateRegion(center: userLocation, latitudinalMeters: 500, longitudinalMeters: 500)
mapView.setRegion(viewRegion, animated: false)
}
}➁ Zoom map with Stepper
- basic settings in
viewDidLoad()
// zoom in/out stepper setup
mapScaleStepperOulet.wraps = true
mapScaleStepperOulet.autorepeat = true
mapScaleStepperOulet.minimumValue = -1000
mapScaleStepperOulet.maximumValue = 1000mapScaleStepper(_ sender: UIStepper)
@IBAction func mapScaleStepper(_ sender: UIStepper) {
var region = mapView.region
if sender.value > mapScaleValue {
region.span.latitudeDelta /= 2.0
region.span.longitudeDelta /= 2.0
mapView.setRegion(region, animated: true)
} else {
region.span.latitudeDelta *= 2.0
region.span.longitudeDelta*= 2.0
mapView.setRegion(region, animated: true)
}
mapScaleValue = sender.value
}➂ Add/Remove multiple Annotations in the map
: Area is the structure implemented in the Model.
- Add
private func addAnnotations(areas: [Area]){
let annotations = areas.map { area -> MKAnnotation in
let annotation = MKPointAnnotation()
annotation.title = area.placeName
annotation.coordinate = area.coordinate
annotation.subtitle = area.placeSubtitle.rawValue
return annotation
}
mapView.addAnnotations(annotations)
}- Remove
@IBAction func nonsmokingAreaSwitchAction(_ sender: UISwitch) {
if sender.isOn {
callXmlApi(baseUrl: URLs.nonsmokingAreaBaseURL.rawValue, endPage: 4) { nonsmokingAreas in
self.addAnnotations(areas: nonsmokingAreas)
}
} else {
// Remove the anotations that have 'Nonsmoking' subtitle.
for annotation in self.mapView.annotations {
if let subtitle = annotation.subtitle, subtitle == Area.PlaceSubtitle.Nonsmoking.rawValue {
mapView.removeAnnotation(annotation)
}
}
}
}➃ Exchange Latitude & Longitude to Address
: This function is used in the nonsmokingAreaSwitchAction() function with the flow shown below.
Get current location’s latitude & longitude → exchange latitude & longitude to Korean Address → Extract the specific factor (ex.
administrativeArea,localityof Address) → get current location’s non-smoking areas’ API using the factor
private func getCoordinateToAddress(coordinate: CLLocationCoordinate2D) {
let coordinateLocation = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
let geocoder = CLGeocoder()
let locale = Locale(identifier: "Ko-kr")
geocoder.reverseGeocodeLocation(coordinateLocation, preferredLocale: locale, completionHandler: {(placemarks, error) in
if let fullAddress: [CLPlacemark] = placemarks {
if let locality = fullAddress.last?.locality, let administrativeArea = fullAddress.last?.administrativeArea {
self.currentDisplayAddress = Address(administrativeArea: administrativeArea, locality: locality)
}
}
})
}➄ API: XML format
: In this project, the Alamofire, CFRunLoop and completion is used to handle the XML type multiple API in the loop.
Calling API with NSXMLParserDelegate, handling its data and other ways to synchronize it will be delt with in another post.
private func callXmlApi(baseUrl: String, endPage: Int, completion: @escaping ([Area])->()){
var nonsmokingAreas = [Area]()
var isEndPage = false
let runLoop = CFRunLoopGetCurrent()
for pageNo in 0...endPage {
if isEndPage { break }
Alamofire.request(url, method: .get).validate().responseString { response in
CFRunLoopStop(runLoop)
switch response.result {
case .success:
if let responseData = response.data {
let responseDataEncoding = NSString(data: responseData, encoding: String.Encoding.utf8.rawValue)
let xml = try! XML.parse(String(responseDataEncoding!))
if let endPageCode = xml["response"]["header"]["code"].text, let endPageCodeName = xml["response"]["header"]["codeNm"].text {
isEndPage = true
print("EndCode: \(endPageCode), EndCodeName: \(endPageCodeName)", isEndPage)
break
}
for element in xml["response"]["body"]["items"]["item"]{
if let placeName = element["prhsmkNm"].text, let latitude = element["latitude"].text, let longitude = element["longitude"].text {
let latitudeDoube: Double = Double(latitude)!
let longitudeDoube: Double = Double(longitude)!
nonsmokingAreas.append(Area(placeSubtitle: Area.PlaceSubtitle.Nonsmoking , placeName: placeName, coordinate: CLLocationCoordinate2D(latitude: latitudeDoube, longitude: longitudeDoube)))
}
}
}
case .failure(_):
print("fail to get response from \(url)")
}
}
CFRunLoopRun()
}
completion(nonsmokingAreas)
}➅ API: JSON format
private func callJsonApi(urlStr: String, completion: @escaping ([Area])->()) {
var smokingAreas = [Area]()
guard let url = URL(string: urlStr) else { return }
let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: url, completionHandler: {(data, response, error) in
guard error == nil else {
print(" > Error: \(error!)")
return
}
guard let resData = data else { return }
do {
let dataDecode = try JSONDecoder().decode(SmokingArea.self, from: resData)
for row in dataDecode.data {
if let latitudeDouble: Double = Double(row.latitude),let longitudeDouble: Double = Double(row.longitude) {
smokingAreas.append(Area(placeSubtitle: Area.PlaceSubtitle.Smoking, placeName: row.installationLocation, coordinate: CLLocationCoordinate2D(latitude: latitudeDouble, longitude: longitudeDouble)))
}
}
completion(smokingAreas)
} catch let error {
print("JSONDecoder Error: \(error)")
}
})
task.resume()
}Further Improvement
Refactoring
- guard
- log(current: print) → warning/error/info/debug
- Other ways to syncßhronize the API function.
- Unit Test
- (UI Test?)
Calling multiple APIs FAST without CPU, Momory overloading
: In version 1.0.0, the application is quite slow especially when calling API.
4. Result
version 1.0.0
- period: Aug 9, 2021 ~ Sep 5, 2021
Comments