[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 = self
moveToCurrentLocation()
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 = 1000
mapScaleStepper(_ 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
,locality
of 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