diff --git a/GeofenceTester.xcodeproj/project.pbxproj b/GeofenceTester.xcodeproj/project.pbxproj index b158d03..816548d 100644 --- a/GeofenceTester.xcodeproj/project.pbxproj +++ b/GeofenceTester.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ CB268BA0285A19DC00B16C3A /* PersistantStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB268B9F285A19DC00B16C3A /* PersistantStorage.swift */; }; CB655F6628534E2600EBCABA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB655F6528534E2600EBCABA /* AppDelegate.swift */; }; CB655F6828534E2600EBCABA /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB655F6728534E2600EBCABA /* SceneDelegate.swift */; }; - CB655F6A28534E2600EBCABA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB655F6928534E2600EBCABA /* ViewController.swift */; }; CB655F6D28534E2600EBCABA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CB655F6B28534E2600EBCABA /* Main.storyboard */; }; CB655F6F28534E2700EBCABA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CB655F6E28534E2700EBCABA /* Assets.xcassets */; }; CB655F7228534E2700EBCABA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CB655F7028534E2700EBCABA /* LaunchScreen.storyboard */; }; @@ -25,7 +24,6 @@ CB655F6228534E2600EBCABA /* GeofenceTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GeofenceTester.app; sourceTree = BUILT_PRODUCTS_DIR; }; CB655F6528534E2600EBCABA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; CB655F6728534E2600EBCABA /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - CB655F6928534E2600EBCABA /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; CB655F6C28534E2600EBCABA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; CB655F6E28534E2700EBCABA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; CB655F7128534E2700EBCABA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -68,7 +66,6 @@ children = ( CB655F6528534E2600EBCABA /* AppDelegate.swift */, CB655F6728534E2600EBCABA /* SceneDelegate.swift */, - CB655F6928534E2600EBCABA /* ViewController.swift */, CB655F7D2856A43B00EBCABA /* RegionsListViewController.swift */, CBE711EE2868EE36008F51C4 /* RegionViewController.swift */, CBE711F2286920FD008F51C4 /* EventsTableViewController.swift */, @@ -153,7 +150,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CB655F6A28534E2600EBCABA /* ViewController.swift in Sources */, CBE711F1286915A8008F51C4 /* EventRecord.swift in Sources */, CB655F6628534E2600EBCABA /* AppDelegate.swift in Sources */, CBE711EF2868EE36008F51C4 /* RegionViewController.swift in Sources */, @@ -324,7 +320,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -352,7 +348,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; diff --git a/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0040.png b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0040.png new file mode 100644 index 0000000..10d9901 Binary files /dev/null and b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0040.png differ diff --git a/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0058.png b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0058.png new file mode 100644 index 0000000..c41b68a Binary files /dev/null and b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0058.png differ diff --git a/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0060.png b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0060.png new file mode 100644 index 0000000..3ed3165 Binary files /dev/null and b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0060.png differ diff --git a/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0080.png b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0080.png new file mode 100644 index 0000000..0636690 Binary files /dev/null and b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0080.png differ diff --git a/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0120-1.png b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0120-1.png new file mode 100644 index 0000000..269220a Binary files /dev/null and b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0120-1.png differ diff --git a/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0120.png b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0120.png new file mode 100644 index 0000000..269220a Binary files /dev/null and b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0120.png differ diff --git a/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0180.png b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0180.png new file mode 100644 index 0000000..2ce7552 Binary files /dev/null and b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0180.png differ diff --git a/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0187.png b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0187.png new file mode 100644 index 0000000..4e550b6 Binary files /dev/null and b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_0187.png differ diff --git a/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_1024.png b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_1024.png new file mode 100644 index 0000000..c19ce33 Binary files /dev/null and b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Compass_1024.png differ diff --git a/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Contents.json b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Contents.json index 5a3257a..43a254f 100644 --- a/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/GeofenceTester/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,41 +1,49 @@ { "images" : [ { + "filename" : "Compass_0040.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { + "filename" : "Compass_0060.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { + "filename" : "Compass_0058.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { + "filename" : "Compass_0187.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { + "filename" : "Compass_0080.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { + "filename" : "Compass_0120-1.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { + "filename" : "Compass_0120.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { + "filename" : "Compass_0180.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" @@ -70,6 +78,11 @@ "scale" : "2x", "size" : "40x40" }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, { "idiom" : "ipad", "scale" : "2x", @@ -81,6 +94,7 @@ "size" : "83.5x83.5" }, { + "filename" : "Compass_1024.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" diff --git a/GeofenceTester/Base.lproj/Main.storyboard b/GeofenceTester/Base.lproj/Main.storyboard index dd80962..15ffe81 100644 --- a/GeofenceTester/Base.lproj/Main.storyboard +++ b/GeofenceTester/Base.lproj/Main.storyboard @@ -8,76 +8,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -121,7 +51,7 @@ - + @@ -132,7 +62,7 @@ - + @@ -142,24 +72,22 @@ - - + + - - - - - - - - - - - - + + + + + + + + + + @@ -168,7 +96,7 @@ - + @@ -179,7 +107,7 @@ - + @@ -203,7 +131,7 @@ - + @@ -214,7 +142,7 @@ - + @@ -223,9 +151,6 @@ - - - diff --git a/GeofenceTester/EventsTableViewController.swift b/GeofenceTester/EventsTableViewController.swift index d7c35a7..ea224be 100644 --- a/GeofenceTester/EventsTableViewController.swift +++ b/GeofenceTester/EventsTableViewController.swift @@ -24,7 +24,7 @@ class EventsTableViewController: UITableViewController { } @IBAction func share() { - let text = storage.storage.reduce("") { partialResult, event in + let text = storage.objects.reduce("") { partialResult, event in partialResult + event.debugDescription + "\n" } let activityController = UIActivityViewController(activityItems: [text], applicationActivities: nil) @@ -38,14 +38,14 @@ class EventsTableViewController: UITableViewController { } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return storage.storage.count + return storage.objects.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Event Cell", for: indexPath) var config = cell.defaultContentConfiguration() - let event = storage.storage[indexPath.row] + let event = storage.objects[indexPath.row] config.text = event.debugDescription cell.contentConfiguration = config return cell diff --git a/GeofenceTester/PersistantStorage.swift b/GeofenceTester/PersistantStorage.swift index b242bea..fdf2530 100644 --- a/GeofenceTester/PersistantStorage.swift +++ b/GeofenceTester/PersistantStorage.swift @@ -7,11 +7,16 @@ import Foundation +protocol Storage { + associatedtype Object: Codable + var objects: Array { get } + func store(_ object: Object) throws +} +class PersistantStorage: Storage { + typealias Object = T -class PersistantStorage { - - public private(set) var storage = Array() + public private(set) var objects = Array() private let decoder = JSONDecoder() private let encoder = JSONEncoder() private let kFilename = "geolocator.log" @@ -21,7 +26,7 @@ class PersistantStorage { let url = try fileURL() let jsonData = try Data(contentsOf: url) // Well, it really should throw, but … - storage = try decoder.decode([T].self, from: jsonData) + objects = try decoder.decode([T].self, from: jsonData) } catch { // File does not exist @@ -30,14 +35,10 @@ class PersistantStorage { } func store(_ object: T) throws { - storage.append(object) + objects.append(object) try save() } - func retrieveAll() -> [T] { - return storage - } - private func fileURL() throws -> URL { let fm = FileManager.default var path: URL! @@ -55,7 +56,7 @@ class PersistantStorage { } private func save() throws { - let json = try encoder.encode(storage) + let json = try encoder.encode(objects) try json.write(to: fileURL()) } } diff --git a/GeofenceTester/RegionsListViewController.swift b/GeofenceTester/RegionsListViewController.swift index 28e5e7e..29eac2b 100644 --- a/GeofenceTester/RegionsListViewController.swift +++ b/GeofenceTester/RegionsListViewController.swift @@ -11,17 +11,29 @@ import os.log class RegionsListViewController: UIViewController { - var locationManager: CLLocationManager! - var storage: PersistantStorage! + var locationManager: CLLocationManager = CLLocationManager() + var storage = PersistantStorage() private var logger = Logger() @IBOutlet var mapView: MKMapView! override func viewDidLoad() { super.viewDidLoad() + + locationManager.delegate = self - self.title = "Monitored Regions" registerMapAnnotationViews() + + switch self.locationManager.authorizationStatus { + case .notDetermined: + self.locationManager.requestAlwaysAuthorization() + case .authorizedAlways: + self.mapView.showsUserLocation = true + mapView.setCenter(mapView.userLocation.coordinate, animated: true) + default: + let alert = UIAlertController(title: "No authorization", message: "This app requires the location services to be authorised", preferredStyle: .alert) + self.present(alert, animated: true) + } } override func viewDidAppear(_ animated: Bool) { @@ -34,7 +46,7 @@ class RegionsListViewController: UIViewController { forAnnotationViewWithReuseIdentifier: defaultReuseIdentifier) } - @IBAction func addRegion() { + @IBAction func addRegionAction() { let alertController = UIAlertController(title: "New Region", message: "Please enter a name", preferredStyle: .alert) alertController.addTextField() alertController.addAction(UIAlertAction(title: "Cancel", @@ -69,7 +81,9 @@ class RegionsListViewController: UIViewController { region.notifyOnExit = true self.locationManager.startMonitoring(for: region) - self.updateMap() + DispatchQueue.main.async { + self.updateMap() + } } })) @@ -86,9 +100,12 @@ class RegionsListViewController: UIViewController { var mapAnnotations = [MKAnnotation]() let existingAnnotations = mapView.annotations mapView.removeAnnotations(existingAnnotations) + mapView.removeOverlays(mapView.overlays) for region in regions { if let region = region as? CLCircularRegion { + let overlay = MKCircle(center: region.center, radius: region.radius) + mapView.addOverlay(overlay) let annotation = MKPointAnnotation() annotation.title = region.identifier annotation.coordinate = region.center @@ -98,7 +115,15 @@ class RegionsListViewController: UIViewController { mapView.addAnnotations(mapAnnotations) mapView.showAnnotations(mapAnnotations, animated: true) } - + + func handleError (_ error: Error) { + handleError(error.localizedDescription) + } + + func handleError (_ error: String) { + print (error) + } + // MARK: - Navigation // In a storyboard-based application, you will often want to do a little preparation before navigation @@ -142,11 +167,92 @@ extension RegionsListViewController: MKMapViewDelegate { return annotationView } + func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { + let renderer = MKCircleRenderer(overlay: overlay) + renderer.fillColor = UIColor.clear + renderer.strokeColor = UIColor.blue + renderer.lineWidth = 2 + return renderer + } + func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl){ self.performSegue(withIdentifier: "RegionDetail", sender: view.annotation) } +} +// MARK: - CLLocationManagerDelegate + +extension RegionsListViewController: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { + let date = Date() + let content = UNMutableNotificationContent() + content.title = "Region Entered" + content.body = "\(region.identifier) \(date)" + + let eventRecord = EventRecord(event: .ENTER, + identifier: region.identifier, + date: date) + do { + try storage.store(eventRecord) + } + catch let error { + self.handleError(error) + } + let request = UNNotificationRequest(identifier: region.identifier, + content: content, + trigger: nil) + UNUserNotificationCenter.current().add(request) + } + + func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { + let content = UNMutableNotificationContent() + content.title = "Region Exited" + content.body = "\(region.identifier) \(Date())" + + let eventRecord = EventRecord(event: .EXIT, + identifier: region.identifier, + date: Date()) + do { + try storage.store(eventRecord) + } + catch let error { + self.handleError(error) + } + let request = UNNotificationRequest(identifier: region.identifier, + content: content, + trigger: nil) + UNUserNotificationCenter.current().add(request) + } + + func locationManager( _ manager: CLLocationManager, + monitoringDidFailFor region: CLRegion?, + withError error: Error + ) { + self.handleError(error) + } + + func locationManager(_ manager: CLLocationManager, + didChangeAuthorization status: CLAuthorizationStatus) { + switch status { + case .restricted, .denied: + // Disable your app's location features + self.handleError("Warning: Location restricted or denied") + + case .authorizedWhenInUse: + // Enable your app's location features. + self.handleError("Warning: Location InUse Only") + + case .authorizedAlways: + self.mapView.showsUserLocation = true + // Enable or prepare your app's location features that can run any time. + + case .notDetermined: + self.handleError("Warning: Location Not Determined") + default: + self.handleError("Warning: Switch fell through") + } + } }