Error when try to unwrap realm object - ios

I have following issue when I try to use data from one of Realm Objects
I have an error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
I'm confused because when I try to do it manually on debug console all works fine
(lldb) print meal?.mealName
(String?) $R0 = "Krem buraczany"
I will attach whole code and show you on which line error appear
Realm Object:
import UIKit
import RealmSwift
class Meal: Object {
#objc dynamic var mealID : String = ""
#objc dynamic var mealName : String = ""
var ingredients = List<String>()
var mealDescription = List<String>()
#objc dynamic var kcal : Int = 0
#objc dynamic var preparingTime : Int = 0
#objc dynamic var imageName : String = ""
convenience init(mealID: String, mealName: String, kcal: Int, preparingTime: Int, imageName: String) {
self.init()
self.mealID = mealID
self.mealName = mealName
self.kcal = kcal
self.preparingTime = preparingTime
self.imageName = imageName
}
override static func primaryKey() -> String? {
return "mealID"
}
}
Sending data via segue:
#IBAction func ideaForMealButtonPressed(_ sender: UIButton) {
performSegue(withIdentifier: "ideaForMeal", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVC = segue.destination as! IdeaForMeal
destinationVC.randomMeal = getRandomMeal()
}
func getRandomMeal() -> String {
var mealsCount = realm.objects(Meal.self).count
mealsCount += 1
let randomMealID = arc4random_uniform(UInt32(mealsCount))
print (randomMealID)
return String(randomMealID)
}
Receiver:
//Global Variables
let realm = try! Realm()
var randomMeal : String? {
didSet {
let selectedMeal = downloadMealData(mealID: randomMeal!)
setUIForMeal(meal: selectedMeal)
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.hideKeyboardWhenTappedAround()
}
//Background Operations
func downloadMealData(mealID: String?) -> Meal? {
let currentMeal = realm.object(ofType: Meal.self, forPrimaryKey: mealID)
return currentMeal
}
func checkPositionOfTopIngredientsLabel() -> CGFloat {
let maxY = ingredientsTopLabel.frame.maxY
return maxY
}
func setUIForMeal(meal: Meal?) {
mealNameLabel.text = meal!.mealName // Error!
mealImage.image = UIImage(named: meal!.imageName)
preparingTimeLabel.text = String(meal!.preparingTime)
kcalLabel.text = String(meal!.kcal)
}
Thanks!
BR
iMat

The error is not related to Realm. It's a very common view life cycle mistake.
Setting randomMeal in the destination view controller triggers the didSet observer. The observer is going to access an IBOutlet which is not connected yet.
A solution is to run the code in viewDidLoad
var randomMeal = ""
override func viewDidLoad() {
super.viewDidLoad()
self.hideKeyboardWhenTappedAround()
if !randomMeal.isEmpty {
let selectedMeal = downloadMealData(mealID: randomMeal)
setUIForMeal(meal: selectedMeal)
}
}

Related

How to retrieve filled TextField from persistence

I have an error when loading secondViewController with a textField and an action button so the user can fill it with some number and get it saved to UserDefaults
The idea is to be able to come back to secondVC and that the TextField shows there with the same value that input originally by the User
class MainViewController: UIViewController {
#IBOutlet weak var A3TextField: UITextField!
#IBAction func calc(_ sender: Any) {
let A3 = Cell(name: "A3", sheet: "", value: Double(A3TextField.text!)!)
print(A3)
Cell.saveCellsUserDefaults(cells: [A3], forKey: "main")
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
}
class SecondViewController: UIViewController {
#IBOutlet weak var B3TextField: UITextField!
#IBAction func calc2(_ sender: Any) {
let B3 = Cell(name: "B3", sheet: "", value: Double(B3TextField.text!)!)
print(B3)
// persist
Cell.saveCellsUserDefaults(cells: [B3], forKey: "second")
}
override func viewDidLoad() {
super.viewDidLoad()
// retrieve from persists and draw in TextField
let cellB3FromUD = Cell.getCellsUserDefaults(forKey: "second")
print("---> cellB3FromUD \(cellB3FromUD[0].value)")
B3TextField.text = "\(cellB3FromUD[0].value)"
}
}
struct Cell: Codable {
var name: String = ""
var sheet: String = ""
var value: Double = 0
init(name: String, sheet: String, value: Double) {
self.name = name
self.sheet = sheet
self.value = value
}
static func saveCellsUserDefaults(cells: [Cell], forKey: String) {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(cells) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: forKey)
}
}
static func getCellsUserDefaults(forKey: String)-> [Cell] {
var cells = [Cell]()
let decoder = JSONDecoder()
if let cellData = UserDefaults.standard.data(forKey: forKey) {
if let cellX = try? decoder.decode([Cell].self, from: cellData) {
cells = cellX
return cellX
}
}
return cells
}
}

