Swift – macOS `batteryLevel` property - swift

In iOS there's a simple approach to get a level of iPhone battery:
UIDevice.current.batteryLevel
What's a macOS batteryLevel equivalent for MacBooks?

Getting the battery level in macOS is not as easy as in iOS, but it is still pretty easy. You have to use the IOKit framework for this.
You can use this code to read almost all of the important information from the internal battery.
InternalBattery.swift
import Foundation
import IOKit.ps
public class InternalBattery {
public var name: String?
public var timeToFull: Int?
public var timeToEmpty: Int?
public var manufacturer: String?
public var manufactureDate: Date?
public var currentCapacity: Int?
public var maxCapacity: Int?
public var designCapacity: Int?
public var cycleCount: Int?
public var designCycleCount: Int?
public var acPowered: Bool?
public var isCharging: Bool?
public var isCharged: Bool?
public var amperage: Int?
public var voltage: Double?
public var watts: Double?
public var temperature: Double?
public var charge: Double? {
get {
if let current = self.currentCapacity,
let max = self.maxCapacity {
return (Double(current) / Double(max)) * 100.0
}
return nil
}
}
public var health: Double? {
get {
if let design = self.designCapacity,
let current = self.maxCapacity {
return (Double(current) / Double(design)) * 100.0
}
return nil
}
}
public var timeLeft: String {
get {
if let isCharging = self.isCharging {
if let minutes = isCharging ? self.timeToFull : self.timeToEmpty {
if minutes <= 0 {
return "-"
}
return String(format: "%.2d:%.2d", minutes / 60, minutes % 60)
}
}
return "-"
}
}
public var timeRemaining: Int? {
get {
if let isCharging = self.isCharging {
return isCharging ? self.timeToFull : self.timeToEmpty
}
return nil
}
}
}
InternalFinder.swift:
import Foundation
import IOKit.ps
public class InternalFinder {
private var serviceInternal: io_connect_t = 0 // io_object_t
private var internalChecked: Bool = false
private var hasInternalBattery: Bool = false
public init() { }
public var batteryPresent: Bool {
get {
if !self.internalChecked {
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
self.hasInternalBattery = sources.count > 0
self.internalChecked = true
}
return self.hasInternalBattery
}
}
fileprivate func open() {
self.serviceInternal = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleSmartBattery"))
}
fileprivate func close() {
IOServiceClose(self.serviceInternal)
IOObjectRelease(self.serviceInternal)
self.serviceInternal = 0
}
public func getInternalBattery() -> InternalBattery? {
self.open()
if self.serviceInternal == 0 {
return nil
}
let battery = self.getBatteryData()
self.close()
return battery
}
fileprivate func getBatteryData() -> InternalBattery {
let battery = InternalBattery()
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
for ps in sources {
// Fetch the information for a given power source out of our snapshot
let info = IOPSGetPowerSourceDescription(snapshot, ps).takeUnretainedValue() as! Dictionary<String, Any>
// Pull out the name and capacity
battery.name = info[kIOPSNameKey] as? String
battery.timeToEmpty = info[kIOPSTimeToEmptyKey] as? Int
battery.timeToFull = info[kIOPSTimeToFullChargeKey] as? Int
}
// Capacities
battery.currentCapacity = self.getIntValue("CurrentCapacity" as CFString)
battery.maxCapacity = self.getIntValue("MaxCapacity" as CFString)
battery.designCapacity = self.getIntValue("DesignCapacity" as CFString)
// Battery Cycles
battery.cycleCount = self.getIntValue("CycleCount" as CFString)
battery.designCycleCount = self.getIntValue("DesignCycleCount9C" as CFString)
// Plug
battery.acPowered = self.getBoolValue("ExternalConnected" as CFString)
battery.isCharging = self.getBoolValue("IsCharging" as CFString)
battery.isCharged = self.getBoolValue("FullyCharged" as CFString)
// Power
battery.amperage = self.getIntValue("Amperage" as CFString)
battery.voltage = self.getVoltage()
// Various
battery.temperature = self.getTemperature()
// Manufaction
battery.manufacturer = self.getStringValue("Manufacturer" as CFString)
battery.manufactureDate = self.getManufactureDate()
if let amperage = battery.amperage,
let volts = battery.voltage, let isCharging = battery.isCharging {
let factor: CGFloat = isCharging ? 1 : -1
let watts: CGFloat = (CGFloat(amperage) * CGFloat(volts)) / 1000.0 * factor
battery.watts = Double(watts)
}
return battery
}
fileprivate func getIntValue(_ identifier: CFString) -> Int? {
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, identifier, kCFAllocatorDefault, 0) {
return value.takeRetainedValue() as? Int
}
return nil
}
fileprivate func getStringValue(_ identifier: CFString) -> String? {
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, identifier, kCFAllocatorDefault, 0) {
return value.takeRetainedValue() as? String
}
return nil
}
fileprivate func getBoolValue(_ forIdentifier: CFString) -> Bool? {
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, forIdentifier, kCFAllocatorDefault, 0) {
return value.takeRetainedValue() as? Bool
}
return nil
}
fileprivate func getTemperature() -> Double? {
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, "Temperature" as CFString, kCFAllocatorDefault, 0) {
return value.takeRetainedValue() as! Double / 100.0
}
return nil
}
fileprivate func getDoubleValue(_ identifier: CFString) -> Double? {
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, identifier, kCFAllocatorDefault, 0) {
return value.takeRetainedValue() as? Double
}
return nil
}
fileprivate func getVoltage() -> Double? {
if let value = getDoubleValue("Voltage" as CFString) {
return value / 1000.0
}
return nil
}
fileprivate func getManufactureDate() -> Date? {
if let value = IORegistryEntryCreateCFProperty(self.serviceInternal, "ManufactureDate" as CFString, kCFAllocatorDefault, 0) {
let date = value.takeRetainedValue() as! Int
let day = date & 31
let month = (date >> 5) & 15
let year = ((date >> 9) & 127) + 1980
var components = DateComponents()
components.calendar = Calendar.current
components.day = day
components.month = month
components.year = year
return components.date
}
return nil
}
}
Usage:
let internalFinder = InternalFinder()
if let internalBattery = internalFinder.getInternalBattery() {
// Internal battery found, access properties here.
}

