In a project that I am working on I have note-esque function that is acting as an Exercise/Training Log. This Training Log is made up of 5 files: Note.swift, NotesTableViewController.swift, NoteDetailViewController.swift, NoteDetailTableViewCell.swift, and NoteStore.swift. The class for this table is NotesTableViewController, which is a UIViewController with UITableViewDelegate, and UITableViewDataSource. This note taking feature works decently, populating the tableview, but fails to delete a note from the .plist file and continues to retrieve it when the app is reopened. I do not know if this is actually failure to save/load, or if something is going wrong somewhere else. I would appreciate any help at all. The files are as follows:
Note.swift
import Foundation
class Note : NSObject, NSCoding {
var title = ""
var text = ""
var date = NSDate() // Defaults to current date / time
// Computed property to return date as a string
var shortDate : NSString {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "MM/dd/yy"
return dateFormatter.stringFromDate(self.date)
}
override init() {
super.init()
}
// 1: Encode ourselves...
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(title, forKey: "title")
aCoder.encodeObject(text, forKey: "text")
aCoder.encodeObject(date, forKey: "date")
}
// 2: Decode ourselves on init
required init(coder aDecoder: NSCoder) {
self.title = aDecoder.decodeObjectForKey("title") as! String
self.text = aDecoder.decodeObjectForKey("text") as! String
self.date = aDecoder.decodeObjectForKey("date") as! NSDate
}
}
NotesTableViewController.swift
import UIKit
class NotesTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var OpenButton: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
// Leverage the built in TableViewController Edit button
self.navigationItem.leftBarButtonItem = self.editButtonItem()
OpenButton.target = self.revealViewController()
OpenButton.action = Selector("revealToggle:")
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
self.tableView.reloadData()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// ensure we are not in edit mode
editing = false
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Here we pass the note they tapped on between the view controllers
if segue.identifier == "NoteDetailPush" {
// Get the controller we are going to
var noteDetail = segue.destinationViewController as! NoteDetailViewController
// Lookup the data we want to pass
var theCell = sender as! NoteDetailTableViewCell
// Pass the data forward
noteDetail.theNote = theCell.theNote
}
}
#IBAction func saveFromNoteDetail(segue:UIStoryboardSegue) {
// We come here from an exit segue when they hit save on the detail screen
// Get the controller we are coming from
var noteDetail = segue.sourceViewController as! NoteDetailViewController
// If there is a row selected....
if let indexPath = tableView.indexPathForSelectedRow() {
// Update note in our store
NoteStore.sharedNoteStore.updateNote(theNote: noteDetail.theNote)
// The user was in edit mode
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
} else {
// Otherwise, add a new record
NoteStore.sharedNoteStore.createNote(theNote: noteDetail.theNote)
// Get an index to insert the row at
var indexPath = NSIndexPath(forRow: NoteStore.sharedNoteStore.count()-1, inSection: 0)
// Update tableview
tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
// MARK: - Table view data source
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Just return the note count
return NoteStore.sharedNoteStore.count()
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Fetch a reusable cell
let cell = tableView.dequeueReusableCellWithIdentifier("NoteDetailTableViewCell", forIndexPath: indexPath) as! NoteDetailTableViewCell
// Fetch the note
var rowNumber = indexPath.row
var theNote = NoteStore.sharedNoteStore.getNote(rowNumber)
// Configure the cell
cell.setupCell(theNote)
return cell
}
// Override to support editing the table view.
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
NoteStore.sharedNoteStore.deleteNote(indexPath.row)
// Delete the note from the tableview
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
}
}
NoteDetailViewController
import UIKit
class NoteDetailViewController: UIViewController {
var theNote = Note()
#IBOutlet weak var noteTitleLabel: UITextField!
#IBOutlet weak var noteTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// The view starts here. By now we either have a note to edit
// or we have a blank note in theNote property we can use
// Update the screen with the contents of theNote
self.noteTitleLabel.text = theNote.title
self.noteTextView.text = theNote.text
// Set the Cursor in the note text area
self.noteTextView.becomeFirstResponder()
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Whenever we leave the screen, update our note model
theNote.title = self.noteTitleLabel.text
theNote.text = self.noteTextView.text
}
#IBAction func CancelNote(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
}
NoteDetailTableViewCell
import UIKit
class NoteDetailTableViewCell : UITableViewCell {
// The note currently being shown
weak var theNote : Note!
// Interface builder outlets
#IBOutlet weak var noteTitleLabel : UILabel!
#IBOutlet weak var noteDateLabel : UILabel!
#IBOutlet weak var noteTextLabel : UILabel!
// Insert note contents into the cell
func setupCell(theNote:Note) {
// Save a weak reference to the note
self.theNote = theNote
// Update the cell
noteTitleLabel.text = theNote.title
noteTextLabel.text = theNote.text
noteDateLabel.text = theNote.shortDate as String
}
}
and finally, NoteStore
import Foundation
class NoteStore {
// Mark: Singleton Pattern (hacked since we don't have class var's yet)
class var sharedNoteStore : NoteStore {
struct Static {
static let instance : NoteStore = NoteStore()
}
return Static.instance
}
// Private init to force usage of singleton
private init() {
load()
}
// Array to hold our notes
private var notes : [Note]!
// CRUD - Create, Read, Update, Delete
// Create
func createNote(theNote:Note = Note()) -> Note {
notes.append(theNote)
return theNote
}
// Read
func getNote(index:Int) -> Note {
return notes[index]
}
// Update
func updateNote(#theNote:Note) {
// Notes passed by reference, no update code needed
}
// Delete
func deleteNote(index:Int) {
notes.removeAtIndex(index)
}
func deleteNote(withNote:Note) {
for (i, note) in enumerate(notes) {
if note === withNote {
notes.removeAtIndex(i)
return
}
}
}
// Count
func count() -> Int {
return notes.count
}
// Mark: Persistence
// 1: Find the file & directory we want to save to...
func archiveFilePath() -> String {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let documentsDirectory = paths.first as! NSString
let path = documentsDirectory.stringByAppendingPathComponent("NoteStore.plist")
return path
}
// 2: Do the save to disk.....
func save() {
NSKeyedArchiver.archiveRootObject(notes, toFile: archiveFilePath())
}
// 3: Do the reload from disk....
func load() {
let filePath = archiveFilePath()
let fileManager = NSFileManager.defaultManager()
if fileManager.fileExistsAtPath(filePath) {
notes = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as! [Note]
} else {
notes = [Note]()
}
}
}
it look's like you're not calling the save method after changing creating,deleting or updating notes
you could add for example :
func deleteNote(index:Int) {
notes.removeAtIndex(index)
save()
}
or call the save methods on vievWillDisappear if you don't want to write a new plist after every change
Related
I'm stack doing my first app, I searched a lot of tutorials about tableviews, arrays and segues but I can't even figure it out how to resolve my problem, here I go:
I need that the app store a value in an array (class) so I can access it latter (not in the next segue), I did a different app more simple than the last one, just with a UITextfield input and a button to add it to the class. When I move from the user input part to the tableView, the tableView is empty. I will put the code here:
TABLE VIEWCONTROLLER
import UIKit
class NameTableViewController: UITableViewController {
var names = [Name]()
override func viewDidLoad() {
super.viewDidLoad()
}
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 names.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "NameTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier", for: indexPath) as? NameTableViewCell else {
fatalError("The dequeueReusable cell is not an instance of NameTableViewCell")
}
let name = names[indexPath.row]
cell.nameLabel.text = name.name
return cell
}
USER INTERFACE VIEWCONTROLLER:
import UIKit
class ViewController: UIViewController {
var name = [Name]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
#IBOutlet weak var nameTextField: UITextField!
#IBAction func addingButton(_ sender: UIButton) {
let writtenName = nameTextField.text ?? "No name written"
let name1 = Name(name: writtenName)
name.append(name1)
}
}
<!-- end snippet -->
VIEWCELL:
class NameTableViewCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
<!-- end snippet -->
NAME CLASS METHOD:
class Name {
var name: String
init(name: String) {
self.name = name
}
}
!-- end snippet -->
TableView
User Input
Sorry if this is a dumb question, as you may have notice I'm new programming and swift is the first language that I'm learning.
You can use nsuserdefaults https://developer.apple.com/documentation/foundation/nsuserdefaults and store a key decodable struct and later on call it everywhere.
// Save Data
struct People: Codable {
let name: String?
}
var peopleArray = [People]()
let mike = People(name: "mike")
peopleArray.append(mike)
UserDefaults.standard.set(peopleArray, forKey: "people")
// Request Stored Data
func getPeople() -> [People]?{
let myPeople = UserDefaults.standard.data(forKey: "people")
if myPeople == nil {
return nil
}
let peopleArray = try! JSONDecoder().decode([People].self, from: myPeople!)
return peopleArray
}
let people = getPeople()
if(people != nil){
for person in people {
print(person.name)
}
}
I have a ViewController in my app where I have to show Settings to the user and user can turn the Settings on or off using UISwitch. I have to store the settings in the local db and based on that display data to user in app.
I am using SugarRecord for Core Data Management. Initially all the settings are turned on.
SugarRecordManager.swift
import Foundation
import SugarRecord
import CoreData
class SugarRecordManager
{
static let sharedInstance = SugarRecordManager()
private init(){
}
// Initializing CoreDataDefaultStorage
func coreDataStorage() -> CoreDataDefaultStorage {
let store = CoreDataStore.named("db")
let bundle = Bundle(for: type(of: self))
let model = CoreDataObjectModel.merged([bundle])
let defaultStorage = try! CoreDataDefaultStorage(store: store, model: model)
return defaultStorage
}
//MARK:- User Settings methods
//update local settings
func updateSettingsModel(userSettings: [UserSetting]){
let db = self.coreDataStorage()
for localSetting in userSettings{
try! db.operation { (context, save) -> Void in
if let settingObjectToUpdate = try! context.request(UserSetting.self).filtered(with: "groupName", equalTo: localSetting.groupName!).fetch().first{
settingObjectToUpdate.groupId = localSetting.groupId! as String
settingObjectToUpdate.groupName = localSetting.groupName! as String
settingObjectToUpdate.isGroupActive = localSetting.isGroupActive
try! context.insert(settingObjectToUpdate)
save()
}
}
}
}
//retrieve settings from storage
func getAllSettings() -> [UserSetting] {
let db = self.coreDataStorage()
var userSettings : [UserSetting]
do {
userSettings = try db.fetch(FetchRequest<UserSetting>())
} catch {
userSettings = []
}
return userSettings
}
//initialise settings for the first time
func initialiseUserSettings(){
let db = self.coreDataStorage()
var groupNameArray = UserDefaults.standard.value(forKey: "groupNamesArrayKey") as? [String]
var groupIdArray = UserDefaults.standard.value(forKey: "groupIdsArrayKey") as? [String]
for i in 0 ..< groupIdArray!.count {
try! db.operation { (context, save) -> Void in
let settingObject: UserSetting = try! context.new()
settingObject.groupId = groupIdArray?[i];
settingObject.groupName = groupNameArray?[i];
settingObject.isGroupActive = true;
try! context.insert(settingObject)
save()
}
}
}
}
SettingsViewController.swift
class SettingsViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate, SettingsCellDelegate {
#IBOutlet weak var btnSideNav: UIBarButtonItem!
#IBOutlet weak var settingsTable: UITableView!
var userSetting = [UserSetting]() //array to hold settings from storage
override func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false;
btnSideNav.target = revealViewController();
btnSideNav.action = #selector(SWRevealViewController.revealToggle(_:));
userSetting = SugarRecordManager.sharedInstance.getAllSettings() //here userSetting contains data and I have checked it
self.settingsTable.reloadData()
self.settingsTable.dataSource = self;
self.settingsTable.delegate = self;
// Do any additional setup after loading the view.
}
//MARK:- Table View Methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("Count of cells = \(self.userSetting.count)") //prints 18 which is good
return self.userSetting.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60;
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let settingsCell : SettingsCell? = tableView.dequeueReusableCell(withIdentifier: "SettingsCell") as? SettingsCell;
settingsCell?.setUpWithModel(model: self.userSetting[indexPath.row], cell: settingsCell!)
settingsCell?.delegate = self as SettingsCellDelegate;
return settingsCell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
func didTappedSwitch(cell: SettingsCell) {
let indexPath = settingsTable.indexPath(for: cell);
userSetting[(indexPath?.row)!].isGroupActive? = cell.isGroupActive.isOn as NSNumber
}
#IBAction func btnSaveTapped(_ sender: UIButton) {
// code to save settings
}
}
SettingsCell.swift
protocol SettingsCellDelegate {
func didTappedSwitch(cell: SettingsCell)
}
class SettingsCell: UITableViewCell {
#IBOutlet weak var groupName: UILabel!
#IBOutlet weak var lblGroupId: UILabel!
#IBOutlet weak var isGroupActive: UISwitch!
var delegate: SettingsCellDelegate!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func setUpWithModel(model: UserSetting, cell: SettingsCell)
{
cell.groupName.text = model.groupName;
cell.lblGroupId.text = model.groupId;
isGroupActive.setOn((model.isGroupActive?.boolValue)!, animated: false)
}
#IBAction func isGroupActiveValueChanged(_ sender: UISwitch) {
delegate.didTappedSwitch(cell: self)
}
}
Now, initally the TableView is populated and all arrays are working fine but as soon as I scroll the TableView all data is gone. Even the userSetting array is nill. I know it's something to do with context but can't figure out what. Any help would be greatly appreciated.
Change your func coreDataStorage() -> CoreDataDefaultStorage like this
// Initializing CoreDataDefaultStorage
lazy var coreDataStorage: CoreDataDefaultStorage = {
let store = CoreDataStore.named("db")
let bundle = Bundle(for: type(of: self))
let model = CoreDataObjectModel.merged([bundle])
let defaultStorage = try! CoreDataDefaultStorage(store: store, model: model)
return defaultStorage
}()
you have this problem because you re-init CoreDataDefaultStorage each time when you do any request.
After you made it lazy - you will have only one CoreDataDefaultStorage for all app life
Basically, it will be good to make coreDataStorage as singleton
Update: Identified the first half of the problem as exerciseNameLabel not rendering the updated text until a app restart. I removed the label element and am using cell.textLabel.text only. Problem #1 solved. Problem #2 is more clean: every time a cell is selected to view details, the indexPath.row is set to the selection and the next added item overwrites the item. Not sure of the mechanics behind indexPath.row, so I'm still searching to figure this one out. Help there is appreciated.
Short Youtube video (0:48) of the below behavior: https://www.youtube.com/watch?v=Zr4lCbzrtLY
I followed Apple's MealTracker walkthrough pretty closely, but my TableView is behaving strangely. Usually the newly created elements will only appear on the TableView after I restart the app. However, the newly created, but invisible items can be clicked. It seems like the label isn't being populated with the name after creation, but the name takes after a restart of the app. (This is shown in the first half of the video above)
Additionally, sometimes there is really unexpected behavior where a newly created item overwrites an old item (~0:32 on the video). When this happens, the label is updated in real time as expected, so the only unexpected part of this is that an old item is getting overwritten.
I have a print statement inside the tableView method which shows the correct index, the right element name, and the right array size.
class MasterTableViewController: UITableViewController {
var exercises = ExerciseProgram(name: "temp", startDate: "temp", program: nil)
override func viewWillAppear(animated: Bool) {
// Load any saved program, otherwise load sample data.
if let savedProgram = loadProgram() {
exercises = savedProgram
} else {
// Load the sample data.
loadSampleProgram()
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.leftBarButtonItem = self.editButtonItem()
}
func loadSampleProgram() {
// snip
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return exercises!.getCount()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "ExerciseTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! ExerciseTableViewCell
// Fetches the appropriate exercise for the data source layout
let exercise = exercises?.getExercise(indexPath.row)
cell.exerciseNameLabel.text = exercise!.name
print("in tableView() indexPath.row: \(indexPath.row) exercise name: \(exercise!.name) exercises count: \(exercises!.getCount())")
// Configure the cell...
return cell
}
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
exercises!.removeExercise(indexPath.row)
saveProgram()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
} //else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
// }
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
if segue.identifier == "ShowDetail" {
let exerciseDetailViewController = segue.destinationViewController as! ExerciseDetailViewController
// Get the cell that generated this segue
if let selectedExerciseCell = sender as? ExerciseTableViewCell {
let indexPath = tableView.indexPathForCell(selectedExerciseCell)!
let selectedExercise = exercises!.getExercise(indexPath.row)
exerciseDetailViewController.exercise = selectedExercise
}
}
}
#IBAction func unwindToExerciseList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? AddExerciseViewController, exercise = sourceViewController.exercise {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update an existing exercise.
exercises!.updateExercise(selectedIndexPath.row, updatedExercise: exercise)
tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
} else {
// Add a new exercise.
let newIndexPath = NSIndexPath(forRow: exercises!.getCount(), inSection: 0)
exercises!.addExercise(exercise)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
}
// Save the program.
saveProgram()
}
}
// MARK: NSCoding
func saveProgram() {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(NSKeyedArchiver.archivedDataWithRootObject(exercises!), forKey: "program")
defaults.synchronize()
}
func loadProgram() -> ExerciseProgram? {
let defaults = NSUserDefaults.standardUserDefaults()
guard let decodedNSData = defaults.objectForKey("program") as? NSData,
let exerciseProgram = NSKeyedUnarchiver.unarchiveObjectWithData(decodedNSData) as? ExerciseProgram
else {
print("Failed")
return nil
}
return exerciseProgram
}
}
Add Element:
class AddExerciseViewController: UIViewController, UINavigationControllerDelegate, UITextFieldDelegate {
#IBOutlet weak var exerciseNameTextField: UITextField!
#IBOutlet weak var doneButton: UIBarButtonItem!
var exercise: Exercise?
override func viewDidLoad() {
super.viewDidLoad()
doneButton.enabled = false
exerciseNameTextField.delegate = self
// Do any additional setup after loading the view.
}
// MARK: UITextFieldDelegate
func textFieldShouldReturn(textField: UITextField) -> Bool {
// Hide the keyboard.
textField.resignFirstResponder()
return true
}
func textFieldDidEndEditing(textField: UITextField) {
checkValidExerciseName()
}
func textFieldDidBeginEditing(textField: UITextField) {
// Disable the Save button while editing.
doneButton.enabled = false
}
func checkValidExerciseName() {
// Disable the Done button if the text field is empty.
let text = exerciseNameTextField.text ?? ""
doneButton.enabled = !text.isEmpty
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func cancelButtonTapped(sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
let isPresentingInAddExerciseMode = presentingViewController is UINavigationController
if isPresentingInAddExerciseMode {
dismissViewControllerAnimated(true, completion: nil)
} else {
navigationController!.popViewControllerAnimated(true)
}
}
// MARK: - Navigation
// This method lets you configure a view controller before it's presented.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if doneButton === sender {
let name = exerciseNameTextField.text ?? ""
let notes = "test form notes"
let weight = 135
// Set the meal to be passed to MealListTableViewController after the unwind segue.
exercise = Exercise(name: name, notes: notes, workoutLog: nil, weight: weight)
}
}
}
ExeciseProgram:
class ExerciseProgram: NSObject, NSCoding {
var name = "Allpro Auto Regulated"
var startDate = "16-04-20"
var program: [Exercise]? = []
init?(name: String, startDate: String, program: [Exercise]?) {
self.name = name
self.startDate = startDate
if program != nil {
self.program = program
} else {
self.program = []
}
if name.isEmpty || startDate.isEmpty {
return nil
}
}
// MARK: Types
struct PropertyKey {
static let nameKey = "name"
static let startDateKey = "startDate"
static let programKey = "program"
}
// MARK: Methods
func addExercise(newExercise: Exercise) {
program!.append(newExercise)
}
func getExercise(index: Int) -> Exercise {
return program![index]
}
func getCount() -> Int {
return program!.count
}
func updateExercise(index: Int, updatedExercise: Exercise) {
program![index] = updatedExercise
}
func removeExercise(index: Int) {
program!.removeAtIndex(index)
}
// MARK: NSCoder
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(startDate, forKey: PropertyKey.startDateKey)
aCoder.encodeObject(program, forKey: PropertyKey.programKey)
}
required convenience init?(coder aDecoder: NSCoder) {
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
let startDate = aDecoder.decodeObjectForKey(PropertyKey.startDateKey) as! String
let program = aDecoder.decodeObjectForKey(PropertyKey.programKey) as! [Exercise]?
// Must call designated initializer.
self.init(name: name, startDate: startDate, program: program)
Exercise:
class Exercise: NSObject, NSCoding {
var name: String
var formNotes: String?
private var workoutLog: [[String:AnyObject]]?
var currentWeights = Weights(heavy: 0)
init?(name: String, notes: String?, workoutLog: [[String:AnyObject]]?, weight: Int?) {
self.name = name
self.formNotes = notes
if (workoutLog != nil) {
self.workoutLog = workoutLog
} else {
self.workoutLog = []
}
self.currentWeights.heavy = weight!
if name.isEmpty {
return nil
}
}
// MARK: Types
// weights of the exercise
struct Weights {
var heavy = 0
var warmup25: Int { return roundToFives(Double(heavy) * 0.25) }
var warmup50: Int { return roundToFives(Double(heavy) * 0.50) }
}
struct PropertyKey {
static let nameKey = "name"
static let formNotesKey = "formNotes"
static let workoutLogKey = "workoutLog"
static let currentWeightsHeavyKey = "currentWeightsHeavy"
}
func recordWorkout(date: String, weight: Int, repsFirstSet: Int, repsSecondSet: Int) {
let newWorkoutLogEntry = ["date": date, "weight": weight, "repsFirstSet": repsFirstSet, "repsSecondSet": repsSecondSet]
workoutLog!.append(newWorkoutLogEntry as! [String : AnyObject])
}
func getLastWorkout() -> [String:AnyObject]? {
return workoutLog?.last
}
func getBarWeightsString(targetWeight: Int) -> String {
if targetWeight < 54 {
return "Bar"
} else {
return calculatePlates(roundToFives(Double(targetWeight)))
}
}
// MARK: NSCoder
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(formNotes, forKey: PropertyKey.formNotesKey)
aCoder.encodeObject(workoutLog, forKey: PropertyKey.workoutLogKey)
aCoder.encodeInteger(currentWeights.heavy, forKey: PropertyKey.currentWeightsHeavyKey)
}
required convenience init?(coder aDecoder: NSCoder) {
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
let formNotes = aDecoder.decodeObjectForKey(PropertyKey.formNotesKey) as! String
let workoutLog = aDecoder.decodeObjectForKey(PropertyKey.workoutLogKey) as! [[String : AnyObject]]
let currentWeights = aDecoder.decodeIntegerForKey(PropertyKey.currentWeightsHeavyKey)
// Must call designated initializer.
self.init(name: name, notes: formNotes, workoutLog: workoutLog, weight: currentWeights)
}
}
Problem 1: missing cell labels for newly added exercises until app restart
The cell.exerciseNameLabel.text inside tableView() is not consistently rendering. I don't know why, but changing this to cell.labelText.text works 100% of the time.
Problem 2: newly added exercises overwriting previously selected cells
The copied code from Apple's MealTracker exercise has a case for updating an item, which is essentially overwriting its old entry. I fixed this by removing the conditional inside unwindToExerciseList(). Everything is working now.
#IBAction func unwindToExerciseList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? AddExerciseViewController, exercise = sourceViewController.exercise {
// Add a new exercise.
let newIndexPath = NSIndexPath(forRow: exercises!.getCount(), inSection: 0)
exercises!.addExercise(exercise)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
saveProgram()
}
}
When I run my app I get an error message that says
fatal error: unexpectedly found nil while unwrapping an Optional value
MealViewController.swift
import UIKit
class MealViewController: UIViewController, UITextFieldDelegate, UINavigationControllerDelegate {
// MARK: Properties
#IBOutlet weak var nameTextField: UITextField!
#IBOutlet weak var saveButton: UIBarButtonItem!
/*
This value is either passed by `MealTableViewController` in `prepareForSegue(_:sender:)`
or constructed as part of adding a new meal.
*/
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if saveButton === sender {
let weightInt: Int? = Int(weightInKilos.text!)
let dehydrationInt: Int? = Int(percentOfDehydration.text!)
let lossesInt: Int? = Int(ongoingLosses.text!)
let factorInt: Int? = Int(Factor.text!)
let name = nameTextField.text ?? ""
let wt = weightInt!
let dn = dehydrationInt!
let ol = lossesInt!
let fr = factorInt!
// Set the meal to be passed to MealListTableViewController after the unwind segue.
meal = Meal(name: name, wt: wt, dn: dn, ol: ol, fr: fr)
}
if calcButton === sender {
if weightInKilos.text == "" && percentOfDehydration.text == "" && ongoingLosses.text == "" && Factor.text == "" {
let alertController = UIAlertController(title: "Fields were left empty.", message:
"You left some fields blank! Please make sure that all fields are filled in before tapping 'Calculate'.", preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
}
else {
let DestViewController: ftrViewController = segue.destinationViewController as! ftrViewController
let weightInt: Int? = Int(weightInKilos.text!)
let dehydrationInt: Int? = Int(percentOfDehydration.text!)
let lossesInt: Int? = Int(ongoingLosses.text!)
let factorInt: Int? = Int(Factor.text!)
let lrs24Int = (30 * weightInt! + 70) * factorInt! + weightInt! * dehydrationInt! * 10 + lossesInt!
let lrsPerHourint = lrs24Int / 24
DestViewController.lrsHr = "\(lrsPerHourint)"
DestViewController.lrs24Hrs = "\(lrs24Int)"
}
}
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
var meal: Meal?
override func viewDidLoad() {
super.viewDidLoad()
// Handle the text field’s user input through delegate callbacks.
nameTextField.delegate = self
// Set up views if editing an existing Meal.
if let meal = meal {
navigationItem.title = meal.name
nameTextField.text = meal.name
var weightInt: Int? = Int(weightInKilos.text!)
var dehydrationInt: Int? = Int(percentOfDehydration.text!)
var lossesInt: Int? = Int(ongoingLosses.text!)
var factorInt: Int? = Int(Factor.text!)
weightInt = meal.wt
dehydrationInt = meal.dn
lossesInt = meal.ol
factorInt = meal.fr
weightInKilos.delegate = self
percentOfDehydration.delegate = self
ongoingLosses.delegate = self
Factor.delegate = self
calcButton.layer.cornerRadius = 4;
resetbutton.layer.cornerRadius = 4;
}
// Enable the Save button only if the text field has a valid Meal name.
checkValidMealName()
}
// MARK: UITextFieldDelegate
func textFieldDidEndEditing(textField: UITextField) {
checkValidMealName()
navigationItem.title = textField.text
}
func textFieldDidBeginEditing(textField: UITextField) {
// Disable the Save button while editing.
saveButton.enabled = false
}
func checkValidMealName() {
// Disable the Save button if the text field is empty.
let text = nameTextField.text ?? ""
saveButton.enabled = !text.isEmpty
}
// MARK: Navigation
#IBAction func cancel(sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
let isPresentingInAddMealMode = presentingViewController is UINavigationController
if isPresentingInAddMealMode {
dismissViewControllerAnimated(true, completion: nil)
} else {
navigationController!.popViewControllerAnimated(true)
}
}
// Calculation outlets & actions
#IBOutlet weak var resetbutton: UIButton!
#IBOutlet weak var calcButton: UIButton!
#IBOutlet weak var weightInKilos: UITextField!
#IBOutlet weak var percentOfDehydration: UITextField!
#IBOutlet weak var ongoingLosses: UITextField!
#IBOutlet weak var Factor: UITextField!
#IBAction func resetButton(sender: AnyObject) {
if weightInKilos.text == "" && percentOfDehydration.text == "" && ongoingLosses.text == "" && Factor.text == "" {
let alertController = UIAlertController(title: "...", message:
"There is no information to reset. Nice try though!", preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
}
else {
weightInKilos.text=""
percentOfDehydration.text=""
ongoingLosses.text=""
Factor.text=""
}
}
}
class ftrViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var lrs24hours: UILabel!
#IBOutlet weak var lrsPerHour: UILabel!
var lrs24Hrs = String()
var lrsHr = String()
override func viewDidLoad() {
lrs24hours.text = lrs24Hrs
lrsPerHour.text = lrsHr
}
}
Meal.swift
import UIKit
class Meal: NSObject, NSCoding {
// MARK: Properties
var name: String
var wt: Int
var dn: Int
var ol: Int
var fr: Int
// MARK: Archiving Paths
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("meals")
// MARK: Types
struct PropertyKey {
static let nameKey = "name"
static let wtKey = "weight"
static let dnKey = "dehydration"
static let olKey = "ongoinglosses"
static let frKey = "factor"
}
// MARK: Initialization
init?(name: String, wt: Int, dn: Int, ol: Int, fr: Int) {
// Initialize stored properties.
self.name = name
self.wt = wt
self.dn = dn
self.ol = ol
self.fr = fr
super.init()
// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty {
return nil
}
}
// MARK: NSCoding
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(wt, forKey: PropertyKey.wtKey)
aCoder.encodeObject(dn, forKey: PropertyKey.dnKey)
aCoder.encodeObject(ol, forKey: PropertyKey.olKey)
aCoder.encodeObject(fr, forKey: PropertyKey.frKey)
}
required convenience init?(coder aDecoder: NSCoder) {
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
let wt = aDecoder.decodeObjectForKey(PropertyKey.wtKey) as! Int
let dn = aDecoder.decodeObjectForKey(PropertyKey.dnKey) as! Int
let ol = aDecoder.decodeObjectForKey(PropertyKey.olKey) as! Int
let fr = aDecoder.decodeObjectForKey(PropertyKey.frKey) as! Int
// Must call designated initializer.
self.init(name: name, wt: wt, dn: dn, ol: ol, fr: fr)!
}
}
MealTableViewController.swift
import UIKit
class MealTableViewController: UITableViewController {
// MARK: Properties
var meals = [Meal]()
override func viewDidLoad() {
super.viewDidLoad()
// Use the edit button item provided by the table view controller.
navigationItem.leftBarButtonItem = editButtonItem()
// Load any saved meals, otherwise load sample data.
if let savedMeals = loadMeals() {
meals += savedMeals
} else {
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return meals.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "MealTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! MealTableViewCell
// Fetches the appropriate meal for the data source layout.
let meal = meals[indexPath.row]
cell.nameLabel.text = meal.name
return cell
}
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
meals.removeAtIndex(indexPath.row)
saveMeals()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
/*
// Override to support rearranging the table view.
override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
}
*/
/*
// Override to support conditional rearranging of the table view.
override func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the item to be re-orderable.
return true
}
*/
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowDetail" {
let mealDetailViewController = segue.destinationViewController as! MealViewController
// Get the cell that generated this segue.
if let selectedMealCell = sender as? MealTableViewCell {
let indexPath = tableView.indexPathForCell(selectedMealCell)!
let selectedMeal = meals[indexPath.row]
mealDetailViewController.meal = selectedMeal
}
}
else if segue.identifier == "AddItem" {
print("Adding new meal.")
}
}
#IBAction func unwindToMealList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? MealViewController, meal = sourceViewController.meal {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update an existing meal.
meals[selectedIndexPath.row] = meal
tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
} else {
// Add a new meal.
let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0)
meals.append(meal)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
}
// Save the meals.
saveMeals()
}
}
// MARK: NSCoding
func saveMeals() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(meals, toFile: Meal.ArchiveURL.path!)
if !isSuccessfulSave {
print("Failed to save meals...")
}
}
func loadMeals() -> [Meal]? {
return NSKeyedUnarchiver.unarchiveObjectWithFile(Meal.ArchiveURL.path!) as! [Meal]
}
}
MealTableViewCell.swift
import UIKit
class MealTableViewCell: UITableViewCell {
// MARK: Properties
#IBOutlet weak var nameLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
try this convenience required init?(coder aDecoder: NSCoder) method
convenience required init?(coder aDecoder: NSCoder) {
if let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as? String,
wt = aDecoder.decodeObjectForKey(PropertyKey.wtKey) as? Int,
dn = aDecoder.decodeObjectForKey(PropertyKey.dnKey) as? Int,
ol = aDecoder.decodeObjectForKey(PropertyKey.olKey) as? Int,
fr = aDecoder.decodeObjectForKey(PropertyKey.frKey) as? Int {
// Must call designated initializer.
self.init(name: name, wt: wt, dn: dn, ol: ol, fr: fr)
} else {
return nil
}
}
I want the user to save the current date (like dd.mm.yyyy) to the NSUserDefaults by clicking a button and load it back in another ViewController to display it as a cell in UITableView. But it won't work. I don't know how to get the date right but this is what I tried:
#IBAction func saveButtonTapped (sender:AnyObject){
let userDefaults = NSUserDefaults.standardUserDefaults()
if var timeList = userDefaults.objectForKey("timeList") as? [NSDate]
{
timeList.append(NSDate())
userDefaults.setObject(timeList, forKey: "timeList")
}
else
{
userDefaults.setObject([NSDate()], forKey: "timeList")
}
userDefaults.synchronize()
}
...
#IBOutlet weak var tableView: UITableView!
var time:NSMutableArray = NSMutableArray();
override func viewDidLoad() {
super.viewDidLoad()
var timecopy = NSMutableArray(array: time)
var userDefaults:NSUserDefaults = NSUserDefaults.standardUserDefaults()
var timeListFromUserDefaults:NSMutableArray? = userDefaults.objectForKey("timeList") as? NSMutableArray
if ((timeListFromUserDefaults) != nil){
time = timeListFromUserDefaults!
}
self.tableView.reloadData()
}
There you go.
class ViewController: UIViewController, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
let userDefaults = NSUserDefaults.standardUserDefaults()
let timeListKey = "TimeList" // Key in user defaults
var timeList: [NSDate] = [NSDate]() {
didSet {
userDefaults.setObject(self.timeList, forKey: timeListKey)
self.tableView.reloadData()
}
}
//MARK: Actions
#IBAction func saveButtonTapped(sender: UIButton) {
self.timeList.append(NSDate())
}
//MARK: TableView DataSource
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.timeList.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "TimeListCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = "\(timeList[indexPath.row])"
return cell
}
//MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Get NSArray from userDefaults
let timeListFromUserDefaults = userDefaults.objectForKey(timeListKey) as? NSArray
// Check if it is an array of NSDate, else set timeList to empty array
self.timeList = (timeListFromUserDefaults as? [NSDate]) ?? [NSDate]()
}
}
Everything you put in NSUserDefaults is saved as immutable. Thus you will get back an NSArray not an NSMutableArray. This may cause your conditional casting to fail and return nil.
By the way, consider that NSUserDefaults is not intended to be some sort of DataBase for your app, but just a small PropertyList of preferenses and stuff like that.
If you are going to have lot of dates to save, delete etc., you should probably consider CoreData or other solutions.