Xcode can generate this from a Core Data entity:
// This file was automatically generated and should not be edited.
import Foundation
import CoreData
extension Media {
#NSManaged public var imageString: String?
}
My colleague has edited it to hide the String and only expose an URL:
extension Media {
#NSManaged fileprivate var imageString: String?
public var image: URL? {
return imageString != nil ? URL(string: imageString!) : nil
}
}
Is fileprivate (or private) OK to use in that case? Is that the best practice to store an URL in Core Data?
That works. Whether it's a good idea depends on how you need to use the URL.
You can just save the URL directly, without having a string property. Just make the property a "transformable" type in the Core Data model editor. Since the URL type conforms to NSCoding, Core Data will automatically convert it to/from an NSData. You would assign a URL to the property, and read URLs back later.
That's good unless you need to fetch objects based on the URL. You can't use transformable attributes in fetch predicates, so with a transformable attribute you couldn't, for example, fetch every object with a URL that contained stackoverflow.com. If you need to do something like that, your approach is a good one. If not, the transformable attribute is simpler.
Related
Context
I am working on a feature, that allows users to add Components to CoreData. Those Components are obviously NSManagedObjects inserted and saved into a Context.
In addition, I also want to give the user a variety of predefined Components. However, I do not like the idea of populating those predefined ones into CoreData at the first App Launch, since this is really prone to bugs, especially when utilising CloudKit. So my idea was to generate a List of predefined NSManagedObjects without inserting them into a Context, which would make them temporarily, but they could be used in the same way as the real ones. However, as far as I understand, creating NSManagedObjects without a Context isn't really working.
Code
let predefinedComponents: [Component] {
var components: [Component] = []
for name in names {
let component = Component() // This was my idea of creating a temporary NSManagedObject without inserting it into a Context.
component.name = name
components.append(component)
}
return components
}
struct ComponentsView: View {
#FetchRequest(sortDescriptors: [SortDescriptor(\.name)]) private var components: FetchedResults<Component>
var body: some View {
ForEach(allComponents) { component in
ComponentRow(component: component)
}
}
private var allComponents: [Component] {
var allComponents: [Component] = predefinedComponents
for component in components {
allComponents.append(component)
}
return allComponents
}
}
struct ComponentRow: View {
#ObservedObject var component: Component
var body: some View {
Text(component.name)
}
}
Question
How can I achieve my goal described above while being able to work with predefined Components without having to populate them into CoreData at the first AppLaunch?
The easiest and most elegant way is to create a "throwaway scratchpad" context just for the pre-defined Components.
This scratchpad context will be a child of the viewContext, or any background context depending on your use case.
This is how you create the scratchpad:
let scratchpadContext = NSManagedObjectContext(.mainQueue)
scratchpadContext.parent = dataProvider.container.viewContext
The example above creates a context for the main queue, which I assume is your case based on your question. But if you need to access it from a background thread, you initialise it with .privateQueue.
So, as long as you don't save the scratchpadContext, your temporary pre-defined Components will never be saved on your Persistent store. And when it's de-initialised, any NSManagedObject that you've created with it will be thrown away.
You can create "free floating" managed objects that don't belong to a context but you need to provide the entity description to do it-- so you would use Component(entity:insertInto:). The first argument is the NSEntityDescription for Component. The second one is a context, but it's an optional, so you can make it nil. If you wanted to add it to a context later, use NSManagedObjectContext.insert().
It might be better to use an in-memory persistent store instead of a SQLite store. Then you would have a context that only existed while the app was running but did not save to a file. You can set one of those up with NSPersistentContainer if you change the persistent store description.
I can't tell from your question which of these would be better for you.
I'm creating a simple NewsApp. I want to create the best app architecture I can made. So my question is that if I want save really simple data like username and maybe 5-6 tags as strings, should I put userDefaults logic into my viewModel or should I create a layer between ViewModel and UserDefaultsAPI which will take care about saving data?
I mean I will create StoreData protocol which UserDefaultsAPI will implement. And if I should do it how I can achieve that? I am using RxSwift and I don't now how to subscribe changing data in UserDefaults by UserDefaultsAPI.
You should create a layer between, but given an Rx/functional architecture, it shouldn't be something heavy weight like a protocol.
Learn How to Control the World and do something like this instead:
struct Storage {
static var shared = Storage()
var saveProperty: (Property) -> Void = { property in
UserDefaults.standard.set(try? JSONEncoder().encode(property), forKey: "property")
}
var deleteProperty: () -> Void = {
UserDefaults.standard.removeObject(forKey: "property")
}
var currentProperty: () -> Observable<Property?> = {
UserDefaults.standard.rx.observe(Data.self, "property")
.map { $0.flatMap { try? JSONDecoder().decode(Property.self, from: $0) } }
.distinctUntilChanged()
}
}
struct Property: Codable, Equatable { }
It depends what your doing creating a seperate layer gives you, the opportunity to have different sources for data that implement the same protocol could be useful, and your data may be complex types than need to be encoded and decoded, so it makes sense to encapsulate that out, you may also want to provide some limit range to some of your values, but UserDefaults is just a service like NotificationCenter, you are not going to automatically wrap NotificationCenter in ever class, just do what is simplest, but doesn't run the risk of painting yourself in a corner later on. You are not going to get every issue decided at the get go right, the skill is to make sure you can quickly change if the need arises, and knowing about what possibility's you will need to take advantage of in the future and how can you avoid needing to make complex changes in the moment that don't add squat. There are lots of things you need to do, and being able to prioritise them is an important part of what you do, don't try make cleaver designs, be cleaver in making designs that can be easy for other to understand and modify.
Working with coredate I used the option codegen: category/extension to be able to create a file where I can put in the re-usable code for finding, updating or deleting database entries.
I started the coredata entity first with the codegen option Class Definition and changed it to category/extension in a later stage.
Now I run against a compile error:
'Property cannot be declared public because its type uses an internal type'
The file name is a generated swift file called:
Gameresults+Coredataproperties.swift
I got the error on the player: TournamentPlayer?
player and round are both relations to another entity.
import Foundation
import CoreData
extension GameResults {
#nonobjc public class func fetchRequest() -> NSFetchRequest<GameResults> {
return NSFetchRequest<GameResults>(entityName: "GameResults")
}
#NSManaged public var earnedRankingPoints: Int16
#NSManaged public var framePoints: Int16
#NSManaged public var highestBreak: Int16
#NSManaged public var isWon: Bool
#NSManaged public var player: TournamentPlayer?
#NSManaged public var round: Rounds?
}
I could not believe that the error did come out of swift so I tried the clean build folder option, saving file, exiting XCode, etc. Nothing worked.
Any tips where to look at how to fix this?
Use the Manual/None option for the codegen.
There are a couple of things that you should consider here.
when you use codegen it automatically creates all classes which are equivalent to your entities as public. I assume TournamentPlayer class is not public and you have definded it yourself. so based on default definition it is internal and the compiler gives you the error correctly. the only thing you can do here is changing the access level and because you're using the codegen and it means you want to change your model, so it's better to change your TournamentPlayer to public. Although it depends on the architecture of your application.
The codegen is automatically regenrated whenever you change your model and is saved in DerivedData.
If you want to take care of it yourself you set it to manual and you handle the code generation yourself manually.
check this article, it gives you a better understanding of codegen https://useyourloaf.com/blog/core-data-code-generation/
one more thing, when you change to manual/none you have to do a clean build otherwise it still uses the generated code in DerivedData
I created a CoreData Model with an Entity "News" :
I set it to "Manuel/None" and created a NSManagedObject :
public class News: NSManagedObject {
#NSManaged var id: String
#NSManaged var newsType: Int16
#NSManaged var newsImageUrl: String
#NSManaged var newsVideoUrl: String
#NSManaged var newsTitle: String
#NSManaged var newsDesc: String
}
I want to override the properties of my entity without touching the CoreData Model, just by doing this :
extension News {
#NSManaged var newsUrl: String
}
Of course, if I do :
news.newsUrl = ""
I get a nice
reason: '-[NSManagedObject setNewsUrl:]: unrecognized selector
How can I add properly new properties in my Entity (without modifying CoreData Model) and, of course, I want this news property to be saved in CoreData ?
TY
The approach you tried doesn't work because it's not enough to just declare the new property, you have to make that property exist in the data model. If you don't edit the model, you have to do the work in your code.
You can modify the entire model in code until you start using it. Once you load your persistent store file, you have to treat the object model as read-only. The basic steps would be
Ask the NSManagedObjectModel for its entities or entitiesByName.
Find the appropriate NSEntityDescription in that list.
Create a new NSAttributeDescription for your new property.
Add the new attribute to the properties array on the entity.
This is not a good idea, and I strongly recommend not doing it, but it's not impossible. In many years of Core Data coding I've only modified the model in code once, to work around a (since fixed) bug in the model compiler.
Keep in mind that this does not let you avoid doing model migration. Your persistent store file must match the data model that you use. Modifying the model in code will make managing model versions more difficult, and will increase the odds of the app crashing because the models don't match.
This is for XCode 6 and Swift...
I'm trying to make a fetch request to the managed object context but it's not returning the correct subclass.
I have already set the subclass in the data model data modeler configuration to the name of my custom subclass and in the code, it is extending the NSManagedObject class.
Any ideas?
Just figured out the solution.
I had to add the #objc attribute to allow the class to be compatible with Objective-C.
Now the fetch request is returning a correct result of Tasks[]
import Foundation
import CoreData
#objc(Task) // make compatible with objective-c
class Task : NSManagedObject
{
#NSManaged var note: String!
#NSManaged var completed: Bool
}
Reference: https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html#//apple_ref/doc/uid/TP40014216-CH4-XID_36
Using #objc(Task) seems to be working but you could also just edit the data model data modeler configuration to the name ToDoList.Task instead of just Task. That will work too and avoid Class conflicts if Task is used anywhere else in the Objective-C code.
Check to make sure that in the "Entity" inspector (right side of the screen, Utilities pane) when Task is selected in your Model that its Class field is properly filled in with "Task" (it's blank by default).