Related

NSKeyedUnarchiver.unarchiveTopLevelObjectWithData return nil

I want to save an array of objects into UserDefaults and load it back.
When trying to unarchive the data it always returns nil.. any idea?
This is my object:
class DbInsideLevel: NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool {
return true
}
let idNum: Int!
var topicId: Int = 0
var tryCount: Int = 0
var score: Int = 0
var isOpen: Bool = false
var lastPlayedDate: Date?
init(idNum: Int, topicId: Int, tryCount: Int = 0, score: Int = 0, open: Bool, lastPlayedDate: Date?) {
self.idNum = idNum
self.topicId = topicId
self.tryCount = tryCount
self.score = score
self.isOpen = open
self.lastPlayedDate = lastPlayedDate
}
convenience required init?(coder: NSCoder) {
guard
let idNum = coder.decodeObject(forKey: "idNum") as? Int,
let topicId = coder.decodeObject(forKey: "topicId") as? Int,
let tryCount = coder.decodeObject(forKey: "tryCount") as? Int,
let score = coder.decodeObject(forKey: "score") as? Int,
let open = coder.decodeObject(forKey: "isOpen") as? Bool,
let lastPlayed = coder.decodeObject(forKey: "lastPlayedDate") as? Date
else {
return nil
}
self.init(idNum: idNum, topicId: topicId, tryCount: tryCount, score: score, open: open, lastPlayedDate: lastPlayed)
}
func encode(with coder: NSCoder) {
coder.encode(idNum, forKey: "idNum")
coder.encode(topicId, forKey: "topicId")
coder.encode(tryCount, forKey: "tryCount")
coder.encode(score, forKey: "score")
coder.encode(isOpen, forKey: "isOpen")
coder.encode(lastPlayedDate, forKey: "lastPlayedDate")
}
func update(score: Int) {
// Update score if necessary
if score > self.score {
self.score = score
}
// Increment try count
self.tryCount = self.tryCount + 1
}
}
Archiving the data:
func archiveData(with object: Any, to key: String) -> Data? {
do {
guard
let data = try? NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: true) else {
return nil
}
return data
}
}
Unarchiving the data:
func unarchiveData(data: Data) -> Any? {
do {
let unarchivedData = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [DbInsideLevel]
return unarchivedData
} catch {
return nil
}
}
Saving to UserDefaults:
class func americanSaveContext(data: [DbInsideLevel]) {
if data.count != 0 {
if let archived = MainDb.sharedInstance.archiveData(with: data, to: AppConstants.Keys.UserDefaults.American) {
MainDb.sharedInstance.dataStore(data: archived)
}
}
}
Loading from UserDefaults:
class func americanDataRetrive(topicId: Int) -> [DbInsideLevel]? {
if let data = MainDb.sharedInstance.dataRetrive() {
let unarchived = MainDb.sharedInstance.unarchiveData(data: data) as! [DbInsideLevel]
return unarchived
}
return nil
}
UserDefaults helpers for saving / loading:
extension MainDb {
// MARK: - UserDefaults helpers
func dataRetrive() -> Data? {
let defaults = UserDefaults.standard
return defaults.value(forKey: AppConstants.Keys.UserDefaults.American) as? Data
}
func dataStore(data: Data) {
let defaults = UserDefaults.standard
defaults.setValue(data, forKey: AppConstants.Keys.UserDefaults.American)
}
func dataReset() {
let defaults = UserDefaults.standard
defaults.removeObject(forKey: AppConstants.Keys.UserDefaults.American)
}
}
Your issue there is that you are using the wrong method to decode your integers and your boolean. I would also implement the required decoder instead of a convenience initializer.
required init(coder decoder: NSCoder) {
self.idNum = decoder.decodeInteger(forKey: "idNum")
self.topicId = decoder.decodeInteger(forKey: "topicId")
self.tryCount = decoder.decodeInteger(forKey: "tryCount")
self.score = decoder.decodeInteger(forKey: "score")
self.isOpen = decoder.decodeBool(forKey: "isOpen")
self.lastPlayedDate = decoder.decodeObject(forKey: "lastPlayedDate") as? Date
}
Note that you can also use Swift 4 or later Codable protocol to encode and decode your custom classes/structures and save them as data inside UserDefaults or as a plist file inside your application support folder. Last but not least don't use setValue and value(forKey:) to save and retrieve your data, user defaults has a specific method for retrieving data called data(forKey:) and set(_ value: Any) to persist your data:
extension UserDefaults {
func decode<T: Decodable>(_ type: T.Type, forKey defaultName: String) throws -> T {
try JSONDecoder().decode(T.self, from: data(forKey: defaultName) ?? .init())
}
func encode<T: Encodable>(_ value: T, forKey defaultName: String) throws {
try set(JSONEncoder().encode(value), forKey: defaultName)
}
}
class DbInsideLevel: Codable {
let idNum: Int!
var topicId: Int = 0
var tryCount: Int = 0
var score: Int = 0
var isOpen: Bool = false
var lastPlayedDate: Date?
init(idNum: Int, topicId: Int, tryCount: Int = 0, score: Int = 0, open: Bool, lastPlayedDate: Date?) {
self.idNum = idNum
self.topicId = topicId
self.tryCount = tryCount
self.score = score
self.isOpen = open
self.lastPlayedDate = lastPlayedDate
}
func update(score: Int) {
if score > self.score {
self.score = score
}
self.tryCount = self.tryCount + 1
}
}
Playground testing
let insideLevel = DbInsideLevel(idNum: 1, topicId: 2, tryCount: 3, score: 4, open: true, lastPlayedDate: Date())
do {
try UserDefaults.standard.encode(insideLevel, forKey: "insideLevel")
let decodedLevel = try UserDefaults.standard.decode(DbInsideLevel.self, forKey: "insideLevel")
print("decodedLevel idNum", decodedLevel.idNum) // decodedLevel idNum Optional(1)
} catch {
print(error)
}
edit/update:
It does work for arrays of Codable types as well:
let insideLevel = DbInsideLevel(idNum: 1, topicId: 2, tryCount: 3, score: 4, open: true, lastPlayedDate: Date())
do {
try UserDefaults.standard.encode([insideLevel], forKey: "insideLevels")
let decodedLevel = try UserDefaults.standard.decode([DbInsideLevel].self, forKey: "insideLevels")
print("decodedLevel idNum", decodedLevel.first?.idNum) // decodedLevel idNum Optional(1)
} catch {
print(error)
}

