I created an app which randomly displays different Strings from an array. To make sure that no array gets repeated I created a struct which only shows the Strings which haven´t already been displayed.
That works pretty well, but my problem is that as soon as I switch scenes the struct gets reset although I tried to save it to user defaults.
Does someone know where I made a mistake when I tried to apply UserDefaults?
Here is my struct containing the saving methods:
struct RandomItems: Codable
{
var items : [String]
var seen = 0
init(items:[String], seen: Int)
{
self.items = items
self.seen = seen
}
init(_ items:[String])
{ self.init(items: items, seen: 0) }
mutating func next() -> String
{
let index = Int(arc4random_uniform(UInt32(items.count - seen)))
let item = items.remove(at:index)
items.append(item)
seen = (seen + 1) % items.count
return item
}
func toPropertyList() -> [String: Any] {
return [
"items": items,
"seen": seen]
}
}
override func viewDidLoad() {
let defaults = UserDefaults.standard
if let quotes = defaults.codable(RandomItems.self, forKey: "quotes") as? RandomItems {
self.quotes = quotes
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector:Selector(("saveData:")), name: Notification.Name.UIApplicationWillTerminate, object:nil)
}
func storeQuotes() {
// Code to save struct
let defaults = UserDefaults.standard
if let quotes = quotes {
defaults.set(codable: quotes, forKey: "quotes")
}
}
override func viewDidAppear(_ animated: Bool) {
// Code to load the struct again after the view appears.
}
override func viewWillDisappear(_ animated: Bool) {
storeQuotes()
}
func saveData(notification: Notification) {
// Save your data here when app is closed
print("Saving data...")
storeQuotes()
}
}
extension UserDefaults {
func set<T: Encodable>(codable: T, forKey key: String) {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(codable)
let jsonString = String(data: data, encoding: .utf8)!
print("Saving \"\(key)\": \(jsonString)")
self.set(jsonString, forKey: key)
} catch {
print("Saving \"\(key)\" failed: \(error)")
}
}
func codable<T: Decodable>(_ codable: T.Type, forKey key: String) -> T? {
guard let jsonString = self.string(forKey: key) else { return nil }
guard let data = jsonString.data(using: .utf8) else { return nil }
let decoder = JSONDecoder()
print("Loading \"\(key)\": \(jsonString)")
return try? decoder.decode(codable, from: data)
}
You can check in your viewDidLoad if you already have a value stored in UserDefaults, and you can remove your code in viewDidAppear:
override func viewDidLoad() {
let defaults = UserDefaults.standard
if let quotes = defaults.codable(RandomItems.self, forKey: "quotes") as? RandomItems {
self.quotes = quotes
}
}
EDIT:
And if you want to store your quotes when your app moves to background:
Add the notification observer in viewWillAppear:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(saveData), name: Notification.Name.UIApplicationDidEnterBackground, object:nil)
}
func storeQuotes() {
// Code to save struct
let defaults = UserDefaults.standard
if let quotes = quotes {
defaults.set(codable: quotes, forKey: "quotes")
}
}
override func viewWillDisappear(_ animated: Bool) {
// Code to save struct before the view disappears.
storeQuotes()
}
func saveData() {
// Save your data here when app is closed
print("Saving data...")
storeQuotes()
}
Related
Hi i'm just getting started with RxSwift and decided to make simple Currency Exchange application. My app has two view's (allCurrenciesList and userFavouritesView). Basically all logic works, but only if i run networking func every single time one of view didAppear/didLoad. My point is two fetch it only once, and received many times, when necessary. Application fetch dictionary of currencies and in ViewModel pass it to BehaviorSubject, and when view being load/appear it just subscribe it, and use it in UITableView. Thanks
class ListViewModel {
let service: CurrencyService!
var curriencies = BehaviorRelay<[Currency]>(value: [])
var currienciesObservable: Observable<[Currency]> {
return curriencies.asObservable().share()
}
let disposeBag = DisposeBag()
init(service: CurrencyService) {
self.service = service
}
func fetchAllCurrencies() {
self.service.fetchAllSymbols { result in
switch result{
case .success(let currencies):
self.dictionaryIntoArray(currencies: currencies["symbols"] as! [String : Any])
case .failure:
print("error")
}
}
}
private func dictionaryIntoArray(currencies: [String: Any]) {
var currencyArray = [Currency]()
for (symbol, name) in currencies {
currencyArray.append(Currency(symbol: symbol, fullName: name as! String))
}
let sortedArray = currencyArray.sorted { $0.fullName < $1.fullName }
self.curriencies.accept(sortedArray)
}
allCurrenciesList
override func viewDidLoad() {
super.viewDidLoad()
setupView()
configureTableViewDataSource()
tableView.delegate = self
fetchData()
}
private func fetchData() {
viewModel.fetchAllCurrencies() // this func is necceserry everysingle time
viewModel.currienciesObservable.subscribe(onNext: { curriencies in
self.applySnapshot(curriencies: curriencies)
}).disposed(by: disposeBag)
}
userFavouritesView
override func viewDidLoad() {
super.viewDidLoad()
viewModel.fetchAllCurrencies() // this func is necceserry everysingle time
viewModel.currienciesObservable.subscribe(onNext: { allCurencies in
let selectedItems = UserDefaults.standard.array(forKey: "SelectedCells") as? [Int] ?? [Int]()
var currenciesArray: [Currency] = []
selectedItems.forEach { int in
self.pickerValues.append(allCurencies[int])
currenciesArray.append(allCurencies[int])
}
self.applySnapshot(curriencies: currenciesArray)
}).disposed(by: disposeBag)
}
The key here is to not use a Subject. They aren't recommended for regular use. Just define the currienciesObservable directly.
Something like this:
class ListViewModel {
let currienciesObservable: Observable<[Currency]>
init(service: CurrencyService) {
self.currienciesObservable = service.rx_fetchAllSymbols()
.map { currencies in
currencies["symbols"]?.map { Currency(symbol: $0.key, fullName: $0.value as! String) }
.sorted(by: { $0.fullName < $1.fullName }) ?? []
}
}
}
extension CurrencyService {
func rx_fetchAllSymbols() -> Observable<[String: [String: Any]]> {
Observable.create { observer in
self.fetchAllSymbols { result in
switch result {
case let .success(currencies):
observer.onNext(currencies)
observer.onCompleted()
case let .failure(error):
observer.onError(error)
}
}
return Disposables.create()
}
}
}
With the above, every time you subscribe to the currenciesObservable the fetch will be called.
As I understand, it's because your fetchAllSymbols function was not stored in the DisposeBag.
func fetchAllCurrencies() {
self.service.fetchAllSymbols { result in
switch result{
case .success(let currencies):
self.dictionaryIntoArray(currencies: currencies["symbols"] as! [String : Any])
case .failure:
print("error")
}
}.dispose(by: disposeBag)
}
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 made a test application following the example of Google with github, but with a few changes (less keys used in the firestore and less filters).
The problem is this, the app crashing when I added new keys in the firestore, but the app works with two keys previously added.
Crashes and shows error on fatalError("error"). I can not understand why with two keys the application works, but if i begin to use the third key (hall) then the app crashes.
What could be the problem?
It's my code:
class ViewControllerTwo: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
private var sweets: [Sweet] = []
private var document: [DocumentSnapshot] = []
fileprivate var query: Query? {
didSet {
if let listener = listener {
listener.remove()
}
}
}
private var listener: FIRListenerRegistration?
fileprivate func observeQuery() {
guard let query = query else { return }
stopObserving()
listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
let models = snapshot.documents.map { (document) -> Sweet in
if let model = Sweet(dictionary: document.data()) {
return model
} else {
fatalError("error")
}
}
self.sweets = models
self.document = snapshot.documents
self.tableView.reloadData()
}
}
#IBAction func filterButton(_ sender: Any) {
present(filters.navigationController, animated: true, completion: nil)
}
lazy private var filters: (navigationController: UINavigationController, filtersController: FilterViewController) = {
return FilterViewController.fromStoryboard(delegate: self)
}()
fileprivate func stopObserving() {
listener?.remove()
}
fileprivate func baseQuery() -> Query {
return Firestore.firestore().collection("sweets").limit(to: 50)
}
override func viewDidLoad() {
super.viewDidLoad()
query = baseQuery()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
observeQuery()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
stopObserving()
}
deinit {
listener?.remove()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sweets.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ViewControllerCell
let sweet = sweets[indexPath.row]
cell.studioNameLabel.text = sweet.name
cell.studioAddressLabel.text = sweet.content
cell.hallNameLabel.text = sweet.hall
return cell
}
}
extension ViewControllerTwo: FiltersViewControllerDelegate {
func query(withCategory title: String?) -> Query {
var filtered = baseQuery()
if let title = title, !title.isEmpty {
filtered = filtered.whereField("title", isEqualTo: title)
}
return filtered
}
func controller(_ controller: FilterViewController, didSelectCategory title: String?) {
let filtered = query(withCategory: title)
self.query = filtered
observeQuery()
}
}
class ViewControllerCell: UITableViewCell {
#IBOutlet weak var studioNameLabel: UILabel!
#IBOutlet weak var studioAddressLabel: UILabel!
#IBOutlet weak var hallNameLabel: UILabel!
}
And my struct:
protocol DocumentSerializable {
init?(dictionary:[String:Any])
}
struct Sweet {
var name:String
var content:String
var hall:String
var dictionary:[String:Any] {
return [
"name": name,
"content" : content,
"hall" : hall
]
}
}
extension Sweet : DocumentSerializable {
static let title = [
"one",
"two",
"three",
"four"
]
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let content = dictionary["content"] as? String,
let hall = dictionary["hall"] as? String else { return nil }
self.init(name: name, content: content, hall: hall)
}
}
My project in google drive
google drive
google service info.plist
You just need to reinstall app once you add any new key to you existing structure.
So you should decide before structure implementation that what keys you will need. Or you can reinstall app if you add new key in future.
I just started to learn firestore, i created simple app like a example from googleFirestore (in github).
When i change or create new data in firestore i get an error when my app is start in this line:
fatalError("Error")
I so understand the app is not like creating new data, how can I avoid this error and create data in real time?
My code:
private var hall: [Hall] = []
private var documents: [DocumentSnapshot] = []
fileprivate var query: Query? {
didSet {
if let listener = listener {
listener.remove()
observeQuery()
}
}
}
private var listener: ListenerRegistration?
fileprivate func observeQuery() {
guard let query = query else { return }
stopObserving()
listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
let models = snapshot.documents.map { (document) -> Hall in
if let model = Hall(dictionary: document.data()) {
return model
} else {
fatalError("Error")
}
}
self.hall = models
self.documents = snapshot.documents
self.tableView.reloadData()
}
}
func stopObserving() {
listener?.remove()
}
func baseQuery() -> Query {
return Firestore.firestore().collection("searchStudios").limit(to: 50)
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = UIView()
query = baseQuery()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
self.setNeedsStatusBarAppearanceUpdate()
observeQuery()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
stopObserving()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
set {}
get {
return .lightContent
}
}
deinit {
listener?.remove()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ResultTableViewCell
cell.populate(hall: hall[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return hall.count
}
I can delete data, but not can add new data.
UPDATE:
struct Hall:
import Foundation
protocol DocumentSerializable {
init?(dictionary: [String: Any])
}
struct Hall {
var description: String
var image: String
var meters: Double
var name: String
var price: Int
var studioHallAddress: String
var studioHallName: String
var studioHallLogo: String
var dictionary: [String: Any] {
return [
"description": description,
"image": image,
"meters": meters,
"name": name,
"price": price,
"studioHallAddrees": studioHallAddress,
"studioHallName": studioHallName,
"studioHallLogo": studioHallLogo
]
}
}
extension Hall: DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let description = dictionary["description"] as? String,
let image = dictionary["image"] as? String,
let meters = dictionary["meters"] as? Double,
let name = dictionary["name"] as? String,
let price = dictionary["price"] as? Int,
let studioHallAddress = dictionary["studioHallAddress"] as? String,
let studioHallName = dictionary["studioHallName"] as? String,
let studioHallLogo = dictionary["studioHallLogo"] as? String else { return nil }
self.init(description: description,
image: image,
meters: meters,
name: name,
price: price,
studioHallAddress: studioHallAddress,
studioHallName: studioHallName,
studioHallLogo: studioHallLogo)
}
}
I was trying to get swfityjson and almo to work in Xcode 8
like in the post in this page:
Showing JSON data on TableView using SwiftyJSON and Alamofire
But I can not get the data from the self.
This helped me a little, but I am stuck with the count. Someone please help me with this, beacuse I am new to swift and have spent the day trying to understand how to get the count of rows and the data out of the almo function.
import UIKit
import Alamofire
import SwiftyJSON
class TableViewController: UITableViewController {
var names = [String]()
var rowCount = [Int]()
var tableTitle = [String]()
typealias JSONStandard = [String : AnyObject]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func callAlamo(){
let url = "http://xxxxxx/xxxx/xxxxxx.php"
//let id = UserDefaults.standard.string(forKey: "UserDefaults")
Alamofire.request(url).responseJSON{(respones)->Void in
if let value = respones.result.value{
let json = JSON(value)
let rows = json["items"].arrayValue
for anItem in rows {
let title: String? = anItem["SupplierName"].stringValue
self.tableTitle.append(title!)
//print(self.tableTitle.count)
}
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidAppear(_ animated: Bool) {
// let isUserLoggedin = UserDefaults.standard.boll(forKey: "userLogIn")
let ststus = UserDefaults.standard.string(forKey: "userLogIn")
if ststus == "false" {
self.performSegue(withIdentifier: "loginView", sender: self)
}else{
callAlamo()
}
}
#IBAction func logout(_ sender: AnyObject) {
UserDefaults.standard.set(false, forKey: "userLogIn")
UserDefaults.standard.synchronize()
self.performSegue(withIdentifier: "loginView", sender: self)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print (tableTitle)
return self.tableTitle.count
}
}
I am unable to get the tableTitle.count. Thanks!
UITableView Delegate methods are called before viewDidAppear. So you need to reload the UITableView after got the response from server
DispatchQueue.main.async {
self.tableView.reloadData()
}
After the for loop.
import UIKit
import Alamofire
import SwiftyJSON
class TableViewController: UITableViewController {
var names = [String]()
var rowCount = [Int]()
var tableTitle = [String]()
typealias JSONStandard = [String : AnyObject]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func callAlamo(){
let url = "http://pos1.dermedia.co.il/iphone/getLasttransactions.php?cardid="
//let id = UserDefaults.standard.string(forKey: "UserDefaults")
Alamofire.request(url).responseJSON{(respones)->Void in
if let value = respones.result.value{
let json = JSON(value)
// print (json["items"].arrayValue)
let rows = json["items"].arrayValue
// print (rows)
for anItem in rows {
let title: String? = anItem["SupplierName"].stringValue
self.tableTitle.append(title!)
print(self.tableTitle.count)
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidAppear(_ animated: Bool) {
// let isUserLoggedin = UserDefaults.standard.boll(forKey: "userLogIn")
let ststus = UserDefaults.standard.string(forKey: "userLogIn")
if ststus == "false" {
self.performSegue(withIdentifier: "loginView", sender: self)
}else{
callAlamo()
print ("gggggg")
print(self.tableTitle.count)
}
}
#IBAction func logout(_ sender: AnyObject) {
UserDefaults.standard.set(false, forKey: "userLogIn")
UserDefaults.standard.synchronize()
self.performSegue(withIdentifier: "loginView", sender: self)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print (self.tableTitle)
print("hhh")
return tableTitle.count
}
}