Reputation: 815
I am writing a simple program that allows me to monitor some sales in an iOS app. Basically, I need a data structure that allows for customers (as a string), and for each customer I need to be able to save one or multiple 'sales', each consisting of a date of sale and a price. I implemented this as a Swift dictionary which reads from and saves to a plist file, in a singleton Swift class because I need to read and write to the same data structure from multiple view controllers. Here is the code for the data class:
import Foundation
import UIKit
class DataSingleton {
static let sharedDataSingleton = DataSingleton()
private(set) var allData = [String: [AnyObject]]()
private(set) var customers: [String] = []
init() {
let fileURL = self.dataFileURL()
if (NSFileManager.defaultManager().fileExistsAtPath(fileURL.path!)) {
allData = NSDictionary(contentsOfURL: fileURL) as! [String : [AnyObject]]
customers = allData.keys.sort()
}
}
func dataFileURL() -> NSURL {
let url = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return url.first!.URLByAppendingPathComponent("data.plist")
}
func addCustomer(customerName: String) {
if !customers.contains(customerName) {
customers.append(customerName)
allData[customerName] = [[NSDate(), 0]]
saveData()
}
}
func addSale(customerName: String, date: NSDate, price: Int) {
allData[customerName]?.append([date, price])
saveData()
}
func saveData() {
let fileURL = self.dataFileURL()
let customerData = allData as NSDictionary
customerData.writeToURL(fileURL, atomically: true)
}
}
As you can see my I used a dictionary where the customer name is the key and the sales are an Array of AnyObject. I have doubts whether this is the optimal and most elegant way of implementing this, so maybe someone can help me with the following questions:
What would be a better way to implement this? Would it make sense to implement sales as a struct and then use arrays of this struct?
When creating a new customer, I need to use a placeholder
allData[customerName] = [[NSDate(), 0]]
because a dictionary key without values won't be saved. Is there a better way to do this?
Why does allData.keys.sort()
produce an array of strings, but allData.keys
does not? This does not make sense to me since it seems that a sort function shouldn't change a type.
In of of my (table) views, each cell shows a customer with the total sales per customer. I want to sort this table by total sum of sales. The current code (see 3.) sorts the customers alphabetically. What would be the best way to implement this: Sort the customers by total sales in the data structure itself or in the view controller? And could someone provide code how this sorting would most elegantly be achieved, perhaps with the sort function and a closure?
Upvotes: 0
Views: 807
Reputation:
Take a look at this editing of your code and see if it works for your purposes:
import Foundation
/// Allows any object to be converted into and from a NSDictionary
///
/// With thanks to [SonoPlot](https://github.com/SonoPlot/PropertyListSwiftPlayground)
protocol PropertyListReadable {
/// Converts to a NSDictionary
func propertyListRepresentation() -> NSDictionary
/// Initializes from a NSDictionary
init?(propertyListRepresentation:NSDictionary?)
}
/// Converts a plist array to a PropertyListReadable object
func extractValuesFromPropertyListArray<T:PropertyListReadable>(propertyListArray:[AnyObject]?) -> [T] {
guard let encodedArray = propertyListArray else {return []}
return encodedArray.map{$0 as? NSDictionary}.flatMap{T(propertyListRepresentation:$0)}
}
/// Saves a PropertyListReadable object to a URL
func saveObject(object:PropertyListReadable, URL:NSURL) {
if let path = URL.path {
let encoded = object.propertyListRepresentation()
encoded.writeToFile(path, atomically: true)
}
}
/// Holds the information for a sale
struct Sale {
let date:NSDate
let price:Int
}
/// Allows a Sale to be converted to and from an NSDictionary
extension Sale: PropertyListReadable {
/// Convert class to an NSDictionary
func propertyListRepresentation() -> NSDictionary {
let representation:[String:AnyObject] = ["date":self.date, "price":self.price]
return representation
}
/// Initialize class from an NSDictionary
init?(propertyListRepresentation:NSDictionary?) {
guard let values = propertyListRepresentation,
date = values["date"] as? NSDate,
price = values["price"] as? Int else { return nil }
self.init(date:date, price:price)
}
}
/// Singleton that holds all the sale Data
final class DataSingleton {
/// Class variable that returns the singleton
static let sharedDataSingleton = DataSingleton(dataFileURL: dataFileURL)
/// Computed property to get the URL for the data file
static var dataFileURL:NSURL? {
get {
let manager = NSFileManager.defaultManager()
guard let URL = manager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first?.URLByAppendingPathComponent("data.plist"),
path = URL.path else { return nil }
let data = NSKeyedArchiver.archivedDataWithRootObject([String:AnyObject]())
guard manager.fileExistsAtPath(path) &&
manager.createFileAtPath(path, contents: data, attributes: nil) else { return nil }
return URL
}
}
/// The dictionary holding all the sale data
private(set) var allData:[String: [Sale]]
/// Computed property to return all the customers
var customers:[String] { get { return Array(allData.keys) }}
/// Designated initializer
///
/// Made private to disallow additional copies of the singleton
private init() { allData = [:] }
/// Adds a customer to the data dictionary
func addCustomer(customerName: String) {
if allData[customerName] == nil {
allData[customerName] = []
saveData()
}
}
/// Adds a sale to the data dictionary, creating a new customer if neccesary
func addSale(customerName: String, date: NSDate, price: Int) {
addCustomer(customerName)
allData[customerName]?.append(Sale(date: date, price: price))
saveData()
}
// Saves the singleton to the plist file
private func saveData() {
if let fileURL = DataSingleton.dataFileURL {
saveObject(self, URL: fileURL)
}
}
}
/// Allows a DataSingleton to be converted to and from an NSDictionary
extension DataSingleton: PropertyListReadable {
/// Convert class to an NSDictionary
func propertyListRepresentation() -> NSDictionary {
return allData.reduce([String:[AnyObject]]()) {
var retval = $0
retval[$1.0] = $1.1.map {$0.propertyListRepresentation()}
return retval
}
}
/// Initialize class from a plist file
///
/// Made private to disallow additional copies of the singleton
private convenience init?(dataFileURL: NSURL?) {
guard let fileURL = dataFileURL,
path = fileURL.path where (NSFileManager.defaultManager().fileExistsAtPath(path)),
let data = NSDictionary(contentsOfFile: path) else { return nil }
self.init(propertyListRepresentation: data)
}
/// Initialize class from an NSDictionary
convenience init?(propertyListRepresentation:NSDictionary?) {
self.init() // satisfies calling the designated init from a convenience init
guard let values = propertyListRepresentation else {return nil}
allData = values.reduce([:]) {
var retvalue = $0
guard let key = $1.key as? String,
value = $1.value as? [AnyObject] else { return retvalue }
retvalue[key] = extractValuesFromPropertyListArray(value)
return retvalue
}
}
}
Usage:
let data = DataSingleton.sharedDataSingleton
data?.addSale("June", date: NSDate(), price: 20)
data?.addSale("June", date: NSDate(), price: 30)
let data2 = DataSingleton.sharedDataSingleton
print(data2?.allData["June"])
// => Optional([Sale(date: 2016-03-10 04:31:49 +0000, price: 20), Sale(date: 2016-03-10 04:31:49 +0000, price: 30)])
To answer 3, you should note the return value of allData.keys
. It's a lazy collection, one which doesn't pull out a value until you ask for it. Calling sort
on it pulls out all the values since you can't sort without them. Because of that it returns a regular, non-lazy Array
. You can get an non-lazy array without using sort
by doing: Array(allData.keys)
Your additional questions in the comments:
1: Hopefully fixed, I switched away from NSCoding
because of its dependancies on NSObject
and NSData
. That meant we couldn't use a struct
with its very desirable value semantics, among other issues.
2: It's a good idea to add functionality through extensions. This allows you to keep code compartmentalized, for example I add compliance to the protocol
PropertyListReadable
in an extension
and then add the necessary methods there too.
3: In this code allData
is initialized in init?(propertyListRepresentation:)
. In there an NSDictionary
is read and each customer and their sales is parsed into the proper data structures. Since I changed Sale
from a class
to a struct
(thanks to the new protocol
PropertyListReadable
) we can just declare the variables it holds and the init
method is auto-generated for us as init(date:NSDate, price:Int)
A struct
like Sale
is better than a Dictionary
because of type checking, auto-completion, the ability to better comment the struct
and automatically produce documentation, the possibility for validation of the values, adding protocols for additional functionality, and so on. It's difficult to know exactly what any old Dictionary
represents when it's buried in code and having a concrete struct
, enum
, or class
makes your life so much easier.
A Dictionary
is great for mapping keys to values but when you want to hold multiple co-dependant values together you want to look toward an enum
, struct
or class
.
Upvotes: 1