Realm accessed from incorrect thread occasional

I have this function
class func addCVals(_ criteres: [[AnyHashable: Any]], _ type: String) {
DispatchQueue.global(qos: .background).async {
autoreleasepool {
if criteres.count > 0 {
if let realm = DBTools.getRealm() {
do {
try realm.transaction {
let oldValues = CriteresVal.objects(in: realm, where: "type = '\(type)'")
if oldValues.count > 0 {
realm.deleteObjects(oldValues)
}
for critere in criteres {
let cval = CriteresVal(critere, type)
if let c = cval {
realm.addOrUpdate(c)
}
}
}
} catch {
DebugTools.record(error: error)
}
realm.invalidate()
}
}
}
}
}
The request that get oldValues occasionally cause an error
Realm accessed from incorrect thread
I don't understand why as I get a new Realm before with this lines:
if let realm = DBTools.getRealm()
My function getRealm:
class func getRealm() -> RLMRealm? {
if !AppPreference.lastAccount.elementsEqual("") {
let config = RLMRealmConfiguration.default()
do {
return try RLMRealm(configuration: config)
} catch {
DebugTools.record(error: error)
DispatchQueue.main.async {
Notifier.showNotification("", NSLocalizedString("UNKNOWN_ERROR_DB", comment: ""), .warning)
}
}
}
return nil
}
CriteresVal is an RLMObject that is composed of this:
#objcMembers
public class CriteresVal: RLMObject {
dynamic var cvalId: String?
dynamic var type: String?
dynamic var text: String?
dynamic var compositeKey: String?
override public class func primaryKey() -> String {
return "compositeKey"
}
private func updatePrimaryKey() {
self.compositeKey = "\(self.cvalId ?? "")/\(self.type ?? "")"
}
required init(_ cvalue: [AnyHashable: Any]?, _ type: String) {
super.init()
if let values = cvalue {
if let cvalId = values["id"] as? String {
self.cvalId = cvalId
} else if let cvalId = values["id"] as? Int {
self.cvalId = "\(cvalId)"
}
self.type = type
if let text = values["text"] as? String {
self.text = text
}
}
updatePrimaryKey()
}
func generateDico() -> [String: Any] {
var dicoSortie = [String: Any]()
if let realm = self.realm {
realm.refresh()
}
if let value = cvalId {
dicoSortie["id"] = value
}
if let value = type {
dicoSortie["type"] = value
}
if let value = text {
dicoSortie["text"] = value
}
return dicoSortie
}
}
compositeKey is the primary key which included cvalId and type
Thanks for help.

