Migration to change the configuration of CoreData - swift

I started a macOS project using Default configuration of CoreData. Application was released and some users started to use it. Now, I need some data to be synced with iCloud and some data to be only stored locally. If I understand correctly, the only way I can achieve this is to create two different configurations (in CoreData data model), add the needed entities in each configuration, and configure the NSPersistentContainer accordingly.
However the above method might lead to some data loss since I wont be using the Default configuration anymore.
Is there any way I can "migrate" the data saved under the Default configuration to another configuration?

After some trial and error I found a solution that seems to do the work (however, it seems dirty).
First, when instantiating the container, I make sure I add my 3 storeDescriptors to persistentStoreDescriptions (each representing an scheme)
let defaultDirectoryURL = NSPersistentContainer.defaultDirectoryURL()
var persistentStoreDescriptions: [NSPersistentStoreDescription] = []
let localStoreLocation = defaultDirectoryURL.appendingPathComponent("Local.sqlite")
let localStoreDescription = NSPersistentStoreDescription(url: localStoreLocation)
localStoreDescription.cloudKitContainerOptions = nil
localStoreDescription.configuration = "Local"
persistentStoreDescriptions.append(localStoreDescription)
let cloudStoreLocation = defaultDirectoryURL.appendingPathComponent("Cloud.sqlite")
let cloudStoreDescription = NSPersistentStoreDescription(url: cloudStoreLocation)
cloudStoreDescription.configuration = "iCloud"
cloudStoreDescription.cloudKitContainerOptions = "iCloud.com.xxx.yyy"
persistentStoreDescriptions.append(cloudStoreDescription)
let defaultStoreLocation = defaultDirectoryURL.appendingPathComponent("Default.sqlite")
let defaultStoreDescription = NSPersistentStoreDescription(url: defaultStoreLocation)
defaultStoreDescription.cloudKitContainerOptions = nil
defaultStoreDescription.configuration = "Default"
persistentStoreDescriptions.append(defaultStoreDescription)
container.persistentStoreDescriptions = persistentStoreDescriptions
Note: One important thing is to make sure that NSPersistentStoreDescription with the Default configuration is added last.
Secondly, I am for-eaching thought all data saved in core data checking if managedObject.objectID.persistentStore?.configurationName is "Default" (or any string containing Default. With my empiric implementation I got to the conclusion that configuration name might be different from case to case). If the above condition is true, create a new managedObject, I copy all properties from the old one to new one, delete the old managed object, and save the context.
for oldManagedObject in managedObjectRepository.getAll() {
guard let configurationName = oldManagedObject.objectID.persistentStore?.configurationName else {
continue
}
if (configurationName == "Default") {
let newManagedObject = managedObjectRepository.newManagedObject()
newManagedObject.uuid = oldManagedObject.uuid
newManagedObject.createDate = oldManagedObject.createDate
......
managedObjectRepository.delete(item: oldManagedObject)
managedObjectRepository.saveContext()
}
}
With this implementation, old data that was previously saved in Default.sqlite is now saved in Local.sqlite or 'Cloud.sqlite' (depending on which configuration contains which entity).

Related

How to dynamically update MGLPointFeature attribute value of MGLShapeSource for Mapbox on iOS?

