Now what I am adding an item to a cart it is appearing correctly. But when I am reopening the page, the data is lost. I am trying to store the cartTableView data in userdefaults. I have tried several methods but couldn't turn up.
Here is my code for cartViewController :
import UIKit
class CartViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var arrdata = [jsonstruct]()
var categorydata = [Categories]()
var imgdata = [Images]()
var cartArray = [CartStruct]()
#IBOutlet var cartTableView: UITableView!
#IBOutlet var totalCount: UILabel!
#IBOutlet var subtotalPrice: UILabel!
#IBOutlet var shippingPrice: UILabel!
#IBOutlet var totalPrice: UILabel!
#IBOutlet var proceedBtn: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
cartArray = (UserDefaults.standard.array(forKey: "cartt") as? [CartStruct])!
cartTableView.reloadData()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cartArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CartCellTableViewCell", for: indexPath) as! CartCellTableViewCell
cell.cartImageView.downloadImage(from: cartArray[indexPath.row].cartItems.images.first?.src ?? "place_holder_image")
cell.productNameCart.text = cartArray[indexPath.row].cartItems.name
cell.prodductDescCart.text = cartArray[indexPath.row].cartItems.categories.first?.type
cell.productPriceCart.text = cartArray[indexPath.row].cartItems.price
cell.addBtn.addTarget(self, action: #selector(add(sender:)), for: .touchUpInside)
cell.addBtn.tag = indexPath.row
let cartQuantity = cartArray[indexPath.row].cartQuantity
cell.prodCount.text = "\(cartQuantity)"
if cartQuantity >= 0 {
cell.subBtn.isUserInteractionEnabled = true;
cell.subBtn.addTarget(self, action: #selector(sub(sender:)), for: .touchUpInside)
cell.subBtn.tag = indexPath.row
} else {
cell.subBtn.isUserInteractionEnabled = false;
}
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
cartArray.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.reloadData()
}
}
#objc func add(sender: UIButton){
if cartArray[sender.tag].cartQuantity >= 0 {
cartArray[sender.tag].cartQuantity += 1
cartTableView.reloadData()
}
}
#objc func sub(sender: UIButton){
if cartArray[sender.tag].cartQuantity > 0 {
cartArray[sender.tag].cartQuantity -= 1
cartTableView.reloadData()
}
}
}
But here is showing some runtime error : " Thread 1: Swift runtime failure: force unwrapped a nil value "
Please suggest me how can I fix the whole thing.. Thanks in advance!
You can use UserDefaults, But You should use core data or realm for better performance.
how to use this code?
// Save:
CartCache.save([Cart])
// Get data:
let cart = CartCache.get()
struct CartCache {
static let key = "CartCache"
static func save(_ value: [CartStruct]!) {
UserDefaults.standard.set(try? PropertyListEncoder().encode(value), forKey: key)
}
static func get() -> [CartStruct]! {
var cartData: User!
if let data = UserDefaults.standard.value(forKey: key) as? Data {
cartData = try? PropertyListDecoder().decode([User].self, from: data)
return cartData!
} else {
return []
}
}
static func remove() {
UserDefaults.standard.removeObject(forKey: key)
}
}
Yes, we can store in UserDefaults, But it is not flexible as compare to Core Data or other storage for add, updates or delete operations.
Below code supposed to work.
You need to make "CartStruct" as Codable
struct CartStruct : Codable {
var cartItems: jsonstruct
var cartQuantity: Int
}
Next, Add this to insert and store it in tap "addToCartbtnTapped" .(DetailViewController)
func saveCart(data: CartStruct) {
let defaults = UserDefaults.standard
if let cdata = defaults.data(forKey: "cartt") {
var cartArray = try! PropertyListDecoder().decode([CartStruct].self, from: cdata)
cartArray.append(data)
cartCount.text = "\(cartArray.count)"
if let updatedCart = try? PropertyListEncoder().encode(cartArray) {
UserDefaults.standard.set(updatedCart, forKey: "cartt")
}
}
}
#IBAction func addToCartbtnTapped(_ sender: Any) {
if let info = detailInfo {
let cartData = CartStruct(cartItems: info, cartQuantity: 1)
self.saveCart(data: cartData)
showAlert()
(sender as AnyObject).setTitle("Go to Cart", for: .normal)
addToCartbtn.isUserInteractionEnabled = false
}
}
#IBAction func cartTappedToNavigate(_ sender: Any) {
let cart = self.storyboard?.instantiateViewController(withIdentifier: "CartViewController") as? CartViewController
self.navigationController?.pushViewController(cart!, animated: true)
}
Now, Fetch your cart data from where you stored.(CartViewController)
override func viewDidLoad() {
super.viewDidLoad()
self.getCartData()
}
func getCartData() {
let defaults = UserDefaults.standard
if let data = defaults.data(forKey: "cartt") {
cartArray = try! PropertyListDecoder().decode([CartStruct].self, from: data)
cartTableView.reloadData()
}
}
You should use different data saving methods other than user defaults. User defaults mostly used to save small informations like name, username, gender etc. I recommend you to use Core Data instead of user defaults and here is a very useful explanation and example projec of Core Data.
https://www.raywenderlich.com/7569-getting-started-with-core-data-tutorial
Have a great day :)
As many people have mentioned UserDefaults is not really meant for storing this type of data.
From the docs:
The UserDefaults class provides a programmatic interface for interacting with the defaults system. The defaults system allows an app to customize its behavior to match a user’s preferences. For example, you can allow users to specify their preferred units of measurement or media playback speed. Apps store these preferences by assigning values to a set of parameters in a user’s defaults database. The parameters are referred to as defaults because they’re commonly used to determine an app’s default state at startup or the way it acts by default.
However I feel like this is still a valid question and something that every IOS/MacOS developer should know how to use.
NSUserDefaults is simple and easy to use, but there are some things you have to understand.
Namely you can only store property list items.
A default object must be a property list—that is, an instance of (or for collections, a combination of instances of) NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any other type of object, you should typically archive it to create an instance of NSData.
This means that CartStruct cannot simply be persisted in UserDefaults, because it is not a NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary or a combination of these.
Now using UserDefaults is simple they have a singleton for the app called standard.
UserDefaults.standard.setValue("Hello Defaults", forKey: "Hello_Key")
let greeting = UserDefaults.standard.value(forKey: "Hello_Key")
print(greeting!)// prints Hello Dafaults
The next question becomes how to turn your struct into data. I'm sure if you ask that question on here and post the code for your struct you'll get a quick and easy solution. Once you have data you can save that in UserDefaults or better yet in the file system. In fact I think that really is the question your trying to ask is how to persist and retrieve your struct data for use in a tableview.
Related
i'm trying to do some simple things here, but i'm struggling a bit as i'm a beginner.
So basically i have a JSON file link to use to populate my tableview with 2 sections. Each cell have a "Favourite button".
i've tried many way to save my array with UserDefault with no luck.
What i wish to achieve:
When the "Favourite button" is pressed, i wish to move the cell to the other section
Save and retrieve those 2 array using UserDefault
I'm open to hear any suggestion also any other differs approach, as i'm sure there is some better one.
I will upload here some code, but also the link for the full project in Git so you can check better (https://github.com/Passeric/UserTableview.git)
I really appreciate any help.
The JSON i'm using:
{
"User": [
{
"Name": "John",
"Age": "34"
},{
"Name": "Sara",
"Age": "19"
}.......]}
My Struct:
class User: Codable {
let user: [UserDetails]
enum CodingKeys: String, CodingKey {
case user = "User"
}
init(user: [UserDetails]) {
self.user = user
}
}
class UserDetails: Codable {
let name: String
let age: String
enum CodingKeys: String, CodingKey {
case name = "Name"
case age = "Age"
}
init(name: String, age: String) {
self.name = name
self.age = age
}
}
And my ViewController:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var arrayDefault = [UserDetails]()
var arrayFavourite = [UserDetails]()
var sec: [Int:[UserDetails]] = [:]
var Default = UserDefaults.standard
#IBOutlet weak var myTableview: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.myTableview.dataSource = self
self.myTableview.delegate = self
DownloadJSON()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
sec = [0 : arrayFavourite, 1 : arrayDefault]
return (sec[section]?.count)!
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let dataRef = arrayDefault[indexPath.row]
let cel: MyCell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
cel.NameLabel.text = dataRef.name
cel.AgeLabel.text = dataRef.age
let imgFav = UIImage(systemName: "star")
let b = imgFav?.withRenderingMode(.alwaysOriginal)
cel.FavButton.setImage(b, for: .normal)
return cel
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
func DownloadJSON() {
let urlll = URL(string: "https://pastebin.com/raw/Wufivqmq")
guard let downloadURL = urlll else { return }
URLSession.shared.dataTask(with: downloadURL) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
return
}
print("downloaded")
do
{
let decoder = JSONDecoder()
let downloadeduser = try decoder.decode(User.self, from: data)
self.arrayDefault.removeAll()
self.arrayDefault.append(contentsOf: downloadeduser.user)
DispatchQueue.main.async {
print("data saved")
self.myTableview.reloadData()
}
} catch {
print(error)
}
}.resume()
}
#IBAction func RefreshButton(_ sender: Any) {
// Here i wish to refresh the table view (so the current saved array) with the updated JSON
// For example if i update the "Age" in the original JSON
// But i don't want to lose the user preference for the "Favourite"
}
}
As you can see is not a big deal thing, but i can't figure out how to properly save and retrive the array, and then move the cell (by the index path) to the other array.
This is the link for the full project: https://github.com/Passeric/UserTableview.git
Thanks again to anyone who will help.❤️
I would make a few changes:
Clean up your data model. Currently you are storing the data as both two arrays and a dictionary. There are minor pros and cons to each approach, but in the grand scheme of things it doesn't really matter which one you use, just pick one. Personally, as a beginner, I would go with two arrays.
Don't change your data model in the data source. Currently you are creating the sec dictionary in the tableView:numberOfRowsInSection function. If you want to keep using sec then create it as the same time you load the data initially. Or, as mentioned above, just remove sec completely.
Assuming you've made the above changes, moving a user to the favorites section is as simple as removing the user from the default list, adding it to the favorites list, and telling the list that the data has changed:
func makeFavorite(index:IndexPath) {
let user = arrayDefault[index.row]
arrayDefault.remove(at: index.row)
arrayFavourite.append(user)
//This updates the entire list. You might want to use moveRow(at:to:) in order to animate the change
tableView.reloadData()
}
When it comes to converting your data model to/from json, you're already most of the way there by conforming to Codable. To load from json:
if let jsonData = UserDefaults.standard.string(forKey: "userKey").data(using: .utf8) {
let decoder = JSONDecoder()
if let user = try? decoder.decode(User.self, from: jsonData) {
//Do something with user here...
}
}
And to output to json:
let encoder = JSONEncoder()
if let data = try? encoder.encode(user),
let jsonString = String(decoding: data, as: .utf8) {
UserDefaults.standard.setValue(jsonString, forKey: "userKey")
}
Do the same thing with the list of favorites (using a different key).
I'm trying to learn iOS programming so I thought it would be a good idea to emulate instagrams feed. Everyone uses this basic feed and I would like to know how to do it.
The basic idea is to have one image/text post show up in a single column. Right now I have a a single image to be shown.
I'm currently extracting the image url correctly from firebase. The only issue is that my CollectionView still is showing up empty. I started this project months ago and I forget where the tutorial is at. Please help me fill in the blanks. Here is the code:
import UIKit
import SwiftUI
import Firebase
import FirebaseUI
import SwiftKeychainWrapper
class FeedViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource{
#IBOutlet weak var collectionview: UICollectionView!
//var posts = [Post]()
var posts = [String](){
didSet{
collectionview.reloadData()
}
}
var following = [String]()
var posts1 = [String]()
var userStorage: StorageReference!
var ref : DatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
posts1 = fetchPosts()
//let myIndexPath = IndexPath(row: 0, section: 0)
//collectionView(collectionview, cellForItemAt: myIndexPath)
//print(self.posts1.count)
}
func fetchPosts() -> [String]{
let uid = Auth.auth().currentUser!.uid
let ref = Database.database().reference().child("posts")
let uids = Database.database().reference().child("users")
uids.observe(DataEventType.value, with: { (snapshot) in
let dict = snapshot.value as! [String:NSDictionary]
for (_,value) in dict {
if let uid = value["uid"] as? String{
self.following.append(uid)
}
}
ref.observe(DataEventType.value, with: { (snapshot2) in
let dict2 = snapshot2.value as! [String:NSDictionary]
for(key, value) in dict{
for uid2 in self.following{
if (uid2 == key){
for (key2,value2) in value as! [String:String]{
//print(key2 + "this is key2")
if(key2 == "urlToImage"){
let urlimage = value2
//print(urlimage)
self.posts1.append(urlimage)
self.collectionview.reloadData()
print(self.posts1.count)
}
}
}
}
}
})
})
//ref.removeAllObservers()
//uids.removeAllObservers()
print("before return")
print(self.posts1.count)
return self.posts1
override func viewDidLayoutSubviews() {
collectionview.reloadData()
}
func numberOfSections(in collectionView: UICollectionView) ->Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return posts1.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PostCell", for: indexPath) as! PostCell
cell.postImage.sd_setImage(with: URL(string: posts1[indexPath.row]))
//creating the cell
//cell.postImage.downloadImage(from: self.posts[indexPath.row])
// let storageRef = Storage.storage().reference(forURL: self.posts[indexPath.row].pathToImage)
//
//
print("im trying")
//let stickitinme = URL(fileURLWithPath: posts1[0])
//cell.postImage.sd_setImage(with: stickitinme)
//cell.authorLabel.text = self.posts[indexPath.row].author
//cell.likeLabel.text = "\(self.posts[indexPath.row].likes) Likes"
return cell
}
#IBAction func signOutPressed(_sender: Any){
signOut()
self.performSegue(withIdentifier: "toSignIn", sender: nil)
}
#objc func signOut(){
KeychainWrapper.standard.removeObject(forKey:"uid")
do{
try Auth.auth().signOut()
} catch let signOutError as NSError{
print("Error signing out: %#", signOutError)
}
dismiss(animated: true, completion: nil)
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
UPDATE
The observe call is not updating the value of posts (the dictionary). Once the observe call exits, the value of posts is set back to empty.
PostCell class as asked:
import UIKit
class PostCell: UICollectionViewCell {
#IBOutlet weak var postImage: UIImageView!
#IBOutlet weak var authorLabel: UILabel!
#IBOutlet weak var likeLabel:UILabel!
#IBOutlet weak var likeBtn:UIButton!
#IBOutlet weak var unlikeBtn:UIButton!
#IBAction func likePressed (_ sender: Any){
}
#IBAction func unlikePressed(_sender: Any){
}
}
I think the problem is:
Your collectionView dataSource is called only once. Since the image url loading is asynchronous, you will need to refresh your collectionview every time new data is appended to your datasource array like this:
self.posts.append(urlimage)
collectionView.reloadData()
or:
var posts = [UIImage](){
didSet{
collectionView.reloadData()
}
}
Hope this helps.
Edit update:
Regarding the asynchronous calls, i think you should use escaping closure that runs the code block once the network request receives a response.
First separate the network call functions like:
func fetchUsers(completion: #escaping(_ dictionary: [String: NSDictionary])->()){
let uid = Auth.auth().currentUser!.uid
let uids = Database.database().reference().child("users")
uids.observe(DataEventType.value, with: { (snapshot) in
let dict = snapshot.value as! [String:NSDictionary]
completion(dict)
})
}
func fetchURLS(completion: #escaping(_ dictionary: [String: String])->()){
let ref = Database.database().reference().child("posts")
ref.observe(DataEventType.value, with: { (snapshot2) in
let dict2 = snapshot2.value as! [String:String]
completionTwo(dict2)
})
}
Then, the parsing functions:
func parseUsers(dictionary: [String: NSDictionary]){
for (_,value) in dictionary {
if let uid = value["uid"] as? String{
self.following.append(uid)
}
}
fetchURLS { (urlDictionary) in
self.parseImageURLS(dictionary: urlDictionary)
}
}
func parseImageURLS(dictionary: [String: String]){
for(key, value) in dictionary{
for uid2 in self.following{
if (uid2 == key){
for (key2,value2) in value as! [String:String]{
//print(key2 + "this is key2")
if(key2 == "urlToImage"){
let urlimage = value2
//print(urlimage)
self.posts1.append(urlimage)
self.collectionview.reloadData()
print(self.posts1.count)
}
}
}
}
}
}
Then you add:
fetchUsers { (usersDictionary) in
self.parseUsers(dictionary: usersDictionary)
}
in viewDidLoad()
Hope this solves your problem. On a side note: I recommend using models and separating the network calls in a different file. Feel free to ask any questions.
I figured out how to do it after more searching.
I was incorrectly assuming that the CollectionView is loaded after the viewDidLoad() function is done. The helper classes for a CollectionView are called to a call of reloadData.
I observed that my reloadData call wasn't being called. In order to make this work, I add 2 lines of code to the viewDidLoad function:
collectionview.delegate = self
collectionview.dataSource = self
With this change, the images now load.
I am trying to use RealmSwift in order to save items to the phone storage in Swift 4. I have two different Views; one for the save functionality and another which will display all saved items into a TableView. I have a buildable form coded but i am throwing an error Thread 1: signal SIGABRT specifically on the line when i call realm.add. When i am in my view which is saving, i am using a IBAction with a button to initiate the save functionality. Can anyone help me with this issue? I think the issue is when i set the var of realm however i am unsure.
UPDATE:
I have changed my implementation to reflect the idea given in this thread about my original issue. After doing so, when the call to add the item to the realm is called i crash EXC_BAD_ACCESS (code=EXC_I386_GPFLT) inside the source code of the API. Specifically I crash at this function of the API
//CODE EXCERPT FROM REALMSWIFT API
// Property value from an instance of this object type
id value;
if ([obj isKindOfClass:_info.rlmObjectSchema.objectClass] &&
prop.swiftIvar) {
if (prop.array) {
return static_cast<RLMListBase *>(object_getIvar(obj,
prop.swiftIvar))._rlmArray;
}
else { // optional
value = static_cast<RLMOptionalBase *>(object_getIvar(obj,
prop.swiftIvar)).underlyingValue; //CRASH OCCURS HERE!!!!!!!!
}
}
else {
// Property value from some object that's KVC-compatible
value = RLMValidatedValueForProperty(obj, [obj
respondsToSelector:prop.getterSel] ? prop.getterName : prop.name,
_info.rlmObjectSchema.className);
}
return value ?: NSNull.null;
import UIKit
import RealmSwift
class DetailsViewController: UIViewController {
var titleOfBook: String?
var author: String?
#IBAction func SavetoFavorites(_ sender: Any) {
DispatchQueue.global().async { [weak self] in
guard let strongSelf = self else { return }
guard let realm = try? Realm() else {
return
}
let newItem = Favorites()
newItem.title = strongSelf.titleOfBook
newItem.author = strongSelf.author
try? realm.write {
realm.add(newItem) // Crashes on this line
}
}
}
import UIKit
import RealmSwift
final class Favorites: Object {
var title: String?
var author: String?
}
class FavoritesTableViewController: UITableViewController {
var items: Array<Favorites> = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier:
"cell")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 0
}
override func tableView(_ tableView: UITableView?,
numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt
indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell",
for: indexPath)
let item = items[indexPath.row]
cell.textLabel?.text = item.title
cell.detailTextLabel?.text = item.author
return cell
}
var selectedIndexPath: NSIndexPath = NSIndexPath()
override func tableView(_ tableView: UITableView, willSelectRowAt
indexPath: IndexPath) -> IndexPath? {
selectedIndexPath = indexPath as NSIndexPath
return indexPath
}
You have to wrap realm.add(newItem) inside a transaction:
try! realm.write {
realm.add(newItem)
}
Please note, that write transactions block the thread they are made on so if you're writing big portions of data you should do so on background thread (realm has to be instantiated on that thread too). You could do it like this:
#IBAction func saveToFavorites(_ sender: Any) {
DispatchQueue.global().async { [weak self] in
guard let strongSelf = self else { return }
guard let realm = try? Realm() else {
// avoid force unwrap, optionally report an error
return
}
let newItem = Favorites()
newItem.title = strongSelf.titleOfBook
newItem.author = strongSelf.author
try? realm.write {
realm.add(newItem)
}
}
}
Update: I haven't noticed that you have an issue with your model too – since Realm is written with Objective C you should mark your model properties with #objc dynamic modifiers:
final class Favorites: Object {
#objc dynamic var title: String?
#objc dynamic var author: String?
}
All changes to Realm managed objects (either creation, modification or deletion) need to happen inside write transactions.
do {
try realm.write {
realm.add(newItem)
}
} catch {
//handle error
print(error)
}
For more information, have a look at the writes section of the official docs.
Another problem you have in there is that in your Favorites class properties are missing #objc dynamic attributes. You can read about why you need that in realm docs.
Your code should look like this then:
final class Favorites: Object {
#objc dynamic var title: String?
#objc dynamic var author: String?
}
I am try to save and retrieve notes data with custom object called Sheet.
But I am having crashes when it runs. Is this the correct way to do it or is there any other ways to solve this?
The Sheet Class
class Sheet {
var title = ""
var content = ""
}
Here is the class for UITableViewController
class NotesListTableVC: UITableViewController {
var notes = [Sheet]()
override func viewDidLoad() {
super.viewDidLoad()
if let newNotes = UserDefaults.standard.object(forKey: "notes") as? [Sheet] {
//set the instance variable to the newNotes variable
notes = newNotes
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return notes.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "notesCELL", for: indexPath)
cell.textLabel!.text = notes[indexPath.row].title
return cell
}
// Add new note or opening existing note
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "editNote" {
var noteContentVC = segue.destination as! NoteContentVC
var selectedIndexPath = tableView.indexPathForSelectedRow
noteContentVC.note = notes[selectedIndexPath!.row]
}
else if segue.identifier == "newNote" {
var newEntry = Sheet()
notes.append(newEntry)
var noteContentVC = segue.destination as! NoteContentVC
noteContentVC.note = newEntry
}
saveNotesArray()
}
// Reload the table view
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tableView.reloadData()
}
// Deleting notes
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
notes.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
}
// Save the notes
func saveNotesArray() {
// Save the newly updated array
UserDefaults.standard.set(notes, forKey: "notes")
UserDefaults.standard.synchronize()
}
}
And where should I call the saveNotesArray function?
You are trying to save an array of custom objects to UserDefaults. Your custom object isn't a property list object You should use Codable to save non-property list object in UserDefaults like this.
Swift 4
Custom Class
class Sheet: Codable {
var title = ""
var content = ""
}
ViewController.swift
class ViewController: UIViewController {
var notes = [Sheet]()
override func viewDidLoad() {
super.viewDidLoad()
getSheets()
addSheets()
getSheets()
}
func getSheets()
{
if let storedObject: Data = UserDefaults.standard.data(forKey: "notes")
{
do
{
notes = try PropertyListDecoder().decode([Sheet].self, from: storedObject)
for note in notes
{
print(note.title)
print(note.content)
}
}
catch
{
print(error.localizedDescription)
}
}
}
func addSheets()
{
let sheet1 = Sheet()
sheet1.title = "title1"
sheet1.content = "content1"
let sheet2 = Sheet()
sheet2.title = "title1"
sheet2.content = "content1"
notes = [sheet1,sheet2]
do
{
UserDefaults.standard.set(try PropertyListEncoder().encode(notes), forKey: "notes")
UserDefaults.standard.synchronize()
}
catch
{
print(error.localizedDescription)
}
}
}
You give answer to the question that you ask.
App crash log.
[User Defaults] Attempt to set a non-property-list object ( "Sheet.Sheet" )
Official Apple info.
A default object must be a property list—that is, an instance of (or
for collections, a combination of instances of): NSData, NSString,
NSNumber, NSDate, NSArray, or NSDictionary.
If you want to store any other type of object, you should typically
archive it to create an instance of NSData. For more details, see
Preferences and Settings Programming Guide.
One of the possible solution:
class Sheet : NSObject, NSCoding{
var title:String?
var content:String?
func encode(with aCoder: NSCoder) {
aCoder.encodeObject(self.title, forKey: "title")
aCoder.encodeObject(self.content, forKey: "content")
}
required init?(coder aDecoder: NSCoder) {
self.title = aDecoder.decodeObject(forKey: "title") as? String
self.content = aDecoder.decodeObject(forKey: "content") as? String
}
}
Save
userDefaults.setValue(NSKeyedArchiver.archivedDataWithRootObject(sheets), forKey: "sheets")
Load
sheets = NSKeyedUnarchiver.unarchiveObjectWithData(userDefaults.objectForKey("sheets") as! NSData) as! [Sheet]
The code you posted tries to save an array of custom objects to NSUserDefaults. You can't do that. Implementing the NSCoding methods doesn't help. You can only store things like Array, Dictionary, String, Data, Number, and Date in UserDefaults.
You need to convert the object to Data (like you have in some of the code) and store that Data in UserDefaults. You can even store an Array of Data if you need to.
When you read back the array you need to unarchive the Data to get back your Sheet objects.
Change your Sheet object to :
class Sheet: NSObject, NSCoding {
var title: String
var content: String
init(title: String, content: String) {
self.title = title
self.content = content
}
required convenience init(coder aDecoder: NSCoder) {
let title = aDecoder.decodeObject(forKey: "title") as! String
let content = aDecoder.decodeObject(forKey: "content") as! String
self.init(title: title, content: content)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(title, forKey: "title")
aCoder.encode(content, forKey: "content")
}
}
into a function like :
func loadData() {
if let decoded = userDefaults.object(forKey: "notes") as? Data, let notes = NSKeyedUnarchiver.unarchiveObject(with: decoded) as? [Sheet] {
self.notes = notes
self.tableView.reloadData()
}
}
and then call :
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.loadData()
}
saveNotesArray can be called after new Notes added with :
func saveNotesArray() {
// Save the newly updated array
var userDefaults = UserDefaults.standard
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: notes)
userDefaults.set(encodedData, forKey: "notes")
userDefaults.synchronize()
}
I am developing an iOS app, I am trying to use a textfield and a button to submit an integer into an array so that I can store it using 'userdefaults'. So far I have got a different textfield adding the String entered into the array and storing it in the user defaults.
#IBAction func workBtn(_ sender: Any)
{
let kidnum: Int = Int(enteredScore.text!)!
kidName.append(enteredName.text!)
kidScore.append(kidnum)
enteredName.text?.removeAll()
enteredScore.text?.removeAll()
}
#IBAction func finishBtn(_ sender: Any)
{
let userDefaults = UserDefaults.standard
userDefaults.set(kidName, forKey: "Pupil Name")
userDefaults.set(kidScore, forKey: "Pupil Score")
userDefaults.synchronize()
}
workBtn is the add button I am using, I did also try to use a string as I don't actually need the integer for working out but just to display a score but I could not get it to save, I am passing it through it to be displayed on a table view controller cell. Thanks
I am using Xcode 9.1 Swift 4, Thanks.
Edit***
After running it I got an error in this function on the line marked stating "Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value"
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let userDefaults = UserDefaults.standard
var nameArray: [String] = userDefaults.object(forKey:"Pupil Name") as! [String]
//Error occurring on next line.
var scoreArray: [Int] = userDefaults.object(forKey: "Pupil Score") as! [Int]
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "\(nameArray[indexPath.item]) -Score: \(scoreArray[indexPath.item])"
userDefaults.synchronize()
return cell
}
The error you are receiving means you are accessing a nil value somewhere where you don't expect it. This is most likely by force-unwrapping some value which you do constantly in your code. Try avoiding the usage of !. In your code it might be any of lines such as let kidnum: Int = Int(enteredScore.text!)! but it might also be some field is missing connection for instance and is declared as #IBOutlet weak var enteredName: UITextField! which unfortunately is a default.
To debug this you might want to enable exception breakpoint: In navigator on your left side of Xcode on the top there is an icon for breakpoints (a bullet). Select it and then check on the bottom left and find a "+" button, press it and select "Exception breakpoint...". Then simply click away. Now your execution will stop whenever an exception like yours is hit. Run your project and find the line, object that is nil and should not be. Then fix your issue.
But in general try writing your code more like the following:
#IBOutlet private weak var scoreTextField: UITextField?
#IBOutlet private weak var nameTextField: UITextField?
private typealias NameScore = (name: String, score: Int)
private var dataArray: [NameScore] = [NameScore]()
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}
#IBAction private func workPressed() {
let scoreValue: Int? = {
guard let string = scoreTextField?.text, let integer = Int(string) else {
return nil
}
return integer
}()
guard let score = scoreValue else {
print("Could not parse integer value") // TODO: Show alert view
return
}
guard let name = nameTextField?.text else {
print("Name was not provided") // TODO: Show alert view
return
}
dataArray.append((name: name, score: score))
// Clear text fields
scoreTextField?.text = nil
nameTextField?.text = nil
}
#IBAction private func finishPressed() {
saveData()
}
private let pupilNameKey: String = "name"
private let pupilScoreKey: String = "score"
private let pupilDataKey: String = "pupil_data"
private func saveData() {
// Let's convert data into something that makes more sense for safety
let dataToSave: [[String: Any]] = dataArray.map { object in
return [pupilNameKey: object.name,
pupilScoreKey: object.score]
}
UserDefaults.standard.set(dataToSave, forKey: pupilDataKey)
UserDefaults.standard.synchronize()
}
private func loadData() {
guard let savedData = UserDefaults.standard.value(forKey: pupilDataKey) as? [[String: Any]] else {
// No saved data or incorrect format
return
}
dataArray = savedData.flatMap { descriptor in
guard let name = descriptor[pupilNameKey] as? String, let score = descriptor[pupilScoreKey] as? Int else {
return nil // Could not parse this object
}
return (name: name, score: score)
}
}
And table view:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let dataObject = dataArray[indexPath.row]
cell.textLabel?.text = "\(dataObject.name) -Score: \(dataObject.score)"
return cell
}
I hope the code speaks for itself on how to avoid force unwrapping and also how to pack your data.