Typecasting causing struct values to change (Swift)

After downcasting an array of structs, my Variables View window shows that all of the values in my struct have shifted "down" (will explain in a second). But when I print(structName), the values are fine. However, when I run an equality check on the struct, it once again behaves as though my values have shifted.
For example, I am trying to downcast Model A to ModelProtocol. var m = Model A and has the values {id: "1234", name: "Cal"}. When I downcast, m now has the values { id:"\0\0", name:"1234" }.
Actual Example Below:
Models that I want to downcast:
struct PrivateSchoolModel: Decodable, SchoolProtocol {
var id: String
var name: String
var city: String
var state: String
}
struct PublicSchoolModel: Decodable, SchoolProtocol {
var id: String
var name: String
var city: String
var state: String
var latitude: String
var longitude: String
}
Protocol I want to downcast to:
protocol SchoolProtocol {
var id: String { get set }
var name: String { get set }
var city: String { get set }
var state: String { get set }
var longitude: Float { get set }
var latitude: Float { get set }
}
extension SchoolProtocol {
var longitude: Float {
get { return -1.0 }
set {}
}
var latitude: Float {
get { return -1.0 }
set {}
}
}
Downcasting:
guard let downcastedArr = privateSchoolArray as? [SchoolProtocol] else { return [] }
Result (item at index 0) or originalArr:
id = "1234"
name = "Leo High School"
city = "Bellview"
state = "WA"
Result (item at index 0) of downcastedArr:
id = "\0\0"
name = "1234"
city = "Leo High School"
state = "Bellview"
But if I print(downcastArr[0]), it will show:
id = "1234"
name = "Leo High School"
city = "Bellview"
state = "WA"
But if I try originalArray[0].id == downcastArr[0].id, it returns false
My Code with the problem:
class SchoolJSONHandler {
private enum JSONFile: String {
case publicSchool = "publicSchool"
case privateSchool = "privateSchool"
}
private lazy var privateSchoolArray = getPrivateSchools()
private lazy var publicSchoolArray = getPublicSchools()
func getSchoolArray(sorted: Bool, filtered: Bool, by stateAbbreviation: String?) -> [SchoolProtocol] {
var schools = combineArrays()
if sorted {
schools.sort(by: { $0.name < $1.name })
}
if filtered {
guard let abbr = stateAbbreviation else { return [] }
schools = schools.filter {
return $0.state == abbr
}
}
return schools
}
private func combineArrays() -> [SchoolProtocol] {
// EVERYTHING IS FINE IN NEXT LINE
let a = privateSchoolArray
// PROBLEM OCCURS IN NEXT 2 LINES WHEN DOWNCASTING
let b = privateSchoolArray as [SchoolProtocol]
let c = publicSchoolArray as [SchoolProtocol]
return b + c
}
private func getPublicSchools() -> [PublicSchoolModel] {
guard let jsonData = getJSONData(from: .publicSchool) else { return [] }
guard let schools = decode(jsonData: jsonData, using: [PublicSchoolModel].self) else { return [] }
return schools
}
private func getPrivateSchools() -> [PrivateSchoolModel] {
guard let jsonData = getJSONData(from: .privateSchool) else { return [] }
guard let schools = decode(jsonData: jsonData, using: [PrivateSchoolModel].self) else { return [] }
return schools
}
private func getJSONData(from resource: JSONFile) -> Data? {
let url = Bundle.main.url(forResource: resource.rawValue, withExtension: "json")!
do {
let jsonData = try Data(contentsOf: url)
return jsonData
}
catch {
print(error)
}
return nil
}
private func decode<M: Decodable>(jsonData: Data, using modelType: M.Type) -> M? {
do {
//here dataResponse received from a network request
let decoder = JSONDecoder()
let model = try decoder.decode(modelType, from:
jsonData) //Decode JSON Response Data
return model
} catch let parsingError {
print("Error", parsingError)
}
return nil
}
}
And then it is just called in another class by schoolJSONHandler.getSchoolArray(sorted: true, filtered: true, by: "WA")