How to change value of variable by UISwitch?

I am a beginner to Xcode and Swift and I am currently creating an application where the user adds a person on the application and after that it right the amount of money they owe that person or that person owes him/her.
Note: I have used core data to store all the value
I actually want to change the value of a variable when switch is on and off. For instance, in the following I want the "amount" to be negative when the switch is on and positive when it is off. Also, when I try to do this and send amount variable to previous view controller I can't send the value depending on the UISwitch because it always shows positive. I am trying to find a solution to this from past 3 days therefore can you please help me? Thanks a lot in advance
Owe ViewController
import UIKit
class NewOweTableViewController: UIViewController {
#IBOutlet weak var titleTextField: UITextField!
#IBOutlet weak var locationTextField: UITextField!
#IBOutlet weak var amountTextField: UITextField!
#IBOutlet weak var datePicker: UIDatePicker!
let context = (UIApplication.shared.delegate as!
AppDelegate).persistentContainer.viewContext
var owe: Owe?
var dataInfo: [Owe] = []
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.viewWithTag(1)?.isHidden = true
let saveBTN = UIBarButtonItem(barButtonSystemItem:UIBarButtonSystemItem.save, target:self,
action: #selector(saveButtonTapped(_:)))
let deleteBTN = UIBarButtonItem(barButtonSystemItem:UIBarButtonSystemItem.trash, target:self,
action: #selector(deleteButtonTapped(_:)))
self.navigationItem.rightBarButtonItems = [saveBTN, deleteBTN]
if !dataInfo.isEmpty {
titleTextField.text = dataInfo[0].name
amountTextField.text = (NSString(format: "%.2f", (dataInfo[0].amount) as CVarArg) as String)
locationTextField.text = dataInfo[0].location
datePicker.date = dataInfo[0].date!
}
}
#objc func saveButtonTapped(_ sender: UIButton){
if !dataInfo.isEmpty{
let data = dataInfo[0]
data.name = titleTextField.text
data.amount = Double(amountTextField.text!)!
data.location = locationTextField.text
data.date = datePicker.date
}
else if titleTextField.text == "" || amountTextField.text == "" || locationTextField.text == "" {
return
}
else{
let data = Owe(context: context)
data.name = titleTextField.text
data.amount = Double(amountTextField.text!)!
data.location = locationTextField.text
data.date = datePicker.date
}
do {
try context.save()
navigationController?.popViewController(animated: true)
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
#objc func deleteButtonTapped(_ sender: UIButton){
if !dataInfo.isEmpty{
let data = dataInfo[0]
context.delete(data)
do {
try context.save()
navigationController?.popViewController(animated: true)
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
#IBAction func oweSwitch(_ sender: UISwitch) {
if sender.isOn {
owe?.amount = (owe?.amount)! * (-1)
amountTextField.textColor = UIColor.green
} else {
owe?.amount = (owe?.amount)! * (1)
amountTextField.textColor = UIColor.red
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Previous View Controller
import UIKit
class PersonDetailTableViewController: UITableViewController {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var totalLabel: UILabel?
var person: People?
var owe: Owe?
#IBOutlet var personTable: UITableView!
var dataInfo: [Owe] = []
var selectedObject: [Owe] = []
var balanceAmount = "Balance: "
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (dataInfo.count)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = personTable
.dequeueReusableCell(withIdentifier: "detailsCell", for: indexPath)
cell.textLabel?.text = dataInfo[indexPath.row].name
cell.detailTextLabel?.text = "₹ \(dataInfo[indexPath.row].amount)"
if dataInfo[indexPath.row].amount < 0 {
cell.detailTextLabel?.textColor = UIColor.red
} else {
cell.detailTextLabel?.textColor = UIColor.green
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedObject = [dataInfo[indexPath.row]]
performSegue(withIdentifier: "addOweDetails", sender: nil)
tableView.deselectRow(at: indexPath, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
getData()
personTable.dataSource = self
addTotalToNav()
print(dataInfo as Any)
}
// MARK: - Table view data source
func addTotalToNav() -> Void {
if let navigationBar = self.navigationController?.navigationBar {
let totalFrame = CGRect(x: 10, y: 0, width: navigationBar.frame.width/2, height: navigationBar.frame.height)
totalLabel = UILabel(frame: totalFrame)
totalLabel?.text = balanceAmount
totalLabel?.tag = 1
totalLabel?.font = UIFont.boldSystemFont(ofSize: 14)
totalLabel?.textColor = UIColor.red
// navigationBar.large = totalLabel?.text
self.title = totalLabel?.text
}
}
func getData() -> Void {
do{
dataInfo = try context.fetch(Owe.fetchRequest())
var total:Double = 0.00
for i in 0 ..< dataInfo.count {
total += dataInfo[i].amount as! Double
}
balanceAmount = "Balance: ₹" + (NSString(format: "%.2f", total as CVarArg) as String)
}
catch{
print("Fetching Failed")
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let vc = segue.destination as! NewOweTableViewController
vc.dataInfo = selectedObject
selectedObject.removeAll()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
getData()
personTable.reloadData()
if (self.navigationController?.navigationBar.viewWithTag(1)?.isHidden == true){
self.navigationController?.navigationBar.viewWithTag(1)?.removeFromSuperview()
addTotalToNav()
}
}
}
Core Data for owe
import UIKit
import CoreData
#objc(Owe)
public class Owe: NSManagedObject {
var date: Date? {
get{
return rawDate as Date?
}
set {
rawDate = newValue as NSDate?
}
}
convenience init?(name: String?, location: String?, amount: Double, date: Date?) {
let appDelegate = UIApplication.shared.delegate as? AppDelegate
guard let context = appDelegate?.persistentContainer.viewContext
else {
return nil
}
self.init(entity: Owe.entity(), insertInto: context)
self.name = name
self.location = location
self.amount = amount
self.date = date
}
}
Hi there and welcome to the Swift Community!
If I understand correctly, you are trying to propagates updates backwards from NewOweTableViewController to PersonDetailTableViewController. If that is the case, an easy way to achieve this with your MVC architecture is by passing a closure to NewOweTableViewController when you initialize it in PersonDetailTableViewController.
In order to do so,
Update NewOweTableViewController and add a closure property.
class NewOweTableViewController: UIViewController {
// ...
var switchValueUpdate: ((Bool) -> ())?
// ...
}
Make sure you call this closure inside your #IBAction that you link to your switch in NewOweTableViewController
#IBAction func oweSwitch(_ sender: UISwitch) {
if sender.isOn {
owe?.amount = (owe?.amount)! * (-1)
amountTextField.textColor = UIColor.green
} else {
owe?.amount = (owe?.amount)! * (1)
amountTextField.textColor = UIColor.red
}
switchValueUpdate?(sender.isOn)
}
update PersonDetailTableViewController to set the closure
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let vc = segue.destination as! NewOweTableViewController
vc.dataInfo = selectedObject
selectedObject.removeAll()
vc.switchValueUpdate = { (isOn) in
// Here you go, update PersonDetailTableViewController to reflect changes related to the switch!
}
}
That's it! Let me know if you have any question on that code, hope it helps!

Attribute a different value to a variable depending on the class from which you are coming (Swift)

In my HomeClass there is a static var globalLimit: Int = 0 and i have to pass his value in other classes, this is my prepare function
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? SelectClass {
vc.limit = Int(steppeR.value)
HomeClass.globalLimit = Int(steppeR.value)
HomeClass.globalRadiusLimit = Int(stepperRadius.value)
}
else if let np = segue.destination as? CourseClass2 {
np.categories = filteredList
np.numberPlaces = filteredList.count
np.radius = Int(stepperRadius.value)
}
}
now the problem is this, in my CourseClass2 class that i can even reach from SelectClass i have the var numberPlaces = 0 and his value is
override func viewDidLoad() {
super.viewDidLoad()
numberPlaces = HomeClass.globalLimit
}
but like you can see in my prepare function when i come in CourseClass2 directly from HomeClass i want that the value of numberPlaces = filteredList.count but of course it does not work because it goes against it numberPlaces = HomeClass.globalLimit in the viewDidload of CourseClass2, so how can i solve this problem?
Try in this way:
Set your numberPlaces var as optional:
var numberPlaces: Int?
Then check in view did load if it is nil:
override func viewDidLoad() {
super.viewDidLoad()
if numberPlaces == nil {
numberPlaces = HomeClass.globalLimit
}
}

Passing data from a class to same instance of a viewcontroller

When a specific event happens(in my case when a tab bar is changed) I want to create a new link from an Array. I have gotten this to work but the problem I am not facing is when i try to pass the generated link to the same viewcontroller i get an error
fatal error: unexpectedly found nil while unwrapping an Optional value
This happens when I try to change the UILabel movietitle and imageview. I think this is because every time it sends the link it creates a new ViewController instead of using the existing one. Might also be that i have missed an unwrapped value somewhere. Hope someone here can help me!
StringBuilder:
import UIKit
class StringBuilder: NSObject {
let urlString = "https://api.themoviedb.org/3/discover/movie?api_key=935f539acb9e5534ddeed3fb57e&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1&with_genres=12"
let urlStringMultipleGenres = "https://api.themoviedb.org/3/discover/movie?api_key=935f539acbf5534ddeed3fb57e&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1&with_genres=28,12,10749"
var currentGenreArray: Array<Int> = []
//This will be run after the user has selected or deselected genres in the genreControllerView
func updateGenres(genreArrayIn: Array<Int>){
print("update genres input: ")
print(genreArrayIn)
//If new array input is the same as old arrayinput, do nothing
if genreArrayIn == currentGenreArray{
return
}
else{
let returnedLink = generateString(genreID: genreArrayIn)
print("Returned link after generate string" + returnedLink)
sendLink(link: returnedLink)
}
}
//After the updated genres have been put into an Array, this function will generate the whole string which
//will be the main String the getMovieRequest follows
func generateString(genreID: Array<Int>) -> String{
let filteredGenreArray = filterZeroes(unfilteredArray: genreID)
currentGenreArray = genreID
print("current genre array: ")
print(currentGenreArray)
let baseString = "https://api.themoviedb.org/3/discover/movie?api_key=935f539acbfed4ddeed3fb57e&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1&with_genres="
var generatedString = baseString
for id in filteredGenreArray{
let k = String(id)
generatedString += k + ","
}
print("Generated Link from Strinbuilder: ")
print(generatedString)
return generatedString
}
func filterZeroes(unfilteredArray: Array<Int>) -> Array<Int>{
let filteredGenreArray = unfilteredArray.filter {$0 > 0}
print("filtered array: ")
print(filteredGenreArray)
return filteredGenreArray
}
func sendLink(link: String){
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let movieVC = storyBoard.instantiateViewController(withIdentifier: "movieView") as! ViewController
movieVC.getMovieData(activeGenreLink: link)
print("new link sent from sendlink()")
}
}
ViewController:
import UIKit
import Alamofire
import AlamofireImage
class ViewController: UIViewController{
static let sharedInstance = ViewController()
var movieIndex = 0
var movieArray:[Movie] = []
var downloadGrp = DispatchGroup()
#IBOutlet var uiMovieTitle: UILabel!
#IBOutlet var uiMoviePoster: UIImageView!
#IBOutlet var posterLoading: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let firstTimeLink = "https://api.themoviedb.org/3/discover/movie?api_key=935f539acb9e5534ddeed3fb57e&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1&with_genres=35,18"
getMovieData(activeGenreLink: firstTimeLink)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(imageTapped(tapGestureRecognizer:)))
uiMoviePoster.isUserInteractionEnabled = true
uiMoviePoster.addGestureRecognizer(tapGestureRecognizer)
print("settings Sucessful")
}
func imageTapped(tapGestureRecognizer: UITapGestureRecognizer){
performSegue(withIdentifier: "detailsSegue", sender: self)
}
#IBAction func yesBtn(_ sender: UIButton) {
movieIndex += 1
updateUI()
}
#IBAction func seenBtn(_ sender: UIButton) {
movieIndex += 1
}
#IBAction func noBtn(_ sender: UIButton) {
movieIndex += 1
}
//Get movie data
func getMovieData(activeGenreLink: String){
//self.posterLoading.startAnimating()
movieIndex = 0
self.downloadGrp.enter()
Alamofire.request(activeGenreLink).responseJSON { response in
//print(response.request) // original URL request
//print(response.response) // HTTP URL response
//print(response.data) // server data
//print(response.result) // result of response serialization
self.movieArray = []
print(self.movieArray)
if let json = response.result.value as? Dictionary<String,AnyObject> {
if let movies = json["results"] as? [AnyObject]{
for movie in movies{
let movieObject: Movie = Movie()
let title = movie["title"] as! String
let releaseDate = movie["release_date"] as! String
let posterPath = movie["poster_path"] as! String
let overView = movie["overview"] as! String
let movieId = movie["id"] as! Int
let genre_ids = movie["genre_ids"] as! [AnyObject]
movieObject.title = title
movieObject.movieRelease = releaseDate
movieObject.posterPath = posterPath
movieObject.overView = overView
movieObject.movieId = movieId
for genre in genre_ids{//Genre ids, fix this
movieObject.movieGenre.append(genre as! Int)
}
Alamofire.request("http://image.tmdb.org/t/p/w1920" + posterPath).responseImage {
response in
//print(response.request)
//print(response.response)
//debugPrint(response.result)
if var image = response.result.value {
image = UIImage(data: response.data!)!
movieObject.poster = image
}
}
self.movieArray.append(movieObject)
}//End of for each movie
}
else{
print("error while making results anyobject")
}
}
else{
print("error while trying to make NSDictionary")}
self.downloadGrp.leave()
}//End of Json request
downloadGrp.notify( queue: .main){
print("all downloads finished")
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
print(self.movieArray[0].title!)
self.updateUI()
print("updatedUI")
}
}
}//End of getmoviedata
override func prepare(for segue: UIStoryboardSegue,sender: Any?){
// Create a variable that you want to send
let currentMovie = self.movieArray[movieIndex]
if let destinationVC = segue.destination as? DetailsViewController{
destinationVC.currentMovie = currentMovie
}
}
func updateUI(){
//self.posterLoading.stopAnimating()
if uiMoviePoster == nil{
print(uiMovieTitle.debugDescription)
}
else{
print("first time debugID: " + uiMovieTitle.debugDescription)
uiMovieTitle.text = self.movieArray[movieIndex].title
uiMoviePoster.image = self.movieArray[movieIndex].poster
}
}
}
You want to grab a sharedInstance not instantiate from a storyboard
let movieVC = ViewController.sharedInstance()
but I still do not understand why do you need to do it like this

