Swift Dictionary of Arrays, Data Structure



我正在编写一个简单的程序,允许我在iOS应用程序中监控一些销售。 基本上,我需要一个允许客户(作为字符串)的数据结构,对于每个客户,我需要能够保存一个或多个"销售",每个"销售"由销售日期和价格组成。我将其实现为 Swift 字典,该字典在单例 Swift 类中读取并保存到 plist 文件中,因为我需要从多个视图控制器读取和写入相同的数据结构。下面是数据类的代码:

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)
    }
}

正如你所看到的,我使用了一个字典,其中客户名称是键,销售是AnyObject的数组。我怀疑这是否是实现此方法的最佳和最优雅的方式,所以也许有人可以帮助我解决以下问题:

    实现
  1. 这一点的更好方法是什么?将 sales 实现为结构,然后使用此结构的数组是否有意义?

  2. 创建新客户时,我需要使用占位符

    allData[customerName] = [[NSDate(), 0]]
    

因为没有值的字典键不会被保存。有没有更好的方法可以做到这一点?

  1. 为什么allData.keys.sort()生成字符串数组,而allData.keys不会?这对我来说没有意义,因为排序函数似乎不应该更改类型。

  2. 在我的(表)视图中,每个单元格显示一个客户以及每个客户的总销售额。我想按总销售额对这个表进行排序。当前代码(请参阅 3)按字母顺序对客户进行排序。实现此目的的最佳方法是什么:在数据结构本身或视图控制器中按总销售额对客户进行排序?有人可以提供代码如何最优雅地实现这种排序,也许使用排序函数和闭包?

看看你的代码的编辑,看看它是否适合你的目的:

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
    }
  }
}

用法:

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)])

要回答 3,您应该注意 allData.keys 的返回值。这是一个懒惰的集合,在你要求它之前不会拉出一个值。调用sort会拉出所有值,因为没有它们就无法排序。因此,它返回一个常规的、非懒惰的Array。你可以通过执行以下操作来获取一个非惰性数组而不使用sortArray(allData.keys)

您在评论中的其他问题:

1:希望修复,我放弃了NSCoding,因为它依赖于NSObjectNSData。这意味着我们不能使用具有非常理想的值语义的struct,以及其他问题。

2:通过扩展添加功能是个好主意。这允许您保持代码分隔,例如,我在extension中添加了对protocol PropertyListReadable的合规性,然后在那里也添加了必要的方法。

3:在此代码中,allDatainit?(propertyListRepresentation:)中初始化。在那里读取NSDictionary,并将每个客户及其销售解析为正确的数据结构。由于我将Saleclass更改为struct(多亏了新的protocol PropertyListReadable),我们可以声明它所包含的变量,并且init方法会自动为我们生成init(date:NSDate, price:Int)

Sale 这样的structDictionary更好,因为类型检查、自动完成、更好地注释struct和自动生成文档的能力、验证值的可能性、为附加功能添加协议等等。当任何旧Dictionary被埋在代码中时,很难确切地知道它代表什么,并且有一个具体的structenumclass让你的生活变得如此轻松。

Dictionary非常适合将键映射到值,但是当您想将多个相互依赖的值放在一起时,您希望查看enumstructclass

最新更新