Repository: samkhawse/OpenStreetAmenities Branch: master Commit: 9a83b3115c78 Files: 40 Total size: 100.4 KB Directory structure: gitextract_axgr4pdw/ ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── LooLocator/ │ ├── AppDelegate.swift │ ├── Assets.xcassets/ │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Image.imageset/ │ │ │ └── Contents.json │ │ └── location.imageset/ │ │ └── Contents.json │ ├── Info.plist │ ├── LaunchScreen.storyboard │ ├── Main.storyboard │ ├── Models/ │ │ └── Location.swift │ ├── Protocols/ │ │ ├── ApiResourceProviding.swift │ │ ├── Extensions/ │ │ │ ├── CLLocationManagerExtension.swift │ │ │ └── NetworkReqeustProvidingExtension.swift │ │ ├── Implementations/ │ │ │ ├── AmenityRequest.swift │ │ │ ├── AmentityResource.swift │ │ │ └── LocationProvider.swift │ │ ├── LocationManagerConfigurable.swift │ │ ├── LocationProvidable.swift │ │ └── NetworkRequestProviding.swift │ ├── ViewControllers/ │ │ └── ViewController.swift │ └── ViewModels/ │ └── AmenityViewModel.swift ├── LooLocator.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm/ │ │ └── Package.resolved │ └── xcshareddata/ │ └── xcschemes/ │ └── LooLocator.xcscheme ├── LooLocatorTests/ │ ├── AmenityMocks.swift │ ├── AmenityRequestTests.swift │ ├── ApiClientTests.swift │ ├── Info.plist │ ├── LocationMocks.swift │ ├── LocationProviderTests.swift │ ├── MapViewModelTests.swift │ ├── MockViewController.swift │ ├── OSMModelTests.swift │ └── stubbedRepsonse.json ├── README.md └── berlin.gpx ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/swift ### Swift ### # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## Build generated build/ DerivedData/ ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata/ ## Other *.moved-aside *.xccheckout *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa *.dSYM.zip *.dSYM ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins .build/ # CocoaPods - Refactored to standalone file # Carthage - Refactored to standalone file # fastlane # # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output # End of https://www.gitignore.io/api/swift android/ Carthage/ ================================================ FILE: .travis.yml ================================================ language: swift os: osx ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2018 Sam Khawase Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: LooLocator/AppDelegate.swift ================================================ // // AppDelegate.swift // LooLocator // // Created by Sam Khawase on 14.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } func applicationWillResignActive(_ application: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. } func applicationDidEnterBackground(_ application: UIApplication) { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. } func applicationWillEnterForeground(_ application: UIApplication) { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. } func applicationDidBecomeActive(_ application: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } func applicationWillTerminate(_ application: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } } ================================================ FILE: LooLocator/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "Icon-App-20x20@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { "filename" : "Icon-App-20x20@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { "filename" : "Icon-App-29x29@1x.png", "idiom" : "iphone", "scale" : "1x", "size" : "29x29" }, { "filename" : "Icon-App-29x29@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { "filename" : "Icon-App-29x29@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { "filename" : "Icon-App-40x40@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { "filename" : "Icon-App-40x40@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { "filename" : "Icon-App-57x57@1x.png", "idiom" : "iphone", "scale" : "1x", "size" : "57x57" }, { "filename" : "Icon-App-57x57@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "57x57" }, { "filename" : "Icon-App-60x60@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { "filename" : "Icon-App-60x60@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { "filename" : "Icon-App-20x20@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { "filename" : "Icon-App-20x20@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { "filename" : "Icon-App-29x29@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { "filename" : "Icon-App-29x29@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { "filename" : "Icon-App-40x40@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { "filename" : "Icon-App-40x40@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { "filename" : "Icon-Small-50x50@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "50x50" }, { "filename" : "Icon-Small-50x50@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "50x50" }, { "filename" : "Icon-App-72x72@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "72x72" }, { "filename" : "Icon-App-72x72@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "72x72" }, { "filename" : "Icon-App-76x76@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { "filename" : "Icon-App-76x76@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { "filename" : "Icon-App-83.5x83.5@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { "filename" : "ItunesArtwork@2x.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: LooLocator/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: LooLocator/Assets.xcassets/Image.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "Amenity_toilets.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: LooLocator/Assets.xcassets/location.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icons8-near-me-50.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "icons8-near-me-100.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "icons8-near-me-500.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: LooLocator/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight NSLocationAlwaysAndWhenInUseUsageDescription We need your current location to find the Amenities near you NSLocationWhenInUseUsageDescription We need your current location to find the Amenities near you ================================================ FILE: LooLocator/LaunchScreen.storyboard ================================================ ================================================ FILE: LooLocator/Main.storyboard ================================================ ================================================ FILE: LooLocator/Models/Location.swift ================================================ // // Location.swift // LooLocator // // Created by Sam Khawase on 15.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import Contacts import MapKit class OSMData: Codable { var elements: [Location]? // MARK: - Codable methods enum CodingKeys: String, CodingKey { case elements case id, lat, lon, tags } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.elements = try container.decode([Location].self, forKey: .elements) } func encode(to encoder: Encoder) throws { } } class Location: NSObject, Codable { // MARK: - variable declarations let id: Int let title: String? let locationDescription: String? let coordinates: (Double, Double) var amenity: String? var fee: Bool? var feeAmount: String? let isAccessible: Bool // MARK: - Convenience initializers init(id: Int, title:String, locationDescription: String, coordintes: (Double, Double), isAccessible: Bool ) { self.id = id self.title = title self.locationDescription = locationDescription self.coordinates = coordintes self.isAccessible = isAccessible } // MARK: - Codable methods enum CodingKeys: String, CodingKey { case id, lat, lon, tags // tags container case name, wheelchair, fee, amenity case englishName = "name:en" } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) if let lat = try? container.decode(Double.self, forKey: .lat), let lon = try? container.decode(Double.self, forKey: .lon) { self.coordinates = (lat, lon) } else { self.coordinates = (0.0, 0.0) } let tagsContainer = try? container.nestedContainer(keyedBy: CodingKeys.self, forKey: .tags) self.locationDescription = try? tagsContainer?.decode(String.self, forKey: .name) if let _name = try? tagsContainer?.decode(String.self, forKey: .englishName) { self.title = _name } else { self.title = self.locationDescription } self.isAccessible = ((try? tagsContainer?.decode(String.self, forKey: .wheelchair)) != nil) } func encode(to encoder: Encoder) throws { } } // MARK: - MKAnnotation support extension Location: MKAnnotation { var coordinate: CLLocationCoordinate2D { return CLLocationCoordinate2D(latitude: CLLocationDegrees(coordinates.0), longitude: CLLocationDegrees(coordinates.1)) } var subtitle: String? { return "\(isAccessible ? "♿︎" : "")" } // Annotation right callout accessory opens this mapItem in Maps app func mapItem() -> MKMapItem { let addressDict = [CNPostalAddressStreetKey: title!] let mapCoordinates = CLLocationCoordinate2D(latitude: CLLocationDegrees(coordinates.0), longitude: CLLocationDegrees(coordinates.1)) let placemark = MKPlacemark(coordinate: mapCoordinates, addressDictionary: addressDict) let mapItem = MKMapItem(placemark: placemark) mapItem.name = title return mapItem } } ================================================ FILE: LooLocator/Protocols/ApiResourceProviding.swift ================================================ // // ApiResourceProviding.swift // LooLocator // // Created by Sam Khawase on 15.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import Foundation protocol ApiResourceProviding { var data: Data { get } var headers:Dictionary { get } } ================================================ FILE: LooLocator/Protocols/Extensions/CLLocationManagerExtension.swift ================================================ // // CLLocationManagerExtension.swift // LooLocator // // Created by Sam Khawase on 15.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import CoreLocation // CoreLocation extenstion for protocol conformance extension CLLocationManager: LocationManagerConfigurable { func setDelegate(to instance: CLLocationManagerDelegate?) { guard let delegate = instance else { return } self.delegate = delegate } // Changed this because CLLocationAccuracy is just a typealias for Double func setDesiredAccuracy(to accuracy: CLLocationAccuracy) { self.desiredAccuracy = accuracy } } ================================================ FILE: LooLocator/Protocols/Extensions/NetworkReqeustProvidingExtension.swift ================================================ // // NetworkReqeustProvidingExtension.swift // LooLocator // // Created by Sam Khawase on 15.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import Foundation extension NetworkRequestProviding { internal var baseUrl: String { let _baseUrl = "https://overpass-api.de/api/interpreter" return _baseUrl } func dataTask(request: NSMutableURLRequest, completion: @escaping (Result) -> Void) { let session = URLSession(configuration: URLSessionConfiguration.default) session.dataTask(with: request as URLRequest) { (data, response, error) -> Void in let decoder = JSONDecoder() if let data = data, let serverResponse = try? decoder.decode(SerializedType.self, from: data), let response = response as? HTTPURLResponse, 200...299 ~= response.statusCode { completion(.success(serverResponse)) } else { completion(.failure(NSError(domain: "in.b3rl", code: 999, userInfo: nil))) } }.resume() } } ================================================ FILE: LooLocator/Protocols/Implementations/AmenityRequest.swift ================================================ // // AmenityRequest.swift // LooLocator // // Created by Sam Khawase on 15.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import Foundation class AmenityRequest: NetworkRequestProviding { func getAmeneties(of type: AmenityType, latitude: Double, longitude: Double, radius: Double, completionBlock: @escaping (Result) -> Void) { let amenityResource = AmenityResource(latitude: String(latitude), longitude: String(longitude), amenity: type.rawValue, radius: String(radius)) guard let amenityUrlRequest = createURLRequest(from: amenityResource) else { return } post(request: amenityUrlRequest, completion: completionBlock) // post(request: amenityUrlRequest) { (success, result) in // if success { // guard let result = result as? OSMData, // let elements = result.elements else { // completionBlock(false, nil) // return // } // var jsonElements: [Location] = [] // for element in elements { // jsonElements.append(Location(jsonElement: element)) // } // completionBlock(success, elements as AnyObject) // } // } } func get(request: NSMutableURLRequest, completion: @escaping (Result) -> Void) { } func post(request: NSMutableURLRequest, completion: @escaping (Result) -> Void) { dataTask(request: request, completion: completion) } func put(request: NSMutableURLRequest, completion: @escaping (Result) -> Void) { } func delete(request: NSMutableURLRequest, completion: @escaping (Result) -> Void) { } func createURLRequest(from resource: T) -> NSMutableURLRequest? where T : ApiResourceProviding { guard let baseUrl = URL(string:self.baseUrl) else { return nil } let locationRequest = NSMutableURLRequest(url:baseUrl, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 1.0) locationRequest.httpMethod = "POST" locationRequest.httpBody = resource.data resource.headers.forEach { (arg) in let (key, value) = arg locationRequest.addValue(value, forHTTPHeaderField: key) } return locationRequest } typealias SerializedType = OSMData } ================================================ FILE: LooLocator/Protocols/Implementations/AmentityResource.swift ================================================ // // AmentityResource.swift // LooLocator // // Created by Sam Khawase on 15.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import Foundation class AmenityResource: ApiResourceProviding { var latitude: String var longitude: String var amenityType: String var radius: String init(latitude: String, longitude: String, amenity: String, radius: String) { self.longitude = longitude self.latitude = latitude self.amenityType = amenity self.radius = radius } // OSM Needs the data in XML format var data: Data { if let _data = """ """.data(using: String.Encoding.utf8) { return _data } else { return Data() } } var headers: Dictionary { var _headers = Dictionary() _headers["Content-Type"] = "application/xml" _headers["Access-Control-Allow-Origin"] = "*" _headers["Access-Control-Allow-Origin"] = "*/*" return _headers } } ================================================ FILE: LooLocator/Protocols/Implementations/LocationProvider.swift ================================================ // // LocationProvider.swift // LooLocator // // Created by Sam Khawase on 14.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import CoreLocation class LocationProvider: NSObject, LocationProvidable, CLLocationManagerDelegate { var listener: LocationObservable? func setListener(listener: LocationObservable) { self.listener = listener } fileprivate var locationManager: LocationManagerConfigurable fileprivate var currentLocation: CLLocation? //fileprivate var observer: LocationObservable? // inject init(locationManager:LocationManagerConfigurable){ self.locationManager = locationManager } func startLocationUpdates() { if (CLLocationManager.locationServicesEnabled()) { locationManager.setDelegate(to: self) locationManager.setDesiredAccuracy(to: kCLLocationAccuracyBest) locationManager.requestAlwaysAuthorization() locationManager.startUpdatingLocation() } } func getCurrentLocation() -> (Double, Double) { guard let currentLocation = currentLocation else { return (0,0) } return (currentLocation.coordinate.latitude, currentLocation.coordinate.longitude) } // CLLocationManager delegate methods func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let lastLocation = locations.last, let listener = self.listener else { return } if currentLocation != nil && Double((currentLocation?.distance(from: lastLocation))!) < 100.0 { //print("current Location \(String(describing: currentLocation?.coordinate)) is same as last Location: \(String(describing: lastLocation.coordinate))") return } currentLocation = lastLocation listener.setCurrentLocation(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude) } } ================================================ FILE: LooLocator/Protocols/LocationManagerConfigurable.swift ================================================ // // LocationManagerConfigurable.swift // LooLocator // // Created by Sam Khawase on 15.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import CoreLocation protocol LocationManagerConfigurable { // wrap var delegate and desiredAccuracy to keep it platform agnostic func setDelegate(to instance: CLLocationManagerDelegate?) func setDesiredAccuracy(to accuracy: CLLocationAccuracy) func requestAlwaysAuthorization() func startUpdatingLocation() } ================================================ FILE: LooLocator/Protocols/LocationProvidable.swift ================================================ // // LocationProvidable.swift // LooLocator // // Created by Sam Khawase on 15.02.20. // Copyright © 2020 LooLocator. All rights reserved. // protocol LocationProvidable { var listener: LocationObservable? { get set } func setListener(listener: LocationObservable) func startLocationUpdates() func getCurrentLocation() -> (Double, Double) } // to be implemented by the VM protocol LocationObservable { func setCurrentLocation(latitude: Double, longitude: Double) } ================================================ FILE: LooLocator/Protocols/NetworkRequestProviding.swift ================================================ // // NetworkRequest.swift // LooLocator // // Created by Sam Khawase on 15.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import Foundation //typealias CompletionBlock = (_ success: Bool, _ object: AnyObject?) -> () protocol NetworkRequestProviding { // The model that the request deals with associatedtype SerializedType : Codable // CRUD interface func get(request: NSMutableURLRequest, completion: @escaping (Result) -> Void ) func post(request: NSMutableURLRequest, completion: @escaping (Result) -> Void) func put(request: NSMutableURLRequest, completion: @escaping (Result) -> Void) func delete(request: NSMutableURLRequest, completion: @escaping (Result) -> Void) // Internal workhorse function: implemented in default extension func dataTask(request: NSMutableURLRequest, completion: @escaping (Result) -> Void) // The implementor needs to implement this to provide the ApiResource that the request needs func createURLRequest(from resource: T) -> NSMutableURLRequest? } ================================================ FILE: LooLocator/ViewControllers/ViewController.swift ================================================ // // ViewController.swift // LooLocator // // Created by Sam Khawase on 14.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import UIKit import MapKit class ViewController: UIViewController { @IBOutlet weak var mapView: MKMapView! private(set) lazy var viewModel: MapViewModel = { let _viewModel = MapViewModel(locationProvider: amenityLocationProvider, amenityRequest: amenityRequest, listener: self) return _viewModel }() private(set) lazy var amenityLocationProvider: LocationProvidable = { let _locationProvider = LocationProvider(locationManager: locationManager) return _locationProvider }() private(set) lazy var amenityRequest: AmenityRequest = { let _amenityRequest = AmenityRequest() return _amenityRequest }() private(set) lazy var locationManager: LocationManagerConfigurable = { let _clLocationManager = CLLocationManager() return _clLocationManager }() let regionRadius: CLLocationDistance = 500 required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) _ = viewModel.getCurrentLocation() } fileprivate func getAmenities() { viewModel.getAmenities(in: 1000, type: .Toilets) { [weak self] result in switch result { case .success(let locations): for location in locations { print("Location.coordinate: \(location.coordinate.latitude) : \(location.coordinate.longitude)") DispatchQueue.main.async { [weak self] in self?.mapView.addAnnotation(location) } } break case .failure(let error): self?.showAlert(error.localizedDescription) break } } } override func viewDidLoad() { super.viewDidLoad() mapView.delegate = self mapView.showsUserLocation = true } @IBAction func resetLocation(_ sender: Any) { viewModel.centerMapToCurrentLocationAction() } func centerMapOnLocation(location: CLLocation) { let coordinateRegion = MKCoordinateRegion.init(center: location.coordinate, latitudinalMeters: regionRadius, longitudinalMeters: regionRadius) DispatchQueue.main.async { [weak self] in self?.mapView.setRegion(coordinateRegion, animated: true) } } fileprivate func showAlert(_ message: String){ let alertController = UIAlertController(title: "Loolocator Alert", message: message, preferredStyle: .alert) let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) alertController.addAction(cancelAction) let OKAction = UIAlertAction(title: "OK", style: .default, handler: nil) alertController.addAction(OKAction) DispatchQueue.main.async { [weak self] in self?.present(alertController, animated: true, completion: nil) } } } extension ViewController: MapViewModelObservable{ typealias Amenity = Location func addAmenityToMap(amenity: Location) { } func setCurrentLocation(latitude: Double, longitude: Double) { print("current latitude: \(latitude), longitude: \(longitude)") let currentLocation = CLLocation(latitude: CLLocationDegrees(latitude), longitude: CLLocationDegrees(longitude)) centerMapOnLocation(location: currentLocation) getAmenities() } func centerMapToCurrentLocation(latitude: Double, longitude: Double) { let currentLocation = CLLocation(latitude: CLLocationDegrees(latitude), longitude: CLLocationDegrees(longitude)) centerMapOnLocation(location: currentLocation) } } extension ViewController: MKMapViewDelegate { func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { guard let annotation = annotation as? Location else { return nil } let identifier = "marker" var view: MKMarkerAnnotationView if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKMarkerAnnotationView { dequeuedView.annotation = annotation view = dequeuedView } else { view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier) view.canShowCallout = true view.calloutOffset = CGPoint(x: -5, y: 5) view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure) } return view } func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { guard let location = view.annotation as? Location else { return } let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeWalking] location.mapItem().openInMaps(launchOptions: launchOptions) } } ================================================ FILE: LooLocator/ViewModels/AmenityViewModel.swift ================================================ // // AmenityViewModel.swift // LooLocator // // Created by Sam Khawase on 14.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import CoreLocation protocol MapViewModelConfirming { func getCurrentLocation() -> (Double, Double) func getAmenities(in range: Int, type: AmenityType, completion: @escaping (Result<[Location], Error>) -> Void) func centerMapToCurrentLocationAction() } protocol MapViewModelObservable { // This needs to be supplied by the VM Observer associatedtype Amenity func setCurrentLocation(latitude: Double, longitude: Double) func addAmenityToMap(amenity:Amenity) func centerMapToCurrentLocation(latitude: Double, longitude: Double) } enum AmenityType: String { case Toilets = "toilets" } // MapviewModel: Protocol implementation class MapViewModel: MapViewModelConfirming, LocationObservable { var locationProvider: LocationProvidable var amenityRequest: AmenityRequest var listenerView: S // inject the dependencies in ctor init(locationProvider: LocationProvidable, amenityRequest: AmenityRequest, listener: S) { self.locationProvider = locationProvider self.locationProvider.startLocationUpdates() self.amenityRequest = amenityRequest self.listenerView = listener defer { self.locationProvider.setListener(listener: self) } } func getCurrentLocation() -> (Double, Double) { let (lat, lon) = locationProvider.getCurrentLocation() return (lat, lon) } func getAmenities(in range: Int, type: AmenityType, completion: @escaping (Result<[Location], Error>) -> Void) { let (lat, lon) = getCurrentLocation() print("latitude: \(lat) longitude: \(lon)") if lat == 0 && lon == 0 { completion(.failure(NSError(domain: "in.b3rl.loolocator", code: 666, userInfo: nil))) } amenityRequest.getAmeneties(of: AmenityType.Toilets, latitude: lat, longitude: lon, radius: Double(range)) { result in switch result { case .success(let osmElement): if let locations = osmElement.elements { completion(.success(locations)) } case .failure(let error): completion(.failure(error)) } } } // This is a message from the location provider func setCurrentLocation(latitude: Double, longitude: Double) { listenerView.setCurrentLocation(latitude: latitude, longitude: longitude) } func centerMapToCurrentLocationAction() { let currentLocation = getCurrentLocation() listenerView.centerMapToCurrentLocation(latitude: currentLocation.0, longitude: currentLocation.1) } } ================================================ FILE: LooLocator.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 52; objects = { /* Begin PBXBuildFile section */ 22801B3A2035B0F100D4C2D2 /* LocationMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B392035B0F100D4C2D2 /* LocationMocks.swift */; }; 22801B3C2035B1E000D4C2D2 /* LocationProvidable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B3B2035B1E000D4C2D2 /* LocationProvidable.swift */; }; 22801B3E2035B20100D4C2D2 /* LocationManagerConfigurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B3D2035B20100D4C2D2 /* LocationManagerConfigurable.swift */; }; 22801B412035B25500D4C2D2 /* CLLocationManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B402035B25500D4C2D2 /* CLLocationManagerExtension.swift */; }; 22801B552035BEC400D4C2D2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 22801B532035BEC400D4C2D2 /* LaunchScreen.storyboard */; }; 22801B562035BEC400D4C2D2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 22801B542035BEC400D4C2D2 /* Main.storyboard */; }; 22801B5A2035E07800D4C2D2 /* ApiResourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B592035E07800D4C2D2 /* ApiResourceProviding.swift */; }; 22801B5C2035E10C00D4C2D2 /* NetworkRequestProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B5B2035E10B00D4C2D2 /* NetworkRequestProviding.swift */; }; 22801B5E2035E40A00D4C2D2 /* AmentityResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B5D2035E40A00D4C2D2 /* AmentityResource.swift */; }; 22801B602035E48200D4C2D2 /* AmenityRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B5F2035E48200D4C2D2 /* AmenityRequest.swift */; }; 22801B652035E59300D4C2D2 /* NetworkReqeustProvidingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B642035E59300D4C2D2 /* NetworkReqeustProvidingExtension.swift */; }; 22801B672036209C00D4C2D2 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B662036209C00D4C2D2 /* Location.swift */; }; 22801B692036D9D700D4C2D2 /* AmenityMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B682036D9D700D4C2D2 /* AmenityMocks.swift */; }; 229B3291203455CA0098E456 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229B3290203455CA0098E456 /* AppDelegate.swift */; }; 229B3293203455CA0098E456 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229B3292203455CA0098E456 /* ViewController.swift */; }; 229B3298203455CA0098E456 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 229B3297203455CA0098E456 /* Assets.xcassets */; }; 229B32BA203459660098E456 /* AmenityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229B32B9203459660098E456 /* AmenityViewModel.swift */; }; 229B32BC203460AE0098E456 /* LocationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229B32BB203460AE0098E456 /* LocationProvider.swift */; }; 22D1387D20386A2A0077E457 /* ApiClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D1387C20386A2A0077E457 /* ApiClientTests.swift */; }; 22D1387F20386D3D0077E457 /* stubbedRepsonse.json in Resources */ = {isa = PBXBuildFile; fileRef = 22D1387E20386D3D0077E457 /* stubbedRepsonse.json */; }; 22D13882203A19450077E457 /* AmenityRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B5F2035E48200D4C2D2 /* AmenityRequest.swift */; }; 22D13883203A19FE0077E457 /* LocationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229B32BB203460AE0098E456 /* LocationProvider.swift */; }; 22D13884203A19FE0077E457 /* AmentityResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B5D2035E40A00D4C2D2 /* AmentityResource.swift */; }; 22D13885203A1A040077E457 /* CLLocationManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B402035B25500D4C2D2 /* CLLocationManagerExtension.swift */; }; 22D13886203A1A040077E457 /* NetworkReqeustProvidingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B642035E59300D4C2D2 /* NetworkReqeustProvidingExtension.swift */; }; 22D13887203A1A040077E457 /* LocationProvidable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B3B2035B1E000D4C2D2 /* LocationProvidable.swift */; }; 22D13888203A1A040077E457 /* LocationManagerConfigurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B3D2035B20100D4C2D2 /* LocationManagerConfigurable.swift */; }; 22D13889203A1A040077E457 /* NetworkRequestProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B5B2035E10B00D4C2D2 /* NetworkRequestProviding.swift */; }; 22D1388A203A1A040077E457 /* ApiResourceProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B592035E07800D4C2D2 /* ApiResourceProviding.swift */; }; 22D1388C203A1A080077E457 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22801B662036209C00D4C2D2 /* Location.swift */; }; 22D1388D203A1A2A0077E457 /* AmenityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229B32B9203459660098E456 /* AmenityViewModel.swift */; }; 22D13890203AD0D30077E457 /* LocationProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229B32A5203455CA0098E456 /* LocationProviderTests.swift */; }; 22D13892203C94AB0077E457 /* berlin.gpx in Resources */ = {isa = PBXBuildFile; fileRef = 22D13891203C94AB0077E457 /* berlin.gpx */; }; 22D13893204022E70077E457 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229B3292203455CA0098E456 /* ViewController.swift */; }; 22D13895204023640077E457 /* MockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D13894204023640077E457 /* MockViewController.swift */; }; 22D138972040269C0077E457 /* MapViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D138962040269C0077E457 /* MapViewModelTests.swift */; }; 8A048946253F41EA00037931 /* OSMModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A048945253F41EA00037931 /* OSMModelTests.swift */; }; 8A2413F9253636AD005B7E5C /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = 8A2413F8253636AD005B7E5C /* Quick */; }; 8A2413FE253636E2005B7E5C /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 8A2413FD253636E2005B7E5C /* Nimble */; }; 8A2414032536370D005B7E5C /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 8A2414022536370D005B7E5C /* OHHTTPStubsSwift */; }; 8A2414052536370D005B7E5C /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = 8A2414042536370D005B7E5C /* OHHTTPStubs */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 229B32A2203455CA0098E456 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 229B3285203455C90098E456 /* Project object */; proxyType = 1; remoteGlobalIDString = 229B328C203455CA0098E456; remoteInfo = LooLocator; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 22801B392035B0F100D4C2D2 /* LocationMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMocks.swift; sourceTree = ""; }; 22801B3B2035B1E000D4C2D2 /* LocationProvidable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationProvidable.swift; sourceTree = ""; }; 22801B3D2035B20100D4C2D2 /* LocationManagerConfigurable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManagerConfigurable.swift; sourceTree = ""; }; 22801B402035B25500D4C2D2 /* CLLocationManagerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocationManagerExtension.swift; sourceTree = ""; }; 22801B532035BEC400D4C2D2 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 22801B542035BEC400D4C2D2 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 22801B592035E07800D4C2D2 /* ApiResourceProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiResourceProviding.swift; sourceTree = ""; }; 22801B5B2035E10B00D4C2D2 /* NetworkRequestProviding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkRequestProviding.swift; sourceTree = ""; }; 22801B5D2035E40A00D4C2D2 /* AmentityResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmentityResource.swift; sourceTree = ""; }; 22801B5F2035E48200D4C2D2 /* AmenityRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmenityRequest.swift; sourceTree = ""; }; 22801B642035E59300D4C2D2 /* NetworkReqeustProvidingExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkReqeustProvidingExtension.swift; sourceTree = ""; }; 22801B662036209C00D4C2D2 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; 22801B682036D9D700D4C2D2 /* AmenityMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmenityMocks.swift; sourceTree = ""; }; 22801B6A2036DF8300D4C2D2 /* RxSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxSwift.framework; path = Carthage/Build/iOS/RxSwift.framework; sourceTree = ""; }; 22801B6C2036DFA100D4C2D2 /* RxCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxCocoa.framework; path = Carthage/Build/iOS/RxCocoa.framework; sourceTree = ""; }; 22801B712036DFC800D4C2D2 /* RxTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxTest.framework; path = Carthage/Build/iOS/RxTest.framework; sourceTree = ""; }; 22801B722036DFC800D4C2D2 /* RxBlocking.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxBlocking.framework; path = Carthage/Build/iOS/RxBlocking.framework; sourceTree = ""; }; 229B328D203455CA0098E456 /* LooLocator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LooLocator.app; sourceTree = BUILT_PRODUCTS_DIR; }; 229B3290203455CA0098E456 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 229B3292203455CA0098E456 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 229B3297203455CA0098E456 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 229B329C203455CA0098E456 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 229B32A1203455CA0098E456 /* LooLocatorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LooLocatorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 229B32A5203455CA0098E456 /* LocationProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationProviderTests.swift; sourceTree = ""; }; 229B32A7203455CA0098E456 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 229B32B1203457AC0098E456 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; 229B32B2203457AC0098E456 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quick.framework; path = Carthage/Build/iOS/Quick.framework; sourceTree = ""; }; 229B32B9203459660098E456 /* AmenityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmenityViewModel.swift; sourceTree = ""; }; 229B32BB203460AE0098E456 /* LocationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationProvider.swift; sourceTree = ""; }; 22D13879203862940077E457 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OHHTTPStubs.framework; path = Carthage/Build/iOS/OHHTTPStubs.framework; sourceTree = ""; }; 22D1387C20386A2A0077E457 /* ApiClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiClientTests.swift; sourceTree = ""; }; 22D1387E20386D3D0077E457 /* stubbedRepsonse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = stubbedRepsonse.json; sourceTree = ""; }; 22D13891203C94AB0077E457 /* berlin.gpx */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = berlin.gpx; sourceTree = SOURCE_ROOT; }; 22D13894204023640077E457 /* MockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockViewController.swift; sourceTree = ""; }; 22D138962040269C0077E457 /* MapViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewModelTests.swift; sourceTree = ""; }; 8A048945253F41EA00037931 /* OSMModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMModelTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 229B328A203455CA0098E456 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 229B329E203455CA0098E456 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 8A2413F9253636AD005B7E5C /* Quick in Frameworks */, 8A2414032536370D005B7E5C /* OHHTTPStubsSwift in Frameworks */, 8A2413FE253636E2005B7E5C /* Nimble in Frameworks */, 8A2414052536370D005B7E5C /* OHHTTPStubs in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 22801B382035B0E200D4C2D2 /* Mocks */ = { isa = PBXGroup; children = ( 22801B392035B0F100D4C2D2 /* LocationMocks.swift */, 22801B682036D9D700D4C2D2 /* AmenityMocks.swift */, 22D13894204023640077E457 /* MockViewController.swift */, ); name = Mocks; sourceTree = ""; }; 22801B462035BA1800D4C2D2 /* Resources */ = { isa = PBXGroup; children = ( 22D13891203C94AB0077E457 /* berlin.gpx */, 229B3297203455CA0098E456 /* Assets.xcassets */, 229B329C203455CA0098E456 /* Info.plist */, ); name = Resources; sourceTree = ""; }; 22801B4C2035BB9F00D4C2D2 /* Protocols */ = { isa = PBXGroup; children = ( 22801B4E2035BBC100D4C2D2 /* Implementations */, 22801B4D2035BBB900D4C2D2 /* Extensions */, 22801B3B2035B1E000D4C2D2 /* LocationProvidable.swift */, 22801B3D2035B20100D4C2D2 /* LocationManagerConfigurable.swift */, 22801B5B2035E10B00D4C2D2 /* NetworkRequestProviding.swift */, 22801B592035E07800D4C2D2 /* ApiResourceProviding.swift */, ); path = Protocols; sourceTree = ""; }; 22801B4D2035BBB900D4C2D2 /* Extensions */ = { isa = PBXGroup; children = ( 22801B402035B25500D4C2D2 /* CLLocationManagerExtension.swift */, 22801B642035E59300D4C2D2 /* NetworkReqeustProvidingExtension.swift */, ); path = Extensions; sourceTree = ""; }; 22801B4E2035BBC100D4C2D2 /* Implementations */ = { isa = PBXGroup; children = ( 229B32BB203460AE0098E456 /* LocationProvider.swift */, 22801B5D2035E40A00D4C2D2 /* AmentityResource.swift */, 22801B5F2035E48200D4C2D2 /* AmenityRequest.swift */, ); path = Implementations; sourceTree = ""; }; 22801B502035BE1800D4C2D2 /* ViewModels */ = { isa = PBXGroup; children = ( 229B32B9203459660098E456 /* AmenityViewModel.swift */, ); path = ViewModels; sourceTree = ""; }; 22801B512035BE2800D4C2D2 /* ViewControllers */ = { isa = PBXGroup; children = ( 229B3292203455CA0098E456 /* ViewController.swift */, ); path = ViewControllers; sourceTree = ""; }; 22801B612035E4D500D4C2D2 /* Models */ = { isa = PBXGroup; children = ( 22801B662036209C00D4C2D2 /* Location.swift */, ); path = Models; sourceTree = ""; }; 229B3284203455C90098E456 = { isa = PBXGroup; children = ( 229B328F203455CA0098E456 /* LooLocator */, 229B32A4203455CA0098E456 /* LooLocatorTests */, 229B328E203455CA0098E456 /* Products */, 229B32B0203457AC0098E456 /* Frameworks */, ); sourceTree = ""; }; 229B328E203455CA0098E456 /* Products */ = { isa = PBXGroup; children = ( 229B328D203455CA0098E456 /* LooLocator.app */, 229B32A1203455CA0098E456 /* LooLocatorTests.xctest */, ); name = Products; sourceTree = ""; }; 229B328F203455CA0098E456 /* LooLocator */ = { isa = PBXGroup; children = ( 22801B612035E4D500D4C2D2 /* Models */, 22801B532035BEC400D4C2D2 /* LaunchScreen.storyboard */, 22801B542035BEC400D4C2D2 /* Main.storyboard */, 229B3290203455CA0098E456 /* AppDelegate.swift */, 22801B502035BE1800D4C2D2 /* ViewModels */, 22801B4C2035BB9F00D4C2D2 /* Protocols */, 22801B462035BA1800D4C2D2 /* Resources */, 22801B512035BE2800D4C2D2 /* ViewControllers */, ); path = LooLocator; sourceTree = ""; }; 229B32A4203455CA0098E456 /* LooLocatorTests */ = { isa = PBXGroup; children = ( 22D1387E20386D3D0077E457 /* stubbedRepsonse.json */, 22801B382035B0E200D4C2D2 /* Mocks */, 229B32A5203455CA0098E456 /* LocationProviderTests.swift */, 229B32A7203455CA0098E456 /* Info.plist */, 22D1387C20386A2A0077E457 /* ApiClientTests.swift */, 22D138962040269C0077E457 /* MapViewModelTests.swift */, 8A048945253F41EA00037931 /* OSMModelTests.swift */, ); path = LooLocatorTests; sourceTree = ""; }; 229B32B0203457AC0098E456 /* Frameworks */ = { isa = PBXGroup; children = ( 22D13879203862940077E457 /* OHHTTPStubs.framework */, 22801B722036DFC800D4C2D2 /* RxBlocking.framework */, 22801B712036DFC800D4C2D2 /* RxTest.framework */, 22801B6C2036DFA100D4C2D2 /* RxCocoa.framework */, 22801B6A2036DF8300D4C2D2 /* RxSwift.framework */, 229B32B1203457AC0098E456 /* Nimble.framework */, 229B32B2203457AC0098E456 /* Quick.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 229B328C203455CA0098E456 /* LooLocator */ = { isa = PBXNativeTarget; buildConfigurationList = 229B32AA203455CA0098E456 /* Build configuration list for PBXNativeTarget "LooLocator" */; buildPhases = ( 229B3289203455CA0098E456 /* Sources */, 229B328A203455CA0098E456 /* Frameworks */, 229B328B203455CA0098E456 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = LooLocator; productName = LooLocator; productReference = 229B328D203455CA0098E456 /* LooLocator.app */; productType = "com.apple.product-type.application"; }; 229B32A0203455CA0098E456 /* LooLocatorTests */ = { isa = PBXNativeTarget; buildConfigurationList = 229B32AD203455CA0098E456 /* Build configuration list for PBXNativeTarget "LooLocatorTests" */; buildPhases = ( 229B329D203455CA0098E456 /* Sources */, 229B329E203455CA0098E456 /* Frameworks */, 229B329F203455CA0098E456 /* Resources */, ); buildRules = ( ); dependencies = ( 229B32A3203455CA0098E456 /* PBXTargetDependency */, ); name = LooLocatorTests; packageProductDependencies = ( 8A2413F8253636AD005B7E5C /* Quick */, 8A2413FD253636E2005B7E5C /* Nimble */, 8A2414022536370D005B7E5C /* OHHTTPStubsSwift */, 8A2414042536370D005B7E5C /* OHHTTPStubs */, ); productName = LooLocatorTests; productReference = 229B32A1203455CA0098E456 /* LooLocatorTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 229B3285203455C90098E456 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 1200; ORGANIZATIONNAME = LooLocator; TargetAttributes = { 229B328C203455CA0098E456 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1200; ProvisioningStyle = Automatic; }; 229B32A0203455CA0098E456 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1200; ProvisioningStyle = Automatic; }; }; }; buildConfigurationList = 229B3288203455C90098E456 /* Build configuration list for PBXProject "LooLocator" */; compatibilityVersion = "Xcode 8.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 229B3284203455C90098E456; packageReferences = ( 8A2413F7253636AD005B7E5C /* XCRemoteSwiftPackageReference "Quick" */, 8A2413FC253636E2005B7E5C /* XCRemoteSwiftPackageReference "Nimble" */, 8A2414012536370D005B7E5C /* XCRemoteSwiftPackageReference "OHHTTPStubs" */, ); productRefGroup = 229B328E203455CA0098E456 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 229B328C203455CA0098E456 /* LooLocator */, 229B32A0203455CA0098E456 /* LooLocatorTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 229B328B203455CA0098E456 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 22801B562035BEC400D4C2D2 /* Main.storyboard in Resources */, 22801B552035BEC400D4C2D2 /* LaunchScreen.storyboard in Resources */, 229B3298203455CA0098E456 /* Assets.xcassets in Resources */, 22D13892203C94AB0077E457 /* berlin.gpx in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 229B329F203455CA0098E456 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 22D1387F20386D3D0077E457 /* stubbedRepsonse.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 229B3289203455CA0098E456 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 22801B5E2035E40A00D4C2D2 /* AmentityResource.swift in Sources */, 22801B5C2035E10C00D4C2D2 /* NetworkRequestProviding.swift in Sources */, 229B3293203455CA0098E456 /* ViewController.swift in Sources */, 22801B3E2035B20100D4C2D2 /* LocationManagerConfigurable.swift in Sources */, 229B32BC203460AE0098E456 /* LocationProvider.swift in Sources */, 22801B672036209C00D4C2D2 /* Location.swift in Sources */, 229B3291203455CA0098E456 /* AppDelegate.swift in Sources */, 22801B5A2035E07800D4C2D2 /* ApiResourceProviding.swift in Sources */, 22801B652035E59300D4C2D2 /* NetworkReqeustProvidingExtension.swift in Sources */, 229B32BA203459660098E456 /* AmenityViewModel.swift in Sources */, 22801B3C2035B1E000D4C2D2 /* LocationProvidable.swift in Sources */, 22801B602035E48200D4C2D2 /* AmenityRequest.swift in Sources */, 22801B412035B25500D4C2D2 /* CLLocationManagerExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 229B329D203455CA0098E456 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 22D13890203AD0D30077E457 /* LocationProviderTests.swift in Sources */, 22801B3A2035B0F100D4C2D2 /* LocationMocks.swift in Sources */, 22D1388D203A1A2A0077E457 /* AmenityViewModel.swift in Sources */, 22D138972040269C0077E457 /* MapViewModelTests.swift in Sources */, 22D13886203A1A040077E457 /* NetworkReqeustProvidingExtension.swift in Sources */, 22D1388C203A1A080077E457 /* Location.swift in Sources */, 22D13889203A1A040077E457 /* NetworkRequestProviding.swift in Sources */, 22D13887203A1A040077E457 /* LocationProvidable.swift in Sources */, 22D1388A203A1A040077E457 /* ApiResourceProviding.swift in Sources */, 22801B692036D9D700D4C2D2 /* AmenityMocks.swift in Sources */, 22D1387D20386A2A0077E457 /* ApiClientTests.swift in Sources */, 22D13884203A19FE0077E457 /* AmentityResource.swift in Sources */, 22D13893204022E70077E457 /* ViewController.swift in Sources */, 22D13895204023640077E457 /* MockViewController.swift in Sources */, 22D13888203A1A040077E457 /* LocationManagerConfigurable.swift in Sources */, 22D13882203A19450077E457 /* AmenityRequest.swift in Sources */, 8A048946253F41EA00037931 /* OSMModelTests.swift in Sources */, 22D13883203A19FE0077E457 /* LocationProvider.swift in Sources */, 22D13885203A1A040077E457 /* CLLocationManagerExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 229B32A3203455CA0098E456 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 229B328C203455CA0098E456 /* LooLocator */; targetProxy = 229B32A2203455CA0098E456 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 229B32A8203455CA0098E456 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 229B32A9203455CA0098E456 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; }; 229B32AB203455CA0098E456 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = CWXKC246LH; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = LooLocator/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = de.sam.LooLocator; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 229B32AC203455CA0098E456 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = CWXKC246LH; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = LooLocator/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = de.sam.LooLocator; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; 229B32AE203455CA0098E456 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = CWXKC246LH; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = LooLocatorTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = de.sam.LooLocatorTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 229B32AF203455CA0098E456 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = CWXKC246LH; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = LooLocatorTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = de.sam.LooLocatorTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 229B3288203455C90098E456 /* Build configuration list for PBXProject "LooLocator" */ = { isa = XCConfigurationList; buildConfigurations = ( 229B32A8203455CA0098E456 /* Debug */, 229B32A9203455CA0098E456 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 229B32AA203455CA0098E456 /* Build configuration list for PBXNativeTarget "LooLocator" */ = { isa = XCConfigurationList; buildConfigurations = ( 229B32AB203455CA0098E456 /* Debug */, 229B32AC203455CA0098E456 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 229B32AD203455CA0098E456 /* Build configuration list for PBXNativeTarget "LooLocatorTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 229B32AE203455CA0098E456 /* Debug */, 229B32AF203455CA0098E456 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ 8A2413F7253636AD005B7E5C /* XCRemoteSwiftPackageReference "Quick" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Quick/Quick.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 3.0.0; }; }; 8A2413FC253636E2005B7E5C /* XCRemoteSwiftPackageReference "Nimble" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Quick/Nimble.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 9.0.0; }; }; 8A2414012536370D005B7E5C /* XCRemoteSwiftPackageReference "OHHTTPStubs" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/AliSoftware/OHHTTPStubs.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 9.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 8A2413F8253636AD005B7E5C /* Quick */ = { isa = XCSwiftPackageProductDependency; package = 8A2413F7253636AD005B7E5C /* XCRemoteSwiftPackageReference "Quick" */; productName = Quick; }; 8A2413FD253636E2005B7E5C /* Nimble */ = { isa = XCSwiftPackageProductDependency; package = 8A2413FC253636E2005B7E5C /* XCRemoteSwiftPackageReference "Nimble" */; productName = Nimble; }; 8A2414022536370D005B7E5C /* OHHTTPStubsSwift */ = { isa = XCSwiftPackageProductDependency; package = 8A2414012536370D005B7E5C /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; productName = OHHTTPStubsSwift; }; 8A2414042536370D005B7E5C /* OHHTTPStubs */ = { isa = XCSwiftPackageProductDependency; package = 8A2414012536370D005B7E5C /* XCRemoteSwiftPackageReference "OHHTTPStubs" */; productName = OHHTTPStubs; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 229B3285203455C90098E456 /* Project object */; } ================================================ FILE: LooLocator.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: LooLocator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: LooLocator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "object": { "pins": [ { "package": "CwlCatchException", "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", "state": { "branch": null, "revision": "f809deb30dc5c9d9b78c872e553261a61177721a", "version": "2.0.0" } }, { "package": "CwlPreconditionTesting", "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", "state": { "branch": null, "revision": "02b7a39a99c4da27abe03cab2053a9034379639f", "version": "2.0.0" } }, { "package": "Nimble", "repositoryURL": "https://github.com/Quick/Nimble.git", "state": { "branch": null, "revision": "e491a6731307bb23783bf664d003be9b2fa59ab5", "version": "9.0.0" } }, { "package": "OHHTTPStubs", "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs.git", "state": { "branch": null, "revision": "e92b5a5746ef16add2a1424f1fc19529d9a75cde", "version": "9.0.0" } }, { "package": "Quick", "repositoryURL": "https://github.com/Quick/Quick.git", "state": { "branch": null, "revision": "0038bcbab4292f3b028632556507c124e5ba69f3", "version": "3.0.0" } } ] }, "version": 1 } ================================================ FILE: LooLocator.xcodeproj/xcshareddata/xcschemes/LooLocator.xcscheme ================================================ ================================================ FILE: LooLocatorTests/AmenityMocks.swift ================================================ // // AmenityMocks.swift // LooLocatorTests // // Created by Sam Khawase on 16.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import Foundation class MockAmenityRequest: AmenityRequest { override func getAmeneties(of type: AmenityType, latitude: Double, longitude: Double, radius: Double, completionBlock: @escaping (Result) -> Void) { let jsonDecoder = JSONDecoder() if let jsonData = """ { "elements": [ { "type": "node", "id": 66917214, "lat": 52.5168607, "lon": 13.3829509, "tags": { "amenity": "toilets", "fee": "yes", "name": "City Toilette", "toilets:wheelchair": "yes", "wheelchair": "yes" } } ] } """.data(using: .utf8), let dummyOSMData = try? jsonDecoder.decode(OSMData.self, from: jsonData) { completionBlock(.success(dummyOSMData)) } } } ================================================ FILE: LooLocatorTests/AmenityRequestTests.swift ================================================ // // AmenityRequestTests.swift // LooLocatorTests // // Created by Sam Khawase on 17.02.18. // Copyright © 2018 LooLocator. All rights reserved. // import XCTest import OHHTTPStubs //@testable import LooLocator class AmenityRequestTests: XCTestCase { override func setUp() { super.setUp() let testHost = "overpass-api.de" stub(condition: isHost(testHost), response: { _ in guard let path = OHPathForFile("stubbedRepsonse.json", type(of: self)) else { preconditionFailure("Could not find expected file in test bundle") } return fixture(filePath: path, status: 200, headers: ["Content-Type":"application/json"]) }) } override func tearDown() { super.tearDown() OHHTTPStubs.removeAllStubs() } func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. let amenityReqeust = AmenityRequest() let expectation = self.expectation(description: "calls the callback with a resource object") var successFlag = false var locations: [Location] = [] let timeout = 1.0 amenityReqeust.getAmeneties(of: AmenityType.Toilets, latitude: 52.51631, longitude: 13.37777, radius: 1000, completionBlock: { (success, results) in print("success is \(success)") successFlag = success if successFlag, let results = results as? [Location] { locations = results expectation.fulfill() } }) self.waitForExpectations(timeout: timeout) { err in XCTAssertNotNil(locations, "Received data should not be nil") } } } ================================================ FILE: LooLocatorTests/ApiClientTests.swift ================================================ // // ApiClientTests.swift // LooLocatorTests // // Created by Sam Khawase on 17.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import XCTest import Quick import Nimble import CoreLocation import OHHTTPStubs import OHHTTPStubsSwift class ApiClientTests: QuickSpec { override func spec() { describe("Amenity Request tests") { let amenityReqeust = AmenityRequest() beforeEach { let testHost = "overpass-api.de" stub(condition: isHost(testHost), response: { _ in guard let path = OHPathForFile("stubbedRepsonse.json", type(of: self)) else { preconditionFailure("Could not find expected file in test bundle") } return fixture(filePath: path, status: 200, headers: ["Content-Type":"application/json"]) }) } afterEach { HTTPStubs.removeAllStubs() } it("should fetch amenities", closure: { // Arrange var successFlag = false var locations: [Location] = [] // Act amenityReqeust.getAmeneties(of: AmenityType.Toilets, latitude: 52.51631, longitude: 13.37777, radius: 1000) { result in switch result { case .success(let osmData): successFlag = true if let results = osmData.elements { locations = results } break case .failure( _): successFlag = false break } } //Assert expect(successFlag).toEventuallyNot(beFalse()) expect(locations).toEventuallyNot(beEmpty()) expect(locations.first?.id).toEventuallyNot(equal(0)) expect(locations.first?.coordinates.0).toEventuallyNot(equal(0)) expect(locations.first?.coordinates.1).toEventuallyNot(equal(0)) }) } } } ================================================ FILE: LooLocatorTests/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleVersion 1 NSLocationAlwaysAndWhenInUseUsageDescription We need this information to locate you on the map, in order to find the amenities near you NSLocationWhenInUseUsageDescription We need this information to locate you on the map, in order to find the amenities near you ================================================ FILE: LooLocatorTests/LocationMocks.swift ================================================ // // LocationMocks.swift // LooLocatorTests // // Created by Sam Khawase on 15.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import MapKit class MockLocationProvider: LocationProvidable { var listener: LocationObservable? func setListener(listener: LocationObservable) { self.listener = listener } func startLocationUpdates() { listener?.setCurrentLocation(latitude: 52.51631, longitude: 13.37777) } func getCurrentLocation() -> (Double, Double) { return (52.51631, 13.37777) } } class MockLocationObservable: LocationObservable { internal var coordinates: (Double, Double)? func setCurrentLocation(latitude: Double, longitude: Double) { coordinates = (latitude, longitude) } } class MockLocationManager: LocationManagerConfigurable { internal var callCount = 0 fileprivate var delegate: LocationProvider? func setDelegate(to instance: CLLocationManagerDelegate?) { callCount += 1 delegate = instance as? LocationProvider } func setDesiredAccuracy(to accuracy: Double) { callCount += 1 } func requestAlwaysAuthorization() { callCount += 1 } func startUpdatingLocation() { callCount += 1 updateLocation() } func updateLocation() { let mockLocation = CLLocation(latitude: CLLocationDegrees(52.51631), longitude: CLLocationDegrees(13.37777)) let mockLocationManager = CLLocationManager() delegate?.locationManager(mockLocationManager, didUpdateLocations: [mockLocation]) } } ================================================ FILE: LooLocatorTests/LocationProviderTests.swift ================================================ // // LooLocatorTests.swift // LooLocatorTests // // Created by Sam Khawase on 14.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import XCTest import Quick import Nimble class LocationProviderTests: QuickSpec { override func spec() { describe("Given a LocationProvider") { context("When it's started with LocationManager", closure: { // Arrange let mockLocationManager = MockLocationManager() let mockLocationObservable = MockLocationObservable() let locationProvider: LocationProvidable = LocationProvider(locationManager: mockLocationManager) beforeEach { mockLocationManager.callCount = 0 // Arrange locationProvider.setListener(listener: mockLocationObservable) //Act locationProvider.startLocationUpdates() } it("then starts location updates", closure: { //Assert expect(mockLocationManager.callCount).toEventually(equal(4)) expect(mockLocationObservable.coordinates).toEventuallyNot(beNil()) expect(mockLocationObservable.coordinates?.0).toEventually(equal(52.51631)) expect(mockLocationObservable.coordinates?.1).toEventually(equal(13.37777)) }) it("then provides current location", closure: { // Act let (lat, lon) = locationProvider.getCurrentLocation() //Assert expect(lat).to(equal(52.51631)) expect(lon).to(equal(13.37777)) }) }) } } } ================================================ FILE: LooLocatorTests/MapViewModelTests.swift ================================================ // // MapViewModelTests.swift // LooLocatorTests // // Created by Sam Khawase on 23.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import XCTest import Quick import Nimble import CoreLocation import OHHTTPStubs class MapViewModelTests: QuickSpec { override func spec() { describe("Given a MapViewModel") { var viewModel: MapViewModel? beforeEach { let mockLocationProvider = MockLocationProvider() let mockAmenityRequest = MockAmenityRequest() let mockViewController = MockViewController() viewModel = MapViewModel(locationProvider: mockLocationProvider, amenityRequest:mockAmenityRequest, listener:mockViewController) } it("get current location", closure: { if let (lat, lon) = viewModel?.getCurrentLocation() { expect(lat).to(equal(52.51631)) expect(lon).to(equal(13.37777)) } }) it("should get all amenities in range", closure: { var successFlag = false var locations: [Location] = [] viewModel?.getAmenities(in: 1000, type: AmenityType.Toilets) { result in switch result { case .success(let _locations): locations = _locations successFlag = true case .failure(_): successFlag = false } } //Assert expect(successFlag).toEventuallyNot(beFalse()) expect(locations).toEventuallyNot(beEmpty()) expect(locations.first?.id).toEventually(equal(66917214)) expect(locations.first?.title).toEventually(equal("City Toilette")) expect(locations.first?.coordinates.0).toEventually(equal(52.5168607)) expect(locations.first?.coordinates.1).toEventually(equal(13.3829509)) }) } } } ================================================ FILE: LooLocatorTests/MockViewController.swift ================================================ // // MockViewController.swift // LooLocatorTests // // Created by Sam Khawase on 23.02.20. // Copyright © 2020 LooLocator. All rights reserved. // import Foundation class MockViewController: MapViewModelObservable { func centerMapToCurrentLocation(latitude: Double, longitude: Double) { } func setCurrentLocation(latitude: Double, longitude: Double) {} func addAmenityToMap(amenity: Location) {} typealias Amenity = Location } ================================================ FILE: LooLocatorTests/OSMModelTests.swift ================================================ // // OSMModelTests.swift // LooLocatorTests // // Created by Sam Khawase on 20.10.20. // Copyright © 2020 LooLocator. All rights reserved. // import Foundation import Quick import Nimble class OSMModelTests: QuickSpec { override func spec() { var jsonData: Data? beforeEach { let currentBundle = Bundle(for: type(of: self)) guard let fileURL = currentBundle.url(forResource: "stubbedRepsonse", withExtension: "json"), let fileContents = try? String(contentsOf: fileURL), let _jsonData = fileContents.data(using: .utf8) else { preconditionFailure("Could not find expected file in test bundle") } jsonData = _jsonData } context("Given a JSON data set") { describe("When loading data from json") { it("should parse the json to Location Model correctly") { // Arrange let decoder = JSONDecoder() // Act let locations = try? decoder.decode(OSMData.self, from: jsonData!) // Assert expect(locations).toNot(beNil()) } } } } } ================================================ FILE: LooLocatorTests/stubbedRepsonse.json ================================================ { "version": 0.6, "generator": "Overpass API 0.7.54.12 054bb0bb", "osm3s": { "timestamp_osm_base": "2018-02-18T20:06:02Z", "copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL." }, "elements": [ { "type": "node", "id": 66917214, "lat": 52.5168607, "lon": 13.3829509, "tags": { "amenity": "toilets", "fee": "yes", "name": "City Toilette", "toilets:wheelchair": "yes", "wheelchair": "yes" } }, { "type": "node", "id": 71180687, "lat": 52.5175448, "lon": 13.3738695, "tags": { "amenity": "toilets", "fee": "yes", "toilets:wheelchair": "yes", "wheelchair": "yes", "wheelchair:description": "Euro-Schlüssel notwendig" } }, { "type": "node", "id": 304575905, "lat": 52.5163769, "lon": 13.3755227, "tags": { "amenity": "toilets", "fee": "yes", "name": "City Toilette", "opening_hours": "24/7", "toilets:wheelchair": "yes", "wheelchair": "yes" } }, { "type": "node", "id": 599913127, "lat": 52.5081387, "lon": 13.3744541, "tags": { "amenity": "toilets", "layer": "-1", "operator": "unknown", "wheelchair": "yes" } }, { "type": "node", "id": 927092067, "lat": 52.5141833, "lon": 13.3914532, "tags": { "amenity": "toilets", "centralkey": "eurokey", "fee": "yes", "name": "City Toilette", "opening_hours": "24/7", "operator": "Wall", "toilets:wheelchair": "yes", "wheelchair": "yes", "wheelchair:description": "Wall City Toilette" } }, { "type": "node", "id": 1453830466, "lat": 52.5226503, "lon": 13.3720210, "tags": { "amenity": "toilets", "wheelchair": "no" } }, { "type": "node", "id": 1453886966, "lat": 52.5225436, "lon": 13.3708178, "tags": { "amenity": "toilets", "wheelchair": "no" } }, { "type": "node", "id": 2351691628, "lat": 52.5165754, "lon": 13.3811272, "tags": { "amenity": "toilets", "fee": "yes", "name": "City Toilette", "toilets:wheelchair": "yes", "wheelchair": "yes" } }, { "type": "node", "id": 2378807163, "lat": 52.5207250, "lon": 13.3875590, "tags": { "amenity": "toilets", "diaper": "yes", "fee": "yes", "female": "yes", "male": "yes", "name": "Bahnhof Friedrichstraße", "opening_hours": "24/7", "wheelchair": "yes" } }, { "type": "node", "id": 3338458355, "lat": 52.5106351, "lon": 13.3830226, "tags": { "access": "customers", "amenity": "toilets", "fee": "yes", "toilets:disposal": "flush", "toilets:position": "seated", "wheelchair": "yes" } }, { "type": "node", "id": 3338459657, "lat": 52.5183901, "lon": 13.3890855, "tags": { "access": "customers", "amenity": "toilets", "fee": "no", "toilets:disposal": "flush", "toilets:position": "seated", "wheelchair": "yes" } }, { "type": "node", "id": 4003667897, "lat": 52.5203163, "lon": 13.3876880, "tags": { "access": "public", "amenity": "toilets", "fee": "yes", "indoor": "yes", "level": "-1", "operator": "Sanifair", "wheelchair": "yes" } }, { "type": "node", "id": 5022093422, "lat": 52.5191343, "lon": 13.3646383, "tags": { "amenity": "toilets" } } ] } ================================================ FILE: README.md ================================================ # LooLocator Find Amenities (*like toilets*) near you! The simple iOS fetches the crowd-sourced data from OpenStreetMap, and shows toilets within walking distance. User can then use AppleMaps to find walking directions to the amenity. ## Design Rationale The following series of bite-sized posts explain the design rationale behind creating the app: 1. [Part 1: Introduction - Writing a modular, and testable iOS App in Swift using MVVM pattern](https://samkhawase.com/blog/mvvm_swift_introduction/) 2. [Part 2: Defining the Data Model](https://samkhawase.com/blog/mvvm_swift_model/) 3. [Part 3: The Location provider](https://samkhawase.com/blog/mvvm_swift_location_provider/) 4. [Part 4: Defining the networking layer](https://samkhawase.com/blog/mvvm_swift_networking/) 5. [Part 5: The ViewModel](https://samkhawase.com/blog/mvvm_swift_view_model/) 6. [Part 6: The Final App - Putting it all together](https://samkhawase.com/blog/mvvm_swift_final_app/) ## Getting Started Here are the steps to get started with the project on your local machine: 1. Clone the git repositiory 2. Run `carthage update --platform iOS --cache-builds --no-use-binaries` to fetch the dependencies. 3. If running on the simulator, you can edit the scheme and set the simulated location in Xcode. (*E.g. Hongkong*) 4. Run the project via Xcode. ### Prerequisites What things you need to install the software and how to install them 1. Mac OS X 2. Xcode 9 3. [Carthage](https://github.com/Carthage/Carthage) 4. Optional: [xcpretty](https://github.com/supermarin/xcpretty) ## Running the tests The app uses BDD style tests using Quick and Nimble. There are unit tests written to test the LocationManager, APIClient (*with Network mocks*), and ViewModel behaviors. To run the test, enter the command on the command line. ``` xcodebuild -scheme 'LooLocator' \ -sdk iphonesimulator \ -configuration Debug \ -destination 'platform=iOS Simulator,name=iPhone 6s,OS=latest' \ test | xcpretty ``` The output will be similar to ``` Test Suite LooLocatorTests.xctest started ApiClientTests ✓ Amenity_Request_tests__should_fetch_amenities (0.027 seconds) LocationProviderTests ✓ Given_a_LocationProvider__When_it_s_started_with_LocationManager__then_starts_location_updates (1.547 seconds) ✓ Given_a_LocationProvider__When_it_s_started_with_LocationManager__then_provides_current_location (0.001 seconds) MapViewModelTests ✓ Given_a_MapViewModel__get_current_location (0.002 seconds) ✓ Given_a_MapViewModel__should_get_all_amenities_in_range (0.004 seconds) ``` ## License This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details ## Acknowledgments * This project is inspired by the MapKit article from [RayWenderlich](https://www.raywenderlich.com/160517/mapkit-tutorial-getting-started) * The [Overpass Turbo API](https://overpass-turbo.eu/) ================================================ FILE: berlin.gpx ================================================ 34.65