I have 2 Controllers: TableViewController and ViewController. TableViewController is responsible for displaying all data, View Controller is responsible for creating new data.
Now I want to make it possible to edit the current data also in ViewController. When we click on data, we need to switch to the ViewController and replace all default values with the current values. When we change and click save, we go back to TableViewController, where we already see the change.
class OperationsViewController: UITableViewController {
// MARK: - Stored Properties
var transactions: Results<Transaction>!
var sections = [(date: Date, items: Results<Transaction>)]()
// MARK: - UITableViewController Methods
override func viewDidLoad() {
super.viewDidLoad()
transactions = realm.objects(Transaction.self)
}
override func viewWillAppear(_ animated: Bool) {
super .viewWillAppear(animated)
assembleGroupedTransactions()
tableView.reloadData()
}
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let indexPath = tableView.indexPathForSelectedRow {
let section = sections[indexPath.section]
let item = section.items[indexPath.row]
print(item)
if segue.identifier == "editOrDeleteOperationCell" {
let addTableViewController = segue.destination as! AddTableViewController
addTableViewController.defaultTransaction = item
}
}
}
}
// MARK: - User Interface
extension OperationsViewController {
#discardableResult private func assembleGroupedTransactions() -> Array<Any> {
// fetch all Items sorted by date
let results = realm.objects(Transaction.self).sorted(byKeyPath: "date", ascending: false)
sections = results
.map { item in
// get start of a day
return Calendar.current.startOfDay(for: item.date)
}
.reduce([]) { dates, date in
// unique sorted array of dates
return dates.last == date ? dates : dates + [date]
}
.compactMap { startDate -> (date: Date, items: Results<Transaction>) in
// create the end of current day
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)!
// filter sorted results by a predicate matching current day
let items = results.filter("(date >= %#) AND (date < %#)", startDate, endDate)
// return a section only if current day is non-empty
return (date: startDate, items: items)
}
return sections
}
But when I trying to send current data to next ViewController I get error:
*** Terminating app due to uncaught exception 'RLMException', reason: 'Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first.'
I guess that I have problem with Category. Look on my model:
class Transaction: Object {
#objc dynamic var controlStatus = 0
#objc dynamic private var privateCategory: String = Category.consumption.rawValue
var category: Category {
get { return Category(rawValue: privateCategory)! }
set { privateCategory = newValue.rawValue }
}
#objc dynamic var amount = "0"
#objc dynamic var date = Date()
#objc dynamic var note = ""
}
controlStatus needs for monitors the status of the transaction that will be needed in the future. Where 0 is the expense, 1 is the income.
The big problem I suppose is that I created categories by enum.
I need to change the arrays with categories depending on the controlStatus.
Now is this my model of Category:
indirect enum Category: String {
case income = "+"
case consumption = "-"
case salary = "salary"
case billingInterest = "billingInterest"
case partTimeJob = "partTimeJob"
etc.
}
extension Category: RawRepresentable {
typealias RawValue = String
init?(rawValue: RawValue) {
switch rawValue {
case "+": self = .income
case "-": self = .consumption
case "salary": self = .salary
case "billingInterest": self = .billingInterest
case "partTimeJob: self = .partTimeJob
case "pleasantFinds": self = .pleasantFinds
case "debtRepayment": self = .debtRepayment
case "noCategories": self = .noCategories
case "food": self = .food
etc.
default:
return nil
}
}
var rawValue: RawValue {
switch self {
case .salary:
return "salary"
case .billingInterest:
return "billingInterest"
case .partTimeJob:
return "partTimeJob"
case .pleasantFinds:
return "pleasantFinds"
case .debtRepayment:
return "debtRepayment"
case .noCategories:
return "noCategories"
case .food:
return "food"
case .cafesAndRestaurants:
return "cafesAndRestaurants"
etc.
}
}
}
You need to enclose your realm write transactions in write block
try realm.write({ () -> Void in
realm.add(object, update: true)
})
From Realm doc
try! realm.write {
realm.add(myDog)
}
Related
I'm building a project management app in Swift using Realm as a database to store my projects.
ThisWeekViewController is my initial View Controller; it incorporates a UITableView to show projects that are due this week. Using a Bar Button, the user can segue to AddProjectViewController, which, as the name suggests, is used to create new projects and save them to the realm database. AddProjectViewController is presented modally.
After entering the required project details, the user can click a save button, which saves the project to the realm database and dismisses the view, returning to ThisWeekViewController. However, I'm having trouble updating the TableView to reflect the addition of a new project.
After consulting the Realm documentation, I now understand that in Realm, it is not necessary to manually add or delete rows from my TableView after the database has changed. Instead, you should use a notification handler. However, I'm not quite sure how and where to incorporate it in order to reload my TableView after dismissing AddProjectViewController.
Calling the handler in ThisWeekViewController's ViewWillAppear method won't work as the view never actually disappears, because AddProjectViewController is presented modally.
AddProjectViewController:
class AddProjectViewController: FormViewController {
// Realm Initialization
let realm = try! Realm()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem?.isEnabled = false
form +++ Section()
<<< TextRow(){ row in
row.placeholder = "Postname"
row.tag = "ProjectName"
}.onChange({ (row) in
if row.cell.textField.hasText {
self.navigationItem.rightBarButtonItem?.isEnabled = true
} else {
self.navigationItem.rightBarButtonItem?.isEnabled = false
}
})
<<< TextAreaRow() { row in
row.placeholder = "Notizen"
row.textAreaHeight = .fixed(cellHeight: 240.0)
row.tag = "ProjectNotes"
}
form +++ Section()
<<< DateTimeInlineRow() { row in
row.title = "Fällig am"
row.value = Date(timeIntervalSinceNow: 0)
row.minuteInterval = 15
row.tag = "ProjectDueDate"
}
navigationOptions = RowNavigationOptions.Enabled.union(.StopDisabledRow)
animateScroll = true
rowKeyboardSpacing = 20
}
// MARK: - User defined functions
// If user presses the cancel button, the view is dismissed from screen.
#IBAction func cancelButtonPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true) {}
}
// If user presses the save button, a new Project() item is created and saved to the Realm database.
#IBAction func saveButtonPressed(_ sender: UIBarButtonItem) {
// print(form.values())
let newProject = Project()
let titleRow: TextRow? = form.rowBy(tag: "ProjectName")
let projectName = titleRow?.value
newProject.title = projectName!
let notesRow: TextAreaRow? = form.rowBy(tag: "ProjectNotes")
let projectNotes = notesRow?.value
newProject.notes = projectNotes
let dueDateRow: DateTimeInlineRow? = form.rowBy(tag: "ProjectDueDate")
let projectDueDate = dueDateRow?.value
newProject.dueDate = projectDueDate
newProject.dateCreated = NSDate.now
print(newProject)
// Save the new Project to the realm database
do {
try self.realm.write {
realm.add(newProject)
}
}
catch {
print("Error saving item to Realm database. \(error)")
}
print(newProject)
self.dismiss(animated: true)
}
}
ThisWeekViewController:
class ThisWeekViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
// MARK: - Variables
#IBOutlet weak var thisWeekTableView: UITableView!
// Realm initialization
let realm = try! Realm()
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
print(Realm.Configuration.defaultConfiguration.fileURL!)
}
// MARK: - Data Source / Delegate Methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 7
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//TODO: Handle user selection of a specific planned post
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Variables
let thisWeekCell = thisWeekTableView.dequeueReusableCell(withIdentifier: ThisWeekTableViewCell.reuseIdentifier()) as! ThisWeekTableViewCell
let today = Date()
let day = Calendar.current.date(byAdding: .day, value: indexPath.row, to: today)
let dayNumber = Calendar.current.component(.day, from: day!)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEEE"
let dayName = dateFormatter.string(from: day!)
let startOfToday = Calendar.current.startOfDay(for: day!)
let endOfToday = Calendar.current.date(byAdding: .second, value: 86399, to: startOfToday)!
let projectNamesForCurrentDay = projectNamesForDay(startOfDay: startOfToday, endOfDay: endOfToday)
thisWeekCell.setupProjectNameLabel(projectNames: projectNamesForCurrentDay)
if indexPath.row == 0 {
thisWeekCell.dayNumberLabel.textColor = UIColor.orange
thisWeekCell.dayTextLabel.text = "Heute (\(dayName))"
}
if indexPath.row == 1 {
thisWeekCell.dayTextLabel.text = "Morgen (\(dayName))"
}
if indexPath.row > 1 {
thisWeekCell.dayTextLabel.text = dayName
}
thisWeekCell.dayNumberLabel.text = String(dayNumber)
return thisWeekCell
}
// MARK: - User definded functions
/**
Initial setup for the TableView. Registers all neccessary custom cells, sets the delegate and dataSource, and enables AutoLayout.
*/
func setupTableView() {
thisWeekTableView.register(UINib(nibName: ThisWeekTableViewCell.nibName(), bundle: nil), forCellReuseIdentifier: ThisWeekTableViewCell.reuseIdentifier())
thisWeekTableView.delegate = self
thisWeekTableView.dataSource = self
thisWeekTableView.rowHeight = UITableView.automaticDimension
thisWeekTableView.estimatedRowHeight = 54.0
}
/**
Takes two boundary objects of type Date and returns an array of strings containing the titles of all projects in the database that have a due date between the boundary dates.
- Parameter startOfDay: The start-of-day boundary object
- Parameter endOfDay: The end-of-day boundary object
- Returns: An array of strings containing project titles
*/
func projectNamesForDay(startOfDay: Date, endOfDay: Date) -> [String] {
let filteredProjects = realm.objects(Project.self).filter("dueDate BETWEEN %#", [startOfDay, endOfDay])
var projectNames = [String]()
for project in filteredProjects {
projectNames.append(project.title)
}
return projectNames
}
}
You should use Realm notification to auto update tableview each time Project Result change:
// Observe Realm Notifications
let token = projectResult.observe { notification, realm in
// reloadTableView
}
// later
token.invalidate()
class SomeClass {
var dates: [Date] {
get {
return ckRecord.object(forKey: "dates") as! [Date]
}
set(newDates) {
ckRecord.setObject(newDates as! CKRecordValue, "dates")
}
}
}
In the previous code, how do I write code in the get and set closures to save to CloudKit and retrieve the data from CloudKit everytime I get one of the values from the array or set one of the values in the array, which means I don't retrieve the whole array or set the whole array, only one of the values at a given index as in the following code:
var obj = SomeClass()
obj.dates[0] = Date()
I don't have a problem using CloudKit. I have a problem figuring out how to arrange the code for the get and set closures so that I properly access the array from the CloudKit record by index. I am attempting to wrap the CloudKit record in class SomeClass.
Any help will be appreciated.
I believe, you can't do that by implementing get/set for your property. But you can do that at least by two ways:
1) Extract logic of getter/setter in function:
func getDate(_ index: Int) -> Date?
func set(date: Date, index: Int)
This will work fine, but it looks ugly.
2) More swifty way is using the subscript. In this case you create class, that holds private dates and this class allows you to access concrete date by using subscript. Simple example:
class Dates {
private var dates: [Date] = []
subscript(index: Int) -> Date? {
get {
guard dates.indices.contains(index) else { return nil }
return dates[index]
}
set(newValue) {
guard let date = newValue else { return }
dates.insert(date, at: index)
}
}
}
My suggestion is an extension of CKRecord with functions to insert, add and get a date by index and index subscription.
To modify the array you always have to get it from the record, change it and put it back.
extension CKRecord {
func date(at index : Int) -> Date? {
guard let dates = self["dates"] as? [Date], index < dates.count else { return nil }
return dates[index]
}
func appendDate(_ date: Date) {
guard var dates = self["dates"] as? [Date] else { return }
dates.append(date)
self["dates"] = dates as CKRecordValue
}
func insertDate(_ date : Date, at index: Int) {
guard var dates = self["dates"] as? [Date], index <= dates.count else { return }
dates.insert(date, at: index)
self["dates"] = dates as CKRecordValue
}
public subscript(index: Int) -> Date? {
get {
guard let dates = self["dates"] as? [Date], index < dates.count else { return nil }
return dates[index]
}
set {
guard let newDate = newValue,
var dates = self["dates"] as? [Date],
dates.indices.contains(index) else { return }
dates[index] = newDate
self["dates"] = dates as CKRecordValue
}
}
}
My code has 2 textfields one for int and the other for string. The way the code is supposed to be sorted is by ascending alphabetical order for the string (a-z)then by descending order for the int (9-1).So i entered 2 entries in a,2 a,1 order but the way the list is being displayed is a,2 a,1 which is not the sorted order. How can I keep the sorted order?
VIEWCONTROLLER
#IBAction func move(_ sender: Any) {
yourArray.append((textA.text!))
number.append(Int(textB.text!)!)
let tuples = zip(yourArray,number)
let sorted = tuples.sorted(by: { this, next in
if this.0 < next.0 {
return true
} else if this.0 == next.0 {
return this.1 < next.1
} else {
return false
}})
bad.mm.append(String(describing: sorted.map { " \($0)" }.joined(separator:"\n")))
}
struct bad {
static var mm = [String]()}
VIEWCONTROLLER2
override func viewDidLoad() {
super.viewDidLoad()
let defaults = UserDefaults.standard
defaults.set(benCarson.text, forKey: "SavedStringArray2")
defaults.synchronize()
benCarson.text = String(describing: bad.mm)
benCarson.numberOfLines = 5000
}
I am working on a coin collection app, and I have recently started integrating core data into my app. I have a core data database which stores custom CoinCategory objects and each CoinCategory object has an array of Coin objects. This way, my app stores everything in terms of categories.
When I integrated the core data, I can add my first category without any errors and delete it without any problems, but when I add in a second coin category, I experience the following error:
2017-06-26 09:55:37.218 CoinCollection[18889:12839563] -[CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0
2017-06-26 09:55:37.219215-0400 CoinCollection[18889:12839563] [error] error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0 with userInfo (null)
CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. -[CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0 with userInfo (null)
Could anyone please advise me on how to fix this? Thank you very much!
I am attaching my code below for my viewcontroller and the app delegate that is running the core data.
AppDelegate.swift
// We customize the app, system-wide
import UIKit
import CoreData
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "CoinCollection")
container.loadPersistentStores(completionHandler: { (storeDescription,
error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext ()
{
let context = persistentContainer.viewContext
if context.hasChanges
{
do
{
try context.save()
}
catch
{
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate.
//You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
And then for my view controller. Notice that I get a Coin object from a sub-viewcontroller that invokes this method, and that we decide whether the Coin object fits into the existing categories. If not, then we add the new Coin.
// Controls the table view controller showing the general coins (one per each category)
import UIKit
import CoreData
class CoinTableViewController: UITableViewController, NSFetchedResultsControllerDelegate {
//this is an array of all the coins in the collection
//each row of this two-dimensional array represents a new category
var coinsByCategory: [CoinCategoryMO] = []
var fetchResultController: NSFetchedResultsController<CoinCategoryMO>!
//other attributes....
////////////////////////////////////////////////////////////////////////
override func viewDidLoad()
{
super.viewDidLoad()
//////////////////////////////////////////////////////////////////////////////////
//we now fetch the data
let fetchRequest : NSFetchRequest<CoinCategoryMO> = CoinCategoryMO.fetchRequest()
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
{
let context = appDelegate.persistentContainer.viewContext
let sortDescriptor = NSSortDescriptor(key: "coinCategory", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
fetchResultController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
fetchResultController.delegate = self
do
{
try fetchResultController.performFetch()
if let fetchedObjects = fetchResultController.fetchedObjects
{
self.coinsByCategory = fetchedObjects
}
}
catch
{
print(error)
}
}
//configure even more....
}
// MARK: - Table view data soure
func deleteCoinCategory(rowPath: IndexPath)
{
if 0 <= rowPath.row && rowPath.row < self.coinsByCategory.count
{
//we have just tested that the rowPath index is valid
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
{
let context = appDelegate.persistentContainer.viewContext
let coinCategoryToDelete = self.fetchResultController.object(at: rowPath)
context.delete(coinCategoryToDelete)
appDelegate.saveContext()
}
}
}
func deleteCoin(c: Coin, indexOfSelectedCategory: IndexPath) -> Bool
{
//we have a coin that we want to delete from this viewcontroller
//and the data contained in it.
//
//the parameter indexOfSelectedCategory refers to the IndexPath of the
//row in the TableView contained in THIS viewcontroller whose category
//of coins we are modifying in this method
//
//Return value: a boolean that indicates whether a single coin has
//been deleted - meaning that the user should return to the parentviewcontroller
if 0 < indexOfSelectedCategory.row && indexOfSelectedCategory.row < self.coinsByCategory.count && self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.hasCoin(c: c) == true
{
//the index is valid as it refers to a category in the coinsByCategory array
//and the examined category has the coin in question
if self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.count == 1
{
//the coin "c" that we are going to delete is the only coin in the entire category
self.deleteCoinCategory(rowPath: indexOfSelectedCategory)
return true
}
else
{
//more than one coin in the category
self.coinsByCategory[indexOfSelectedCategory.row].coinCategory?.removeCoin(c: c)
//we save the changes in the database...
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
{
appDelegate.saveContext()
}
return false
}
}
return false
}
func addCoin(coinToAdd: Coin)
{
//we check over each category to see if the coin can be added
for category in self.coinsByCategory
{
if category.coinCategory?.coinFitsCategory(aCoin: coinToAdd) == true
{
//we can add the coin to the category
category.coinCategory?.addCoin(newCoin: coinToAdd)
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
{
//we save changes to the database
appDelegate.saveContext()
//we are DONE with this function
return
}
}
}
//since the coinToAdd does not fall in the existing categories, we create a new one
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate)
{
let newCategory = CoinCategoryMO(context: appDelegate.persistentContainer.viewContext)
newCategory.coinCategory = CoinCategory(coins: [coinToAdd], categoryType: CoinCategory.CategoryTypes.COUNTRY_VALUE_AND_CURRENCY)
print("Saving data to context ...")
appDelegate.saveContext()
}
}
//delegate methods control the core data database
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
{
tableView.beginUpdates()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)
{
switch type
{
case .insert :
if let newIndexPath = newIndexPath
{
tableView.insertRows(at: [newIndexPath], with: .fade)
}
case .delete:
if let indexPath = indexPath
{
tableView.deleteRows(at: [indexPath], with: .fade)
}
case .update:
if let indexPath = indexPath
{
tableView.reloadRows(at: [indexPath], with: .fade)
}
default:
tableView.reloadData()
}
if let fetchedObjects = controller.fetchedObjects
{
self.coinsByCategory = fetchedObjects as! [CoinCategoryMO]
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
{
tableView.endUpdates()
}
I am looking forward to hearing your thoughts and thanks in advance for the help!
EDIT:
My code for the CoinCategory class:
// This class is supposed to represent a category of coin objects in CoreData
import Foundation
import CoreData
public class CoinCategory: NSObject, NSCoding
{
//These are the various types of categories that a user can create out of their coin collection
enum CategoryTypes : NSString
{
case COUNTRY_VALUE_AND_CURRENCY = //...
case COUNTRY = //...
case YEAR = //...
case CURRENCY = //...
case NO_CATEGORY = //...
}
//this struct is used to encode data in Key-Value pairs per the NSCoding protocol
struct Keys
{
static let Current_Category_Type = "current_category_type"
static let Coins_In_Category = "coins_in_category"
}
//this is the collection of the coins in the category
var coinsInCategory: [Coin] = [] //initially we have no coins in the collection
var currentCategoryType : CategoryTypes.RawValue = ""
public var count : NSNumber
{
get
{
//..number of coins in category
}
}
public var array : [Coin]
{
get
{
//read-only copy of the Coins array..
}
}
public required init?(coder aDecoder: NSCoder)
{
//we decode this object's information
if let categoryTypeObject = aDecoder.decodeObject(forKey: Keys.Current_Category_Type) as? CategoryTypes.RawValue
{
self.currentCategoryType = categoryTypeObject
}
if let coinsInCategoryArrayObject = aDecoder.decodeObject(forKey: Keys.Coins_In_Category) as? [Coin]
{
self.coinsInCategory = coinsInCategoryArrayObject
}
}
public func encode(with aCoder: NSCoder)
{
//we encode this object's information
aCoder.encode(currentCategoryType, forKey: Keys.Current_Category_Type)
aCoder.encode(self.coinsInCategory, forKey: Keys.Coins_In_Category)
}
override init()
{
super.init()
self.currentCategoryType = CategoryTypes.COUNTRY_VALUE_AND_CURRENCY.rawValue
self.coinsInCategory = []
}
convenience init(coins: [Coin], categoryType: CategoryTypes.RawValue)
{
self.init()
self.coinsInCategory = coins
if isACategoryType(categoryType: categoryType) == true
{
self.currentCategoryType = categoryType
}
else
{
self.currentCategoryType = CategoryTypes.NO_CATEGORY.rawValue
}
}
func isACategoryType(categoryType: NSString) -> Bool
{
switch categoryType
{
case CategoryTypes.COUNTRY_VALUE_AND_CURRENCY.rawValue:
return true
case CategoryTypes.COUNTRY.rawValue:
return true
case CategoryTypes.YEAR.rawValue:
return true
case CategoryTypes.CURRENCY.rawValue:
return true
default:
return false
}
}
func addCoin(newCoin: Coin)
{
//we are adding a new Coin object to this category
//if it falls into the category's type
if self.coinFitsCategory(aCoin: newCoin) == true
{
self.coinsInCategory.append(newCoin)
}
}
func coinFitsCategory(aCoin: Coin) -> Bool
{
//this function tests if aCoin fits into the category type
//but that all varies depending on which category the coin is
if self.coinsInCategory.count == 0
{
//this category is currently empty, so any addition goes!
return true
}
//otherwise, this category is not empty... so we are now going to
//examine the situation more critically
let testCoin = self.coinsInCategory[0]
switch self.currentCategoryType
{
case CategoryTypes.COUNTRY_VALUE_AND_CURRENCY.rawValue:
return (testCoin.getCountry().lowercased == aCoin.getCountry().lowercased) && (testCoin.getValue() == aCoin.getValue()) && (testCoin.getDenomination().lowercased == aCoin.getDenomination().lowercased)
case CategoryTypes.COUNTRY.rawValue:
return testCoin.getCountry().lowercased == aCoin.getCountry().lowercased
case CategoryTypes.CURRENCY.rawValue:
return testCoin.getDenomination().lowercased == aCoin.getDenomination().lowercased
case CategoryTypes.YEAR.rawValue:
return testCoin.getYear() == aCoin.getYear()
default:
return false
}
}
func getIndexOfCoinInCollection(coin: Coin) -> Int
{
//we are going to return -1 if the coin does not exist in the collection
//and are going to return the index otherwise if yes
for i in 0..<self.coinsInCategory.count
{
if coinsInCategory[i] == coin
{
return i
}
}
//have not found anything
return -1
}
func removeCoin(at: Int)
{
//we remove the coin at the index if it is in a valid range of the coinInCategory array
if isValidArrayIndex(index: at)
{
self.coinsInCategory.remove(at: at)
}
}
func getCoin(at: Int) -> Coin?
{
//we return nil if there is an issue in accessing the coin
if isValidArrayIndex(index: at)
{
return self.coinsInCategory[at]
}
else
{
return nil
}
}
func assignCoin(at: Int,c: Coin)
{
if isValidArrayIndex(index: at)
{
self.coinsInCategory[at].assign(right: c)
}
}
func deleteAllCoins()
{
//we delete all the coin in this category
self.coinsInCategory.removeAll()
}
func removeCoin(c: Coin)
{
//we delete a coin from the category
for i in 0..<self.coinsInCategory.count
{
if self.coinsInCategory[i] == c
{
//the coin at index "i" is equal to the coin "c" that we want to delete from the category
self.coinsInCategory.remove(at: i)
return
}
}
}
func hasCoin(c: Coin) -> Bool
{
return getIndexOfCoinInCollection(coin: c) != -1
}
func swapValuesWithAnotherCategory(other: CoinCategory)
{
swap(&self.currentCategoryType, &other.currentCategoryType)
swap(&self.coinsInCategory,&other.coinsInCategory)
}
func swapCoins(indexOne: Int, indexTwo: Int)
{
if isValidArrayIndex(index: indexOne) && isValidArrayIndex(index: indexTwo)
{
swap(&self.coinsInCategory[indexOne],&self.coinsInCategory[indexTwo])
}
}
private func isValidArrayIndex(index: Int) -> Bool
{
return (0 <= index && index < coinsInCategory.count)
}
}
And then the coin category class:
// This provides the class definition of Class Coin for the project
import UIKit
import CoreData
enum TimePeriods: String
{
//this enumeration represents the different time periods that a
//coin was minted in, for the sake of this programn
case BCE = "BCE"
case CE = "CE"
}
public class Coin : NSObject, NSCoding
{
//this struct represents all the keys used in encoding and decoding this object
struct Keys
{
static let Country = "country"
static let Mint = "mint"
static let Year = "year"
static let Currency = "currency"
static let Value = "value"
static let Grade = "grade"
static let Comments = "comments"
static let NumInstances = "numberInstances"
static let Description = "description"
static let Obverse = "obverse"
static let Reverse = "reverse"
}
//this represents a coin in the table view
static let GRADING_LOWER_LIMIT: NSNumber = 1
static let GRADING_UPPER_LIMIT: NSNumber = 70
//this represents the default strings returned if a field does not have the needed information
static let DEFAULT_DESCRIPTIVE_NAME: NSString = "(Description?)"
static let DEFAULT_COMMENTS: NSString = "(Comments?)"
static let DEFAULT_DENOMINATION: NSString = "(Denomination?)"
static let DEFAULT_MINT: NSString = "(Mint?)"
static let DEFAULT_COUNTRY: NSString = "(Country?)"
static let DEFAULT_YEAR: NSString = "(Year?)"
static let DEFAULT_GRADE: NSString = "(Grade?)"
static let DEFAULT_VALUE_AND_DENOMINATION: NSString = "(Value?) (Currency?)"
static let OBVERSE_IMAGE_STRING : NSString = "Obverse"
static let REVERSE_IMAGE_STRING : NSString = "Reverse"
static private let BULLET = "➣ " //represents the kind of bullet to be used to build a complete summary of the coin
//declare members with setters and getters
private var country: NSString = "" //what country/empire/etc. used in?
private var mint: NSString = "" //where minted? EX: US Mint, St. Petersburg
private var year: NSNumber? = nil //what year minted? per gregorian calendar
//the year can be negative to represent the BCE period
//positive to represent the CE periods
private var typeCurrency: NSString = "" //what is the unit of value? EX: Cents, dollars, centavos, etc
private var theValue: NSNumber = 0 //how many? EX: how many dollars, cents, centavos, etc.?
//additional information about the coin
private var grade: NSNumber? //on the american grading scale for coins. 1-70
private var comments: NSString = "" //extra comments stored by the user for himself
private var numberOfInstances: NSNumber = 0 //number of coins exactly like this. EX: 1,2,3,4...etc? For each instance, it must be >= 1.
//This describes the type of the coin
//EX: Walking Liberty Quarter, Barber Quarter, Standing Liberty Quarter... etc
private var descriptiveName: NSString = ""
private var obverseImage: UIImage? = nil
private var reverseImage: UIImage? = nil
public var valueAndDenomination: NSString
{
get
{
//need to check four cases
//case 1: we have the right values for value and denomination
//case 2: we do not have a value but do have denomination
//case 3: we have a value but do not have denomination
//case 4: we do not have both
//
//the reason why we consider 0 to be an empty value is because a coin that was worth
//nothing would not have been minted in the first place!!!
if (self.theValue != 0 && self.typeCurrency != "")
{
//have value and denomination
return "\(self.theValue) \(self.typeCurrency)" as NSString //like "20 Cents"
}
else if (self.theValue == 0 && self.typeCurrency != "" )
{
//do not have value, but have denomination
return "(Value?) \(self.typeCurrency)" as NSString
}
else if (self.theValue != 0 && self.typeCurrency == "")
{
//we have value, but do not have denomination
return "\(self.theValue) (Currency?)" as NSString
}
else
{
//we do not have both
return Coin.DEFAULT_VALUE_AND_DENOMINATION as NSString
}
}
}
public required init?(coder aDecoder: NSCoder)
{
//we decode this object's information
if let countryObject = aDecoder.decodeObject(forKey: Keys.Country) as? NSString
{
self.country = countryObject
}
if let mintObject = aDecoder.decodeObject(forKey: Keys.Country) as? NSString
{
self.mint = mintObject
}
if let yearObject = aDecoder.decodeObject(forKey: Keys.Year) as? NSNumber
{
self.year = yearObject
}
if let currencyObject = aDecoder.decodeObject(forKey: Keys.Currency) as? NSString
{
self.typeCurrency = currencyObject
}
if let valueObject = aDecoder.decodeObject(forKey: Keys.Value) as? NSNumber
{
self.theValue = valueObject
}
if let gradeObject = aDecoder.decodeObject(forKey: Keys.Grade) as? NSNumber
{
self.grade = gradeObject
}
if let commentObject = aDecoder.decodeObject(forKey: Keys.Comments) as? NSString
{
self.comments = commentObject
}
if let numInstancesObject = aDecoder.decodeObject(forKey: Keys.NumInstances) as? NSNumber
{
self.numberOfInstances = numInstancesObject
}
if let descriptiveNameObject = aDecoder.decodeObject(forKey: Keys.Description) as? NSString
{
self.descriptiveName = descriptiveNameObject
}
if let obverseImageObject = aDecoder.decodeObject(forKey: Keys.Obverse) as? UIImage
{
self.obverseImage = obverseImageObject
}
if let reverseImageObject = aDecoder.decodeObject(forKey: Keys.Reverse) as? UIImage
{
self.reverseImage = reverseImageObject
}
}
override init()
{
//default initializer
super.init()
self.country = ""
self.mint = ""
self.year = nil
self.typeCurrency = ""
self.theValue = 0
self.comments = ""
self.numberOfInstances = 1
self.descriptiveName = ""
self.obverseImage = nil
self.reverseImage = nil
}
init(country: NSString,year: Int?,typeCurrency: NSString, theValue: NSNumber,mint: NSString,grade: Int?,numInstances: NSNumber = 1,description: NSString, comments: NSString)
{
super.init()
self.country = country
self.mint = mint
self.year = year! as NSNumber
self.typeCurrency = typeCurrency
self.theValue = theValue
self.comments = comments
self.numberOfInstances = numInstances
self.descriptiveName = description
self.obverseImage = nil
self.reverseImage = nil
}
init(country: NSString,year: NSNumber?,typeCurrency: NSString, theValue: NSNumber,mint: NSString,grade: NSNumber?,numInstances: NSNumber = 1,description: NSString, comments: NSString,obverseImage: UIImage, reverseImage: UIImage)
{
super.init()
self.country = country
self.mint = mint
self.year = year
self.typeCurrency = typeCurrency
self.theValue = theValue
self.comments = comments
self.numberOfInstances = numInstances
self.descriptiveName = description
}
public func encode(with aCoder: NSCoder)
{
//we encode the coin's information
aCoder.encode(self.country, forKey: Keys.Country)
aCoder.encode(self.mint, forKey: Keys.Mint)
aCoder.encode(self.year, forKey: Keys.Year)
aCoder.encode(self.typeCurrency, forKey: Keys.Currency)
aCoder.encode(self.theValue, forKey: Keys.Value)
aCoder.encode(self.grade, forKey: Keys.Grade)
aCoder.encode(self.comments, forKey: Keys.Comments)
aCoder.encode(self.numberOfInstances, forKey: Keys.NumInstances)
aCoder.encode(self.descriptiveName, forKey: Keys.Description)
aCoder.encode(self.obverseImage, forKey: Keys.Obverse)
aCoder.encode(self.reverseImage, forKey: Keys.Reverse)
}
//setter and getter functions for class members...
func getCompleteSummary() -> NSString
{
//returns a bulleted list that represents the coin
//and it describes every single detail...
}
func getIncompleteSummary() -> String
{
//returns a bulleted list string that represents the coin
//and it describes every single detail...
}
/////////////////////////////////////////////////////////////////////////////////
func ofSameType(rhs: Coin) -> Bool
{
return (self.getCountry().lowercased == rhs.getCountry().lowercased) && (self.getValue() == rhs.getValue()) && (self.getDenomination().lowercased == rhs.getDenomination().lowercased)
}
public static func==(lhs: Coin, rhs: Coin) -> Bool
{
//we compare two coin objects for equality in ALL Categories
return lhs.country.lowercased == rhs.country.lowercased &&
lhs.theValue == rhs.theValue &&
lhs.typeCurrency.lowercased == rhs.typeCurrency.lowercased &&
lhs.mint.lowercased == rhs.mint.lowercased &&
lhs.year == rhs.year &&
lhs.grade == rhs.grade &&
lhs.comments == rhs.comments &&
lhs.numberOfInstances == rhs.numberOfInstances &&
lhs.descriptiveName == rhs.descriptiveName &&
lhs.obverseImage == rhs.obverseImage &&
lhs.reverseImage == rhs.reverseImage
}
func assign(right: Coin)
{
//we implement this instead of overloading the assignment "=" operator
//as it is not possible to overload the "=" operator
//we assign the right-hand-coin's field values
//to the left-hand coin's side
self.country = right.country
self.theValue = right.theValue
self.typeCurrency = right.typeCurrency
self.mint = right.mint
self.year = right.year
self.grade = right.grade
self.comments = right.comments
self.numberOfInstances = right.numberOfInstances
self.descriptiveName = right.descriptiveName
self.obverseImage = right.obverseImage
self.reverseImage = right.reverseImage
}
}
It looks like the problem is that your CoinCategory class does not implement the compare: method:
[CoinCollection.CoinCategory compare:]: unrecognized selector sent to instance 0x608000236cc0
My guess is that the FRC is trying to compare the coinCategory attribute of the second CoinCategoryMO to the coinCategory attribute of the first CoinCategoryMO in order to get them in the correct order (since your FRC is sorted using the coinCategory attribute).
So the direct answer is that you need to implement a compare: method for your CoinCategory class. But I think this is just an indicator that you need to change your approach. Rather than having an Entity with attributes that are custom objects (or collections thereof), you should probably aim to have multiple Entities (eg. a CoinMO entity, a CoinCategoryMO entity, a CoinCategoryTypeMO entity, etc) with to-many relationships between them. If you show the code for your custom objects (Coin and CoinCategory) it will be easier to advise how best to model them in CoreData.
I am practicing iOS (Swift) with Firebase. the first viewcontroller retrieves all the records from firebase db and populate the tableView from an array. when the user selects an item from that tableview a new viewcontroller pops up segueing the object from the listView viewcontroller to the detail viewcontroller. data is populated to the fields successfully!
however when i try to update any of the textfield, the moment i switch to another textfield the initial value is restored in the edited textfield.
I have tried to removeAllObservers... but nothing worked. i even removed "import Firebase" and all associated objects and still it restores the initial value.
am i missing any concept here?
this is the code from the ListViewController:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated);
self.activityIndicator.startAnimating();
self.observeIngrdUpdates = ref.observeEventType(.Value, withBlock: { (snapshot) in
self.ingredients.removeAll();
for child in snapshot.children {
var nutrition:String = "";
var type:Int = 0;
var desc:String = "";
var img:String = "";
var name:String = "";
var price:Double = 0.0;
if let _name = child.value["IngredientName"] as? String {
name = _name;
}
if let _nutrition = child.value["NutritionFacts"] as? String {
nutrition = _nutrition;
}
if let _type = child.value["IngredientType"] as? Int {
type = _type;
}
if let _desc = child.value["UnitDescription"] as? String {
desc = _desc;
}
if let _img = child.value["IngredientImage"] as? String {
img = _img;
}
if let _price = child.value["IngredientPrice"] as? Double {
price = _price;
}
let ingredient = Ingredient(name: name, type: type, image: img, unitDesc: desc, nutritionFacts: nutrition, price: price);
ingredient.key = child.key;
self.ingredients.append(ingredient);
}
self.tableView.reloadData();
})
}
and the PrepareForSegue:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let destinationVC = segue.destinationViewController as! EditIngredientVC;
if segue.identifier?.compare(SEGUE_EDIT_INGREDIENTS) == .OrderedSame {
destinationVC.ingredient = ingredients[tableView.indexPathForSelectedRow!.row];
destinationVC.controllerTitle = "Edit";
} else if segue.identifier?.compare(SEGUE_ADD_INGREDIENT) == .OrderedSame {
destinationVC.controllerTitle = "New";
}
}
this is the code for populating the fields in DetailViewController:
override func viewDidLayoutSubviews() {
lblControllerTitle.text = controllerTitle;
if controllerTitle?.localizedCaseInsensitiveCompare("NEW") == .OrderedSame {
self.segVegFru.selectedSegmentIndex = 0;
} else {
if ingredient != nil {
self.txtIngredientName.text = ingredient!.name;
self.txtUnitDesc.text = ingredient!.unitDesc;
self.segVegFru.selectedSegmentIndex = ingredient!.typeInt;
self.txtNutritionFacts.text = ingredient!.nutritionFacts;
self.txtPrice.text = "\(ingredient!.price)";
}
}
}
Thank you all for your help.
It's probably because you are putting your textField populating code, in viewDidLayoutSubviews() method.
This method will be triggered every time the layout of a views changes in viewcontroller.
Move it to viewDidLoad() and it should be fine.
The reason that it's being reverted to the previous value is because. You are populating the textField.text from the "ingredient" object. The ingredient will have the same value retained that you passed from previous view controller. Until you mutate it at some point.
And by the way Firebase is very cool I like it too :)