How declaring and add data in new object correctly

I have this code:
struct Calculators
{
var calculators: [Calculator]?
var activeCalculator: Int = -1
var activeSummary: Bool = false
var activeProfits: Bool = false
public func getCalculators()-> [Calculator]
{
return calculators!
}
}
struct Calculator
{
var priceSum: Float = 0
var weightSum: Float = 0
var pricePerMix: Float = 0
var pricePerPortion: Decimal?
var portionDivider: Float?
var nettoPortionCost: Float?
var profitPerPortion: Float?
var pricePerKgAfterPrepare: Float?
var weightPerPortionInGrams: Float?
var products : [CountedProduct]?
init() {
createTime = NSDate().timeIntervalSince1970 * 1000
}
}
struct CountedProduct
{
var productCode: String
var productCountable: Bool
var pricePerKg: Decimal?
var weightYieldInPercent: Decimal?
var pieceWeight: Decimal?
}
var countedProduct = CountedProduct(productCode: produkt.code!, productCountable: productCountable, pricePerKg: pricePerKg, weightYieldInPercent: weightYieldInPercent, pieceWeight: pieceWeight)
var activeCalculators = Calculators()
var calculatorToAdd = Calculator()
calculatorToAdd.priceSum = 0
calculatorToAdd.weightSum = 0 //?
calculatorToAdd.pricePerMix = Float(countedProduct.weightAfterPrepareKg as! NSNumber)
calculatorToAdd.pricePerPortion = (countedProduct.costOfTheMixturePerPortion as! NSNumber).decimalValue
calculatorToAdd.portionDivider = 0 //?
calculatorToAdd.nettoPortionCost = Float(countedProduct.costOfTheMixturePerPortion as! NSNumber)
calculatorToAdd.profitPerPortion = Float(countedProduct.overheadOnServing as! NSNumber)
calculatorToAdd.pricePerKgAfterPrepare = Float(countedProduct.pricePerKgAfterPrepare as! NSNumber)
calculatorToAdd.weightPerPortionInGrams = Float(countedProduct.weightAfterPrepareKg as! NSNumber)
calculatorToAdd.products?.append(countedProduct)
calculatorToAdd.products?.append(countedProduct)
I can not change the form of these class.
How to correctly initialize a Calculators object?
I have data in produkt.
Is this code correct?
I would like to create a calculator object and insert it into Calculators.
The class code can not be changed.
I assume you can add to the code at the end
activeCalculators.calculators = []
activeCalculators.calculators.append(calculatorToAdd)

NSKeyedUnarchiver can't decode some of my keys

