Im writing this little post like a reference for my self but if anyone else found it useful thats great suggestions are also Wellcome
So a little bit of context im rewriting my app from SwiftUI to SwiftUI 2 to use the new Multiplatform Apps I was using the UIViewRepresentable but I read a little about the new Map on Pauls post an want to give it a look sadly the example was very symple
So this is what I learned for the Apple Documentation an few hours of googling
Get user Location
To get the user location im gonna use this LocationFetcher class im learned from #100daysofswiftui but I had to do some twits to make it work on macOS
So the first thing you can notice is manager.requestWhenInUseAuthorization() is commented this is because Macs apps make the request on startUpdatingLocation() but make it work you have to go to the Signing & Capabilities of you macOS target and on the AppSandBox check location
Making the MapView
Make the map its really easy just like this
@State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
var body: some View {
Map(coordinateRegion: $region)
}
Now you can add more configurations like show the user location userTrackingMode: $trackingMode with showsUserLocation: true and allow user interactions interactionModes:.all
But the best for me is how easy you can show MapAnnotations
Show MapAnnotation
To do this just need a collection of data that conforms to Identifiable annotationItems: collection and have a coordinate property best of all I can use my CoreData model to do this
Just had to make it conforms to Identifiable
extension User: Identifiable { }
also I added the coordinate property
public var coordinate : CLLocationCoordinate2D {
let latitude = CLLocationDegrees(wrappedMap_cor_lat) ?? 41.158906
let longitude = CLLocationDegrees(wrappedMap_cor_log) ?? -74.255084
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
you can call it the way you like it
Next its just crearte a view for your annotation can be anything just a SwiftUI view
On my last post I added Core Data to my SwiftUI Multiplatform project, I a get some feedback on twitter and now I can show your how to add CloudKit to Synchronize data between your devices
How about in the App doing: @StateObject var coreData = CoreData(). And then in the body: ContentView().environment(.managedObjectContext, coreData.persistentContainer.viewContext). CoreData also needs to be an ObservableObject but that might be useful for container reloading.
next we are changing or var persistentContainer to be lazy and also to return a NSPersistentCloudKitContainer
...
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "yourModelDataName")
...
also we are adding the next unwrapper
...
guard let description = container.persistentStoreDescriptions.first else {
fatalError("No descriptions found")
}
description.setOption(true as NSObject, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
...
So this is what we have now
import CoreData
public class PersistentCloudKitContainer: ObservableObject {
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "ruleOfThree")
guard let description = container.persistentStoreDescriptions.first else {
fatalError("No descriptions found")
}
description.setOption(true as NSObject, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
NotificationCenter.default.addObserver(self , selector: #selector(processUpdate), name: .NSPersistentStoreRemoteChange, object: nil)
return container
}()
}
Next we are gonna make a little changes on or @main
import SwiftUI
import CoreData
@main
struct ruleOfThreeApp: App {
@StateObject var coreData = PersistentCloudKitContainer()
var body: some Scene {
WindowGroup {
ContentView().environment(\.managedObjectContext, coreData.persistentContainer.viewContext)
}
}
}
Cool now we can add CloudKit
Step 2
adding Cloud Kit
So now we go to our Project file and were we have our targets we chose iOS, then singning & Capabilities
Now on the button + capability we chose iCloud
Once added we select the CloudKit checkbox and then on containers we select our container if you don’t have one make a new one just write the name and its ready.
We make the same for macOs
Step 3
Now we go to our Data Modelo and under Configurations > Default we open the inspector on the lateral side an chose Use with CloudKit
And that’s all, we now can run or simulator and test, you need to login on the simulator with the same iCloud account
En mi entrada anterior agregué Core Data a mi App MultiPlaforma ahora veamos si podemos agregar CloudKit para que se sincronice entre nuestros diferentes dispositivos.
Si no has leído el articulo anterior seria bueno que le dieras una leída
Paso 1
En primer lugar tenemos que añadir CloudKit a nuestro proyecto
Para eso nos dirigimos al archivo de nuestro proyecto donde podemos ver nuestros targets
Seleccionamos nuestro target y luego singning & Capabilities
Ahora seleccionamos el botón + capability y elegimos iCloud
Una vez añadido seleccionamos la casilla de CloudKit y en Containers elegimos alguno que ya tengamos o creamos uno nuevo.
Lo mismo para iOS y nuestros demás targets (Exceptuando los test )
También es necesario que en iOS añadamos BackGround Modes
y marcamos Remote Notifications
Paso 2
Así que ahora en nuestra clase PersistentCloudKitContainer.swift vamos cambiar nuestro container
En nuestra clase PersistentCloudKitContainer
let container = NSPersistentContainer(name: "ruleOfThree")
Por este que funciona con CloudKit
let container = NSPersistentCloudKitContainer(name: "ruleOfThree")
y lo mismo en el valor que retorna
...
public static var persistentContainer: NSPersistentContainer = {
...
por este otro
...
public static var persistentContainer: NSPersistentCloudKitContainer = {
...
De forma que nuestra clase queda de la siguiente forma
import CoreData
public class PersistentCloudKitContainer {
// MARK: - Define Constants / Variables
public static var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
// MARK: - Initializer
private init() {}
// MARK: - Core Data stack
public static var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "ruleOfThree")
guard let description = container.persistentStoreDescriptions.first else {
fatalError("No descriptions found")
}
description.setOption(true as NSObject, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
// MARK: - Core Data Saving support
public static func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
Paso 3
Ahora el siguiente paso es dirigirnos a nuestro modelo de datos y marcar bajo Configurations > Default y en la vista del inspector marcamos used with CloudKit
Con eso ya podemos correr nuestros simuladores y probar sí están sincronizando. Para esto es necesario loggearnos en nuestros dos simuladores
Paso 4 (Extra)
Bien en este punto CloudKit y la sincronización ya funciona de manera correcta pero quiero añadir algunos ajustes extras para optimizar mis registros, evitar duplicados y poder reordenar los elementos.
así que regresamos a nuestra clase y añadimos el siguiente bloque de código
guard let description = container.persistentStoreDescriptions.first else {
fatalError("No descriptions found")
}
description.setOption(true as NSObject, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
Con este código podemos recibir notificaciones cuando algo cambie en nuestro modelo de datos
Ahora gracias a la recomendación de un profesional en twitter encontré una mejor forma de implementar la sincronización
How about in the App doing: @StateObject var coreData = CoreData(). And then in the body: ContentView().environment(.managedObjectContext, coreData.persistentContainer.viewContext). CoreData also needs to be an ObservableObject but that might be useful for container reloading.
Dub Dub is over and SwiftUI now rules, now it can be used to make full apps through the all ecosystem . But one thing missing on the first beta of Xcode 12 is the Core Data checkbox, it’s a bug? I don’t know, hope we get it back soon but this is how I added Core Data manually
Step 1
Make a new Multiplatform SwiftUI App, so on Xcode new Project> Multiplatform> App and new
This make an Multiplatform app for iOS, iPadOS and macOs new Multiplatform Apps don’t have AppDelegate and SceneDelegate
Step 2
Time to add or new data model, this was default when we make a core data app on iOS but here we have to added it manually.
so ⌘ + N and we chose Data Model from Core Data section
Don’t forget to check all our targets iOS and macOs also to add the DataModel to the shared Folder
Now we can make or data model like always
Im adding a constraints on name to avoid duplicated values
Step 3
make or SubClass for the data model, this is optional but let me create wrappers for the optionals Strings and Dates so is easy to handle on SwiftUI
First of all now forget to change codegen to manual/none to avoid have an error like this
this happens when we create the subclass but not tell to the data model we are handling it manual
So once we do that we go to editor menu and chose Editor > Create NSManagedObject Subclass
Now we chose or data model
and or entity
same way like we did it before we chose or targets and save the subclass on the shared folder
Voilà we have or subclass now we can handle the wrappers
to avoid optimal string we simply add a new public var
public var wname : String {
name ?? "No name"
}
and to make easy the relationship handling we add a new var when we put the relationship like and array
public var elementArray: [Element] {
let set = elements as? Set<Element> ?? []
return set.sorted {
$0.wName > $1.wName
}
}
we do the same on all others entity subclass
Step 4
Connect SwiftUI and CoreData
ok this is the important part, our apps is declared here full on swiftUI
import SwiftUI
@main
struct ruleOfThreeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
The new SwiftUI App is elegante and simple, so we are just adding what we need
import CoreData
let context = PersistentCloudKitContainer.persistentContainer.viewContext
Dub Dub ah llegado a su fin y SwiftUI ha evolucionado para convertirse en el rey de los frameworks, ahora podemos usarlo para crear apps a través de todo el ecosistema. Sin embargo la nueva arquitectura de las apps multi plataforma tiene un problema, hemos perdido la casilla de CoreData 😨, así que ahora tendremos que agregarlo a mano.
En este blog voy a documentar la manera de añadir CoreData a las nuevas apps multiplataforma de SwiftUI.
Paso 1
Creamos nuestra nueva app multi plataforma para eso solo seleccionamos crear nuevo proyecto, seleccionamos Multiplataforma > App y Nueva
Con eso tenemos una nueva aplicación multi plataforma que corre tanto en iOS como en MacOS
Así que aquí es donde comenzamos el hermoso nacimiento de una app multi plataforma, ahora vemos que necesitamos para añadir CoreData
Paso 2
Añadir nuestro nuevo modelo de datos, esto venia por default cuando creábamos un app con CoreData en iOS Anteriormente pero ahora tendremos que hacerlo a mano.
Para añadir nuestro modelo de datos solo presionamos ⌘ + N y elegimos Data Model en la sección de Core Data
Un punto importante al crear el modelo es poner dentro de Shared y elegir como targets tanto nuestros test como nuestras apps para iOS y macOS
Ahora creamos nuestro modelo de datos como siempre con sus entidades y relaciones
En este caso estoy haciendo una App para guardar reglas de tres
Nota: no olvides definir tus constraints para evitar que se te dupliquen tus valores
Paso 3
Crear las Clases de nuestro modelo de datos, esto no es algo obligatorio pero nos permite crear wrappers para no tener problemas con los optionals, principalmente en los Strings ademas nos permitirá agregar un mejor manejo de nuestras relaciones entre entidades, como explicare a continuación.
Primero que nada, no olvidemos en el inspector de la entidad definir que el codegen sera manual/none si no tendremos un error como el que se muestra a continuación
Esto ocurre cuando generamos nuestras subclases de manera manual y olvidamos decírselo al modelo de datos.
Una ves hecho eso, nos dirigimos al menu Editor > Create NSManagedObject Subclass
Seleccionamos nuestro modelo de datos
Seleccionamos las entidades de las cuales queremos crear su subclases
y de la misma forma que lo hicimos anteriormente nos aseguramos de que nuestros targets estén seleccionados y que la carpeta sea shared
Voilà con eso ya tenemos listas nuestras subclases ahora podemos deshacernos de los valores opcionales
Para eliminar los valores opcionales en las strings es tan fácilmente como crear una nueva variable
public var wname : String {
name ?? "No name"
}
y para facilitar el manejo de nuestra relaciones agregamos otra variable con un Array de nuestra relación
public var elementArray: [Element] {
let set = elements as? Set<Element> ?? []
return set.sorted {
$0.wName > $1.wName
}
}
Lo mismo haremos en la otra entidad que creamos, si prestas atención al código a continuación veras que añadí una nueva variable para que Date no sea opcional
ok esta la parte importante, primero nos dirigimos a donde nuestra app es declarada
import SwiftUI
@main
struct ruleOfThreeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
La nueva estructura de SwiftUI es Elegante y simple
Aquí añadiremos «import CoreData» y nuestro context «let context = PersistentCloudKitContainer.persistentContainer.viewContext» ademas de que lo añadimos a nuestro ContentView con «ContentView().environment(.managedObjectContext, context)»
Esto que antes iba en AppDelegate ahora se incorpora en @main que es la declaración de nuestra app.
import SwiftUI
import CoreData
@main
struct ruleOfThreeApp: App {
let context = PersistentCloudKitContainer.persistentContainer.viewContext
var body: some Scene {
WindowGroup {
ContentView().environment(\.managedObjectContext, context)
}
}
}
Muy bien finalmente ahora añadiremos una clase para conectar Core Data y nuestro modelo de datos.
Lo mismo que antes nuevo archivo ⌘ + N y nos aseguramos que sea en shared y que seleccionemos nuestros targets de manera apropiada
Ahora hay dos puntos importantes en el siguiente código
El primero en la linea 18
let container = NSPersistentContainer(name: “ruleOfThree”)
Donde definimos el nombre de nuestro modelo de datos previamente creado
//
// PersistentCloudKitContainer.swift
// ruleOfThree
//
// Created by Francisco Misael Landero Ychante on 27/06/20.
//
import CoreData
public class PersistentCloudKitContainer {
// MARK: - Define Constants / Variables
public static var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
// MARK: - Initializer
private init() {}
// MARK: - Core Data stack
public static var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "ruleOfThree")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
// MARK: - Core Data Saving support
public static func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
Paso 5
Ahora solo resta probar que nuestro modelo funciona y que podemos guardar datos en él, para simplemente cree una lista y un botón para añadir elementos de prueba.
import SwiftUI
struct ContentView: View {
// MARK: - Core Data
@Environment(\.managedObjectContext) var moc
@FetchRequest(entity: Rule.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Rule.order, ascending: true)] ) var rules: FetchedResults<Rule>
var body: some View {
List{
ForEach(self.rules, id: \.self){ rule in
Text("\(rule.order) \(rule.wname)")
}
Button(action: {
saveTheRule()
}) {
Text("Add Rule")
}
}
}
func saveTheRule(){
//New Rule
let newRule = Rule(context: self.moc)
newRule.baseLabel = "test"
newRule.name = "Test name \(Int.random(in: 1..<100))"
//simple rule
let newElement = Element(context: self.moc)
newElement.name = "parameter name"
newElement.rule = newRule
try? self.moc.save()
}
}
y bueno ahi esta en lo que agregan la casilla de CoreData otra vez esta es la forma manual de implementarlo.
En el siguiente post vamos a añadir sincronización con CloudKit