How to save data from ViewController using NSCoding in Swift [duplicate]

This question already has an answer here:
How to store value generated from a ViewController using NSCoding in Swift
(1 answer)
Closed 7 years ago.
I need to save data from a segued ViewController (“ScoreView.swift”) to “ScoreHistory.swift” using NSCoding. I tried but the data isn't showing up in "ScoreTableViewController.swift". What am I missing?
I have this ScoreView.swift which has the following code: (Pls note that this is a "segued" view where data has been passed from another ViewController)
class ScoreView: UIViewController {
var dateToday = NSDate()
var score: ScoreHistory?
var numberofquestions:String = ""
var scorepassed:String = ""
var scorepercentpassed:String = ""
var scoreremarkspassed:String = ""
var totalduration:String!
var incorrectanswerspassed:String = ""
var skippedquestionspassed:String = ""
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
datePlayedLabel.text = dateToday.description
totalScoreLabel.text = scorepassed
scorePercentage.text = scorepercentpassed
totalAnsweredLabel.text = numberofquestions
totalDurationLabel.text = totalduration
gameStatusLabel.text = "Exam Finished"
// NSCoding
if let score = score {
datePlayedLabel.text = score.datePlayed
totalScoreLabel.text = score.totalScore
totalAnsweredLabel.text = score.totalAnswered
totalDurationLabel.text = score.totalDuration
gameStatusLabel.text = score.gameStatus
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if backMenu === sender {
let datePlayed = datePlayedLabel.text ?? ""
let totalScore = totalScoreLabel.text ?? ""
let totalAnswered = totalAnsweredLabel.text ?? ""
let totalDuration = totalDurationLabel.text ?? ""
let gameStatus = gameStatusLabel.text ?? ""
// Set the score to be passed to ScoreTableViewController after the unwind segue.
score = ScoreHistory(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)
}
NSKeyedArchiver.archiveRootObject(score!, toFile: ScoreHistory.ArchiveURL.path!)
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
datePlayedLabel.text = dateToday.description
totalScoreLabel.text = scorepassed
scorePercentage.text = scorepercentpassed
totalAnsweredLabel.text = numberofquestions
totalDurationLabel.text = totalduration
gameStatusLabel.text = "Exam Finished"
}
// Labels
}
}
I have ScoreHistory.swift, which has the following code:
class ScoreHistory: NSObject, NSCoding {
// MARK: Properties
var datePlayed: String
var totalScore: String
var totalAnswered: String
var totalDuration: String
var gameStatus: String
// MARK: Archiving Paths
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("scores")
// MARK: Types
struct PropertyKey {
static let datePlayedKey = "datePlayed"
static let totalScoreKey = "totalScore"
static let totalAnsweredKey = "totalAnswered"
static let totalDurationKey = "totalDuration"
static let gameStatusKey = "gameStatus"
}
// MARK: Initialization
init?(datePlayed: String, totalScore: String, totalAnswered: String, totalDuration: String, gameStatus: String) {
// Initialize stored properties.
self.datePlayed = datePlayed
self.totalScore = totalScore
self.totalAnswered = totalAnswered
self.totalDuration = totalDuration
self.gameStatus = gameStatus
super.init()
}
// MARK: NSCoding
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(datePlayed, forKey: PropertyKey.datePlayedKey)
aCoder.encodeObject(totalScore, forKey: PropertyKey.totalScoreKey)
aCoder.encodeObject(totalAnswered, forKey: PropertyKey.totalAnsweredKey)
aCoder.encodeObject(totalDuration, forKey: PropertyKey.totalDurationKey)
aCoder.encodeObject(gameStatus, forKey: PropertyKey.gameStatusKey)
}
required convenience init?(coder aDecoder: NSCoder) {
let datePlayed = aDecoder.decodeObjectForKey(PropertyKey.datePlayedKey) as! String
let totalScore = aDecoder.decodeObjectForKey(PropertyKey.totalScoreKey) as! String
let totalAnswered = aDecoder.decodeObjectForKey(PropertyKey.totalAnsweredKey) as! String
let totalDuration = aDecoder.decodeObjectForKey(PropertyKey.totalDurationKey) as! String
let gameStatus = aDecoder.decodeObjectForKey(PropertyKey.gameStatusKey) as! String
// Must call designated initializer.
self.init(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)
}
}
Here is the full code of ScoreTableViewController.swift:
class ScoreTableViewController: UITableViewController {
// MARK: Properties
var scores = [ScoreHistory]()
var dateToday = NSDate()
override func viewDidLoad() {
super.viewDidLoad()
// Load any saved scores, otherwise load sample data.
if let savedScores = loadScores() {
scores += savedScores
} else {
// Load the sample data.
loadSampleScores()
}
}
func loadSampleScores() {
let score1 = ScoreHistory(datePlayed: dateToday.description, totalScore: "0", totalAnswered: "0", totalDuration: "0", gameStatus: "started")!
scores += [score1]
}
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 scores.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 = "ScoreHistoryTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! ScoreHistoryTableViewCell
// Fetches the appropriate note for the data source layout.
let score = scores[indexPath.row]
cell.datePlayedLabel.text = score.datePlayed
cell.totalScoreLabel.text = score.datePlayed
cell.totalScoreLabel.text = score.totalScore
cell.totalAnsweredLabel.text = score.totalAnswered
cell.totalDurationLabel.text = score.totalDuration
cell.gameStatusLabel.text = score.gameStatus
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
scores.removeAtIndex(indexPath.row)
saveScores()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
}
// 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 scoreDetailViewController = segue.destinationViewController as! ScoreViewController
// Get the cell that generated this segue.
if let selectedScoreCell = sender as? ScoreHistoryTableViewCell {
let indexPath = tableView.indexPathForCell(selectedScoreCell)!
let selectedScore = scores[indexPath.row]
scoreDetailViewController.score = selectedScore
}
}
}
// MARK: NSCoding
func saveScores() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(scores, toFile: ScoreHistory.ArchiveURL.path!)
if !isSuccessfulSave {
print("Failed to save scores...")
}
}
func loadScores() -> [ScoreHistory]? {
return NSKeyedUnarchiver.unarchiveObjectWithFile(ScoreHistory.ArchiveURL.path!) as? [ScoreHistory]
}
#IBAction func unwindToScoreList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? ScoreViewController, score = sourceViewController.score {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update an existing note.
scores[selectedIndexPath.row] = score
tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
// Add a new score.
let newIndexPath = NSIndexPath(forRow: scores.count, inSection: 0)
scores.append(score)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
saveScores()
}
}
}
}
GOAL: My goal is to record/store all session data from “ScoreView.swift” whenever a user finishes a quiz game.
The "ScoreView" is shown after each quiz game, I plan to record each quiz results in "ScoreHistory.swift." How do I do it?
The easiest solution is to save the changed values from the UITextField instances back to the score instance in ScoreView (why is score optional at all since you always pass a non-optional score instance ??) and unwind the segue.
Then the array is saved in the method unwindToScoreList of ScoreTableViewController
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if backMenu === sender {
score?.datePlayed = datePlayedLabel.text ?? ""
score?.totalScore = totalScoreLabel.text ?? ""
score?.totalAnswered = totalAnsweredLabel.text ?? ""
score?.totalDuration = totalDurationLabel.text ?? ""
score?.gameStatus = gameStatusLabel.text ?? ""
}
}
No archiving in ScoreView !
Your loadScores function is loading an archived array of scores:
func loadScores() -> [ScoreHistory]? {
return NSKeyedUnarchiver.unarchiveObjectWithFile(ScoreHistory.ArchiveURL.path!) as? [ScoreHistory]
}
In your segue, you are only archiving a single score. You can't archive a ScoreHistory instance and expect to unarchive a ScoreHistory array. Where you currently have:
score = ScoreHistory(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)
You need to change this to:
var scores = loadScores() ?? []
score = ScoreHistory(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)
scores.append(score)
saveScores(scores)
Where loadScores and saveScores are the same as the code in ScoreTableViewController, although I've added the scores to save as a parameter given this code creates a local var.
UPDATE: It's late and I wasn't paying enough attention. You need to handle loadScores returning nil, and of course scores should be var not let or you won't be able to add to it. With these changes, scores should no longer be optional, so you won't need to unwrap it.

Resources