I have some keys I want to decode, but it only decodes my first string and then it gives error.
class Profile: NSCoder, NSCoding {
struct Constants {
// Age
static let minimumAge: Int = 5
static let maximumAge: Int = 120
static let defaultAge: Int = 25
// Gender
static let defaultGender: Int = 0
// Daily meals
static let defaultDailyMeals: Int = 3
static let minimumDailyMeals: Int = 1
static let maximumDailyMeals: Int = 10
// Do you train
static let defaultIsTraining: Bool = false
}
//MARK: Properties
private var name: String
private var age: Int
private var gender: Int
private var target: Int
private var dailyMeals: Int
private var isTraining: Bool
private var trainingWhen: Int?
private var trainingDays: [(Int)];
//MARK: Archiving Paths
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("profile")
//MARK: Types
struct PropertyKey {
static let name = "profileName"
static let age = "profileAge"
static let gender = "profileGender"
static let target = "profileTarget"
static let dailyMeals = "profileDailyMeals"
static let isTraining = "profileIsTraining"
static let trainingWhen = "profileTrainingWhen"
static let trainingDays = "trainingDays"
}
// Main init
init?(name: String, age: Int, gender: Int, target: Int, dailyMeals: Int, isTraining: Bool, trainingWhen: Int?, trainingDays: [(Int)]) {
guard !name.isEmpty else {
print("Name empty")
return nil
}
guard age > Constants.minimumAge && age < Constants.maximumAge else {
print("Age not in range")
return nil
}
self.name = name
self.age = age
self.gender = gender
self.target = target
self.dailyMeals = dailyMeals
self.isTraining = isTraining
self.trainingWhen = trainingWhen
self.trainingDays = trainingDays
}
//MARK: NSCoding
func encode(with aCoder: NSCoder) {
aCoder.encode(self.name, forKey: PropertyKey.name)
aCoder.encode(self.age, forKey: PropertyKey.age)
aCoder.encode(self.gender, forKey: PropertyKey.gender)
aCoder.encode(self.target, forKey: PropertyKey.target)
aCoder.encode(self.dailyMeals, forKey: PropertyKey.dailyMeals)
aCoder.encode(self.isTraining, forKey: PropertyKey.isTraining)
aCoder.encode(self.trainingWhen, forKey: PropertyKey.trainingWhen)
aCoder.encode(self.trainingDays, forKey: PropertyKey.trainingDays)
}
func dump_data(){
print("Name: " + self.name)
print("Age: " + String(self.age))
print("Gender: " + String(self.gender))
print("Target: " + String(self.target))
print("Daily Meals: " + String(self.dailyMeals))
print("Does train?: " + String(self.isTraining))
print("When: " + String(describing: self.trainingWhen))
print("Days: " + String(describing: self.trainingDays))
}
required convenience init?(coder aDecoder: NSCoder) {
guard let name = aDecoder.decodeObject(forKey: PropertyKey.name) as? String else {
print("Unable to decode the name for a Profile object.")
return nil
}
print("Decoded: Name - \(name)")
guard let age = aDecoder.decodeObject(forKey: PropertyKey.age) as? Int else {
print("Unable to decode the age for a Profile object.")
return nil
}
guard let gender = aDecoder.decodeObject(forKey: PropertyKey.gender) as? Int else {
print("Unable to decode the gender for a Profile object.")
return nil
}
guard let target = aDecoder.decodeObject(forKey: PropertyKey.target) as? Int else {
print("Unable to decode the target for a Profile object.")
return nil
}
guard let dailyMeals = aDecoder.decodeObject(forKey: PropertyKey.dailyMeals) as? Int else {
print("Unable to decode the daily meals for a Profile object.")
return nil
}
guard let isTraining = aDecoder.decodeObject(forKey: PropertyKey.isTraining) as? Bool else {
print("Unable to decode the training state for a Profile object.")
return nil
}
let trainingWhen: Int? = aDecoder.decodeObject(forKey: PropertyKey.trainingWhen) as? Int ?? nil
let trainingDays: [(Int)] = aDecoder.decodeObject(forKey: PropertyKey.trainingDays) as? [(Int)] ?? []
// Must call designated initializer.
self.init(name: name, age: age, gender: gender, target: target, dailyMeals: dailyMeals, isTraining: isTraining, trainingWhen: trainingWhen, trainingDays: trainingDays)
}
The error message I get is
Unable to decode the age for a Profile object.
As long as I know this means that my aDecoder.decodeObject(forKey: PropertyKey.gender) as? Int returns nil and I have completely no idea why this happens, because I encode it as you can see in the code. :/
You should probably use the decodeInteger(forKey:_) method, like this:
self.age = aDecoder.decodeInteger(forKey: PropertyKey.age)
With that said, what I usually do in these cases (considering you only need one instance of the class) is this:
//: Playground - noun: a place where people can play
import UIKit
class Profile: NSCoder, NSCoding {
struct Constants {
// Age
static let minimumAge: Int = 5
static let maximumAge: Int = 120
static let defaultAge: Int = 25
// Gender
static let defaultGender: Int = 0
// Daily meals
static let defaultDailyMeals: Int = 3
static let minimumDailyMeals: Int = 1
static let maximumDailyMeals: Int = 10
// Do you train
static let defaultIsTraining: Bool = false
}
//MARK: Properties
var name: String = ""
var age: Int = 0
var gender: Int = 0
var target: Int = 0
var dailyMeals: Int = 0
var isTraining: Bool = false
var trainingWhen: Int? = nil
var trainingDays = [Int]();
//MARK: Archiving Paths
static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].path
static let fileName = "profile"
static let path = "\(Profile.documentsDirectory)/\(fileName)"
//MARK: Types
struct PropertyKey {
static let name = "profileName"
static let age = "profileAge"
static let gender = "profileGender"
static let target = "profileTarget"
static let dailyMeals = "profileDailyMeals"
static let isTraining = "profileIsTraining"
static let trainingWhen = "profileTrainingWhen"
static let trainingDays = "trainingDays"
}
// Main init
static var shared = Profile()
fileprivate override init() { }
//MARK: NSCoding
func encode(with archiver: NSCoder) {
archiver.encode(self.name, forKey: PropertyKey.name)
archiver.encode(self.age, forKey: PropertyKey.age)
archiver.encode(self.gender, forKey: PropertyKey.gender)
archiver.encode(self.target, forKey: PropertyKey.target)
archiver.encode(self.dailyMeals, forKey: PropertyKey.dailyMeals)
archiver.encode(self.isTraining, forKey: PropertyKey.isTraining)
archiver.encode(self.trainingWhen, forKey: PropertyKey.trainingWhen)
archiver.encode(self.trainingDays, forKey: PropertyKey.trainingDays)
}
override var description: String {
return """
Name: \(self.name)
Age: \(self.age)
Gender: \(self.gender)
Target: \(self.target)
Daily Meals: \(self.dailyMeals)
Does train?: \(self.isTraining)
When: \(self.trainingWhen ?? 0)
Days: \(self.trainingDays)
"""
}
func save() -> Bool {
return NSKeyedArchiver.archiveRootObject(self, toFile: Profile.path)
}
required init(coder unarchiver: NSCoder) {
super.init()
if let name = unarchiver.decodeObject(forKey: PropertyKey.name) as? String {
self.name = name
}
self.age = unarchiver.decodeInteger(forKey: PropertyKey.age)
self.gender = unarchiver.decodeInteger(forKey: PropertyKey.gender)
self.target = unarchiver.decodeInteger(forKey: PropertyKey.target)
self.dailyMeals = unarchiver.decodeInteger(forKey: PropertyKey.dailyMeals)
self.isTraining = unarchiver.decodeBool(forKey: PropertyKey.isTraining)
if let trainingWhen = unarchiver.decodeObject(forKey: PropertyKey.trainingWhen) as? Int {
self.trainingWhen = trainingWhen
}
if let trainingDays = unarchiver.decodeObject(forKey: PropertyKey.trainingDays) as? [Int] {
self.trainingDays = trainingDays
}
}
}
// TEST:
Profile.shared.name = "Daniel"
Profile.shared.age = 10
Profile.shared.gender = 30
Profile.shared.target = 10
Profile.shared.dailyMeals = 90
Profile.shared.isTraining = true
Profile.shared.trainingWhen = 20
Profile.shared.trainingDays = [1,2,5]
Profile.shared.save()
if let profile = NSKeyedUnarchiver.unarchiveObject(withFile: Profile.path) as? Profile {
print(profile.description)
}
/*
Output:
--------
Name: Daniel
Age: 10
Gender: 30
Target: 10
Daily Meals: 90
Does train?: true
When: 20
Days: [1, 2, 5]
*/