Swift: Bug when try to access value of object in array - swift

I encounter a very strange bug, I have a PodEvent type array, and with a second function I get another array that I cast, when I make a print of the casted array with the result it shows me all the objects of the board...
PodEvent {
eventID = 2;
podID = 1;
drinkDate = 2018-09-25 10:00:00 +0000;
actualDrinkingTime = (null);
keyDate = 2018-09-25 13:00;
rawState = Forgotten;
}
But when I want to access the value of the object it returns to me as the default values! for example:
print(self.podEvents[1].eventID)
print(self.podEvents[1].podID)
outpout:
-1
-1
Here my class:
class PodEvent: Object {
#objc var eventID: Int = -1
#objc var podID: Int = -1
#objc var drinkDate: Date = Date()
#objc var actualDrinkingTime: Date? = Date()
var state: PodState = .future
#objc var keyDate: String = ""
#objc private var rawState: String!
convenience init(eventID: Int, podID: Int, drinkDate: Date, actualDrinkingTime: Date?, state: PodState){
self.init()
self.eventID = eventID
self.podID = podID
self.drinkDate = drinkDate
self.actualDrinkingTime = actualDrinkingTime
self.state = state
self.rawState = state.state
}
func setState(state: PodState){
self.state = state
rawState = state.state
}
override class func primaryKey() -> String? {
return "keyDate"
}
This bug is very strange
My code to fetch my array:
//Fetch pod history in internal database
self.databaseManager.fetch(object: PodEvent.self, predicate: nil, sortedBy: "keyDate", ascending: true) { success, results, error in
guard error == nil else {
Alerts.alertMessage(for: self, title: "ERROR".localized,
message: error!.localizedDescription,
closeHandler: nil)
return
}
self.podEvents = (results as! [PodEvent])
self.pods = pods
print(self.podEvents[1].eventID)
print(self.podEvents[1].podID)
}
and:
func fetch(object: Object.Type, predicate: NSPredicate?, sortedBy: String, ascending: Bool, completion: DatabaseCompletion?) {
do {
let realm = try Realm()
let objects: Results<Object>!
if let predicate = predicate {
objects = realm.objects(object).filter(predicate).sorted(byKeyPath: sortedBy, ascending: ascending)
} else {
objects = realm.objects(object).sorted(byKeyPath: sortedBy, ascending: ascending)
}
let objectsArray = Array(objects)
completion?(true, objectsArray, nil)
} catch let error {
print("Could not write object (type \(object)) to Realm:", error.localizedDescription)
completion?(false, nil, error)
}
}
im using realm

Your object definition is flawed, you need to add the dynamic keyword to all persisted properties.
class PodEvent: Object {
#objc dynamic var eventID = -1
#objc dynamic var podID = -1
#objc dynamic var drinkDate = Date()
#objc dynamic var actualDrinkingTime: Date? = Date()
var state: PodState = .future
#objc dynamic var keyDate = ""
#objc dynamic private var rawState: String!
...
}

Related

I want to sort the List of RealmSwift

I am using RealmSwift.
Data is a one-to-many relationship.
I'm having trouble because I don't know how to sort the list in RealmSwift.
I want to sort the tasks linked to the TaskList.
Thank you.
class TaskList: Object, Identifiable {
#objc dynamic var id = NSUUID().uuidString
#objc dynamic var title = ""
#objc dynamic var createdAt = NSDate()
var tasks: List<Task> = List<Task>()
override static func primaryKey() -> String? {
return "id"
} }
class Task: Object, Identifiable {
#objc dynamic var id = NSUUID().uuidString
#objc dynamic var title = ""
#objc dynamic var createdAt = NSDate()
private let lists = LinkingObjects(fromType: TaskList.self, property: "tasks")
var list: TaskList { return list.first! }
override static func primaryKey() -> String? {
return "id"
} }
If you want your tasks stored in an ordered fashion you'll have to manually do an ordered insert.
extension List {
func insert<V: Comparable>(_ object: Element, orderedBy keyPath: KeyPath<Element, V>) {
var index = 0
for i in 0..<count {
if self[i][keyPath: keyPath] >= object[keyPath: keyPath] {
break
}
index = i + 1
}
insert(object, at: index)
}
}
let list = TaskList()
let tasks = [
Task(title: "J"),
Task(title: "Z"),
Task(title: "T"),
Task(title: "J"),
Task(title: "Z"),
]
tasks.forEach {
list.tasks.insert($0, orderedBy: \.title)
}
However, I find it much easier to keep Lists unsorted and retrieve sorted Results whenever I need to display the data. To sort by a single property just call sorted(byKeyPath:):
let sortedTasks = taskList.tasks.sorted(byKeyPath: "title")
To sort by multiple fields call sorted(by:):
let sortedTasks = taskList.tasks.sorted(by: [
SortDescriptor(keyPath: "title"),
SortDescriptor(keyPath: "createdAt")
])
Alternatively, you can add a sorted property to your Model:
class TaskList: Object, Identifiable {
#objc dynamic var id = UUID().uuidString
#objc dynamic var title = ""
#objc dynamic var createdAt = Date()
var tasks = List<Task>()
lazy var sortedTasks: Results<Task> = tasks.sorted(byKeyPath: "title")
override class func ignoredProperties() -> [String] {
return ["sortedTasks"]
}
override static func primaryKey() -> String? {
return "id"
}
}
You cannot get a LinkingObjects itself to be sorted, but you can call LinkingObjects.sorted(byKeyPath:), which returns a Results instance containing all elements of the LinkingObjects, just sorted.
class Task: Object, Identifiable {
#objc dynamic var id = NSUUID().uuidString
#objc dynamic var title = ""
#objc dynamic var createdAt = NSDate()
private let lists = LinkingObjects(fromType: TaskList.self, property: "tasks")
lazy var sortedLists = lists.sorted(byKeyPath: "createdAt")
var list: TaskList { return list.first! }
override static func primaryKey() -> String? {
return "id"
}
}
I think this is a one line answer. If you know which TaskList object you want to get the tasks for, and you want them ordered by say, creation date. This will do it
let taskResults = realm.objects(Task.self)
.filter("ANY lists == %#", taskList)
.sorted(byKeyPath: "createdAt")

Issue with creating and then updating existing elements in Realm database

I have a realm database which consists of Year -> Month -> Day -> Items
arrows go from parent category to the child category.
I am able to save the calculation once but if I do it again it will throw an error that Month of a primary key 0 already exists. I do know what it exists that is why I want Realm to update it. As I want to save each month only once and each year only once.
//MARK: - Realm Database Saving option
#objc func saveCalculation(){
let date = Date()
let dateElements = getDate()
let yearObject = Year()
yearObject.number = dateElements.year
let monthObject = Month()
monthObject.number = dateElements.month
monthObject.monthName = dateElements.monthName
let dayObject = Day()
dayObject.id = realm.objects(Day.self).count
dayObject.date = date
dayObject.number = dateElements.day
dayObject.dayName = dateElements.dayNameInWeek
dayObject.hoursPerDay = hoursPerDay
dayObject.ratePerHour = ratePerHour
dayObject.minutesOfNormalPay = minutesOfNormalPay
dayObject.earnedPerDay = earnedPerDay
dayObject.howManyRows = rowViews.count
do {
try realm.write {
realm.add(yearObject, update: true)
yearObject.months.append(monthObject)
realm.add(monthObject, update: true)
monthObject.days.append(dayObject)
realm.add(dayObject, update: false)
for (index, row) in rowViews.enumerated() {
let item = Item()
item.id = index
item.date = date
item.amount = Double(row.amountTextField.text!) ?? 0.0
item.price = Double(row.priceTextField.text!) ?? 0.0
print("Item amount: \(item.amount) and price: \(item.price)")
dayObject.items.append(item)
realm.add(item)
}
}
} catch {
print(error.localizedDescription)
}
}
func getDate() -> (day: Int, dayNameInWeek: String, month: Int, monthName: String, year: Int) {
let date = Date()
let dayNameInWeek = date.getDayName()
let monthName = date.getMonthName()
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month, .day], from: date)
let year = components.year!
let month = components.month!
let day = components.day!
print(year)
print(month)
print(monthName)
print(day)
print(dayNameInWeek)
return (day,dayNameInWeek, month, monthName, year)
}
each database class respectively:
import Foundation
import RealmSwift
class Year: Object {
//#objc dynamic var year = Date()
#objc dynamic var id: Int = 0
#objc dynamic var number: Int = 2008
let months = List<Month>() //forward relationship to Months
override class func primaryKey() -> String {
return "id"
}
}
Month:
import Foundation
import RealmSwift
class Month: Object {
#objc dynamic var id: Int = 0
#objc dynamic var number: Int = 0
#objc dynamic var monthName: String = ""
#objc dynamic var monthYear: Int = 0
let days = List<Day>() //forward relationship to Days
let parentYear = LinkingObjects(fromType: Year.self, property: "months") //back relationship to year
override class func primaryKey() -> String {
return "id"
}
}
Day:
import Foundation
import RealmSwift
class Day: Object {
#objc dynamic var id: Int = 0
#objc dynamic var date: Date = Date()
#objc dynamic var number: Int = 0
#objc dynamic var dayName: String = ""
#objc dynamic var hoursPerDay: Double = 0.0
#objc dynamic var ratePerHour: Double = 0.0
#objc dynamic var minutesOfNormalPay: Double = 0.0
#objc dynamic var earnedPerDay: Double = 0.0
#objc dynamic var howManyRows: Int = 0
let items = List<Item>() //forward relationship to Items
let parentMonth = LinkingObjects(fromType: Month.self, property: "days") //back relationship to months
override class func primaryKey() -> String {
return "id"
}
}
Item:
import Foundation
import RealmSwift
class Item: Object {
#objc dynamic var id: Int = 0
#objc dynamic var date: Date = Date()
#objc dynamic var amount: Double = 0.0
#objc dynamic var price: Double = 0.0
let parentDay = LinkingObjects(fromType: Day.self, property: "items") //back relationship to days
}
I have managed to solve the problem with some if else conditions. If someone knows a cleeaner way of writing it then please post it below, as I am really interested of getting this code shortened.
//MARK: - Realm Database Saving option
#objc func saveCalculation(){
let date = Date()
let dateElements = getDate()
var isParentYear = false
var isParentMonth = false
let yearObject: Year
if let currentYearObject = realm.objects(Year.self).first(where: {$0.number == dateElements.year}) {
yearObject = currentYearObject
isParentYear = true
} else {
yearObject = Year()
yearObject.id = realm.objects(Year.self).count
yearObject.number = dateElements.year
}
let monthObject: Month
if let currentMonthObject = realm.objects(Month.self).first(where: {$0.monthYear == dateElements.year && $0.number == dateElements.month}) {
monthObject = currentMonthObject
isParentMonth = true
} else {
monthObject = Month()
monthObject.id = realm.objects(Month.self).count
monthObject.number = dateElements.month
monthObject.monthYear = dateElements.year
monthObject.monthName = dateElements.monthName
}
let dayObject = Day()
dayObject.id = realm.objects(Day.self).count
dayObject.date = date
dayObject.number = dateElements.day
dayObject.dayName = dateElements.dayNameInWeek
dayObject.hoursPerDay = hoursPerDay
dayObject.ratePerHour = ratePerHour
dayObject.minutesOfNormalPay = minutesOfNormalPay
dayObject.earnedPerDay = earnedPerDay
dayObject.howManyRows = rowViews.count
do {
try realm.write {
if isParentYear && isParentMonth{
print("Parent Year is: \(isParentYear) and parent Month is: \(isParentMonth)")
monthObject.days.append(dayObject)
realm.add(dayObject, update: false)
} else if isParentYear && !isParentMonth {
print("Parent Year is: \(isParentYear) and parent Month is: \(isParentMonth)")
yearObject.months.append(monthObject)
realm.add(monthObject, update: true)
monthObject.days.append(dayObject)
realm.add(dayObject, update: false)
} else if !isParentYear && !isParentMonth {
print("Parent Year is: \(isParentYear) and parent Month is: \(isParentMonth)")
realm.add(yearObject, update: true)
yearObject.months.append(monthObject)
realm.add(monthObject, update: true)
monthObject.days.append(dayObject)
realm.add(dayObject, update: false)
} else {
print("THISH SHOULD NEVER BE CALLED IF CALLED SOMETHING IS VERY WRONG")
}
for (index, row) in rowViews.enumerated() {
let item = Item()
item.id = index
item.date = date
item.amount = Double(row.amountTextField.text!) ?? 0.0
item.price = Double(row.priceTextField.text!) ?? 0.0
print("Item amount: \(item.amount) and price: \(item.price)")
dayObject.items.append(item)
realm.add(item)
}
}
} catch {
print(error.localizedDescription)
}
}