My application stores data, which includes coordinates and other info, in a local database. Due to the number of data points, the application uses clustering to display the data with Mapbox on iOS. Some of the marker styling is based on data, which can change on the run. Map setup is:
// fetch data from DB
let dataArray: [MyData] = fetchData()
// build features from data array
var features = [MGLPointFeature]()
dataArray.forEach({ (data) in
let feature = MGLPointFeature()
feature.identifier = data.id
feature.coordinate = CLLocationCoordinate2D(latitude: data.lat, longitude: data.lng)
// our attributes
feature.attributes = [
"amount": data.amount
"marked": false
]
features.append(feature)
})
// make and add source
let source = MGLShapeSource(identifier: "MySourceId", features: features, options: [
.clustered: true
])
style.addSource(source)
// regular marker layer
let layer = MGLSymbolStyleLayer(identifier: "unclustered", source: source)
layer.iconImageName = NSExpression(forConstantValue: "MyIcon")
layer.text = NSExpression(forKeyPath: "amount")
layer.iconScale = NSExpression(forMGLConditional: NSPredicate(format: "%# == true", NSExpression(forKeyPath: "marked")), trueExpression: NSExpression(forConstantValue: 2.0), falseExpression: NSExpression(forConstantValue: 1.0))
layer.predicate = NSPredicate(format: "cluster != YES")
style.addLayer(layer)
// point_count layers
...
The above code is simplified to help illustrate the concept more clearly. Array of MGLPoint is used because data is stored in DB, so we don't have a GeoJSON file or a URL. MGLShapeSource is used because clustering is required, and that's what I found in the examples. MGLShapeSource constructor taking "features" as a parameter is used because that's the one that matches the data I have. The regular marker layer is setup to show different size icons based on the value of "marked" attribute in iconScale.
During runtime, the value of "marked" attribute can change (for example, when a marker is tapped), and the corresponding icon's size needs to be updated to reflect the change in "marker" value. However, I am unable to figure out how to change the marker attribute value. MGPShapeSource only shows access to shape and URL, neither of which is the features array I initialized the source with. I need to access the features array the source was constructed with, change the marker value, and have the marker icons updated.
I have thought about remaking the source on each data change. But with the number of markers involved, this would perform poorly. Plus I believe I would also need to remake all of the style layers, as they're constructed with the actual source object, which would make this perform even worse.
I need help figuring out how to change the attribute value of MGLPointFeature within MGLShapeSource during runtime and have the map updated.
I did not find a solution close to what I was hoping for, but I did find something that's better than remaking everything every time.
On feature attribute value change, instead of remaking everything, including the source and all the layers, I update the source's shape with the MGLPointFeature array. I do have to remake the MGLPointFeature array, but then I can make a MGLShapeCollectionFeature with this array and set it as existing source's shape. This way, the existing layers don't need to be changed, either. Something like:
// build features from data array
...same as in original question, but with updated feature.attributes values as needed.
// look for existing source and update it if found; otherwise, make a new source
if let existingSource = style.source(withIdentifier: "MySourceId") {
// update data
guard let shapeSource = existingSource as? MGLShapeSource else {
throw <some_error>
}
shapeSource.shape = MGLShapeCollectionFeature(shapes: features)
} else {
// make new (same as original post)
let source = MGLShapeSource(identifier: "MySourceId", features: features, options: [.clustered: true])
style.addSource(source)
}

How to index and deindex NSUserActivities from Spotlight

I am trying to wrap my head around NSUserActivitys and I am not entirely sure on how to use them properly. I have setup my NSUserActivity properly like so:
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
attributeSet.title = "Title"
attributeSet.contentDescription = "Description"
let activity = NSUserActivity(activityType: ActivityType.activity.rawValue)
activity.persistentIdentifier = ActivityIdentifier.activity.rawValue
activity.title = "Title"
activity.requiredUserInfoKeys = ["Key"]
activity.userInfo = ["Key": data]
activity.isEligibleForSearch = true
activity.contentAttributeSet = attributeSet
self.userActivity = activity
self.userActivity!.becomeCurrent()
Now the activity gets indexed via the becomeCurrent() method. When I click on the activity in Spotlight everything works fine and the activity can be restored using the userInfo property.
But how do I delete the activity from Spotlight once it has be used (restored)? In this post the user recommends to use either deleteAllSavedUserActivities(completionHandler:) which works but I can't use since I don't want to delete all activities or deleteSavedUserActivities(withPersistentIdentifiers:completionHandler:) which does not work. For the first method the documentation says following however to the second method this does not apply:
Deletes all user activities stored by Core Spotlight...
Instead I could index the activities with the Core Spotlight API like so:
let item = CSSearchableItem(uniqueIdentifier: ActivityIdentifier.activity.rawValue, domainIdentifier: "DomainID", attributeSet: attributeSet)
CSSearchableIndex.default().indexSearchableItems([item]) { error in
if error != nil {
print(error!)
} else {
print("successfully indexed item")
}
}
and delete them with the deleteSearchableItems(withIdentifiers:completionHandler:) method. The problem with that is, I have to set the relatedUniqueIdentifier of my attributeSet and then the userInfo will be empty once I try to restore the activity (regarding post).
So what should I do, should I use both Core Spotlight and NSUserActivity and use CSSearchableItemAttributeSet to save the data instead of using the userInfo (why would apple to that?, why would they add the userInfo then?) or should I index my activity without Core Spotlight, but how do I delete the activity from Spotlight in this case?
There is just one thing I figured out: In the apple documentation for the domainIdentifier property of the CSSearchableAttributeSet it sounds like you are supposed to use this property to delete the NSUserActivity
Specify a domain identifier to group items together and to make it
easy to delete groups of items from the index. For example, to delete
a user activity, you can set this property on the contentAttributeSet
property of the NSUserActivity object and then call
deleteSearchableItems(withDomainIdentifiers:completionHandler:) on the
default().

