I would like to save this kind of arrays with Core Data:
let crypto1 = Cryptos(name: "Bitcoin", code: "bitcoin", symbol: "BTC", placeholder: "BTC Amount", amount: "0.0")
let crypto2 = Cryptos(name: "Bitcoin Cash", code: "bitcoinCash", symbol: "BCH", placeholder: "BCH Amount", amount: "0.0")
Is that even possible?
I know I can create an array to save like that...
let name = "Bitcoin"
let code = "bitcoin"
let symbol = "BTC"
let placeholder = "BTC Amount"
let amount = "0.0"
let cryptos = CryptoArray(context: PersistenceService.context)
cryptos.name = name
cryptos.code = code
cryptos.symbol = symbol
cryptos.placeholder = placeholder
cryptos.amount = amount
crypto.append(cryptos)
PersistenceService.saveContext()
...but this seems pretty inconvenient when a theoretical infinite number of arrays will be created by the user.
What would be the best way for me to save data, load it, edit it and delete it?
This is a question for a tutorial rather than a straight forward answer. I suggest you give some time to read about CoreData. Having said that, your question sounds generic, "Saving array to CoreData in Swift", so I guess it doesn't hurt to explain a simple implementation step by step:
Step 1: Create your model file (.xcdatamodeld)
In Xcode, file - new - file - iOS and choose Data Model
Step 2: Add entities
Select the file in Xcode, find and click on Add Entity, name your entity (CryptosMO to follow along), click on Add Attribute and add the fields you like to store. (name, code, symbol... all of type String in this case). I'll ignore everything else but name for ease.
Step 3 Generate Object representation of those entities (NSManagedObject)
In Xcode, Editor - Create NSManagedObject subclass and follow the steps.
Step 4 Lets create a clone of this subclass
NSManagedObject is not thread-safe so lets create a struct that can be passed around safely:
struct Cryptos {
var reference: NSManagedObjectID! // ID on the other-hand is thread safe.
var name: String // and the rest of your properties
}
Step 5: CoreDataStore
Lets create a store that gives us access to NSManagedObjectContexts:
class Store {
private init() {}
private static let shared: Store = Store()
lazy var container: NSPersistentContainer = {
// The name of your .xcdatamodeld file.
guard let url = Bundle().url(forResource: "ModelFile", withExtension: "momd") else {
fatalError("Create the .xcdatamodeld file with the correct name !!!")
// If you're setting up this container in a different bundle than the app,
// Use Bundle(for: Store.self) assuming `CoreDataStore` is in that bundle.
}
let container = NSPersistentContainer(name: "ModelFile")
container.loadPersistentStores { _, _ in }
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
// MARK: APIs
/// For main queue use only, simple rule is don't access it from any queue other than main!!!
static var viewContext: NSManagedObjectContext { return shared.container.viewContext }
/// Context for use in background.
static var newContext: NSManagedObjectContext { return shared.container.newBackgroundContext() }
}
Store sets up a persistent container using your .xcdatamodeld file.
Step 6: Data source to fetch these entities
Core Data comes with NSFetchedResultsController to fetch entities from a context that allows extensive configuration, here is a simple implementation of a data source support using this controller.
class CryptosDataSource {
let controller: NSFetchedResultsController<NSFetchRequestResult>
let request: NSFetchRequest<NSFetchRequestResult> = CryptosMO.fetchRequest()
let defaultSort: NSSortDescriptor = NSSortDescriptor(key: #keyPath(CryptosMO.name), ascending: false)
init(context: NSManagedObjectContext, sortDescriptors: [NSSortDescriptor] = []) {
var sort: [NSSortDescriptor] = sortDescriptors
if sort.isEmpty { sort = [defaultSort] }
request.sortDescriptors = sort
controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
}
// MARK: DataSource APIs
func fetch(completion: ((Result) -> ())?) {
do {
try controller.performFetch()
completion?(.success)
} catch let error {
completion?(.fail(error))
}
}
var count: Int { return controller.fetchedObjects?.count ?? 0 }
func anyCryptos(at indexPath: IndexPath) -> Cryptos {
let c: CryptosMO = controller.object(at: indexPath) as! CryptosMO
return Cryptos(reference: c.objectID, name: c.name)
}
}
All we need from an instance of this class is, number of objects, count and item at a given indexPath. Note that the data source returns the struct Cryptos and not an instance of NSManagedObject.
Step 7: APIs for add, edit and delete
Lets add this apis as an extension to NSManagedObjectContext:
But before that, these actions may succeed or fail so lets create an enum to reflect that:
enum Result {
case success, fail(Error)
}
The APIs:
extension NSManagedObjectContext {
// MARK: Load data
var dataSource: CryptosDataSource { return CryptosDataSource(context: self) }
// MARK: Data manupulation
func add(cryptos: Cryptos, completion: ((Result) -> ())?) {
perform {
let entity: CryptosMO = CryptosMO(context: self)
entity.name = cryptos.name
self.save(completion: completion)
}
}
func edit(cryptos: Cryptos, completion: ((Result) -> ())?) {
guard cryptos.reference != nil else {
print("No reference")
return
}
perform {
let entity: CryptosMO? = self.object(with: cryptos.reference) as? CryptosMO
entity?.name = cryptos.name
self.save(completion: completion)
}
}
func delete(cryptos: Cryptos, completion: ((Result) -> ())?) {
guard cryptos.reference != nil else {
print("No reference")
return
}
perform {
let entity: CryptosMO? = self.object(with: cryptos.reference) as? CryptosMO
self.delete(entity!)
self.save(completion: completion)
}
}
func save(completion: ((Result) -> ())?) {
do {
try self.save()
completion?(.success)
} catch let error {
self.rollback()
completion?(.fail(error))
}
}
}
Step 8: Last step, use case
To fetch the stored data in main queue, use Store.viewContext.dataSource.
To add, edit or delete an item, decide if you'd like to do on main queue using viewContext, or from any arbitrary queue (even main queue) using newContext or a temporary background context provided by Store container using Store.container.performInBackground... which will expose a context.
e.g. adding a cryptos:
let cryptos: Cryptos = Cryptos(reference: nil, name: "SomeName")
Store.viewContext.add(cryptos: cryptos) { result in
switch result {
case .fail(let error): print("Error: ", error)
case .success: print("Saved successfully")
}
}
Simple UITableViewController that uses the cryptos data source:
class ViewController: UITableViewController {
let dataSource: CryptosDataSource = Store.viewContext.dataSource
// MARK: UITableViewDataSource
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withIdentifier: "YourCellId", for: indexPath)
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let cryptos: Cryptos = dataSource.anyCryptos(at: indexPath)
// TODO: Configure your cell with cryptos values.
}
}
You cannot save arrays directly with CoreData, but you can create a function to store each object of an array. With CoreStore the whole process is quite simple:
let dataStack: DataStack = {
let dataStack = DataStack(xcodeModelName: "ModelName")
do {
try dataStack.addStorageAndWait()
} catch let error {
print("Cannot set up database storage: \(error)")
}
return dataStack
}()
func addCrypto(name: String, code: String, symbol: String, placeholder: String, amount: Double) {
dataStack.perform(asynchronous: { transaction in
let crypto = transaction.create(Into<Crypto>())
crypto.name = name
crypto.code = code
crypto.symbol = symbol
crypto.placeholder = placeholder
crypto.amount = amount
}, completion: { _ in })
}
You can show the objects within a UITableViewController. CoreStore is able to automatically update the table whenever database objects are added, removed or updated:
class CryptoTableViewController: UITableViewController {
let monitor = dataStack.monitorList(From<Crypto>(), OrderBy(.ascending("name")), Tweak({ fetchRequest in
fetchRequest.fetchBatchSize = 20
}))
override func viewDidLoad() {
super.viewDidLoad()
// Register self as observer to monitor
self.monitor.addObserver(self)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.monitor.numberOfObjects()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CryptoTableViewCell", for: indexPath) as! CryptoTableViewCell
let crypto = self.monitor[(indexPath as NSIndexPath).row]
cell.update(crypto)
return cell
}
}
// MARK: - ListObjectObserver
extension CryptoTableViewController : ListObjectObserver {
// MARK: ListObserver
func listMonitorWillChange(_ monitor: ListMonitor<Crypto>) {
self.tableView.beginUpdates()
}
func listMonitorDidChange(_ monitor: ListMonitor<Crypto>) {
self.tableView.endUpdates()
}
func listMonitorWillRefetch(_ monitor: ListMonitor<Crypto>) {
}
func listMonitorDidRefetch(_ monitor: ListMonitor<Crypto>) {
self.tableView.reloadData()
}
// MARK: ListObjectObserver
func listMonitor(_ monitor: ListMonitor<Crypto>, didInsertObject object: Switch, toIndexPath indexPath: IndexPath) {
self.tableView.insertRows(at: [indexPath], with: .automatic)
}
func listMonitor(_ monitor: ListMonitor<Crypto>, didDeleteObject object: Switch, fromIndexPath indexPath: IndexPath) {
self.tableView.deleteRows(at: [indexPath], with: .automatic)
}
func listMonitor(_ monitor: ListMonitor<Crypto>, didUpdateObject object: Crypto, atIndexPath indexPath: IndexPath) {
if let cell = self.tableView.cellForRow(at: indexPath) as? CryptoTableViewCell {
cell.update(object)
}
}
func listMonitor(_ monitor: ListMonitor<Crypto>, didMoveObject object: Switch, fromIndexPath: IndexPath, toIndexPath: IndexPath) {
self.tableView.deleteRows(at: [fromIndexPath], with: .automatic)
self.tableView.insertRows(at: [toIndexPath], with: .automatic)
}
}
Assuming that you have a CryptoTableViewCell with the function update registered to the CryptoTableViewController.
Related
I've got this JSON from an API:
{
"oldest": "2019-01-24T00:00:00+00:00",
"activities": [
{
"message": "<strong>Henrik</strong> didn't resist a guilty pleasure at <strong>Starbucks</strong>.",
"amount": 2.5,
"userId": 2,
"timestamp": "2019-05-23T00:00:00+00:00"
},
{
"message": "<strong>You</strong> made a manual transfer.",
"amount": 10,
"userId": 1,
"timestamp": "2019-01-24T00:00:00+00:00"
}
]
}
It has a lote more activities. How can I access it and fill my cells with its data? So far I've got this code:
MainViewController:
struct Activities: Decodable {
var oldest: String
var activities: [Activity]
}
struct Activity: Decodable {
var message: String
var amount: Float
var userId: Int
var timestamp: String
}
class MainTableViewController: UITableViewController, UITableViewDataSourcePrefetching {
var activityList: [Activities] = []
var activity: [Activity] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.prefetchDataSource = self
let activitiesJSONURLString = "https://qapital-ios-testtask.herokuapp.com/activities?from=2016-05-23T00:00:00+00:00&to=2019-05-23T00:00:00+00:00"
guard let activitiesURL = URL(string: activitiesJSONURLString) else { return }
URLSession.shared.dataTask(with: activitiesURL) { (data, response, err) in
// perhaps check err
// also perhaps check response status 200 OK
guard let data = data else { return }
do {
// Activities
let activities = try JSONDecoder().decode(Activities.self, from: data)
} catch let jsonErr {
print("Error serializing json: ", jsonErr)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}.resume()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return activityList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ActivityCell", for: indexPath) as! MainTableViewCell
return cell
}
// Prefetching
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
// if indexPaths.contains(where: isLoadingCell) {
// viewModel.fetchModerators()
// }
}
}
But I think something is off. Or I have no clue on how to start. I could really use some help or any tips you can give me. Please and thank you!
First of all the naming of the structs is pretty confusing. Name the root object with something unrelated like Response or Root.
And we are going to decode the timestamps as Date
struct Root: Decodable {
var oldest: Date
var activities: [Activity]
}
struct Activity: Decodable {
var message: String
var amount: Float
var userId: Int
var timestamp: Date
}
Second of all as the data is received in all the conformance to UITableViewDataSourcePrefetching is pointless. Remove it and delete also the prefetchRowsAt method.
Declare only one data source array and name it activities
var activities = [Activity]()
and delete
var activityList: Activities!
In the completion handler of the data task decode Root and assign the activities array to the data source array
do {
// Activities
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let result = try decoder.decode(Root.self, from: data)
self.activities = result.activities
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch {
print("Error serializing json: ", error)
}
The table view data source methods are
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return activities.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ActivityCell", for: indexPath) as! MainTableViewCell
let activity = activities[indexPath.row]
// assign the activity data to the UI for example
// cell.someLabel = activity.amount
return cell
}
Because you are using the activityList to determine the number of rows, I'm assuming that you want to use the data from activityList in order to populate your ActivityCells. That is, unless, you meant for activityList to be a single instance of Activities instead of an array of Activities, in which case you would likely use activityList.activities.count in order to determine the number of rows. In either case, lets just call the array of data you want to use to fill the cells activityList.
In this case, you should make sure to update activityList to the activities that you have fetched from the API. Once you have the activityList, you can then use reloadData which will trigger your table view delegate methods. In tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) you can then use activityList in order to update the dequeued cell.
Something like this might be what you want:
class MainTableViewController: UITableViewController, UITableViewDataSourcePrefetching {
var activityList: Activities!
var activity: [Activity] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.prefetchDataSource = self
let activitiesJSONURLString = "https://qapital-ios-testtask.herokuapp.com/activities?from=2016-05-23T00:00:00+00:00&to=2019-05-23T00:00:00+00:00"
guard let activitiesURL = URL(string: activitiesJSONURLString) else { return }
URLSession.shared.dataTask(with: activitiesURL) { (data, response, err) in
// perhaps check err
// also perhaps check response status 200 OK
guard let data = data else { return }
do {
// Activities
let activities = try JSONDecoder().decode(Activities.self, from: data)
self.activityList = activities
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let jsonErr {
print("Error serializing json: ", jsonErr)
}
}.resume()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return activityList.activities.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ActivityCell", for: indexPath) as! MainTableViewCell
if let activity = activityList?[indexPath.row] {
// UPDATE CELL ACCORDING TO activity
}
return cell
}
}
I am creating an ios app that will show me a user specific set of tasks everyday that i click to delete to show its done. I save the tasks in core data, and just delete the tableview row on click. I dont delete the data in coredata as its userdefined and need to reload it everyday. I use a newDay() function to decide to load data from coredata if the app is opened on a new day. What should i do to remember which all tasks have been done for the day? Do i need to create another enitity to remember which all tasks are completed or is there an simpler way?
var tasks: [NSManagedObject] = []
let defaults = UserDefaults.standard
var calender = Calendar.current
override func viewDidLoad() {
super.viewDidLoad()
title = "DailyTasker"
navigationItem.leftBarButtonItem = editButtonItem
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let checkDate = newDay()
if checkDate{
//1
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
//2
let fetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Task")
//3
do {
tasks = try managedContext.fetch(fetchRequest)
defaults.set(Date(), forKey: "LastRun")
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
}
func newDay() -> Bool{
if let lastRun = defaults.object(forKey: "LastRun") as? Date{
if !calender.isDateInToday(lastRun){
return true
} else {
return false
}
} else {
return true
}
}
#IBAction func addName(_ sender: UIBarButtonItem) {
let alert = UIAlertController(title: "New Task",
message: "Add a new task",
preferredStyle: .alert)
let saveAction = UIAlertAction(title: "Save",
style: .default) {
[unowned self] action in
guard let textField = alert.textFields?.first,
let nameToSave = textField.text else {
return
}
self.save(name: nameToSave)
self.tableView.reloadData()
}
let cancelAction = UIAlertAction(title: "Cancel",
style: .default)
alert.addTextField()
alert.addAction(saveAction)
alert.addAction(cancelAction)
present(alert, animated: true)
}
func save(name: String) {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
// 1
let managedContext =
appDelegate.persistentContainer.viewContext
// 2
let entity =
NSEntityDescription.entity(forEntityName: "Task",
in: managedContext)!
let task = NSManagedObject(entity: entity,
insertInto: managedContext)
// 3
task.setValue(name, forKeyPath: "name")
// 4
do {
try managedContext.save()
tasks.append(task)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return tasks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let task = tasks[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "TaskerCell", for: indexPath)
cell.textLabel?.text = task.value(forKeyPath: "name") as? String
return cell
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Delete the row from the data source
tasks.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tasks.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
You could add a date attribute to your task and name it lastDonefor instance. Then you set that to current date time when a task is done and also use a predicate when fetching task instances so you only get those not done today.
task.lastDone = Date()
I am not sure how you define "today" but this question should help you create a predicate that properly filters your task although you might also want to include tasks where lastDone is null.
I am not sure Why you can't delete the data. Is it due to specifications.
If not then, when you save your task in coredata simply assign it with a unique Identifer(id) and then you can create your own data stack Method to delete the specific task.
You can create a data Model Class or struct for the task example
class TaskData {
var id: Int!
var task: String!
init(id: Int, task: String) {
self.id = id
self.task = task
}
}
Save task as this dataClass into coreData.
When you delete the row at that time captue the Task Id and delete it from coreDataStack.
A good way will be to create a TaskManager Singelton class to handle all the core data Methods.
I am trying to use RealmSwift in order to save items to the phone storage in Swift 4. I have two different Views; one for the save functionality and another which will display all saved items into a TableView. I have a buildable form coded but i am throwing an error Thread 1: signal SIGABRT specifically on the line when i call realm.add. When i am in my view which is saving, i am using a IBAction with a button to initiate the save functionality. Can anyone help me with this issue? I think the issue is when i set the var of realm however i am unsure.
UPDATE:
I have changed my implementation to reflect the idea given in this thread about my original issue. After doing so, when the call to add the item to the realm is called i crash EXC_BAD_ACCESS (code=EXC_I386_GPFLT) inside the source code of the API. Specifically I crash at this function of the API
//CODE EXCERPT FROM REALMSWIFT API
// Property value from an instance of this object type
id value;
if ([obj isKindOfClass:_info.rlmObjectSchema.objectClass] &&
prop.swiftIvar) {
if (prop.array) {
return static_cast<RLMListBase *>(object_getIvar(obj,
prop.swiftIvar))._rlmArray;
}
else { // optional
value = static_cast<RLMOptionalBase *>(object_getIvar(obj,
prop.swiftIvar)).underlyingValue; //CRASH OCCURS HERE!!!!!!!!
}
}
else {
// Property value from some object that's KVC-compatible
value = RLMValidatedValueForProperty(obj, [obj
respondsToSelector:prop.getterSel] ? prop.getterName : prop.name,
_info.rlmObjectSchema.className);
}
return value ?: NSNull.null;
import UIKit
import RealmSwift
class DetailsViewController: UIViewController {
var titleOfBook: String?
var author: String?
#IBAction func SavetoFavorites(_ sender: Any) {
DispatchQueue.global().async { [weak self] in
guard let strongSelf = self else { return }
guard let realm = try? Realm() else {
return
}
let newItem = Favorites()
newItem.title = strongSelf.titleOfBook
newItem.author = strongSelf.author
try? realm.write {
realm.add(newItem) // Crashes on this line
}
}
}
import UIKit
import RealmSwift
final class Favorites: Object {
var title: String?
var author: String?
}
class FavoritesTableViewController: UITableViewController {
var items: Array<Favorites> = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier:
"cell")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 0
}
override func tableView(_ tableView: UITableView?,
numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt
indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell",
for: indexPath)
let item = items[indexPath.row]
cell.textLabel?.text = item.title
cell.detailTextLabel?.text = item.author
return cell
}
var selectedIndexPath: NSIndexPath = NSIndexPath()
override func tableView(_ tableView: UITableView, willSelectRowAt
indexPath: IndexPath) -> IndexPath? {
selectedIndexPath = indexPath as NSIndexPath
return indexPath
}
You have to wrap realm.add(newItem) inside a transaction:
try! realm.write {
realm.add(newItem)
}
Please note, that write transactions block the thread they are made on so if you're writing big portions of data you should do so on background thread (realm has to be instantiated on that thread too). You could do it like this:
#IBAction func saveToFavorites(_ sender: Any) {
DispatchQueue.global().async { [weak self] in
guard let strongSelf = self else { return }
guard let realm = try? Realm() else {
// avoid force unwrap, optionally report an error
return
}
let newItem = Favorites()
newItem.title = strongSelf.titleOfBook
newItem.author = strongSelf.author
try? realm.write {
realm.add(newItem)
}
}
}
Update: I haven't noticed that you have an issue with your model too – since Realm is written with Objective C you should mark your model properties with #objc dynamic modifiers:
final class Favorites: Object {
#objc dynamic var title: String?
#objc dynamic var author: String?
}
All changes to Realm managed objects (either creation, modification or deletion) need to happen inside write transactions.
do {
try realm.write {
realm.add(newItem)
}
} catch {
//handle error
print(error)
}
For more information, have a look at the writes section of the official docs.
Another problem you have in there is that in your Favorites class properties are missing #objc dynamic attributes. You can read about why you need that in realm docs.
Your code should look like this then:
final class Favorites: Object {
#objc dynamic var title: String?
#objc dynamic var author: String?
}
Im getting user's input and creating CoreData entity for it, and put in to the tableView, but before i need to designate some properties for entity by making a couple network requests. And there is a problem. First entity is always nil (and create a empty cell for table view) but if i save context and open app again - here it is, entity is ok. If i create second (and so on) entity it works fine. Its also seems like table always create am empty cell for start and then delete it and create proper cell…
I cant get why completion handlers doesn't work properly and network requests makes asynchronously. Please help me to understand.
Creation metod starts in textFieldShouldReturn
import UIKit
import CoreData
class ViewController: UIViewController, UITextFieldDelegate {
var managedObjectContext: NSManagedObjectContext?
lazy var fetchedResultsController: NSFetchedResultsController<Word> = {
let fetchRequest: NSFetchRequest<Word> = Word.fetchRequest()
let createdSort = NSSortDescriptor(key: "created", ascending: false)
let idSort = NSSortDescriptor(key: "id", ascending: false)
fetchRequest.sortDescriptors = [createdSort, idSort]
var fetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: AppDelegate.viewContext,
sectionNameKeyPath: nil,
cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
}()
private var hasWords: Bool {
guard let fetchedObjects = fetchedResultsController.fetchedObjects else { return false }
return fetchedObjects.count > 0
}
let creator = WordManager.shared
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var messageLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
managedObjectContext = AppDelegate.viewContext
fetchWords()
updateView()
}
func updateView() {
tableView.isHidden = !hasWords
messageLabel.isHidden = hasWords
}
............
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard let managedObjectContext = managedObjectContext else { return false }
guard let title = textField.text, !title.isEmpty, !title.hasPrefix(" ") else {
showAllert(title: "Warning!", message: "Title may not to be nil!")
return false
}
let word = Word(context: managedObjectContext)
guard let fetchedWordsCount = fetchedResultsController.fetchedObjects?.count else { return false }
creator.createWord(title: title, counter: fetchedWordsCount, word: word)
self.updateView()
textField.text = nil
return true
}
func fetchWords() {
do {
try fetchedResultsController.performFetch()
} catch {
fatalError("Cant fetch words. Error \(error.localizedDescription)")
}
}
}
Creating entity
import CoreData
import UIKit
class WordManager {
static var shared = WordManager()
private init() {}
private var api = DictionaryAPI()
private var api2 = MeaningAPI()
func createWord(title: String, counter: Int, word: Word) {
self.api.fetch(word: title, completion: { translation in
self.api2.fetchTranscription(word: title, completion: { transcription in
word.id = Int16(counter) + 1
word.title = title
word.translation = translation
word.transcription = "[\(transcription)]"
word.created = NSDate()
})
})
}
}
Nothing interesting in tableView
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
guard let sections = fetchedResultsController.sections else { return 0 }
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = fetchedResultsController.sections?[section] else { return 0 }
return section.numberOfObjects
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "mainCell", for: indexPath) as! MainCell
let word = fetchedResultsController.object(at: indexPath)
cell.configure(with: word)
return cell
}
}
Configure cell method
func configure(with word: Word) {
titleField.text = word.title
translationField.text = word.translation
transcriptionLabel.text = word.transcription
totalCounter.text = "\(word.showCounter)"
correctCounter.text = "\(word.correctCounter)"
wrongCounter.text = "\(word.wrongCounter)"
}
Fetched Results Delegate
extension ViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
updateView()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .delete:
guard let indexPath = indexPath else { return }
tableView.deleteRows(at: [indexPath], with: .fade)
case .insert:
guard let indexPath = newIndexPath else { return }
tableView.insertRows(at: [indexPath], with: .fade)
case .move:
guard let indexPath = indexPath else { return }
tableView.deleteRows(at: [indexPath], with: .fade)
guard let newIndexPath = newIndexPath else { return }
tableView.insertRows(at: [newIndexPath], with: .fade)
default:
break
}
}
}
Problem:
The data you have in core data is not in a state that is ready to be shown in the app.
The NSFetchedResultsController just happens to pick this data and shows it in the app.
Approach:
When a user creates a records, the user wants to see the changes immediately in the app and shouldn't have to wait for the app to update in the server to see the changes. So a user would still be able to create records even when there is no network connectivity.
Create a child context from view context
Use a background thread to upload to the server and when update records on to the child context.
Save child context only when the data is in a consistent state.
NSFetchedResultsController should be using the view context so would be unaware of the child context records.
When you save the child context, the NSFetchedResultsController would pick these records.
I have two classes that form a one to many relationship:
class FooA: Object {
dynamic var someAValue: String
let fooBList = List<FooB>()
}
class FooB: Object {
dynamic var someBValue: String
}
I add the object into Realm in this fashion:
let fooA = FooA()
fooA.someAValue = 'a value'
let fooB = FooB()
fooB.someBValue = 'a value'
fooA.fooBList.append(fooB)
let realm = try! Realm()
try! realm.write {
realm.add(fooA)
}
Everything goes well, the object is inserted into Realm. However, when I attempt to access the FooB object like this:
fooA.fooBList[0]
It goes BOOM and the app crashes! So, obviously, I did not insert the relationship object into Realm correctly and the relationship is corrupted. What did I miss in the Realm documentation? Must I create the Array first and add it to the parent object in another fashion?
To update on my question, this is the operation that wants to blow up:
FooApiClient.getFooAObjects()
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .default))
.observeOn(MainScheduler.instance)
.subscribe { [weak self] in
switch $0 {
case .next(let fooAObjects):
self?.fooAObjects = fooAObjects
case .completed:
self?.fooTableView.reloadData()
loadingView?.hide()
case .error(let error):
loadingView?.hide()
self?.handleError(error)
self?.showNoFooView()
}
}
.addDisposableTo(disposeBag)
The invocation: self?.fooTableView.reloadData() causes to the table view to attempt to display the foo objects retrieved and where it goes BOOM.
Try something like this. It may have some mistakes.
class SomeTableViewController: UITableViewController {
var fooA : FooA?
override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
if let aFooA = realm.objects(FooA.self).first{
fooA = aFooA
self.reloadTableView()
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let _ = fooA{
return fooA!.fooBList.count
}
return 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SomeCell", for: indexPath)
// Configure the cell...
let fooB = fooA!.fooBList[indexPath.row]
cell.titleLabel.text = fooB.someBValue
return cell
}
}