I have a SwiftUI app where I would like to enable NSPersistentCloudKitContainer to start using CloudKit and be able to sync the content of the app between multiple devices, but I'm not sure how to handle the user experience. Should this be a feature that the user turns On and Off in the app settings screen or does the user expects the syncing mechanism to work by default without having to do anything, can someone please share your experience on how you implemented CloudKit/Sync in your app.
Do I need to offer a login mechanism so the user logs in?
Should I include an On/Off option so the user decides if they want the sync option or not?
In general, can someone be so kind and explain what the user experience should be when an app provides ClouldKit/sync?
FYI - I'm not asking how to implement CloudKit sync I already know what it is and how it works. I just need to know what the expedition is from an app the provides that feature.
Thanks!
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
How can I reload the NSPersistentCloudKitContainer from a SwiftUI View?
I have a SwiftUI + MVVM + CloudKit app that successfully syncs to CloudKit but what I would like to be able to do is reload the NSPersistentCloudKitContainer from some View in the app to be able to evaluate if the app should sync to CloudKit or not by setting the cloudKitContainerOptions to nil (description.cloudKitContainerOptions = nil) if the user doesn't want to sync.
In other words, I need to reload the code inside the init() method in the CoreDataManager file when a method in the View Model is called. See the code and comments below.
Here is the code...
Core Data Manager
class CoreDataManager{
static let instance = CoreDataManager()
let container: NSPersistentCloudKitContainer
let context: NSManagedObjectContext
init(){
container = NSPersistentCloudKitContainer(name: "CoreDataContainer")
guard let description = container.persistentStoreDescriptions.first else{
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
/// This is the code that I would like to control from other view in the app
/// if iCloudSync is off, do nothing otherwise set it to nil
if !iCloudSync{
description.cloudKitContainerOptions = nil
}
container.loadPersistentStores { (description, error) in
if let error = error{
print("Error loading Core Data. \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
context = container.viewContext
}
func save(){
do{
try context.save()
print("Saved successfully!")
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
View Model
Somehow I would like to be able to have a way to control the reload process in the View Model.
class CarViewModel: ObservableObject{
let manager = CoreDataManager.instance
@Published var cars: [Car] = []
init(){
getCars()
}
func addCar(){}
func getCars(){}
func deleteCar(){}
func save(){
self.manager.save()
}
}
SwiftUI View
Then from a view, I would like to control the reload process through the View Model by calling a method.
struct ContentView: View {
@State private var isSync = false
@StateObject var viewModel = CarViewModel()
var body: some View {
VStack {
Toggle("iCloud Sync", isOn: $isSync)
.toggleStyle(SwitchToggleStyle(tint: .red))
if isSync {
// RELOAD the container, something like this
viewModel.reloadContainer(withSync: true)
}
}
}
}
Any help would be appreciated.
Thanks
Hi, I have a SwiftUI ProgressBar View that displays the percentage progress based on a percentage-value you enter as a Bindable parameter. The progress percentage-value is calculated by a ReminderHelper class, which takes two Ints as its parameters, totalDays and daysLeft, the values input to the ReminderHelper class come from a Reminder object saved in Core Data.
I'm very confused as to how to structure my code to accomplish such of thing due to the poor understanding of how the SwiftUI/Combine, @Binding, @Published, @State, etc. work.
Based on the code below, what I'm expecting to see is two reminders, Cut the Grass at 20% and Power Wash Siding at 50%. Again, the two Ints that determine the total percentage progress come from the Reminder object saved in Core Data and the actual total percentage result comes from the RemindersHelper class.
Any idea how to accomplish what I describe above?
Model:
This is saved in Core Data.
class Reminder:Identifiable{
var name = ""
var totalDays = 0
var daysLeft = 0
init(name:String, totalDays:Int, daysLeft:Int){
self.name = name
self.totalDays = totalDays
self.daysLeft = daysLeft
}
}
Helper class
This needs to be in charge of calculating the total percentage that will be passed to the ProgressBar View with the values coming
from the Reminder object saved in Core Data.
class ReminderHelper:ObservableObject{
@Published var percentageLeft: Float = 0.80
func calculatePerentageLeft(daysLeft: Int, totalDays:Int)->Float{
percentageLeft = Float(daysLeft / totalDays)
return percentageLeft
}
}
Content View:
Here I'm calling the calculatePerentageLeft method to prepare the percentageLeft property before presenting the ProgressBar. Which of course is not working.
I see an error:
Static method 'buildBlock' requires that 'Float' conform to 'View'
struct ContentView: View {
var reminders = [Reminder(name: "Cut the Grass", totalDays: 50, daysLeft: 10),
Reminder(name: "Power Wash Siding", totalDays: 30, daysLeft: 15)]
@StateObject var reminderModel = ReminderHelper()
var body: some View {
List {
ForEach(reminders) { reminder in
HStack{
Text(reminder.name)
reminderModel.calculatePerentageLeft(daysLeft: reminder.daysLeft, totalDays: reminder.totalDays)
ProgressBar(progress: reminderModel.percentageLeft)
}
}
}
}
}
ProgressBar View
This is the view in charge of drawing and displaying the percentage value.
struct ProgressBar: View {
@Binding var progress: Float
var body: some View {
ZStack {
Circle()
.stroke(lineWidth:5.0)
.opacity(0.3)
.foregroundColor(Color.orange)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 5.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.orange)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear, value: progress)
VStack{
Text(String(format: "%.0f %%", min(self.progress, 1.0)*100.0))
.font(.caption2)
}
}
}
}
Hi,
What would be the best animation method in SwiftUI to reproduce the UIKit animation shown below?
What I need is basically a throwing effect starting from point A and ending at Point B. I don't need any bouncing effects, this is more of an effect to let the user know that something was added from point A to Point B; similar to the effect we get when we add a photo to an album in Photos.
I tried using .rotationEffect() but the animation I get is too circular and with no option to add the point locations. I need a more natural throwing-paper-like animation where you define three points, start, apex and end, see the image below.
Please note that Point A and Point B will be some views on the screen and the throwing object will be a simple SF symbol Image(systemName: "circle.fill").
Any suggestions?
What I have tried in SwiftUI that cannot make it look like a throwing paper animation.
struct KeyframeAnimation: View {
@State private var ascend = false
@State private var ascendDescend = false
@State private var descend = false
var body: some View {
ZStack{
VStack{
ZStack{
Image(systemName: "circle.fill")
.font(.title)
.offset(x: -157)
.foregroundColor(Color.red)
.rotationEffect(.degrees(ascend ? 17: 0))
.rotationEffect(.degrees(ascendDescend ? 145: 0))
.rotationEffect(.degrees(descend ? 18: 0))
.onAppear{
withAnimation(Animation.easeInOut(duration: 0.5)){
self.ascend.toggle()
}
withAnimation(Animation.easeInOut(duration: 1)){
self.ascendDescend.toggle()
}
withAnimation(Animation.easeInOut(duration: 2).delay(8)){
self.descend.toggle()
}
}
}
}
}
}
}
UIKit Animation
This is what I need.
@IBAction func startAnimation(_ sender: Any) {
UIView.animateKeyframes(withDuration: 3.0, delay: 0.0, options: [.calculationModeCubic], animations: {
// start
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0/5.0, animations: {
self.myView.center = CGPoint(x: self.pointA.center.x, y: self.pointA.center.y)
})
// apex
UIView.addKeyframe(withRelativeStartTime: 1.0/5.0, relativeDuration: 1.0/5.0, animations: {
self.myView.center = CGPoint(x: self.pointB.center.x - 100, y: self.pointB.center.y - 100)
})
// end
UIView.addKeyframe(withRelativeStartTime: 2.0/5.0, relativeDuration: 1.0/5.0, animations: {
self.myView.center = CGPoint(x: self.pointB.center.x, y: self.pointB.center.y)
})
}
)
}
Any idea why if I tap on Field 1 and immediately after I tap on Field 5, Field 5 gets hidden by the keyboard?
To replicate my issue, copy and paste the code below, run it in the simulator and make sure the Toggle Software Keybaord is checked, then go and tap on Field 1 and then on Field 5.
I tried wrapping the fields in a List and I got the same result.
It almost feels like a bug. The issue seems to occur when moving from the regular .default keyboard type to the .decimalPad keyboard.
import SwiftUI
struct TextFieldScrollingIssue: View {
@State private var testInput:String = ""
@State private var decimalInput:String = ""
var body: some View {
VStack{
Form {
TextField("Field 1", text:$testInput)
.id("Field 1")
.keyboardType(.default)
Spacer()
Spacer()
Spacer()
Spacer()
Spacer()
Spacer()
Section(header: Text("Section 2: ")) {
TextField("Field 2", text:$testInput)
.id("Field 2")
.keyboardType(.decimalPad)
TextField("Field 3", text:$decimalInput)
.id("Field 3")
.keyboardType(.decimalPad)
}
Section(header: Text("Section 3: ")) {
TextField("Field 4", text:$testInput)
.id("Field 4")
.keyboardType(.default)
TextField("Field 5", text:$decimalInput)
.id("Field 5")
.keyboardType(.decimalPad)
}
}
}
}
}
struct TextFieldScrollingIssue_Previews: PreviewProvider {
static var previews: some View {
TextFieldScrollingIssue()
}
}
I'm currently syncing core data with the CloudKit private and public databases, as you can see in the code below, I'm saving the private database in the default configuration in Core Data and the public in a configuration called Public everything works fine when NSPersistentCloudKitContainer syncs, what I'm having an issue with is trying to save to the public data store PublicStore, for instance when I try to save with func createIconImage(imageName: String) it saves the image to the "default" store, not the PublicStore(Public configuration).
What could I do to make the createIconImage() function save to the PublicStore sqlite database?
class CoreDataManager: ObservableObject{
static let instance = CoreDataManager()
private let queue = DispatchQueue(label: "CoreDataManagerQueue")
@AppStorage(UserDefaults.Keys.iCloudSyncKey) private var iCloudSync = false
lazy var context: NSManagedObjectContext = {
return container.viewContext
}()
lazy var container: NSPersistentContainer = {
return setupContainer()
}()
init(inMemory: Bool = false){
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
}
func updateCloudKitContainer() {
queue.sync {
container = setupContainer()
}
}
private func getDocumentsDirectory() -> URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
private func getStoreURL(for storeName: String) -> URL {
return getDocumentsDirectory().appendingPathComponent("\(storeName).sqlite")
}
func setupContainer()->NSPersistentContainer{
let container = NSPersistentCloudKitContainer(name: "CoreDataContainer")
let cloudKitContainerIdentifier = "iCloud.com.example.MyAppName"
guard let description = container.persistentStoreDescriptions.first else{
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if iCloudSync{
if description.cloudKitContainerOptions == nil {
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)
description.cloudKitContainerOptions = options
}
}else{
print("Turning iCloud Sync OFF... ")
description.cloudKitContainerOptions = nil
}
// Setup public database
let publicDescription = NSPersistentStoreDescription(url: getStoreURL(for: "PublicStore"))
publicDescription.configuration = "Public" // this is the configuration name
if publicDescription.cloudKitContainerOptions == nil {
let publicOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)
publicOptions.databaseScope = .public
publicDescription.cloudKitContainerOptions = publicOptions
}
container.persistentStoreDescriptions.append(publicDescription)
container.loadPersistentStores { (description, error) in
if let error = error{
print("Error loading Core Data. \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}
func save(){
do{
try context.save()
//print("Saved successfully!")
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
class PublicViewModel: ObservableObject {
let manager: CoreDataManager
@Published var publicIcons: [PublicServiceIconImage] = []
init(coreDataManager: CoreDataManager = .instance) {
self.manager = coreDataManager
}
func createIconImage(imageName: String) {
let newImage = PublicServiceIconImage(context: manager.context)
newImage.imageName = imageName
newImage.id = UUID()
save()
}
func save() {
self.manager.save()
}
}
I have an app that uses local notifications and everything is working fine but I'm debating how to handle pending notifications after the user turned off and turned back on notifications for your app. In other words, let's say that a notification is created and it's meant to be triggered on a certain day but the user turns notifications off the day before the trigger date, then he/she deletes the notification from the app (UI) while notifications for your app were turned off and then after a few days later the user turns on notifications for your app again, at that point there may be notifications that the user no longer wants (since he/she deleted them from the UI) and some that have been expired already.
How to find pending notifications that were deleted from the UI but not from the notification center in the event described above?
FYI - I know how to delete pending notifications and how to compare notifications in the UI vs notifications in the notifications center. What I'm not sure about is when to do the comparison. Is there a way to get notified when the user turned back on notifications for your app so a method can be triggered to find orphan notifications?
Thanks!
How can I make the following animation move in a more circular/arc way?
What I need is to be able to animate the red circle in an arc shape, where it goes up and then down forming a curve/arc. Right now the animation looks almost linear with just a little bit of curve, and it doesn't look smooth.
import SwiftUI
struct GeometryEffectTesting: View {
@State private var isOn: Bool = false
var body: some View {
VStack{
ZStack{
Image(systemName: "circle.fill")
.font(.title)
.foregroundColor(.orange)
.animation(.easeOut(duration: 0.3), value: isOn)
Image(systemName: "circle.fill")
.font(.title2)
.foregroundColor(.red)
.modifier(SpecialEffect(isAnimating: isOn))
.animation(.easeOut(duration: 0.3), value: isOn)
.onTapGesture {
self.isOn.toggle()
}
}
}
.padding()
}
}
struct SpecialEffect: GeometryEffect{
private var percentage : CGFloat
private var isAnimating : Bool
init(isAnimating: Bool){
self.percentage = isAnimating ? 1 : 0
self.isAnimating = isAnimating
}
var animatableData: CGFloat{
get{return percentage}
set{percentage = newValue}
}
func effectValue(size: CGSize) -> ProjectionTransform {
let transform: CGAffineTransform
if isAnimating{
transform = Self.transform(forPercent: percentage)
}else{
transform = Self.transform(forPercent: 0)
}
return ProjectionTransform(transform)
}
static func transform(forPercent percent: CGFloat) -> CGAffineTransform {
let xPos: CGFloat = 50
let yPos: CGFloat = 50
let transform: CGAffineTransform
if percent == 0{
transform = .init(translationX: 0, y: 0)
return transform
}else{
if percent < 0.25 {
transform = .init(translationX: percent * xPos, y: -percent * yPos)
} else if percent < 0.5 {
transform = .init(translationX: (percent * xPos) * 2, y: -(percent * yPos) * 2)
} else if percent < 0.75 {
transform = .init(translationX: (percent * xPos) * 3, y: -(percent * yPos) * 2)
} else {
transform = .init(translationX: (percent * xPos) * 4, y: -(percent * yPos) * 2)
}
return transform
}
}
}
Hi, I need to send a user notification when the user enter a
certain location/region. I noticed that there is a UNLocationNotificationTrigger
class to do just this insed the UserNotification framework. I have a little
experience using User Notifications with time intervals but haven’t used the
trigger by location. I have a couple of questions when using UNLocationNotificationTrigger.
1.
When you set a user notification by location,
does that means that the phone will constantly be searching the user’s location,
or the GPS will automatically trigger the notification when the user enters the
designated location without constant searching?
2.
Does UNLocationNotificationTrigger drain the
phones battery?
Thanks
In the following code, I have a LocationManager class which provides the city name of the current location via the @Published property wrapper lastSearchedCity.
Then I have a SearchManagerViewModel class that should be in charge of presenting the city name on SwiftUI views based on some conditions (not currently shown in the code below) via the @Published property wrapper cityName. It properly shows the city name when I call the searchAndSetCity() method from ContentView.swift inside an onAppear modifier.
My issue is that if the user turned Location Services off and turns it back On while he/she is in the ContentView.swift the Text view doesn't update, which is understandable since the searchAndSetCity() method would need to be called again.
How can I call the searchAndSetCity() method located inside the SearchManagerViewModel class every time the locationManagerDidChangeAuthorization(_ manager: CLLocationManager) method is called? I believed this method is called every time the authorization status changes.
LocationManager Class
final class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
@Published var lastSearchedCity = ""
var hasFoundOnePlacemark:Bool = false
func checkIfLocationServicesIsEnabled(){
DispatchQueue.global().async {
if CLLocationManager.locationServicesEnabled(){
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest/// kCLLocationAccuracyBest is the default
self.checkLocationAuthorization()
}else{
// show message: Services desabled!
}
}
}
private func checkLocationAuthorization(){
switch locationManager.authorizationStatus{
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
// show message
case .denied:
// show message
case .authorizedWhenInUse, .authorizedAlways:
/// app is authorized
locationManager.startUpdatingLocation()
default:
break
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationAuthorization()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
hasFoundOnePlacemark = false
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error)-> Void in
if error != nil {
self.locationManager.stopUpdatingLocation()
// show error message
}
if placemarks!.count > 0 {
if !self.hasFoundOnePlacemark{
self.hasFoundOnePlacemark = true
let placemark = placemarks![0]
self.lastSearchedCity = placemark.locality ?? ""
}
self.locationManager.stopUpdatingLocation()
}else{
// no places found
}
})
}
}
SearchManagerViewModel Class
class SearchManagerViewModel: ObservableObject{
@Published var cityName = "" // use this directly in SwifUI views
@ObservedObject private var locationManager = LocationManager()
// Call this directly fron onAppear in SwiftUI views
// This method is more complex than what is shown here. It handles other things like HTTP requests etc.
func searchAndSetCity(){
locationManager.checkIfLocationServicesIsEnabled()
self.cityName = locationManager.lastSearchedCity
}
}
ContentView.swift
struct ContentView: View {
@StateObject private var searchManager = SearchManagerViewModel()
var body: some View {
VStack {
Text(searchManager.cityName)
.font(.callout)
}
.onAppear{
searchManager.searchAndSetCity()
}
}
}
Hi, a Service class where I process all Core Data transactions for a single entity/Object and I often find my self needing to access information from other classes and I was wondering if there was an issue by calling these classes in non-UI related classes.
In the following code I'm calling the dogs array from the DogService class inside the DogHouseService class, and I was wondering if this could be an issue.
Are there any possible issue by calling @Published properties from an ObservableObject class inside other classes?
ObservableObject class
class DogService: ObservableObject{
let manager: CoreDataManager
@Published var dogs: [Dog] = []
init(coreDataManager: CoreDataManager = .instance){
self.manager = coreDataManager
loadDogs()
}
//Adds, Deletes, Updates, etc.
func loadDogs(){
let request = NSFetchRequest<Dog>(entityName: "Dog")
do{
dogs = try manager.context.fetch(request)
}catch let error{
print("Error fetching dogs. \(error.localizedDescription)")
}
}
func save(){
self.manager.save()
}
}
Other Class
class DogHouseService{
let dogService = DogService()
for dog in dogService.dogs{
// do something
}
}
I have the following UIKit animation inside a UIViewRepresentable which works fine, it animates a view in a throwing effect like animation from point A to point B. What I would like to be able to do is, set the start and the end position in ContentView when assigning the animation to a view.
Here is the code...
Static Position Animation
import SwiftUI
struct ContentView: View {
@State private var isAnimating = false
var body: some View {
HStack{
Image(systemName: "circle.fill")
.font(.system(size: 65))
.foregroundColor(.blue)
.throwAnimation(isAnimating: $isAnimating)
.onTapGesture {
isAnimating.toggle()
}
}
}
}
struct ThrowAnimationWrapper<Content: View>: UIViewRepresentable{
@ViewBuilder let content: () -> Content
@Binding var isAnimating: Bool
func makeUIView(context: Context) -> UIView {
UIHostingController(rootView: content()).view
}
func updateUIView(_ uiView: UIView, context: Context) {
if isAnimating{
UIView.animateKeyframes(withDuration: 1.5, delay: 0.0, options: [.calculationModeCubic], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.2, animations: {
uiView.center = CGPoint(x: 250, y: 300) // how can I make this dynamic
})
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.9, animations: {
uiView.center = CGPoint(x: 100 + 75, y: 100 - 50 )
uiView.transform = CGAffineTransform(scaleX: 0.75, y: 0.75)
})
UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.7, animations: {
uiView.center = CGPoint(x: 100, y: 100)// how can I make this dynamic
uiView.transform = CGAffineTransform(scaleX: 0.2, y: 0.2)
})
}, completion: { _ in
uiView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
})
}
}
}
extension View {
func throwAnimation( isAnimating: Binding<Bool>) -> some View {
modifier(ThrowAnimationViewModifier(isAnimating: isAnimating))
}
}
struct ThrowAnimationViewModifier: ViewModifier {
@Binding var isAnimating: Bool
func body(content: Content) -> some View {
ThrowAnimationWrapper(content: {
content
}, isAnimating: $isAnimating)
}
}
How I would like to be able to call it from ContentView
@State private var isAnimating = false
var body: some View {
HStack{
Image(systemName: "circle.fill")
.font(.system(size: 65))
.foregroundColor(.blue)
.throwAnimation(isAnimating: $isAnimating, startPos:CGPoint(x: 250, y: 300), endPos:CGPoint(x: 100, y: 100))
.onTapGesture {
isAnimating.toggle()
}
}
}
}
How can I modify this code in a way that I can enter the start and end position when assigning to a view?
Thanks!
In the following code, I'm saving and syncing objects in Core Data and CloudKit, everything is working fine, once the user creates some objects, the data starts syncing as soon as the user turns the Toggle switch On in the SettingsView. The issue I'm having is that the data continue syncing even after the switch is turned Off until I kill and relaunch the app. After relaunching the app, the data stops syncing.
Any idea what could I do to make sure that the data stops syncing as soon as the toggle switch is turned off?
Again, everything would work fine if the user would kill and relaunch the app right after turning it off, the data stops syncing.
I thought that by calling carViewModel.updateCloudKitContainer() right after turning it off would do the trick since I'm disabling the CloudKit container by making it nil, description.cloudKitContainerOptions = nil but obviously is not enough.
Core Data Manager
class CoreDataManager{
// Singleton
static let instance = CoreDataManager()
@AppStorage(UserDefaults.Keys.iCloudSyncKey) private var iCloudSync = false
static var preview: CoreDataManager = {
// Create preview objects
do {
try viewContext.save()
} catch {
}
return result
}()
lazy var context: NSManagedObjectContext = {
return container.viewContext
}()
lazy var container: NSPersistentContainer = {
return setupContainer()
}()
init(inMemory: Bool = false){
/// for preview purposes only, remove if no previews are needed.
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
}
func setupContainer()->NSPersistentContainer{
print("Assgning persistent container... ")
let container = NSPersistentCloudKitContainer(name: "CoreDataContainer")
guard let description = container.persistentStoreDescriptions.first else{
fatalError("###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if iCloudSync{
let cloudKitContainerIdentifier = "iCloud.com.sitename.myApp"
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)
description.cloudKitContainerOptions = options
}else{
description.cloudKitContainerOptions = nil // turn cloud sync off
}
container.loadPersistentStores { (description, error) in
if let error = error{
print("Error loading Core Data. \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}
func save(){
do{
try context.save()
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
View Model
class CarViewModel: ObservableObject{
let manager: CoreDataManager
@Published var cars: [Car] = []
init(coreDataManager: CoreDataManager = .instance){
self.manager = coreDataManager
loadCars()
}
func updateCloudKitContainer(){
manager.container = manager.setupContainer()
}
func addCar(model:String, make:String?){
// create car
save()
loadCars()
}
func deleteCar(car: Car){
// delete car
save()
loadCars()
}
func loadCars(){
let request = NSFetchRequest<Car>(entityName: "Car")
let sort = NSSortDescriptor(keyPath: \Car.model, ascending: true)
request.sortDescriptors = [sort]
do{
cars = try manager.context.fetch(request)
}catch let error{
print("Error fetching businesses. \(error.localizedDescription)")
}
}
func save(){
self.manager.save()
}
}
Settings View to Turn ClouldKit ON and OFF
struct SettingsView: View {
@ObservedObject var carViewModel:CarViewModel
@AppStorage(UserDefaults.Keys.iCloudSyncKey) private var iCloudSync = false
var body: some View {
VStack{
Toggle(isOn: $iCloudSync){// turns On and Off sync
HStack{
Image(systemName: iCloudSync ? "arrow.counterclockwise.icloud" : "lock.icloud")
.foregroundColor(Color.fsRed)
Text(" iCloud Sync")
}
}
.tint(Color.fsRed)
.onChange(of: iCloudSync){ isSwitchOn in
if isSwitchOn{
iCloudSync = true
carViewModel.updateCloudKitContainer()
}else{
iCloudSync = false // turn Off iCloud Sync
carViewModel.updateCloudKitContainer()
}
}
}
}
}
Display Cars View
struct CarsView: View {
@ObservedObject var carViewModel:CarViewModel
// Capture NOTIFICATION
var didRemoteChange = NotificationCenter.default.publisher(for: .NSPersistentStoreRemoteChange).receive(on: RunLoop.main)
var body: some View {
NavigationView{
VStack{
List {
ForEach(carViewModel.cars) { car in
HStack{
VStack(alignment:.leading){
Text(car.model ?? "")
.font(.title2)
Text(car.make ?? "")
.font(.callout)
}
}
.swipeActions {
Button( role: .destructive){
deleteCar = car
showDeleteActionSheet = true
}label:{
Label("Delete", systemImage: "trash.fill")
}
}
}
}
.navigationBarTitle("Cars")
.onAppear{
carViewModel.loadCars()
}
// reload cars on NOTIFICATION
.onReceive(self.didRemoteChange){ _ in
carViewModel.loadCars()
}
}
}
}
}
Can someone please help me understand PassthroughSubject and CurrentValueSubject? What I understand so far is that they are subjects where subscribers can listen to changes made to these subjects, but I'm really straggling to understand the following.
I'm I correct by saying that PassthroughSubject or CurrentValueSubject could replace delegation and asynchronous function calls?
Is it possible to delare a subject in Class A and subscribe to listen to those subject changes in Class B and in some other classes or are listeners meant to only be used direclty in SwiftUI structs?
Thanks
Hi, I'm trying to understand how TestFlight works since a major update of my app is coming soon. I currently have an app in the app store with a few thousand users, the current app was written in UIKit and I'm now rewriting it in SwiftUI and making major updates such as, moving from Realm to Core Data and allowing iCloudSync etc. I don't have users emails, only from the people who have contacted me with questions so, I was wondering if I could somehow invite users to test the new version without having their emails.
Can someone please describe the typical process when using TestFlight?
Would the following process be considered a good practice?
Add a message in the existing app to invite users by asking for their email so they can join TestFlight at a later date.
Release a new Beta version in TestFlight.
Invite the users who subscribed via the old app.
Release to production after users test it.
Thanks