Upgrading my application to include prepopulated data

I've decided to improve upon my initial design and pre-populate certain parts of my Application. I am trying to get the sqlite to be read following this tutorial: http://www.appcoda.com/core-data-preload-sqlite-database/
However the line of code that goes like this:
if !NSFileManager.defaultManager().fileExistsAtPath(url.path!) {
let sourceSqliteURLs = [NSBundle.mainBundle().URLForResource("CoreDataDemo", withExtension: "sqlite")!, NSBundle.mainBundle().URLForResource("CoreDataDemo", withExtension: "sqlite-wal")!, NSBundle.mainBundle().URLForResource("CoreDataDemo", withExtension: "sqlite-shm")!]
let destSqliteURLs = [self.applicationDocumentsDirectory.URLByAppendingPathComponent("CoreDataDemo.sqlite"),
self.applicationDocumentsDirectory.URLByAppendingPathComponent("CoreDataDemo.sqlite-wal"),
self.applicationDocumentsDirectory.URLByAppendingPathComponent("CoreDataDemo.sqlite-shm")]
var error:NSError? = nil
for var index = 0; index < sourceSqliteURLs.count; index++ {
NSFileManager.defaultManager().copyItemAtURL(sourceSqliteURLs[index], toURL: destSqliteURLs[index], error: &error)
}
}
isn't quite right for my application as I am using a shared application folder. The problem I am facing is that the application loads fine and passes the lines of code but when I am looking at the page that shows the info nothing is there. I downloaded the sqlite reader to see if there are objects in the database and there are so I know that isn't the issue. What modifications do I have to make to these lines of code in order for the share app group to perform this function correctly? The directory to the Shared App Group is something like this:
let directory = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.groupname.appname")
I am struggling to find any info on this any help would be greatly appreciated.
Maybe I'm missing something in your questions, but if all you want to do is change the destination directory, then just change these lines...
let destSqliteURLs = [
self.applicationDocumentsDirectory.URLByAppendingPathComponent("CoreDataDemo.sqlite"),
self.applicationDocumentsDirectory.URLByAppendingPathComponent("CoreDataDemo.sqlite-wal"),
self.applicationDocumentsDirectory.URLByAppendingPathComponent("CoreDataDemo.sqlite-shm")]
into this...
let baseURL = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.groupname.appname")
let destSqliteURLs = [
baseURL.URLByAppendingPathComponent("CoreDataDemo.sqlite"),
baseURL.URLByAppendingPathComponent("CoreDataDemo.sqlite-wal"),
baseURL.URLByAppendingPathComponent("CoreDataDemo.sqlite-shm")]

How to get the current tool SitePage and/or its Properties?

With the ToolManager I can get the the current placement, the context and of course, the Site through the SiteService. But I want to get the current SitePage properties the user is currently accessing.
This doubt can be extended to the current Tool properties with a
little more emphasis considering that once I have the Tool I could not
find any methods covering the its properties.
I could get the tool properties and I'm using it (it is by instance) through Properties got with sitepage.getTool(TOOLID).getConfig(). To save a property, I'm using the ToolConfiguration approach and saving the data after editing with the ToolConfiguration.save() method. Is it the correct approach?
You can do this by getting the current tool session and then working your way backward from that. Here is a method that should do it.
public SitePage findCurrentPage() {
SitePage sp = null;
ToolSession ts = SessionManager.getCurrentToolSession();
if (ts != null) {
ToolConfiguration tool = SiteService.findTool(ts.getPlacementId());
if (tool != null) {
String sitePageId = tool.getPageId();
sp = s.getPage(sitePageId);
}
}
return sp;
}
Alternatively, you could use the current tool to work your way to it but I think this method is harder.
String toolId = toolManager.getCurrentTool().getId();
String context = toolManager.getCurrentPlacement().getContext();
Site s = siteService.getSite( context );
ToolConfiguration tc = s.getTool(toolId);
String sitePageId = tc.getPageId();
SitePage sp = s.getPage(sitePageId);
NOTE: I have not tested this code to make sure it works.