Pull out data from a numbered variable using a loop in swift

I have a realm database. So that the database is more readable. I have designed the questionbank as follows:
class Question: Object {
#objc dynamic var id: Int = 0
#objc dynamic var name: String = ""
#objc dynamic var answered: Bool = false
#objc dynamic var lastAnswer: Bool = false
#objc dynamic var howManyTimesAnswered: Int = 0
#objc dynamic var answer0: String = ""
#objc dynamic var answer1: String = ""
#objc dynamic var answer2: String = ""
#objc dynamic var answer3: String = ""
#objc dynamic var correctAnswer: Int = 0
let parentCategory = LinkingObjects(fromType: Category.self, property: "questions") //back relationship to category
}
I am trying to pull out the answers and putting them into an array of tuples (String, Bool)
var currentAnswers = [(String, Bool)]()
for i in 0...3 {
if i == question.correctAnswer {
currentAnswers.append(("question.answer2", true))
} else {
currentAnswers.append((question.answer1, false))
}
}
what I want to achieve is something like this where the answer pulled out is equal to i obviously the one below will not compile
for i in 0...3 {
if i == question.correctAnswer {
currentAnswers.append((question.answer(i), true))
} else {
currentAnswers.append((question.answer(i), false))
}
}
You can use Key path Try this code
var currentAnswers = [(String, Bool)]()
for i in 0...3 {
if i == question.correctAnswer {
currentAnswers.append((question.value(forKey: "answer\(i)") as! String, true))
} else {
currentAnswers.append((question.value(forKey: "answer\(i)") as! String, false))
}
}
I have managed to get a workaround it. Only one line of extra code.
I hope this will help if someone wants to achieve something similar.
var currentAnswers = [(String, Bool)]()
let answers = [question.answer0, question.answer1, question.answer2, question.answer3]
for i in 0...3 {
if i == question.correctAnswer {
currentAnswers.append((answers[i], true))
} else {
currentAnswers.append((answers[i], false))
}
}
I have been using tuple above for true randomization so that the answers always show up in different order on the answer buttons.

