Here is my code and I tried to list stored data from Core Data in WidgetKit its not showing at all. I already created app group and the data are showing at preview but when we add widget to home screen nothing shows up. I'm not sure the way I did is correct or not.
What is the best way to list Core Data records in WidgetKit?
import WidgetKit
import SwiftUI
import CoreData
// MARK: For Core Data
public extension URL {
/// Returns a URL for the given app group and database pointing to the sqlite database.
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}
var managedObjectContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
var workingContext: NSManagedObjectContext {
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = managedObjectContext
return context
}
var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "Countdowns")
let storeURL = URL.storeURL(for: "group.app-group-countdowns", databaseName: "Countdowns")
let description = NSPersistentStoreDescription(url: storeURL)
container.loadPersistentStores(completionHandler: { storeDescription, error in
if let error = error as NSError? {
print(error)
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
return container
}()
// MARK: For Widget
struct Provider: TimelineProvider {
var moc = managedObjectContext
init(context : NSManagedObjectContext) {
self.moc = context
}
func placeholder(in context: Context) -> SimpleEntry {
return SimpleEntry(date: Date())
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
return completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .minute, value: 1, to: currentDate)!
let entry = SimpleEntry(date: entryDate)
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
}
struct CountdownsWidgetEntryView : View {
var entry: Provider.Entry
#FetchRequest(entity: Countdown.entity(), sortDescriptors: []) var countdowns: FetchedResults<Countdown>
var body: some View {
return (
VStack {
ForEach(countdowns, id: \.self) { (memoryItem: Countdown) in
Text(memoryItem.title ?? "Default title")
}.environment(\.managedObjectContext, managedObjectContext)
Text(entry.date, style: .time)
}
)
}
}
#main
struct CountdownsWidget: Widget {
let kind: String = "CountdownsWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider(context: managedObjectContext)) { entry in
CountdownsWidgetEntryView(entry: entry)
.environment(\.managedObjectContext, managedObjectContext)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct CountdownsWidget_Previews: PreviewProvider {
static var previews: some View {
CountdownsWidgetEntryView(entry: SimpleEntry(date: Date()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
Just call execute on the managed object context & since you are using a private context, make sure you call performBlock.
NSManagedObjectContext* moc = [self getManagedObjectContext];
[moc performBlock:^{
NSString *entityName = #"myEntity";
NSFetchRequest *fRequest = [[NSFetchRequest alloc]initWithEntityName:entityName];
NSPredicate *predicate = [NSPredicate predicateWithFormat: #"SELECT * FROM myEntity"];
[fRequest setPredicate:predicate];
NSError *error = nil;
NSArray *results = [moc executeFetchRequest:fRequest error:&error];
}];
Related
I am working on a CoreData application with two entities MyList and MyListItem. MyList can have many MyListItem (one to many). When the app is launched, I can see all the lists. I can tap on a list to go to the list items. On that screen, I tap a button to add an item to the selected list. After, adding the item when I go back to the all lists screen I cannot see the number of items reflected in the count. The reason is that MyListsView is not rendered again since the number of lists have not changed.
The complete code is shown below:
import SwiftUI
import CoreData
extension MyList {
static var all: NSFetchRequest<MyList> {
let request = MyList.fetchRequest()
request.sortDescriptors = []
return request
}
}
struct DetailView: View {
#Environment(\.managedObjectContext) var viewContext
let myList: MyList
var body: some View {
VStack {
Text("Detail View")
Button("Add List Item") {
let myListP = viewContext.object(with: myList.objectID) as! MyList
let myListItem = MyListItem(context: viewContext)
myListItem.name = randomString()
myListItem.myList = myListP
try? viewContext.save()
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
class ViewModel: NSObject, ObservableObject {
#Published var myLists: [MyList] = []
private var fetchedResultsController: NSFetchedResultsController<MyList>
private(set) var context: NSManagedObjectContext
override init() {
self.context = CoreDataManager.shared.context
fetchedResultsController = NSFetchedResultsController(fetchRequest: MyList.all, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
guard let myLists = fetchedResultsController.fetchedObjects else { return }
self.myLists = myLists
} catch {
print(error)
}
}
}
extension ViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let myLists = controller.fetchedObjects as? [MyList] else { return }
self.myLists = myLists
}
}
struct MyListsView: View {
let myLists: [MyList]
var body: some View {
List(myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
struct ContentView: View {
#StateObject private var vm = ViewModel()
#Environment(\.managedObjectContext) var viewContext
var body: some View {
NavigationView {
VStack {
// when adding an item to the list the MyListView view is
// not re-rendered
MyListsView(myLists: vm.myLists)
Button("Change List") {
}
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
Inside ContentView there is a view called "MyListsView". That view is not rendered when the items are added. Since, according to that view nothing changed since the number of lists are still the same.
How do you solve this problem?
UPDATE:
What happens if I add one more level of views like for ListCellView as shown below:
struct MyListCellView: View {
#StateObject var vm: ListCellViewModel
init(vm: ListCellViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
HStack {
Text(vm.name)
Spacer()
Text("\((vm.items).count)")
}
}
}
#MainActor
class ListCellViewModel: ObservableObject {
let myList: MyList
init(myList: MyList) {
self.myList = myList
self.name = myList.name ?? ""
self.items = myList.items!.allObjects as! [MyListItem]
print(self.items.count)
}
#Published var name: String = ""
#Published var items: [MyListItem] = []
}
struct MyListsView: View {
#StateObject var vm: ViewModel
init(vm: ViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
let _ = Self._printChanges()
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
MyListCellView(vm: ListCellViewModel(myList: myList))
}
}
}
}
Now the count is again not being updated.
Your ViewModel is an ObserveableObject, but you are not observing it in MyListsView. When you initialized MyListsView, you set a let constant. Of course that won't update. Do this instead:
struct MyListsView: View {
#ObservedObject var vm: ViewModel
init(viewModel: ViewModel) {
self.vm = viewModel
}
var body: some View {
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
Now the #Published in ViewModel will cause MyListView to change when it does, and that includes adding a related entity.
We don't need MVVM in SwiftUI, the View data structs already fill that role and property wrappers make them behave like objects giving best of both worlds. In your case use the #FetchRequest property wrapper for the list and #ObservedObject for the detail and body will be called on any changes to the model data. Examine the code in the app template in Xcode with Core Data checked. It looks like this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
DetailView(item: item)
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
...
struct DetailView: View {
#ObservedObject var item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
I have a CoreData table that has several repeated records that don't need to be stored but do need to be displayed in my user interface. I have manually created my arrays based on the CoreData table. I have made them Observable Objects so they should automatically update and I have made them Hashable and Equatable.
My problem is that the list does not update when the database records are updated. This works fine when records are being added.
Here is my much simplified code in full that demonstrates the problem:
import SwiftUI
let persistentContainerQueue = OperationQueue()
let firstNames = ["Michael", "Damon", "Jacques", "Mika", "Fernando", "Kimi", "Lewis", "Jenson", "Sebastion", "Nico"]
let lastNames = ["Schumacher", "Hill", "Villeneuve", "Hakkinen", "Alonso", "Raikkonen", "Hamilton", "Button", "Vettel", "Rosberg"]
class RepeatedPerson: ObservableObject, Hashable
{
#Published var person: Person
#Published var index: Int
#Published var tested: Bool
init (person: Person, index: Int, tested: Bool)
{
self.person = person
self.index = index
self.tested = tested
}
func hash(into hasher: inout Hasher)
{
hasher.combine(person.firstName)
hasher.combine(person.lastName)
hasher.combine(index)
}
static func == (lhs: RepeatedPerson, rhs: RepeatedPerson) -> Bool
{
return lhs.person.firstName == rhs.person.firstName &&
lhs.person.lastName == rhs.person.lastName &&
lhs.index == rhs.index
}
}
class RepeatedPeople: ObservableObject
{
#Published var people: [RepeatedPerson] = []
}
func getRepeatedPeople() -> [RepeatedPerson]
{
var repeatedPeople:[RepeatedPerson] = []
let records = allRecords(Person.self)
for person in records
{
for index in 1...3
{
repeatedPeople.append(RepeatedPerson(person: person, index: index, tested: true))
}
}
return repeatedPeople
}
struct ContentView: View
{
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var repeatedPeople = RepeatedPeople()
init()
{
repeatedPeople.people = getRepeatedPeople()
}
var body: some View
{
VStack
{
List()
{
ForEach(repeatedPeople.people, id: \.self)
{ repeatedPerson in
Text("\(repeatedPerson.index)) \(repeatedPerson.person.firstName!) \(repeatedPerson.person.lastName!)")
}
}
HStack
{
Button("Add Record", action:
{
addItem()
repeatedPeople.people = getRepeatedPeople()
})
Button("Change Record", action:
{
let q = allRecords(Person.self)
let oldLastName = q[0].lastName
q[0].lastName = lastNames.randomElement()!
print ("changed \(q[0].firstName!) \(oldLastName!) -> \(q[0].firstName!) \(q[0].lastName!)")
saveDatabase()
})
Button("Reset Database", action:
{
deleteAllRecords(Person.self)
})
}
}
}
private func addItem()
{
withAnimation
{
let newItem = Person(context: viewContext)
newItem.timestamp = Date()
newItem.firstName = firstNames.randomElement()!
newItem.lastName = lastNames.randomElement()!
print ("added \(newItem.firstName!) \(newItem.lastName!)")
saveDatabase()
}
}
}
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
let context = PersistenceController.shared.container.viewContext
let request = T.fetchRequest()
if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func deleteAllRecords<T: NSManagedObject>(_ type : T.Type)
{
let context = PersistenceController.shared.container.viewContext
let results = allRecords(T.self)
for record in results
{
context.delete(record as NSManagedObject)
}
saveDatabase()
}
func saveDatabase()
{
persistentContainerQueue.addOperation()
{
let context = PersistenceController.shared.container.viewContext
context.performAndWait
{
try? context.save()
}
}
}
To reproduce the problem, add a few records. These will be shown in the list. Then click the 'Update Record' button. The CoreData record will be updated (you can see this the next time you run the app) but the changes will not be shown.
How do I get the new changes to show?
If you add another record the changes will then be shown. A side effect is that the list introduces wild spaces between the records. I have seen this is in other places. Is this a SwiftUI bug?
OK it turned out to be really quite simple. All I actually had to do was remove some of the #Published and provide a UUID for the repeatedPerson record (and for == and hash).
import SwiftUI
import CoreData
let persistentContainerQueue = OperationQueue()
let firstNames = ["Michael", "Damon", "Jacques", "Mika", "Fernando", "Kimi", "Lewis", "Jenson", "Sebastion", "Nico"]
let lastNames = ["Schumacher", "Hill", "Villeneuve", "Hakkinen", "Alonso", "Raikkonen", "Hamilton", "Button", "Vettel", "Rosberg"]
class RepeatedPerson: ObservableObject, Hashable
{
var id: UUID = UUID()
var index: Int
var person: Person?
init (person: Person, index: Int)
{
self.person = person
self.index = index
}
func hash(into hasher: inout Hasher)
{
hasher.combine(id)
}
static func == (lhs: RepeatedPerson, rhs: RepeatedPerson) -> Bool
{
return lhs.id == rhs.id
}
}
class RepeatedPeople: ObservableObject
{
#Published var people: [RepeatedPerson] = []
}
func getRepeatedPeople() -> [RepeatedPerson]
{
var repeatedPeople:[RepeatedPerson] = []
let records = allRecords(Person.self)
for person in records
{
for index in 1...3
{
repeatedPeople.append(RepeatedPerson(person: person, index: index))
}
}
return repeatedPeople
}
struct ContentView: View
{
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var repeatedPeople = RepeatedPeople()
init()
{
repeatedPeople.people = getRepeatedPeople()
}
var body: some View
{
VStack
{
List()
{
ForEach(repeatedPeople.people, id: \.self)
{ repeatedPerson in
Text("\(repeatedPerson.index)) \(repeatedPerson.person!.firstName!) \(repeatedPerson.person!.lastName!)")
}
}
HStack
{
Button("Add Record", action:
{
addItem()
repeatedPeople.people = getRepeatedPeople()
})
Button("Change Record", action:
{
let q = allRecords(Person.self)
let r = q.randomElement()!
let oldLastName = r.lastName
r.lastName = lastNames.randomElement()!
print ("changed \(r.firstName!) \(oldLastName!) -> \(r.firstName!) \(r.lastName!)")
saveDatabase()
repeatedPeople.people = getRepeatedPeople()
})
Button("Reset Database", action:
{
print ("Reset database")
deleteAllRecords(Person.self)
repeatedPeople.people = getRepeatedPeople()
})
}
}
}
private func addItem()
{
withAnimation
{
let newItem = Person(context: viewContext)
newItem.timestamp = Date()
newItem.firstName = firstNames.randomElement()!
newItem.lastName = lastNames.randomElement()!
print ("added \(newItem.firstName!) \(newItem.lastName!)")
saveDatabase()
}
}
}
func query<T: NSManagedObject>(_ type : T.Type, predicate: NSPredicate? = nil, sort: NSSortDescriptor? = nil) -> [T]
{
let context = PersistenceController.shared.container.viewContext
let request = T.fetchRequest()
if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
if let predicate = predicate
{
request.predicate = predicate
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
return query(T.self, sort: sort)
}
func deleteAllRecords<T: NSManagedObject>(_ type : T.Type)
{
let context = PersistenceController.shared.container.viewContext
let results = allRecords(T.self)
for record in results
{
context.delete(record as NSManagedObject)
}
saveDatabase()
}
func saveDatabase()
{
persistentContainerQueue.addOperation()
{
let context = PersistenceController.shared.container.viewContext
context.performAndWait
{
try? context.save()
}
}
}
I have tried to make an app with a textfield to let user input a location, using MKLocalSearchCompleter to complete the searching. After that i would like to get the coordinate and display on the MapKit. However, I failed to get the coordinate using the Geocoder.
class LocationSearchService: NSObject, ObservableObject, MKLocalSearchCompleterDelegate {
#Published var searchQuery = ""
var completer: MKLocalSearchCompleter
#Published var completions: [MKLocalSearchCompletion] = []
var cancellable: AnyCancellable?
override init() {
completer = MKLocalSearchCompleter()
super.init()
cancellable = $searchQuery.assign(to: \.queryFragment, on: self.completer)
completer.delegate = self
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
self.completions = completer.results
}
}
The location manager as follows:
class LocationManager: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
private let geocoder = CLGeocoder()
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var status: CLAuthorizationStatus? {
willSet { objectWillChange.send() }
}
#Published var location: CLLocation? {
willSet { objectWillChange.send() }
}
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
#Published var placemark: CLPlacemark? {
willSet { objectWillChange.send() }
}
private func lookupLocation() {
guard let location = self.location else { return }
geocoder.reverseGeocodeLocation(location, completionHandler: { (places, error) in
if error == nil {
self.placemark = places?[0]
} else {
self.placemark = nil
}
})
}
// !!! This is the function I would like to use to get the Coordinate from the address obtained from LocationSearchService
func getCoordinate(address: String) {
geocoder.geocodeAddressString(address, completionHandler: { (places, error) in
if error == nil {
self.placemark = places?[0]
self.location = self.placemark?.location
} else {
self.placemark = nil
self.location = nil
}
})
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
self.status = status
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return } //.first or .last?
self.location = location
self.lookupLocation()
}
}
Content View like this:
struct ContentView: View {
#State private var location: String = ""
#ObservedObject var lm = LocationManager()
private let completer = MKLocalSearchCompleter()
#ObservedObject var locationSearchService = LocationSearchService()
var body: some View {
NavigationView {
VStack {
AddressSearchBar(text: $locationSearchService.searchQuery)
List(locationSearchService.completions, id: \.self) { completion in
VStack(alignment: .leading) {
Text(completion.title)
// Error here, I cannot translate the address to location
//Text(lm.getCoordinate(address: completion.title))
}
}.navigationTitle("Search Location")
}
}
A few issues here:
I would like to convert the user selected item (which I failed to implement here) to the address (completion.title) -- i.e., need to get user selection on the suggested item.
I would like to convert the address found in the suggestion to a coordinate, so that I can mark on MapView.
I am trying to re-use a code from swift 7.3. I feel I am very close.
When I try the code on the simulator, the picker view it is empty, but when I try to use it I have only one data show up.
I also try to adapt and other code to load data from core data to it, didn't work.
Any Idea?
Thank you
import CoreData
class ViewController: UIViewController, UIPickerViewDelegate, UIImagePickerControllerDelegate {
#IBOutlet weak var TextField: UITextField!
#IBOutlet weak var stylePicker: UIPickerView!
var styleData = [PilotBase]()
override func viewDidLoad() {
super.viewDidLoad()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "PilotBase")
do {
styleData = try context.fetch(fetchRequest) as! [PilotBase]
} catch let err as NSError {
print(err.debugDescription)
}
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "PilotBase")
request.returnsObjectsAsFaults = false
do{
let result = try context.fetch(request)
for data in result as! [NSManagedObject]{
print(data.value(forKey: "name") as! String)
}
}catch{
print("Failed")
}
}
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0 {
return styleData.count
} else {
return 0
}
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0 {
return styleData[row].name
} else {
return nil
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
print(styleData[row])
stylePicker.selectRow(row, inComponent: 0, animated: true)
TextField.text = styleData[row].name
}
}
Below the code to add data to Core Data.
#IBAction func Save(_ sender: Any) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "PilotBase", in: context)
let newUser = NSManagedObject(entity: entity!, insertInto: context)
newUser.setValue(TextField.text!, forKey: "name")
do{
try context.save()
}catch{
print("failed saving")
}
}
Every time the app runs it downloads the data from the server, how can I stop it from download if the data is already in the device?
import UIKit
import CoreData
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
preloadData()
return true
}
func applicationWillResignActive(application: UIApplication) {
}
func applicationDidEnterBackground(application: UIApplication) {
}
func applicationWillEnterForeground(application: UIApplication) {
}
func applicationDidBecomeActive(application: UIApplication) {
}
func applicationWillTerminate(application: UIApplication) {
}
// MARK: - Core Data stack
lazy var applicationDocumentsDirectory: NSURL = {
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return urls[urls.count-1]
}()
lazy var managedObjectModel: NSManagedObjectModel = {
let modelURL = NSBundle.mainBundle().URLForResource("CoreDataDemo", withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("CoreDataDemo.sqlite")
var failureReason = "There was an error creating or loading the application's saved data."
do {
try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
} catch {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
return coordinator
}()
lazy var managedObjectContext: NSManagedObjectContext = {
let coordinator = self.persistentStoreCoordinator
var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
// MARK: - Core Data Saving support
func saveContext () {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
abort()
}
}
}
// MARK: - CSV Parser Methods
func parseCSV (contentsOfURL: NSURL, encoding: NSStringEncoding) -> [(name:String, detail:String, price: String)]? {
// Load the CSV file and parse it
let delimiter = ","
var items:[(name:String, detail:String, price: String)]?
do {
let content = try String(contentsOfURL: contentsOfURL, encoding: encoding)
print(content)
items = []
let lines:[String] = content.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as [String]
for line in lines {
var values:[String] = []
if line != "" {
// For a line with double quotes
// we use NSScanner to perform the parsing
if line.rangeOfString("\"") != nil {
var textToScan:String = line
var value:NSString?
var textScanner:NSScanner = NSScanner(string: textToScan)
while textScanner.string != "" {
if (textScanner.string as NSString).substringToIndex(1) == "\"" {
textScanner.scanLocation += 1
textScanner.scanUpToString("\"", intoString: &value)
textScanner.scanLocation += 1
} else {
textScanner.scanUpToString(delimiter, intoString: &value)
}
// Store the value into the values array
values.append(value as! String)
// Retrieve the unscanned remainder of the string
if textScanner.scanLocation < textScanner.string.characters.count {
textToScan = (textScanner.string as NSString).substringFromIndex(textScanner.scanLocation + 1)
} else {
textToScan = ""
}
textScanner = NSScanner(string: textToScan)
}
} else {
values = line.componentsSeparatedByString(delimiter)
}
let item = (name: values[0], detail: values[1], price: values[2])
items?.append(item)
}
}
} catch {
print(error)
}
return items
}
func preloadData () {
// Load the data file. For any reasons it can't be loaded, we just return
guard let remoteURL = NSURL(string: "https://drive.google.com/open?id=0B4xB0m95siM2OVRCclRIRXZWZXM/menudata.csv") else {
"https://googledrive.com/host/0ByZhaKOAvtNGTHhXUUpGS3VqZnM/menudata.csv"
return
}
// Remove all the menu items before preloading
removeData()
if let items = parseCSV(remoteURL, encoding: NSUTF8StringEncoding) {
// Preload the menu items
for item in items {
let menuItem = NSEntityDescription.insertNewObjectForEntityForName("MenuItem", inManagedObjectContext: managedObjectContext) as! MenuItem
menuItem.name = item.name
menuItem.detail = item.detail
menuItem.price = (item.price as NSString).doubleValue
do {
try managedObjectContext.save()
} catch {
print(error)
}
}
}
}
func removeData () {
// Remove the existing items
let fetchRequest = NSFetchRequest(entityName: "MenuItem")
do {
let menuItems = try managedObjectContext.executeFetchRequest(fetchRequest) as! [MenuItem]
for menuItem in menuItems {
managedObjectContext.deleteObject(menuItem)
}
} catch {
print(error)
}
}
}
If it's always the exact same data, then you can just try to fetch one entity...
ObjC...
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"MyEntity"];
fetchRequest.fetchLimit = 1;
NSError *error;
NSArray *result = [context executeFetchRequest:fetchRequest error:&error];
if (result == nil) {
// Handle error...
} else if (result.count == 0) {
// You know you do not have any items, so download
}
However, if your server data can change, and it's bulk, then you may want to compute a hash of the data (SHA-1 or similar). You can store the hash from the last bulk data you downloaded, and ask the server for the current hash value.
If the values are different (or if you have no hash value), then get the data from the server.
If it's incremental, you can use the same hash, or just use a timestamp from the server from the last modification to the server data. The client can store that. If they are different, then pull down data since the last timestamp -- (note it does not have to be a timestamp... it could easily just be an incrementing number).
EDIT
I have yet to have any reason to learn swift (though you probably should at least learn to read ObjC since the vast majority of all iOS/OSX code is written in ObjC), so this is just a feeble, uncompiled attempt.
In particular, I'm not sure if "let" creates a constant where the variable binding can't be changed, or if it makes it "const" in a C++ sense, where it can't accept mutating methods, so setting the fetchLimit may or may not work.
let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "MyEntity")
fetchRequest.fetchLimit = 1
do {
let result = try context.executeFetchRequest(fetchRequest)
// I assume this code only gets executed if there is no error
if result.count == 0 {
// You know you do not have any items, so download
}
} catch let error as NSError {
// Handle error
}