Enterprise Library Fluent API and Rolling Log Files Not Rolling

I am using the Fluent API to handle various configuration options for Logging using EntLib.
I am building up the loggingConfiguration section manually in code. It seems to work great except that the RollingFlatFileTraceListener doesn't actually Roll the file. It will respect the size limit and cap the amount of data it writes to the file appropriately, but it doesn't not actually create a new file and continue the logs.
I've tested it with a sample app and the app.config and it seems to work. So I'm guess that I am missing something although every config option that seems like it needs is there.
Here is the basics of the code (with hard-coded values to show a config that doesn't seem to be working):
//Create the config builder for the Fluent API
var configBuilder = new ConfigurationSourceBuilder();
//Start building the logging config section
var logginConfigurationSection = new LoggingSettings("loggingConfiguration", true, "General");
logginConfigurationSection.RevertImpersonation = false;
var _rollingFileListener = new RollingFlatFileTraceListenerData("Rolling Flat File Trace Listener", "C:\\tracelog.log", "----------------------", "",
10, "MM/dd/yyyy", RollFileExistsBehavior.Increment,
RollInterval.Day, TraceOptions.None,
"Text Formatter", SourceLevels.All);
_rollingFileListener.MaxArchivedFiles = 2;
//Add trace listener to current config
logginConfigurationSection.TraceListeners.Add(_rollingFileListener);
//Configure the category source section of config for flat file
var _rollingFileCategorySource = new TraceSourceData("General", SourceLevels.All);
//Must be named exactly the same as the flat file trace listener above.
_rollingFileCategorySource.TraceListeners.Add(new TraceListenerReferenceData("Rolling Flat File Trace Listener"));
//Add category source information to current config
logginConfigurationSection.TraceSources.Add(_rollingFileCategorySource);
//Add the loggingConfiguration section to the config.
configBuilder.AddSection("loggingConfiguration", logginConfigurationSection);
//Required code to update the EntLib Configuration with settings set above.
var configSource = new DictionaryConfigurationSource();
configBuilder.UpdateConfigurationWithReplace(configSource);
//Set the Enterprise Library Container for the inner workings of EntLib to use when logging
EnterpriseLibraryContainer.Current = EnterpriseLibraryContainer.CreateDefaultContainer(configSource);
Any help would be appreciated!
Your timestamp pattern is wrong. It should be yyy-mm-dd instead of MM/dd/yyyy. The ‘/’ character is not supported.
Also, you could accomplish your objective by using the fluent configuration interface much easier. Here's how:
ConfigurationSourceBuilder formatBuilder = new ConfigurationSourceBuilder();
ConfigurationSourceBuilder builder = new ConfigurationSourceBuilder();
builder.ConfigureLogging().LogToCategoryNamed("General").
SendTo.
RollingFile("Rolling Flat File Trace Listener")
.CleanUpArchivedFilesWhenMoreThan(2).WhenRollFileExists(RollFileExistsBehavior.Increment)
.WithTraceOptions(TraceOptions.None)
.RollEvery(RollInterval.Minute)
.RollAfterSize(10)
.UseTimeStampPattern("yyyy-MM-dd")
.ToFile("C:\\logs\\Trace.log")
.FormatWith(new FormatterBuilder().TextFormatterNamed("textFormatter"));
var configSource = new DictionaryConfigurationSource();
builder.UpdateConfigurationWithReplace(configSource);
EnterpriseLibraryContainer.Current = EnterpriseLibraryContainer.CreateDefaultContainer(configSource);
var writer = EnterpriseLibraryContainer.Current.GetInstance<LogWriter>();
DateTime stopWritingTime = DateTime.Now.AddMinutes(10);
while (DateTime.Now < stopWritingTime)
{
writer.Write("test", "General");
}