Cannot avoid optional() for string print statements?

I have a data struct which contains some string parameters. The struct is below:
struct pulledMessage{
var convoWithUserID: String
var convoWithUserName: String
}
I have a function which assigns a value to variables based on the values within a particular pulledMessage. For some more complicated, out-of-the-scope-of-the-question, reasons, these values come from [pulledMessage] array. The pulledMessage always changes in the actual function but for illustration purposes I will write it as a constant:
var messageArray = [pulledMessage]()
func assignValues(){
messageArray.append(pulledMessage(convoWithUserID: "abc123", convoWithUserName: "Kevin"))
let convoWithUserID = messageArray[0].convoWithUserID
let convoWithUserName = messageArray[0].convoWithUserName
print(convoWithUserID) //returns optional("abc123")
print(convoWithUserName) // returns optional("Kevin")
}
I have tried adding ! to unwrap the values in different ways:
messageArray[0]!.convoWithUserID
This tells gives me an error that I cannot unwrap a non-optional type of pulledMessage.
messageArray[0].convoWithUserID!
This gives me an error that I cannot unwrap a non-optional type of String.
This stack question suggests utilizing if let to get rid of the optional:
if let convoWithUserIDCheck = messageArray[0].convoWithUserID{
convoWithUserID = convoWithUserIDCheck
}
This gives me a warning that there is no reason to do if let with a non-optional type of string. I have no idea how to get it to stop returning the values wrapped by optional().
Update: The more complicated, complete code
The SQL Database functions:
class FMDBManager: NSObject {
static let shared: FMDBManager = FMDBManager()
let databaseFileName = "messagesBetweenUsers.sqlite"
var pathToDatabase: String!
var database: FMDatabase!
override init() {
super.init()
let documentsDirectory = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString) as String
pathToDatabase = documentsDirectory.appending("/\(databaseFileName)")
}
func loadMessageData(){//will need a struct to load the data into a struct
if openDatabase(){
let query = "select * from messages order by messageNumber asc"
do{
print(database)
let results: FMResultSet = try database.executeQuery(query, values: nil)
while results.next(){
let message = pulledMessage(convoWithUserID: String(describing: results.string(forColumn: "convoWithUserID")), convoWithUserName: String(describing: results.string(forColumn: "convoWithUserName")), messageString: String(describing: results.string(forColumn: "messageString")), senderID: String(describing: results.string(forColumn: "senderID")), timeSent: String(describing: results.string(forColumn: "timeSent")), messageNumber: Int(results.int(forColumn: "messageNumber")))
if messagesPulled == nil{
messagesPulled = [pulledMessage]()
}
messagesPulled.append(message)
print("The message that we have pulled are \(message)")
}
}
catch{
print(error.localizedDescription)
}
database.close()
}
}
}
Running the population of the data at the onset of app launch:
func applicationDidBecomeActive(_ application: UIApplication) {
// if FMDBManager.shared.createDatabase() {
// FMDBManager.shared.insertMessageData()
// }else{
// print("Not a chance, sonny")
// FMDBManager.shared.insertMessageData()
// }
FMDBManager.shared.loadMessageData()
}
Organizing the SQL data in order:
struct pulledMessage{//global struct
var convoWithUserID: String
var convoWithUserName: String
var messageString: String
var senderID: String
var timeSent: String
var messageNumber: Int
}
var messagesPulled: [pulledMessage]!
var messageConvoDictionary = [String: [pulledMessage]]()
//For the individual message convos
var fullUnorderedMessageArray = [[pulledMessage]]()
var fullOrderedMessageArray = [[pulledMessage]]()
//For the message table
var unorderedLastMessageArray = [pulledMessage]()
var orderedLastMessageArray = [pulledMessage]()
//For the table messages... FROM HERE..........................................
func organizeSQLData(messageSet: [pulledMessage]){
var i = 0
var messageUserID = String()
while i < messageSet.count{
if (messageSet[i]).convoWithUserID != messageUserID{
print("It wasn't equal")
print(messageSet[i])
messageUserID = messageSet[i].convoWithUserID
if messageConvoDictionary[messageUserID] != nil{
messageConvoDictionary[messageUserID]?.append(messageSet[i])
}else{
messageConvoDictionary[messageUserID] = []
messageConvoDictionary[messageUserID]?.append(messageSet[i])
}
i = i + 1
}else{
messageConvoDictionary[messageUserID]?.append(messageSet[i])
i = i + 1
}
}
}
func getLastMessages(messageSet: [String:[pulledMessage]]){
for (_, messages) in messageSet{
let orderedMessages = messages.sorted(by:{ $0.timeSent.compare($1.timeSent) == .orderedAscending})
let finalMessage = orderedMessages[0]
unorderedLastMessageArray.append(finalMessage)
}
print(unorderedLastMessageArray)
}
func orderLastMessage(messageSet: [pulledMessage]){
orderedLastMessageArray = messageSet.sorted(by:{ $0.timeSent.compare($1.timeSent) == .orderedDescending})
messagesListTableView.reloadData()
print("It wasn't\(orderedLastMessageArray)")
}
func getMessagesReady(){//for observer type function calls
organizeSQLData(messageSet: messagesPulled)
getLastMessages(messageSet: messageConvoDictionary)
orderLastMessage(messageSet: unorderedLastMessageArray)
//This one is for the individual full convos for if user clicks on a cell... its done last because its not required for the page to show up
orderedFullMessageConvos(messageSet: messageConvoDictionary)
let openedMessageConversation = fullOrderedMessageArray[(indexPath.row)]//not placed in its appropriate location, but it is just used to pass the correct array (actually goes in a prepareforSegue)
}
override func viewDidLoad() {
super.viewDidLoad()
getMessagesReady()
}
Then segue to the new controller (passing openedMessageConversation to messageConvo) and run this process on a button click:
let newMessage = pulledMessage(convoWithUserID: messageConvo[0].convoWithUserID, convoWithUserName: messageConvo[0].convoWithUserName, messageString: commentInputTextfield.text!, senderID: (PFUser.current()?.objectId)!, timeSent: String(describing: Date()), messageNumber: 0)
messageConvo.append(newMessage)
let newMessageSent = PFObject(className: "UserMessages")
newMessageSent["convoWithUserID"] = newMessage.convoWithUserID
newMessageSent["convoWithUserName"] = newMessage.convoWithUserName
newMessageSent["messageString"] = newMessage.messageString
newMessageSent["senderID"] = newMessage.senderID
let acl = PFACL()
acl.getPublicWriteAccess = true
acl.getPublicReadAccess = true
acl.setWriteAccess(true, for: PFUser.current()!)
acl.setReadAccess(true, for: PFUser.current()!)
newMessageSent.acl = acl
newMessageSent.saveInBackground()
It is the newMessageSent["convoWithUserID"] and newMessageSent["convoWithUserName"] that read with the optional() in the database.
So it turns out that the reason for this stems from the function run from loadMessageData. The use of String(describing: results.string(forColumn:) requires an unwrapping of results.String(forColumn:)!. This issue propagated throughout the data modification for the whole app and caused the optional() wrapping for the print statements that I was seeing.

How to reduce code duplication for class properties (Swift 3)

In my application I want to implement separate class to keep all the temporary variables of Now Playing item for Music Player.
It has lots of properties with different types, but they should be handled in the same way. They should be handled in the class method "updateData" (see the end of code)
This is my code:
struct DataDefaults {
//MARK: Default properties
let albumTitle: String? = "Unknown Album"
let albumArtist: String? = "Unknown Artist"
let title: String? = "Unknown Title"
let artist: String? = "Unknown Artist"
let artwork: UIImage? = UIImage(named: "noartwork")!
let genre: String? = ""
let lyrics: String? = "No Lyrics"
let releaseDate: Date? = nil
let playbackDuration: TimeInterval? = 0
let rating: Int? = 0
let assetURL: URL? = nil
let isExplicitItem: Bool? = false
let isCloudItem: Bool? = false
let hasProtectedAsset: Bool? = false
}
class SongInfo: NSObject {
static let sharedData = SongInfo()
let defaults = DataDefaults()
//MARK: Properties
var albumTitle: String
var albumArtist: String
var title: String
var artist: String
var artwork: UIImage
var genre: String
var lyrics: String
var releaseDate: Date?
var playbackDuration: TimeInterval
var rating: Int
var assetURL: URL?
var isExplicitItem: Bool
var isCloudItem: Bool
var hasProtectedAsset: Bool
//MARK: Init
private override init () {
self.albumTitle = defaults.albumTitle!
self.albumArtist = defaults.albumArtist!
self.title = defaults.title!
self.artist = defaults.artist!
self.artwork = defaults.artwork!
self.genre = defaults.genre!
self.lyrics = defaults.lyrics!
self.releaseDate = defaults.releaseDate
self.playbackDuration = defaults.playbackDuration!
self.rating = defaults.rating!
self.assetURL = defaults.assetURL
self.isExplicitItem = defaults.isExplicitItem!
self.isCloudItem = defaults.isCloudItem!
self.hasProtectedAsset = defaults.hasProtectedAsset!
}
//MARK: Set properties
func updateData(allData: DataDefaults) {
var wasUpdated: Bool = false
if allData.albumTitle == self.albumTitle {
//pass
} else if allData.albumTitle == nil || allData.albumTitle == "" {
self.albumTitle = defaults.albumTitle!
wasUpdated = true
} else {
self.albumTitle = allData.albumTitle!
wasUpdated = true
}
//Need to repeat same IF for all properties
}
}
Is there any way I can use property name to make some reusage of the same code instead of duplicating it?
Rather than trying to find a solution to a weird design, I re-designed for what you're trying to accomplish 🙂
struct SongData: Equatable {
static let defaultData = SongData(albumTitle: "Unknown Album",
albumArtist: "Unknown Artist",
title: "Unknown Title",
artist: "Unknown Artist",
artwork: UIImage(named: "noartwork"),
genre:"",
lyrics: "No Lyrics",
releaseDate: nil,
playbackDuration: 0,
rating: 0,
assetURL: nil,
isExplicitItem: false,
isCloudItem: false,
hasProtectedAsset: false)
//MARK: Default properties
var albumTitle: String?
var albumArtist: String?
var title: String?
var artist: String?
var artwork: UIImage?
var genre: String?
var lyrics: String?
var releaseDate: Date?
var playbackDuration: TimeInterval?
var rating: Int?
var assetURL: URL?
var isExplicitItem: Bool?
var isCloudItem: Bool?
var hasProtectedAsset: Bool?
/// This initializer will set the properties to the defaultData properties if a passed value is nil
init(albumTitle: String?, albumArtist: String?, title: String?, artist: String?, artwork: UIImage?, genre: String?, lyrics: String?, releaseDate: Date?, playbackDuration: TimeInterval?, rating: Int?, assetURL: URL?, isExplicitItem: Bool?, isCloudItem: Bool?, hasProtectedAsset: Bool?) {
// initialize properties where the default is nil
self.releaseDate = releaseDate
self.assetURL = assetURL
//initialize other properties with the passed values, or use the default value if nil
self.albumTitle = SongData.valueOrDefault(albumTitle, SongData.defaultData.albumTitle)
self.albumArtist = SongData.valueOrDefault(albumArtist, SongData.defaultData.albumArtist)
self.title = SongData.valueOrDefault(title, SongData.defaultData.title)
self.artist = SongData.valueOrDefault(artist, SongData.defaultData.artist)
self.artwork = artwork ?? SongData.defaultData.artwork
self.genre = SongData.valueOrDefault(genre, SongData.defaultData.genre)
self.lyrics = SongData.valueOrDefault(lyrics, SongData.defaultData.lyrics)
self.playbackDuration = playbackDuration ?? SongData.defaultData.playbackDuration
self.rating = rating ?? SongData.defaultData.rating
self.isExplicitItem = isExplicitItem ?? SongData.defaultData.isExplicitItem
self.isCloudItem = isCloudItem ?? SongData.defaultData.isCloudItem
self.hasProtectedAsset = hasProtectedAsset ?? SongData.defaultData.hasProtectedAsset
}
static func ==(leftItem: SongData, rightItem: SongData) -> Bool {
return (leftItem.albumTitle == rightItem.albumTitle) &&
(leftItem.albumArtist == rightItem.albumArtist) &&
(leftItem.title == rightItem.title) &&
// Comparing a reference type here. may need to be handled differently if that's a problem
(leftItem.artwork === rightItem.artwork) &&
(leftItem.genre == rightItem.genre) &&
(leftItem.lyrics == rightItem.lyrics) &&
(leftItem.releaseDate == rightItem.releaseDate) &&
(leftItem.playbackDuration == rightItem.playbackDuration) &&
(leftItem.rating == rightItem.rating) &&
(leftItem.assetURL == rightItem.assetURL) &&
(leftItem.isExplicitItem == rightItem.isExplicitItem) &&
(leftItem.isCloudItem == rightItem.isCloudItem) &&
(leftItem.hasProtectedAsset == rightItem.hasProtectedAsset)
}
//simple helper function to avoid long turneries in the init
static func valueOrDefault(_ value: String?, _ defaultValue: String?) -> String? {
guard let value = value, !value.isEmpty else {
return defaultValue
}
return value
}
}
class SongInfo {
static let sharedData = SongInfo()
var data: SongData
//MARK: Init
private init ()
{
self.data = SongData.defaultData
}
//MARK: Set properties
func updateData(newData: SongData) {
if(newData != self.data) {
self.data = newData
}
}
}
I changed your struct to act more like it appears you're wanting it to be used, and the struct's init will fall back to using the default values if the init values are nil. My design also contains no force unwraps, which are almost always bad.
You could set the defaults directly in your class definition without using a separate struct and have a static unaltered instance with the default values.
For example:
class SongInfo: NSObject {
static let sharedData = SongInfo()
static let defaults = SongInfo()
//MARK: Properties
var albumTitle: String? = "Unknown Album"
var albumArtist: String? = "Unknown Artist"
var title: String? = "Unknown Title"
var artist: String? = "Unknown Artist"
var artwork: UIImage? = UIImage(named: "noartwork")!
var genre: String? = ""
var lyrics: String? = "No Lyrics"
var releaseDate: Date? = nil
var playbackDuration: TimeInterval? = 0
var rating: Int? = 0
var assetURL: URL? = nil
var isExplicitItem: Bool? = false
var isCloudItem: Bool? = false
var hasProtectedAsset: Bool? = false
//MARK: Init
private override init ()
{
// nothing to do here
}
//MARK: Set properties
func updateData(allData: DataDefaults) {
var wasUpdated: Bool = false
if allData.albumTitle == self.albumTitle {
//pass
} else if allData.albumTitle == nil || allData.albumTitle == "" {
self.albumTitle = SongInfo.defaults.albumTitle!
wasUpdated = true
} else {
self.albumTitle = allData.albumTitle!
wasUpdated = true
}
//Need to repeat same IF for all properties
}
}
If you also need to manipulate the basic data without the whole class functionality, you could define a SongInfoData class with only the properties and make SingInfo inherit from that class. Then the static variable for defaults could be in the SongInfoData class and the SingInfo subclass wouldn't need any property declarations.
[EDIT] avoiding code repetition in update function ...
You can generalize the property update process by adding a generic function to your class:
For example:
func assign<T:Equatable>(_ variable:inout T?, _ getValue:(SongInfo)->T?) -> Int
{
let newValue = getValue(self)
if variable == newValue
{ return 0 }
var valueIsEmpty = false
if let stringValue = newValue as? String, stringValue == ""
{ valueIsEmpty = true }
if newValue == nil || valueIsEmpty
{
variable = getValue(SongInfo.defaults)
return 1
}
variable = newValue
return 1
}
func update(with newInfo:SongInfo)
{
let updates = newInfo.assign(&albumTitle) {$0.albumTitle}
+ newInfo.assign(&albumArtist) {$0.albumArtist}
+ newInfo.assign(&title) {$0.title}
+ newInfo.assign(&artist) {$0.artist}
+ newInfo.assign(&artwork) {$0.artwork}
+ newInfo.assign(&genre) {$0.genre}
+ newInfo.assign(&lyrics) {$0.lyrics}
// ...
if updates > 0
{
// react to update
}
}
It seems to me, that you're using MPMedia item.
If so, you don't have to store all these properties at all.
You just need to store persistent ID of the item (convert from UInt64 to string), and later fetch MPMediaItem by using MPMediaQuery with predicate, something like this:
func findSong(persistentIDString: String) -> MPMediaItem? {
let predicate = MPMediaPropertyPredicate(value: persistentIDString, forProperty: MPMediaItemPropertyPersistentID)
let songQuery = MPMediaQuery()
songQuery.addFilterPredicate(predicate)
return songQuery.items.first
}