I am new to Swift and i'm trying to mess around with some UITableViews and Arrays.
I have an array of type [Day], named daysArray.
I'm initiating the array in ViewDidLoad(), daysArray has 7 "days" in it.
Also, I have UITableView, which is being populated with the daysArray.
When I'm trying to change one day (one element in the array), the whole daysArray days are changed and consequently, all cells in the UITableView are the same.
This is so peculiar, I really don't know what is wrong (:
The Day class:
import Foundation
class Day {
private var _name: String!
private var _starts: Double!
private var _ends: Double!
var name: String! {
get {
if _name == nil {
_name = "Sunday"
}
return _name
}
set {
_name = newValue
}
}
var starts: Double! {
get {
if _starts == nil {
_starts = 8.00
}
return _starts
}
set {
_starts = newValue
}
}
var ends: Double! {
get {
if _ends == nil {
_ends = 20.00
}
return _ends
}
set {
_ends = newValue
}
}
init(dayDict: Dictionary<String, AnyObject>) {
if let dictName = dayDict["name"] as! String! {
self._name = dictName
}
if let dictIsWorking = dayDict["isWorking"] as! Bool! {
self._isWorking = dictIsWorking
}
if let dictStarts = dayDict["starts"] as! Double! {
self._starts = dictStarts
}
if let dictEnds = dayDict["ends"] as! Double! {
self._ends = dictEnds
}
}
}
The code that seems to be problematic is:
import UIKit
let GeneralDayDict: [String : AnyObject] = ["name" : "Sunday" as AnyObject, "starts": 8.00 as AnyObject, "ends" : 20.00 as AnyObject]
let someDay: Day = Day(dayDict: GeneralDayDict)
class ViewController: UIViewController {
var daysArray: [Day]! = []
override func viewDidLoad() {
super.viewDidLoad()
for i in 0...6 {
let generalDay: Day = someDay
daysArray.append(generalDay)
}
changeArray()
// Do any additional setup after loading the view, typically from a nib.
}
func changeArray() {
daysArray[5].starts = 6.00
for day in daysArray {
print("Each day in the array starts at: ", day.starts)
}
}
}
The print command in changeArray prints this:
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
Each day in the array starts at: 6.0
As I said, very very peculiar...
Thank you for just reading my question, and also for answering of course (:
In your loop you instantiate objects with the same reference.
You have to replace :
let generalDay: Day = someDay
by this :
let generalDay = Day(dayDict: GeneralDayDict)
Then you will be able to change attributes of generalDay individually
Classes in Swift are passed by reference. Meaning each item in your array points to the same thing. So when you update one, you update them all.
Try doing this with a struct instead of using a class and you will see the difference.
See this Documentation for a better explanation and the reasoning behind this.
Set the class of Day to be NSObject
class Day : NSObject {
}
for i in 0...6 {
let generalDay: Day = someDay
daysArray.append(generalDay.copy())
}
then just append the copy of the generalDay Object
Hope it helps!!
Related
I'm tryin' to obtain a list of activities ("dd/mm/YY: goal achieved/missed goal") which has to be setted every week. The problem is that I obtain a list of activities with the same date and the same result of the previous one. For example:
28/02/2022: goal achieved
28/02/2022: goal achieved
28/02/2022: goal achieved
and the next day:
01/03/2022: missed goal
01/03/2022: missed goal
01/03/2022: missed goal
01/03/2022: missed goal
I want to obtain, instead, a list like:
28/02/2022: goal achieved
01/03/2022: missed goal
02/03/2022: goal achieved...
These are useful structs:
struct Persistent {
#AppStorage("goalAchieved") static var goalAchieved : Bool = false
#AppStorage("activityList") static var activityList : [String] = []
}
struct obj {
static var currentDate = Date()
static var stringDate = ""
static var activity = Activity(date:Persistent.lastUpdatedDate)
}
This is the ActivityListView:
import SwiftUI
func activitystring(activity:Activity) -> String{
var output = ""
output = "\(activity.date): \(activity.reachedobj(goalAchieved: Persistent.goalAchieved))"
return output
}
struct Activity: Identifiable{
let id = UUID()
let date: String
func reachedobj(goalAchieved: Bool) -> String {
var output = ""
if Persistent.goalAchieved == false { output = "Missed goal" }
if Persistent.goalAchieved == true { output = "Goal Achieved!"}
return output
}
}
struct ActivityRow: View{
var activity: Activity
var body: some View{
Text(activitystring(activity: activity))
Divider()
}
}
struct ActivityListView: View {
var body: some View {
ScrollView{
Text("Week summary").font(.system(size: 15)).foregroundColor(Color.green)
Text("")
ForEach(Persistent.activityList, id: \.self) { activity in
let activity = Activity(date: Persistent.lastUpdatedDate)
ActivityRow(activity: activity)
}
}
}
}
Finally this is the useful code in the ApplicationApp file (main) where I update activity list:
MenuView().onAppear(){
if Persistent.activityList.count>7{
Persistent.activityList.removeAll()
}
obj.currentDate = Date()
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/YY"
obj.stringDate = formatter.string(from:obj.currentDate)
if Persistent.lastUpdatedDate != obj.stringDate{
Persistent.goalAchieved = false
let activity = Activity(date: Persistent.lastUpdatedDate)
Persistent.activityList.append(activitystring(activity: activity))
Persistent.lastUpdatedDate = obj.stringDate
}
}
What's wrong on this?
You're calling obj.activity in your ForEach and ActivityRow, that's why it repeats that same static property all over the place.
You better just drop your struct obj and try again without it
In your Persistent object you have an array of many activities, called activitylist , but one single boolean that tells if the goal is achieved - goalachieved indeed.
Your view is iterating through the array of Persistent.activitylist, so you will have many lines for one single result - achieved or not achieved. You might actually want to iterate over an array of Persistent objects - meaning that somewhere you should probably store [Persistent] in some variable. In this way, you will see one line only for each result.
If I also may suggest: use the conventions for naming variables, Swift uses "camelCaseConventionForVariables", easier to read than "thewholevariableislowercase"
Edit:
Let me try to change a little bit your code (I would personally change it more radically, but that's not the scope of the answer).
Instead of having only one goalAchieved for all elements on the array activityList, make it a dictionary:
struct Persistent {
// Drop this variable
// #AppStorage("goalAchieved") static var goalAchieved : Bool = false
// Make this a dictionary, the date will be the key and the goalAchieved will be the value
#AppStorage("activityList") static var activityList : [String: Bool] = [:]
}
Add values to the dictionary (#meomeomeo is right, you don't need obj):
MenuView().onAppear() {
if Persistent.activityList.count > 7 {
Persistent.activityList.removeAll()
}
let currentDate = Date()
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/YY"
let stringDate = formatter.string(from: currentDate)
if Persistent.lastUpdatedDate != stringDate {
let activity = Activity(date: Persistent.lastUpdatedDate)
Persistent.activityList[activitystring(activity: activity))] = false // Will create something like ["01/03/2022": false]
Persistent.lastUpdatedDate = stringDate
}
}
Iterate on the dictionary in your ForEach; for more info: read here.
Reading snapshots from firebase is fairly simple, although transferring the information to arrays is more complicated. I have this snapshot
Snap (01-08-2019) {
Sleep = "6.25 hrs";
"Time Uploaded" = "07:10 AM";
}
Snap (01-09-2019) {
Sleep = "6.72 hrs";
"Time Uploaded" = "07:19 AM";
}
Snap (01-10-2019) {
Sleep = "6.55 hrs";
"Time Uploaded" = "07:10 AM";
}
How would I be able to make one array for the date, one for the sleep, and one for the time uploaded.
I think you should reconsider how you store your data in firebase. To Look something similar to this.
Also I would consider to create a data model for day that looks something like this.
class Day {
var date: String
var sleep: String
var timeUploaded: String
init(date: String, sleep: String, timeUploaded: String) {
self.date = date
self.sleep = sleep
self.timeUploaded = timeUploaded
}
}
Then you can just fetch your snapshots like this.
var days = [Day]()
private func fetchDays() {
print(days.count)
let ref = Database.database().reference().child("days")
ref.observeSingleEvent(of: .value) { (snapshot) in
guard let days = snapshot.value as? [String: Any] else { return }
for (_,value) in days.enumerated() {
guard let dayDict = value.value as? [String: String] else { return }
let date = dayDict["date"] ?? ""
let sleep = dayDict["sleep"] ?? ""
let timeUploaded = dayDict["time_uploaded"] ?? ""
//If you really want 3 different arrays just add them here
// dateArray.append(date) and so on for the other two arrays
let day = Day(date: date, sleep: sleep, timeUploaded: timeUploaded)
self.days.append(day)
}
print(self.days.count)
}
}
}
Hope this helps. Couldn't comment to ask how your data was structured.
I would suggest not keeping the data in different arrays, it may be better to store the data from each node within a class, and then keep an array of those classes.
Let's start with a class that keeps all of the data
class ChronoClass {
var node_id = ""
var sleep = ""
var time_uploaded = ""
init(withSnapshot: DataSnapshot) {
let nodeId = withSnapshot.key
let someSleep = withSnapshot.childSnapshot(forPath: "sleep").value as? String ?? ""
let someTime = withSnapshot.childSnapshot(forPath: "time_uploaded").value as? String ?? ""
self.node_id = nodeId
self.sleep = someSleep
self.time_uploaded = someTime
}
}
and then a class array to keep all of the classes
var sleepArray = [ChronoClass]()
and finally the code to read in each node, populate the class and store the classes in an array.
func readFirebaseDataAndPopulateArray() {
let sleepNode = self.ref.child("sleep_node")
sleepNode.observeSingleEvent(of: .value, with : { snapshot in
for child in snapshot.children {
let snap = child as! DataSnapshot
let aChrono = ChronoClass(withSnapshot: snap)
self.sleepArray.append(aChrono)
}
for x in self.sleepArray { //just prints out what's in the array
print(x.node_id, x.sleep, x.time_uploaded)
}
})
}
and the output based on your structure
01-08-2019 6.25 hrs 07:10 AM
01-09-2019 6.72 hrs 07:19 AM
01-10-2019 6.55 hrs 07:10 AM
The advantage with using a class is you can sort, search, extrapolate or do a variety of other functions on the objects instead of working with three separate arrays.
I am trying to save a copy of my custom class to a file, my class has 2 arrays of CGPoints which I append to every so often, they look like this:
class BlockAttributes: NSObject {
var positions:[CGPoint] = []
var spawns:[CGPoint] = []
}
Everything is working great as far as just as using and accessing the class goes, but archiving it does not work. I can archive arrays of Strings, Bools, and Ints just fine in my other classes but my game fails every time I try to use NSCoder to encode my arrays of CGPoints. Here is my code for archiving:
func encodeWithCoder(coder: NSCoder!) {
coder.encodeObject(positions, forKey: "positions")
coder.encodeObject(spawns, forKey: "spawns")
}
....
class ArchiveData: NSObject {
var documentDirectories:NSArray = []
var documentDirectory:String = ""
var path:String = ""
func saveData(data: BlockAttributes) {
documentDirectories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
documentDirectory = documentDirectories.objectAtIndex(0) as! String
path = documentDirectory.stringByAppendingPathComponent("data.archive")
if NSKeyedArchiver.archiveRootObject(data, toFile: path) {
print("Success writing to file!")
} else {
print("Unable to write to file!")
}
}
func retrieveData() -> NSObject {
var dataToRetrieve = BlockAttributes()
documentDirectories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
documentDirectory = documentDirectories.objectAtIndex(0) as! String
path = documentDirectory.stringByAppendingPathComponent("data.archive")
if let dataToRetrieve2 = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? BlockAttributes {
dataToRetrieve = dataToRetrieve2 as BlockAttributes
}
return(dataToRetrieve)
}
}
....
And to save:
let archiveData = ArchiveData()
archiveData.saveData(myBlockActionsObject)
I even tried creating my own custom class to save the CGPoints to, which I call MyCGPoint (I read somewhere on SO that creating custom classes for some data types resolves some NSCoder issues):
class MyCGPoint: NSObject {
var x: CGFloat = 0.0
var y: CGFloat = 0.0
init(X: CGFloat, Y: CGFloat) {
x = X
y = Y
}
override init() {
}
}
....
class BlockAttributes: NSObject {
var positions:[MyCGPoint] = []
var spawns:[MyCGPoint] = []
}
But alas, I am still getting this error:
[Game.MyCGPoint encodeWithCoder:]:
unrecognized selector sent to instance 0x137f1d1a0 Game[20953:5814436]
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[Game.MyCGPoint encodeWithCoder:]:
unrecognized selector sent to instance 0x137f1d1a0'
Any idea how I can use encodeObject to encode my array of CGPoints/MyCGPoints?
You can convert them to and from strings:
//To string
let point = CGPointMake(0, 0)
let string = NSStringFromCGPoint(point)
//Or if you want String instead of NSString
let string = String(point)
//From string
let point2 = CGPointFromString(string)
CGPoint (and its Cocoa's twin NSPoint) are structs, i.e. value type, so you can't encode them directly. Wrap them in NSValue:
let positionValues = positions.map { NSValue(point:$0) }
let spawnValues = spawns.map { NSValue(point:$0) }
coder.encodeObject(positionValues, forKey: "positions")
coder.encodeObject(spawnValues, forKey: "spawns")
// Decode:
positons = (coder.decodeObjectForKey("positions") as! [NSValue]).map { $0.pointValue }
spawns = (coder.decodeObjectForKey("spawns") as! [NSValue]).map { $0.pointValue }
When you write your custom wrapper class, you have to make it compliant with NSCoding too, which NSValeu had already done for you, for free.
I need some help filtering an array of Structs.
This is what I am doing currently, it filters the array but not correctly.
For example lets say I search for an item in the array with "Mid" I have one item that should be shown however the item shown starts with "Bad".
var array = breweries.filter() { $0.name?.lowercaseString.rangeOfString(searchController.searchBar.text.lowercaseString) != nil }
results = array
here is my Struct
struct Breweries {
let name: String?
let breweryId: String?
let distance: Double?
let largeIconURL: String?
let streetAddress: String?
let locality: String?
let region: String?
let phone: String?
let website: String?
init(brewDictionary: [String: AnyObject]) {
name = brewDictionary["brewery"]?["name"] as? String
breweryId = brewDictionary["breweryId"] as? String
distance = brewDictionary["distance"] as? Double
largeIconURL = brewDictionary["brewery"]?["images"]??.objectForKey("large") as? String
streetAddress = brewDictionary["streetAddress"] as? String
locality = brewDictionary["locality"] as? String
region = brewDictionary["region"] as? String
phone = brewDictionary["phone"] as? String
website = brewDictionary["website"] as? String
}
}
Please point in the right direction!
Note: I am using Swift 1.2
Update:
I thought a video would be of help to better explain what I am trying to do.
Demo Of issue
What I want is to find the filter the array so only the item with a similar name is shown.
Update 2: As it turns out I forgot to handle the case when my UISearchController was active.
Assuming your Struct name is Breweries and it has a name property, try this:
let array = breweries.filter() {
($0.name!.lowercaseString as NSString).containsString(searchController.searchBar.text.lowercaseString)
}
Your usage of filter is correct, but your closure seem to be complicated with no clear goal. I suggest you to write an extension (or possibly use what I am using):
extension String {
func contains(search: String, ignoreCase: Bool = false, ignoreDiacritic: Bool = false) -> Bool {
var options = NSStringCompareOptions.allZeros
if ignoreCase { options |= NSStringCompareOptions.CaseInsensitiveSearch }
if ignoreDiacritic { options |= NSStringCompareOptions.DiacriticInsensitiveSearch }
return self.rangeOfString(search, options: options) != nil
}
}
This way you can use closure like this to search:
breweries.filter() {
$0.name?.contains("x") // Precise search
$0.name?.contains("x", ignoreCase: true, ignoreDiacritics: true) // Ignores diacritics and lower / upper case
}
of course, you can use | or & to search for multiple parameters
breweries.filter() {
$0.name?.contains("x") || $0.streetAddress?.contains("x")
}
Hope it helps!
Here is an example from an investing app with struct:
import Foundation
public struct SNStock {
public let ticker:NSString
public let name:NSString
init(ticker:NSString, name:NSString) {
self.ticker = ticker
self.name = name
}
}
Search on Main Thread:
public func searchStocksByKeyword(keyword:String) -> [SNStock] {
let lowercaseKeyword = keyword.lowercaseString
var searchResults:[SNStock] = []
searchResults = stocks.filter({ (stock:SNStock) -> Bool in
return stock.ticker.lowercaseString.hasPrefix(lowercaseKeyword)
})
if (searchResults.count == 0) {
searchResults = stocks.filter({ (stock:SNStock) -> Bool in
return stock.name.lowercaseString.hasPrefix(lowercaseKeyword)
})
}
searchResults.sortInPlace {
($0.ticker as String) < ($1.ticker as String)
}
return searchResults;
}
Search on Background Thread:
public func searchStocksByKeyword(keyword:String, completion:(stocks:[SNStock])->()) {
let qualityOfServiceClass = QOS_CLASS_USER_INTERACTIVE
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
let stocks:[SNStock] = self.searchStocksByKeyword(keyword)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
completion(stocks: stocks)
})
})
}
I'm trying to sort an array as laid out in the accepted answer to this question, but am running into the problem which Isuru mentions in the comments on that answer. Namely, the code which should sort the array by the entity's "date" attribute brings the compiler complaint "could not find member 'date'"
Here is the NSManagedObject subclass describing the entity:
import Foundation
import CoreData
#objc(Entry)
class Entry: NSManagedObject {
#NSManaged var date: NSDate
#NSManaged var reflections: AnyObject
#NSManaged var contactComment: NSSet
#NSManaged var person: NSSet
override func awakeFromInsert() {
let now:NSDate = NSDate()
self.date = now;
}
}
And here is the code which tries to sort the array:
lazy var entries:[Entry] = {
var days:[Entry] = self.managedObjectContext!.requestEntity("Entry")as [Entry]
days.sort({$0.date < $1.date})
var today:Entry = days.last!
println(today.date)
return days
}()
Note that in the latter part of that code, I am able to access and log the "date" property for one of the entries, and the Compiler doesn't have a problem with it.
Is my syntax for sorting correct? Is there another issue with this code I'm not seeing?
This is partly an issue with the Swift compiler not giving you a helpful error. The real issue is that NSDate can't be compared with < directly. Instead, you can use NSDate's compare method, like so:
days.sort({ $0.date.compare($1.date) == NSComparisonResult.OrderedAscending })
Alternatively, you could extend NSDate to implement the Comparable protocol so that it can be compared with < (and <=, >, >=, ==):
public func <(a: NSDate, b: NSDate) -> Bool {
return a.compare(b) == NSComparisonResult.OrderedAscending
}
public func ==(a: NSDate, b: NSDate) -> Bool {
return a.compare(b) == NSComparisonResult.OrderedSame
}
extension NSDate: Comparable { }
Note: You only need to implement < and == and shown above, then rest of the operators <=, >, etc. will be provided by the standard library.
With that in place, your original sort function should work just fine:
days.sort({ $0.date < $1.date })
In Swift 3, dates are now directly comparable:
let aDate = Date()
let bDate = Date()
if aDate < bDate {
print("ok")
}
Old swift:
An alternative would be to sort on the timeIntervalSince1970 property of the date object, which is directly comparable.
days.sort({$0.date.timeIntervalSince1970 < $1.date.timeIntervalSince1970})
In swift2.1 use this lines of code
array.sortInPlace({ $0.date.compare($1.date) == NSComparisonResult.OrderedAscending })
Update__ Swift 4,
let sortedData = dataToSort.sorted(by: { (obj1, obj2) -> Bool in
return obj1.date < obj2. date
})
I having one dictionary whose keys are in the date format so i have to sort the dictionary by date . i copied all the keys in nsarray then sort that array by using the following code.
let keys : NSArray = stackedBarDataDict.allKeys // stackedBarDataDict is Data Dictionary
let dataArray = NSMutableArray()
for index in 0...(stackedBarDataDict.allKeys.count - 1)
{
let dateone = keys.objectAtIndex(index)
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let date1 = dateFormatter.dateFromString(dateone as! String)
dataArray.addObject(date1!)
}
let array : NSArray = (dataArray as NSArray)
let sortedArray = array.sortedArrayUsingComparator {
(obj1, obj2) -> NSComparisonResult in
let p1 = obj1 as! NSDate
let p2 = obj2 as! NSDate
let result = p1.compare(p2)
return result
}
print(sortedArray)