Implement changes for Day 69
This commit is contained in:
parent
8c5c9844c1
commit
37f2923e70
|
@ -13,7 +13,7 @@ For each day that proves conducive to some kind of code or content, I'll make a
|
|||
|
||||
### Personal Note
|
||||
|
||||
I'm currently seeking freelance, remote opportunities as an iOS developer! If you're looking for an experienced software engineer who's been diving deep into SwiftUI, Combine, and iOS 13 since WWDC (and who prefers being paid in Bitcoin 🙂), and you could use some help with any of those things, please feel free to [reach out](mailto:CypherPoet@gmail.com) ✌️.
|
||||
I'm currently seeking freelance, remote opportunities as an iOS developer! If you're looking for an experienced software engineer who's been diving deep into SwiftUI, Combine, and iOS 13 since WWDC (and who relishes being paid in Bitcoin 🙂), and you could use some help with any of those things, please feel free to [reach out](mailto:CypherPoet@gmail.com) ✌️.
|
||||
|
||||
|
||||
## Days
|
||||
|
@ -74,10 +74,11 @@ I'm currently seeking freelance, remote opportunities as an iOS developer! If yo
|
|||
- **Day 65:** [_Project 13: Instafilter_ (Part Four)](./day-065/)
|
||||
- **Day 66:** [_Project 13: Instafilter_ (Part Five)](./day-066/)
|
||||
- **Day 67:** [_Project 13: Instafilter_ (Part Six)](./day-067/)
|
||||
- **Day 68:** [_Project 14: PlaceCase_ (Part One)](./day-068/)
|
||||
|
||||
</details>
|
||||
|
||||
- **Day 68:** [_Project 14: PlaceCase_ (Part One)](./day-068/)
|
||||
- **Day 69:** [_Project 14: PlaceCase_ (Part Two)](./day-069/)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -10,10 +10,20 @@
|
|||
F36D28F023987DE700095B66 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36D28EF23987DE700095B66 /* AppDelegate.swift */; };
|
||||
F36D28F223987DE700095B66 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36D28F123987DE700095B66 /* SceneDelegate.swift */; };
|
||||
F36D28F523987DE700095B66 /* PlaceCase.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F36D28F323987DE700095B66 /* PlaceCase.xcdatamodeld */; };
|
||||
F36D28F723987DE700095B66 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36D28F623987DE700095B66 /* ContentView.swift */; };
|
||||
F36D28F923987DE900095B66 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F36D28F823987DE900095B66 /* Assets.xcassets */; };
|
||||
F36D28FC23987DE900095B66 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F36D28FB23987DE900095B66 /* Preview Assets.xcassets */; };
|
||||
F36D28FF23987DE900095B66 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F36D28FD23987DE900095B66 /* LaunchScreen.storyboard */; };
|
||||
F36D295F239A071F00095B66 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36D295E239A071F00095B66 /* CoreDataManager.swift */; };
|
||||
F36D2965239A0ACA00095B66 /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36D2963239A0ACA00095B66 /* MapView.swift */; };
|
||||
F36D2966239A0ACA00095B66 /* MapView+Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36D2964239A0ACA00095B66 /* MapView+Coordinator.swift */; };
|
||||
F374DC0A239A431E0016CC65 /* LocationCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F374DC09239A431E0016CC65 /* LocationCollectionView.swift */; };
|
||||
F374DC14239A4B2C0016CC65 /* Location+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F374DC12239A4B2B0016CC65 /* Location+CoreDataClass.swift */; };
|
||||
F374DC15239A4B2C0016CC65 /* Location+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F374DC13239A4B2B0016CC65 /* Location+CoreDataProperties.swift */; };
|
||||
F374DC18239A4B790016CC65 /* LocationCollection+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F374DC16239A4B790016CC65 /* LocationCollection+CoreDataClass.swift */; };
|
||||
F374DC19239A4B790016CC65 /* LocationCollection+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F374DC17239A4B790016CC65 /* LocationCollection+CoreDataProperties.swift */; };
|
||||
F374DC1C239A57620016CC65 /* Annotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F374DC1B239A57620016CC65 /* Annotations.swift */; };
|
||||
F374DC36239B96780016CC65 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F374DC35239B96770016CC65 /* AuthenticationService.swift */; };
|
||||
F3BF5BE1239BA2E900B9F8D8 /* LocationCollectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BF5BE0239BA2E900B9F8D8 /* LocationCollectionViewModel.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
@ -21,11 +31,21 @@
|
|||
F36D28EF23987DE700095B66 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
F36D28F123987DE700095B66 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
F36D28F423987DE700095B66 /* PlaceCase.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = PlaceCase.xcdatamodel; sourceTree = "<group>"; };
|
||||
F36D28F623987DE700095B66 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
F36D28F823987DE900095B66 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
F36D28FB23987DE900095B66 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
F36D28FE23987DE900095B66 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
F36D290023987DE900095B66 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
F36D295E239A071F00095B66 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = "<group>"; };
|
||||
F36D2963239A0ACA00095B66 /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = "<group>"; };
|
||||
F36D2964239A0ACA00095B66 /* MapView+Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MapView+Coordinator.swift"; sourceTree = "<group>"; };
|
||||
F374DC09239A431E0016CC65 /* LocationCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCollectionView.swift; sourceTree = "<group>"; };
|
||||
F374DC12239A4B2B0016CC65 /* Location+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Location+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
F374DC13239A4B2B0016CC65 /* Location+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Location+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
F374DC16239A4B790016CC65 /* LocationCollection+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LocationCollection+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
F374DC17239A4B790016CC65 /* LocationCollection+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LocationCollection+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
F374DC1B239A57620016CC65 /* Annotations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Annotations.swift; sourceTree = "<group>"; };
|
||||
F374DC35239B96770016CC65 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
|
||||
F3BF5BE0239BA2E900B9F8D8 /* LocationCollectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCollectionViewModel.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -58,14 +78,15 @@
|
|||
F36D28EE23987DE700095B66 /* PlaceCase */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F36D2960239A093800095B66 /* Reusables */,
|
||||
F36D290023987DE900095B66 /* Info.plist */,
|
||||
F36D28EF23987DE700095B66 /* AppDelegate.swift */,
|
||||
F36D28F123987DE700095B66 /* SceneDelegate.swift */,
|
||||
F36D28F623987DE700095B66 /* ContentView.swift */,
|
||||
F36D28F823987DE900095B66 /* Assets.xcassets */,
|
||||
F36D295A239A04B800095B66 /* Data */,
|
||||
F36D28FD23987DE900095B66 /* LaunchScreen.storyboard */,
|
||||
F36D290023987DE900095B66 /* Info.plist */,
|
||||
F36D28F323987DE700095B66 /* PlaceCase.xcdatamodeld */,
|
||||
F36D28FA23987DE900095B66 /* Preview Content */,
|
||||
F36D295B239A04BC00095B66 /* Resources */,
|
||||
F36D295C239A04C300095B66 /* Scenes */,
|
||||
);
|
||||
path = PlaceCase;
|
||||
sourceTree = "<group>";
|
||||
|
@ -73,11 +94,108 @@
|
|||
F36D28FA23987DE900095B66 /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F374DC1A239A57470016CC65 /* SampleData */,
|
||||
F36D28FB23987DE900095B66 /* Preview Assets.xcassets */,
|
||||
);
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F36D295A239A04B800095B66 /* Data */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F374DC0F239A4ACA0016CC65 /* Models */,
|
||||
F36D28F323987DE700095B66 /* PlaceCase.xcdatamodeld */,
|
||||
F36D295E239A071F00095B66 /* CoreDataManager.swift */,
|
||||
);
|
||||
path = Data;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F36D295B239A04BC00095B66 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F36D28F823987DE900095B66 /* Assets.xcassets */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F36D295C239A04C300095B66 /* Scenes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F374DC08239A42C90016CC65 /* Location Collection */,
|
||||
);
|
||||
path = Scenes;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F36D2960239A093800095B66 /* Reusables */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F36D2961239A093D00095B66 /* Views */,
|
||||
F374DC35239B96770016CC65 /* AuthenticationService.swift */,
|
||||
);
|
||||
path = Reusables;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F36D2961239A093D00095B66 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F36D2962239A0A6900095B66 /* MapView */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F36D2962239A0A6900095B66 /* MapView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F36D2963239A0ACA00095B66 /* MapView.swift */,
|
||||
F36D2964239A0ACA00095B66 /* MapView+Coordinator.swift */,
|
||||
);
|
||||
path = MapView;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F374DC08239A42C90016CC65 /* Location Collection */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F374DC09239A431E0016CC65 /* LocationCollectionView.swift */,
|
||||
F3BF5BE0239BA2E900B9F8D8 /* LocationCollectionViewModel.swift */,
|
||||
);
|
||||
path = "Location Collection";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F374DC0F239A4ACA0016CC65 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F374DC11239A4AEC0016CC65 /* Location */,
|
||||
F374DC10239A4AE00016CC65 /* Location Collection */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F374DC10239A4AE00016CC65 /* Location Collection */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F374DC16239A4B790016CC65 /* LocationCollection+CoreDataClass.swift */,
|
||||
F374DC17239A4B790016CC65 /* LocationCollection+CoreDataProperties.swift */,
|
||||
);
|
||||
path = "Location Collection";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F374DC11239A4AEC0016CC65 /* Location */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F374DC12239A4B2B0016CC65 /* Location+CoreDataClass.swift */,
|
||||
F374DC13239A4B2B0016CC65 /* Location+CoreDataProperties.swift */,
|
||||
);
|
||||
path = Location;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F374DC1A239A57470016CC65 /* SampleData */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F374DC1B239A57620016CC65 /* Annotations.swift */,
|
||||
);
|
||||
path = SampleData;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
@ -149,10 +267,20 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F374DC18239A4B790016CC65 /* LocationCollection+CoreDataClass.swift in Sources */,
|
||||
F36D28F023987DE700095B66 /* AppDelegate.swift in Sources */,
|
||||
F36D28F223987DE700095B66 /* SceneDelegate.swift in Sources */,
|
||||
F36D28F723987DE700095B66 /* ContentView.swift in Sources */,
|
||||
F36D2965239A0ACA00095B66 /* MapView.swift in Sources */,
|
||||
F374DC19239A4B790016CC65 /* LocationCollection+CoreDataProperties.swift in Sources */,
|
||||
F36D2966239A0ACA00095B66 /* MapView+Coordinator.swift in Sources */,
|
||||
F374DC1C239A57620016CC65 /* Annotations.swift in Sources */,
|
||||
F374DC14239A4B2C0016CC65 /* Location+CoreDataClass.swift in Sources */,
|
||||
F374DC15239A4B2C0016CC65 /* Location+CoreDataProperties.swift in Sources */,
|
||||
F3BF5BE1239BA2E900B9F8D8 /* LocationCollectionViewModel.swift in Sources */,
|
||||
F36D295F239A071F00095B66 /* CoreDataManager.swift in Sources */,
|
||||
F36D28F523987DE700095B66 /* PlaceCase.xcdatamodeld in Sources */,
|
||||
F374DC36239B96780016CC65 /* AuthenticationService.swift in Sources */,
|
||||
F374DC0A239A431E0016CC65 /* LocationCollectionView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -13,12 +13,14 @@ import CoreData
|
|||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
CoreDataManager.shared.setup()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// MARK: UISceneSession Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
|
@ -27,56 +29,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
|
||||
// MARK: - Core Data stack
|
||||
|
||||
lazy var persistentContainer: NSPersistentContainer = {
|
||||
/*
|
||||
The persistent container for the application. This implementation
|
||||
creates and returns a container, having loaded the store for the
|
||||
application to it. This property is optional since there are legitimate
|
||||
error conditions that could cause the creation of the store to fail.
|
||||
*/
|
||||
let container = NSPersistentContainer(name: "PlaceCase")
|
||||
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
||||
if let error = error as NSError? {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
|
||||
/*
|
||||
Typical reasons for an error here include:
|
||||
* The parent directory does not exist, cannot be created, or disallows writing.
|
||||
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
|
||||
* The device is out of space.
|
||||
* The store could not be migrated to the current model version.
|
||||
Check the error message to determine what the actual problem was.
|
||||
*/
|
||||
fatalError("Unresolved error \(error), \(error.userInfo)")
|
||||
}
|
||||
})
|
||||
return container
|
||||
}()
|
||||
|
||||
// MARK: - Core Data Saving support
|
||||
|
||||
func saveContext () {
|
||||
let context = persistentContainer.viewContext
|
||||
if context.hasChanges {
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
let nserror = error as NSError
|
||||
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by Brian Sipple on 12/4/19.
|
||||
// Copyright © 2019 CypherPoet. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
Text("Hello, World!")
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
//
|
||||
// CoreDataManager.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by CypherPoet on 12/5/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
|
||||
final class CoreDataManager {
|
||||
typealias LoadCompletionHandler = (() -> Void)
|
||||
|
||||
private var storeType: String = NSSQLiteStoreType
|
||||
|
||||
|
||||
// MARK: - PersistentContainer
|
||||
lazy var persistentContainer: NSPersistentContainer = {
|
||||
let container = NSPersistentContainer(name: ManagedObjectModelName.application)
|
||||
let description = container.persistentStoreDescriptions.first
|
||||
|
||||
description?.type = storeType
|
||||
|
||||
return container
|
||||
}()
|
||||
|
||||
|
||||
// MARK: - Managed Object Contexts
|
||||
lazy var backgroundContext: NSManagedObjectContext = {
|
||||
let context = self.persistentContainer.newBackgroundContext()
|
||||
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
|
||||
return context
|
||||
}()
|
||||
|
||||
|
||||
lazy var mainContext: NSManagedObjectContext = {
|
||||
let context = self.persistentContainer.viewContext
|
||||
context.automaticallyMergesChangesFromParent = true
|
||||
|
||||
return context
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private Methods
|
||||
extension CoreDataManager {
|
||||
|
||||
private func loadPersistentStore(then completionHandler: @escaping LoadCompletionHandler) {
|
||||
persistentContainer.loadPersistentStores { description, error in
|
||||
guard error == nil else {
|
||||
fatalError("Error while loading store: \(error!)")
|
||||
}
|
||||
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Public Methods
|
||||
extension CoreDataManager {
|
||||
|
||||
func setup(storeType: String = NSSQLiteStoreType, then completionHandler: LoadCompletionHandler? = nil) {
|
||||
self.storeType = storeType
|
||||
|
||||
loadPersistentStore() {
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func save(_ context: NSManagedObjectContext) {
|
||||
context.performAndWait {
|
||||
if context.hasChanges {
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
// TODO: Better error handling would be needed here for a production app
|
||||
let nsError = error as NSError
|
||||
print("Error while attempting to save Core Data context named: \"\(context.name ?? "")\": \(nsError), \(nsError.userInfo)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func saveContexts() {
|
||||
[mainContext, backgroundContext].forEach(save(_:))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension CoreDataManager {
|
||||
static let shared = CoreDataManager()
|
||||
}
|
||||
|
||||
|
||||
extension CoreDataManager {
|
||||
enum ManagedObjectModelName {
|
||||
static let application = "PlaceCase"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// LocationCollection+CoreDataClass.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by Brian Sipple on 12/6/19.
|
||||
// Copyright © 2019 CypherPoet. All rights reserved.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(LocationCollection)
|
||||
public class LocationCollection: NSManagedObject {
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// LocationCollection+CoreDataProperties.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by Brian Sipple on 12/6/19.
|
||||
// Copyright © 2019 CypherPoet. All rights reserved.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
|
||||
extension LocationCollection {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<LocationCollection> {
|
||||
return NSFetchRequest<LocationCollection>(entityName: "LocationCollection")
|
||||
}
|
||||
|
||||
@NSManaged public var name: String?
|
||||
@NSManaged public var locations: NSSet?
|
||||
}
|
||||
|
||||
|
||||
// MARK: Generated accessors for locations
|
||||
extension LocationCollection {
|
||||
|
||||
@objc(addLocationsObject:)
|
||||
@NSManaged public func addToLocations(_ value: Location)
|
||||
|
||||
@objc(removeLocationsObject:)
|
||||
@NSManaged public func removeFromLocations(_ value: Location)
|
||||
|
||||
@objc(addLocations:)
|
||||
@NSManaged public func addToLocations(_ values: NSSet)
|
||||
|
||||
@objc(removeLocations:)
|
||||
@NSManaged public func removeFromLocations(_ values: NSSet)
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// Location+CoreDataClass.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by Brian Sipple on 12/6/19.
|
||||
// Copyright © 2019 CypherPoet. All rights reserved.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(Location)
|
||||
public class Location: NSManagedObject {
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Location+CoreDataProperties.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by Brian Sipple on 12/6/19.
|
||||
// Copyright © 2019 CypherPoet. All rights reserved.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import CoreLocation
|
||||
|
||||
|
||||
extension Location {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<Location> {
|
||||
return NSFetchRequest<Location>(entityName: "Location")
|
||||
}
|
||||
|
||||
@NSManaged public var name: String?
|
||||
@NSManaged public var latitude: Double
|
||||
@NSManaged public var longitude: Double
|
||||
@NSManaged public var placemark: CLPlacemark?
|
||||
@NSManaged public var locationDescription: String?
|
||||
@NSManaged public var collection: LocationCollection?
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15508" systemVersion="19B88" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Location" representedClassName="Location" syncable="YES">
|
||||
<attribute name="latitude" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="locationDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="longitude" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="placemark" optional="YES" attributeType="Transformable" customClassName="CLPlacemark"/>
|
||||
<relationship name="collection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LocationCollection" inverseName="locations" inverseEntity="LocationCollection"/>
|
||||
</entity>
|
||||
<entity name="LocationCollection" representedClassName="LocationCollection" syncable="YES">
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<relationship name="locations" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Location" inverseName="collection" inverseEntity="Location"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="LocationCollection" positionX="-63" positionY="-18" width="128" height="73"/>
|
||||
<element name="Location" positionX="-54" positionY="9" width="128" height="133"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -49,6 +49,8 @@
|
|||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>This app would like to use authentication to save the locations you create and list.</string>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
|
||||
<elements/>
|
||||
</model>
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// Annotations.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by CypherPoet on 12/6/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
#if DEBUG
|
||||
|
||||
import Foundation
|
||||
import MapKit
|
||||
|
||||
|
||||
enum SampleData {}
|
||||
|
||||
|
||||
extension SampleData {
|
||||
|
||||
enum Annotations {
|
||||
|
||||
static let santorini: MKPointAnnotation = {
|
||||
let annotation = MKPointAnnotation()
|
||||
|
||||
annotation.title = "Santorini"
|
||||
annotation.subtitle = "An an island in the southern Aegean Sea, speculated to be the inspiration for the city of Atlantis."
|
||||
annotation.coordinate = CLLocationCoordinate2D(latitude: 36.416667, longitude: 25.433333)
|
||||
|
||||
return annotation
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif
|
|
@ -0,0 +1,104 @@
|
|||
//
|
||||
// AuthenticationService.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by CypherPoet on 12/7/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import LocalAuthentication
|
||||
|
||||
|
||||
/// Protocol describing some core functionality of the `LAContext` class
|
||||
protocol LAContextType {
|
||||
init()
|
||||
|
||||
func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool
|
||||
func evaluatePolicy(_ policy: LAPolicy, localizedReason: String, reply: @escaping (Bool, Error?) -> Void)
|
||||
|
||||
}
|
||||
|
||||
extension LAContext: LAContextType {}
|
||||
|
||||
|
||||
|
||||
protocol AuthenticatingService {
|
||||
|
||||
/// - Parameter reason: The app-provided reason for requesting authentication,
|
||||
/// which displays in the authentication dialog presented to the user.
|
||||
func authenticate(reason: String) -> AnyPublisher<Void, Error>
|
||||
}
|
||||
|
||||
|
||||
|
||||
final class AuthenticationService: AuthenticatingService {
|
||||
static let authReason = "Please authenticate to unlock this app and access saved locations."
|
||||
|
||||
enum Error: Swift.Error {
|
||||
case noBiometricsEnabled(Swift.Error?)
|
||||
case evaluationFailed(Swift.Error?)
|
||||
}
|
||||
|
||||
|
||||
private let laContextType: LAContextType.Type
|
||||
private var context: LAContextType?
|
||||
|
||||
|
||||
init(laContextType: LAContextType.Type) {
|
||||
self.laContextType = laContextType
|
||||
}
|
||||
|
||||
|
||||
func authenticate(reason: String) -> AnyPublisher<Void, Swift.Error> {
|
||||
let context = laContextType.init()
|
||||
self.context = context
|
||||
|
||||
var error: NSError?
|
||||
|
||||
guard context.canEvaluatePolicy(
|
||||
.deviceOwnerAuthenticationWithBiometrics,
|
||||
error: &error
|
||||
) else {
|
||||
defer { self.context = nil }
|
||||
|
||||
return Fail(error: Error.noBiometricsEnabled(error))
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
return Future { promise in
|
||||
context.evaluatePolicy(
|
||||
.deviceOwnerAuthenticationWithBiometrics,
|
||||
localizedReason: reason
|
||||
) { (wasSuccessful, error) in
|
||||
defer { self.context = nil }
|
||||
|
||||
guard wasSuccessful else {
|
||||
promise(.failure(Error.evaluationFailed(error)))
|
||||
return
|
||||
}
|
||||
|
||||
promise(.success(()))
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if DEBUG
|
||||
|
||||
extension SampleData {
|
||||
|
||||
class AuthService: AuthenticatingService {
|
||||
func authenticate(reason: String) -> AnyPublisher<Void, Error> {
|
||||
Empty().eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// MapView +Coordinator.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by CypherPoet on 12/5/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import MapKit
|
||||
|
||||
|
||||
extension MapView {
|
||||
|
||||
class Coordinator: NSObject {
|
||||
enum ReuseIdentifier {
|
||||
static let pinAnnotation = "Location List Pin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension MapView.Coordinator {
|
||||
|
||||
func configure(_ annotationView: MKAnnotationView) {
|
||||
annotationView.canShowCallout = true
|
||||
annotationView.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - MKMapViewDelegate
|
||||
extension MapView.Coordinator: MKMapViewDelegate {
|
||||
|
||||
func mapView(
|
||||
_ mapView: MKMapView,
|
||||
viewFor annotation: MKAnnotation
|
||||
) -> MKAnnotationView? {
|
||||
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(
|
||||
withIdentifier: ReuseIdentifier.pinAnnotation
|
||||
)
|
||||
?? MKPinAnnotationView(annotation: annotation, reuseIdentifier: ReuseIdentifier.pinAnnotation)
|
||||
|
||||
|
||||
configure(annotationView)
|
||||
|
||||
return annotationView
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
//
|
||||
// MapView.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by CypherPoet on 12/5/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
|
||||
struct MapView {
|
||||
var annotations: [MKPointAnnotation] = []
|
||||
}
|
||||
|
||||
|
||||
// MARK: - UIViewRepresentable
|
||||
extension MapView: UIViewRepresentable {
|
||||
|
||||
func makeCoordinator() -> MapView.Coordinator {
|
||||
Self.Coordinator()
|
||||
}
|
||||
|
||||
|
||||
func makeUIView(
|
||||
context: UIViewRepresentableContext<MapView>
|
||||
) -> MKMapView {
|
||||
let mapView = MKMapView()
|
||||
|
||||
mapView.addAnnotations(annotations)
|
||||
mapView.delegate = context.coordinator
|
||||
|
||||
return mapView
|
||||
}
|
||||
|
||||
|
||||
func updateUIView(
|
||||
_ mapView: MKMapView,
|
||||
context: UIViewRepresentableContext<MapView>
|
||||
) {
|
||||
// TODO
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Preview
|
||||
struct MapView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
MapView(
|
||||
annotations: [SampleData.Annotations.santorini]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -8,33 +8,43 @@
|
|||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import LocalAuthentication
|
||||
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
|
||||
// Get the managed object context from the shared persistent container.
|
||||
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
|
||||
|
||||
// Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
|
||||
// Add `@Environment(\.managedObjectContext)` in the views that will need the context.
|
||||
let contentView = ContentView().environment(\.managedObjectContext, context)
|
||||
let managedObjectContext = CoreDataManager.shared.mainContext
|
||||
|
||||
// Create the SwiftUI view that provides the window contents.
|
||||
let entryView = LocationCollectionView(
|
||||
viewModel: LocationCollectionViewModel(
|
||||
// authService: appState.authService // TODO: Use an `AppState` object and store the authService there.
|
||||
authService: AuthenticationService(laContextType: LAContext.self)
|
||||
)
|
||||
)
|
||||
.accentColor(.pink)
|
||||
.environment(\.managedObjectContext, managedObjectContext)
|
||||
|
||||
// Use a UIHostingController as window root view controller.
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.rootViewController = UIHostingController(rootView: contentView)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
window.rootViewController = UIHostingController(rootView: entryView)
|
||||
self.window = window
|
||||
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
// Called as the scene is being released by the system.
|
||||
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||
|
@ -61,11 +71,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
// Called as the scene transitions from the foreground to the background.
|
||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||
// to restore the scene back to its current state.
|
||||
|
||||
// Save changes in the application's managed object context when the application transitions to the background.
|
||||
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
|
||||
CoreDataManager.shared.saveContexts()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// LocationCollectionView.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by CypherPoet on 12/6/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
|
||||
struct LocationCollectionView: View {
|
||||
@ObservedObject private(set) var viewModel: LocationCollectionViewModel
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Body
|
||||
extension LocationCollectionView {
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if viewModel.isAuthenticated {
|
||||
MapView(annotations: [SampleData.Annotations.santorini])
|
||||
} else {
|
||||
Text("This is app is locked.")
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.onAppear(perform: viewModel.onAppear)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Computeds
|
||||
extension LocationCollectionView {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - View Variables
|
||||
extension LocationCollectionView {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Preview
|
||||
struct LocationCollectionView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
LocationCollectionView(
|
||||
viewModel: LocationCollectionViewModel(
|
||||
authService: SampleData.AuthService()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// LocationCollectionViewModel.swift
|
||||
// PlaceCase
|
||||
//
|
||||
// Created by CypherPoet on 12/7/19.
|
||||
// ✌️
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
|
||||
final class LocationCollectionViewModel: ObservableObject {
|
||||
private var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
private let authService: AuthenticatingService
|
||||
|
||||
|
||||
// MARK: - Published Properties
|
||||
@Published var isAuthenticated: Bool = false
|
||||
|
||||
|
||||
// MARK: - Init
|
||||
init(
|
||||
authService: AuthenticatingService
|
||||
) {
|
||||
self.authService = authService
|
||||
|
||||
setupSubscribers()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Publishers
|
||||
extension LocationCollectionViewModel {
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Computeds
|
||||
extension LocationCollectionViewModel {
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Public Methods
|
||||
extension LocationCollectionViewModel {
|
||||
|
||||
func onAppear() {
|
||||
authService
|
||||
.authenticate(reason: AuthenticationService.authReason)
|
||||
|
||||
// 📝 Attempting to use `receive(on:)` here will cause the event to be dropped.
|
||||
// This appears to be a bug: https://forums.swift.org/t/combine-receive-on-runloop-main-loses-sent-value-how-can-i-make-it-work/28631/47
|
||||
//
|
||||
// .receive(on: DispatchQueue.main)
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
print("Authentication failed: \(error)")
|
||||
self.isAuthenticated = false
|
||||
default:
|
||||
print("Authentication completed")
|
||||
}
|
||||
},
|
||||
receiveValue: {
|
||||
print("Authentication value received")
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.isAuthenticated = true
|
||||
}
|
||||
}
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Private Helpers
|
||||
private extension LocationCollectionViewModel {
|
||||
|
||||
func setupSubscribers() {
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue