The problem is described in title, but to be more specific here is a full picture.
I have a custom table view cell subclass with label inside it displaying the countdown timer. When there a small portion of timers it works fine, but with a lot of data I need to display timers far beyond the visible cells and when I scroll down fast and then scroll up fast, the timer values in cells start to show different values until a certain point in time, after which it shows the right value.
I tried different variants for those reuseable cells, but I can’t spot a problem. Help needed!!!
Here is the code of implementation of logic.
Custom cell subclass:
let calendar = Calendar.current
var timer: Timer?
var deadlineDate: Date? {
didSet {
updateTimeLabel()
}
}
override func awakeFromNib() {
purchaseCellCardView.layer.cornerRadius = 10
let selectedView = UIView(frame: CGRect.zero)
selectedView.backgroundColor = UIColor.clear
selectedBackgroundView = selectedView
}
override func prepareForReuse() {
super.prepareForReuse()
if timer != nil {
print("Invalidated!")
timer?.invalidate()
timer = nil
}
}
func configure(for purchase: Purchase) {
purchaseSubjectLabel.text = purchase.subject
startingPriceLabel.text = purchase.NMC
stageLabel.text = purchase.stage
fzImageView.image = purchase.fedLaw.contains("44") ? #imageLiteral(resourceName: "FZ44") : #imageLiteral(resourceName: "FZ223")
timeLabel.isHidden = purchase.stage == "Работа комиссии"
warningImageView.image = purchase.warningImage
}
func updateTimeLabel() {
setTimeLeft()
timer = Timer(timeInterval: 1, repeats: true) { [weak self] _ in
guard let strongSelf = self else {return}
strongSelf.setTimeLeft()
}
RunLoop.current.add(timer!, forMode: .commonModes)
}
#objc private func setTimeLeft() {
let currentDate = getCurrentLocalDate()
if deadlineDate?.compare(currentDate) == .orderedDescending {
var components = calendar.dateComponents([.day, .hour, .minute, .second], from: currentDate, to: deadlineDate!)
let dayText = (components.day! == 0 || components.day! < 0) ? "" : String(format: "%i", components.day!)
let hourText = (components.hour == 0 || components.hour! < 0) ? "" : String(format: "%i", components.hour!)
switch (dayText, hourText) {
case ("", ""):
timeLabel.text = String(format: "%02i", components.minute!) + ":" + String(format: "%02i", components.second!)
case ("", _):
timeLabel.text = hourText + " ч."
default:
timeLabel.text = dayText + " дн."
}
} else {
stageLabel.text = "Работа комиссии"
timeLabel.text = ""
timeLabel.isHidden = true
timer?.invalidate()
}
}
private func getCurrentLocalDate() -> Date {
var now = Date()
var nowComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: now)
nowComponents.timeZone = TimeZone(abbreviation: "UTC")
now = calendar.date(from: nowComponents)!
return now
}
deinit {
print("DESTROYED")
timer?.invalidate()
timer = nil
}
The most important part of tableView(_cellForRowAt:)
case .results:
if filteredArrayOfPurchases.isEmpty {
let cell = tableView.dequeueReusableCell(
withIdentifier: TableViewCellIdentifiers.nothingFoundCell,
for: indexPath)
let label = cell.viewWithTag(110) as! UILabel
switch segmentedControl.index {
case 1:
label.text = "Нет закупок способом\n«Запрос предложений»"
case 2:
label.text = "Нет закупок способом\n«Конкурс»"
case 3:
label.text = "Нет закупок способом\n«Аукцион»"
default:
label.text = "Нет закупок способом\n«Запрос котировок»"
}
return cell
} else {
let cell = tableView.dequeueReusableCell(
withIdentifier: TableViewCellIdentifiers.purchaseCell,
for: indexPath) as! PurchaseCell
cell.containerViewTopConstraint.constant = indexPath.row == 0 ? 8.0 : 4.0
cell.containerViewBottomConstraint.constant = indexPath.row == filteredArrayOfPurchases.count - 1 ? 8.0 : 4.0
let purchase = filteredArrayOfPurchases[indexPath.row]
cell.configure(for: purchase)
if cell.timer != nil {
cell.updateTimeLabel()
} else {
search.getDeadlineDateAndTimeToApply(purchase.purchaseURL, purchase.fedLaw, purchase.stage, completion: { (date) in
cell.deadlineDate = date
})
}
return cell
}
And the last piece of a puzzle:
func getDeadlineDateAndTimeToApply(_ url: URL?, _ fedLaw: String, _ stage: String, completion: #escaping (Date) -> ()) {
var deadlineDateAndTimeToApply = Date()
guard stage != "Работа комиссии" else { return }
if let url = url {
dataTask = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error as NSError?, error.code == -403 {
// TODO: Add alert here
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data, let html = String(data: data, encoding: .utf8), let purchasePageBody = try? SwiftSoup.parse(html), let purchaseCard = try? purchasePageBody.select("td").array() else {return}
let mappedArray = purchaseCard.map(){String(describing: $0)}
if fedLaw.contains("44") {
guard let deadlineDateToApplyString = try? purchaseCard[(mappedArray.index(of: "<td class=\"fontBoldTextTd\">Дата и время окончания подачи заявок</td>"))! + 1].text().components(separatedBy: " ") else {return}
dateFormatter.dateFormat = "dd.MM.yyyy HH:mm"
let deadlineDateToApply = deadlineDateToApplyString.first!
let deadlineTimeToApply = deadlineDateToApplyString[1]
guard let deadlineDateAndTimeToApplyCandidate = dateFormatter.date(from: "\(deadlineDateToApply) \(deadlineTimeToApply)") else {return}
deadlineDateAndTimeToApply = deadlineDateAndTimeToApplyCandidate
} else {
guard let deadlineDateToApplyString = try? purchaseCard[(mappedArray.index(of: "<td>Дата и время окончания подачи заявок<br> (по местному времени заказчика)</td>"))! + 1].text().components(separatedBy: " ") else {return}
dateFormatter.dateFormat = "dd.MM.yyyy HH:mm"
let deadlineDateToApply = deadlineDateToApplyString.first!
let deadlineTimeToApply = deadlineDateToApplyString[2]
guard let deadlineDateAndTimeToApplyCandidate = dateFormatter.date(from: "\(deadlineDateToApply) \(deadlineTimeToApply)") else {return}
deadlineDateAndTimeToApply = deadlineDateAndTimeToApplyCandidate
}
DispatchQueue.main.async {
completion(deadlineDateAndTimeToApply)
}
})
dataTask?.resume()
}
}
A few notes:
Tried resetting deadlineDate to nil in prepareForReuse() - doesn’t help;
Using SwiftSoup Framework to parse HTML as you can see in the last code example if it matters.
This is quite a lot of code but from what you are describing your issue is in reusing cells.
You would do well to separate the timers out of the cells and put them inside your objects. It is where they belong (or in some manager like view controller). Imagine having something like the following:
class MyObject {
var timeLeft: TimeInterval = 0.0 {
didSet {
if timeLeft > 0.0 && timer == nil {
timer = Timer.scheduled...
} else if timeLeft <= 0.0, let timer = timer {
timer.invalidate()
self.timer = nil
}
delegate?.myObject(self, updatedTimeLeft: timeLeft)
}
}
weak var delegate: MyObjectDelegate?
private var timer: Timer?
}
Now all you need is is a cell for row at index path to assign your object: cell.myObject = myObjects[indexPath.row].
And your cell would do something like:
var myObject: MyObject? {
didSet {
if oldValue.delegate == self {
oldValue.delegate = nil // detach from previous item
}
myObject.delegate = self
refreshUI()
}
}
func myObject(_ sender: MyObject, updatedTimeLeft timeLeft: TimeInterval) {
refreshUI()
}
I believe the rest should be pretty much straight forward...
Your problem is here:
search.getDeadlineDateAndTimeToApply(purchase.purchaseURL,
purchase.fedLaw,
purchase.stage,
completion: { (date) in
cell.deadlineDate = date
})
getDeadlineDateAndTimeToApply runs asynchronously, calculates something, and then updates the cell.deadlineData in the main thread (which is fine). But in the meantime, while calculating something, the user might have scrolled up and down, the cell might have been reused for another row, and now the update updates the cell incorrectly.
What you need to do is: Do not store the UITableViewCell directly. Instead, keep track of the IndexPath to be updated, and once the caluclation is done, retrieve the the cell that belongs to that IndexPath and update this.
Related
I'm trying to include timers so that a button appears a week, a month, 2 months, etc. after the initial launch date of the app.
I've used Date.addingTimeInterval (see code below), but it doesn't seem as effective for longer periods of time (week/months). The Swift documentation recommends using Calendar, but I'm not sure how to do that.
override func viewDidLoad() {
super.viewDidLoad()
if installedDate == nil {
installedDate = Date()
let timer = Calendar.current
Button1.isHidden = true
Button2.isHidden = true
Button3.isHidden = true
}
else {
print("Not first run")
print(installedDate!)
let Button1Date = Date().addingTimeInterval(604800) //for after 1 week
let Button2Date = Date().addingTimeInterval(2592000) //for after 1 month
let Button3Date = Date().addingTimeInterval(5184000) //for after 2 months
if installedDate == Button1Date {
Button1.isHidden = false
}
if installedDate == Button2Date {
Button2.isHidden = false
}
if installedDate == Button3Date {
Button.isHidden = false
}
}
}
var installedDate: Date? {
get {
return UserDefaults.standard.object(forKey: "installedDateKey") as? Date
}
set {
UserDefaults.standard.set(newValue, forKey: "installedDateKey")
}
}
What is a more efficient way of comparing the launch date to the expected date I need?
From what I understand of your question, you want a button to display if they have had the app for more than a certain amount of time. If that is the case, I've written some code that will be helpful to you:
let timeSinceInstalled = -(installedDate ?? Date()).timeIntervalSinceNow
Button1.isHidden = timeSinceInstalled < 604800
Button2.isHidden = timeSinceInstalled < 2592000
Button3.isHidden = timeSinceInstalled < 5184000
You can actually replace most of the code inside of your viewDidLoad() function, even the part where you check if installedDate == nil. Here is your updated code:
override func viewDidLoad() {
super.viewDidLoad()
let timeSinceInstalled = -(installedDate ?? Date()).timeIntervalSinceNow
Button1.isHidden = timeSinceInstalled < 604800
Button2.isHidden = timeSinceInstalled < 2592000
Button3.isHidden = timeSinceInstalled < 5184000
}
var installedDate: Date? {
get {
return UserDefaults.standard.object(forKey: "installedDateKey") as? Date
}
set {
UserDefaults.standard.set(newValue, forKey: "installedDateKey")
}
}
If you want to set the title to "You have none available" when the button should not be clickable, here is the code for that:
override func viewDidLoad() {
super.viewDidLoad()
let timeSinceInstalled = -(installedDate ?? Date()).timeIntervalSinceNow
setTitle(Button1, timeSinceInstalled < 604800)
setTitle(Button2, timeSinceInstalled < 2592000)
setTitle(Button3, timeSinceInstalled < 5184000)
}
func setTitle(_ button: UIButton, _ statement: Bool) {
button.setTitle(statement ? "You have none available" : "Click me", for: .normal)
button.isEnabled = !statement
}
var installedDate: Date? {
get {
return UserDefaults.standard.object(forKey: "installedDateKey") as? Date
}
set {
UserDefaults.standard.set(newValue, forKey: "installedDateKey")
}
}
I am using xcode 8 and swift 3. I have made a mini game where you have to answer questions as fast as you can. You are timed by a stopwatch. I have the code to actually store the times(eg 23 seconds) but instead of storing the time, it replaces it with "Label". My stopwatch label is called down and the label that displays the values is called resultLabel I have been told the problem is when i set the text of the label. Any ideas on where i need to fix it. An example is ["Label", "Label", "Label"]
Stopwatch code:
if timerSecond != nil {
timerSecond?.invalidate()
timerSecond = nil
} else {
timerSecond = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.currentTime += 1
let minutePortion = String(format: "%02d", self.currentTime / 60 )
let secondsPortion = String(format: "%02d", self.currentTime % 60 )
self.down.text = "\(minutePortion):\(secondsPortion)"
}
}
extension for User Defaults:
extension UserDefaults {
var timesSecond: [String] {
get {
if let times = UserDefaults.standard.object(forKey: "times") as? [String] {
return times
} else {
return []
}
}
set {
UserDefaults.standard.set(newValue, forKey: "times")
}
}
}
Button code to display the array of values:
let arrayOfTimes = UserDefaults.standard.timesSecond
resultLabel.text = "\(arrayOfTimes)"
code for storing the data:
UserDefaults.standard.timesSecond.append(resultLabel.text!)
I have a Swift script below relating to Sleep Analysis.
Error:
View Controller has no member 'updateTime'.
I tried adding this target using file inspector and adding it to target membership, but the target itself won't show up, which is beyond weird. Any feedback will be greatly appreciated.
PS: Another error message not saying that 'error' is an unresolved identifier for an if != nil statement keeps popping up. Any help here, would also be highly appreciated.
import UIKit
import HealthKit
let healthStore = HKHealthStore()
class ViewController: UIViewController {
#IBOutlet var displayTimeLabel: UILabel!
var startTime = TimeInterval()
var timer:Timer = Timer()
var endTime: NSDate!
var alarmTime: NSDate!
func saveSleepAnalysis() {
//1. startTime(alarmTime) and endTime are NSDate Objects//
if let sleepType = HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier.sleepAnalysis) {
//we create a new object that we want to add into our Health app(This is our INBED object)//
let object1 = HKCategorySample(type:sleepType, value: HKCategoryValueSleepAnalysis.inBed.rawValue, start: self.alarmTime as Date, end: self.endTime as Date)
// Time to save the object//
healthStore.save(object1, withCompletion: { (success, errpr) -> Void in
if error != nil
{
return
}
if success {
print("My new data was saved in HealthKit")
} else {
//something happened again//
}
})
//This our ASLEEP object//
let object2 = HKCategorySample(type:sleepType, value: HKCategoryValueSleepAnalysis.asleep.rawValue, start: self.alarmTime as Date, end: self.endTime as Date)
//now we save our objects to our mainLibrary known as HealthStore
healthStore.save(object2, withCompletion: { (success, error) -> Void in
if error != nil {
//Something went wrong//
return
if success {
print("My new data (2: Asleep data) was saved into HealthKit")
} else {
//something happened again//
}
}
}
)}
func retrieveSleepAnalysis() {
//first, define our object type that we watn again in BOOLEAN FORMAT//
if let sleepType = HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier.sleepAnalysis) {
//use sortDescriptor to get teh recent data first: so from MostRecentData to PastData//
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
//we create our query with a block completion to execute
let query = HKSampleQuery(sampleType: sleepType, predicate: nil, limit:30, sortDescriptors: [sortDescriptor]) { (query, tmpResult, error) -> Void in
if error != nil {
//something happends//
return
}
if let result = tmpResult {
//then i want the computer to do something with my data//
for item in result {
if let sample = item as? HKCategorySample {
let value = (sample.value == HKCategoryValueSleepAnalysis.inBed.rawValue) ? "InBed" : "Asleep"
print("Healthkit sleep: \(sample.startDate) \(sample.endDate) = value: \(value)")
}
}
}
}
//finally, we execute our query: Print out our output file //
healthStore.execute(query)
}
}
func viewDidLoad() {
super.viewDidLoad()
let typestoRead = Set([
HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier.sleepAnalysis)!
])
let typestoShare = Set([
HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier.sleepAnalysis)!
])
healthStore.requestAuthorization(toShare: typestoShare, read: typestoRead) { (success, error) -> Void in
if success == false {
NSLog(" Display not allowed")
}
}
}
func updateTime() {
let currentTime = NSDate.timeIntervalSinceReferenceDate
//Find the difference between current time and start time.
var elapsedTime: TimeInterval = currentTime - startTime
//calculate the minutes in elapsed time.
let minutes = UInt8(elapsedTime / 60.0)
elapsedTime -= (TimeInterval(minutes) * 60)
//calculate the seconds in elapsed time.
let seconds = UInt8(elapsedTime)
elapsedTime -= TimeInterval(seconds)
//find out the fraction of milliseconds to be displayed.
let fraction = UInt8(elapsedTime * 100)
//add the leading zero for minutes, seconds and millseconds and store them as string constants
let strMinutes = String(format: "%02d", minutes)
let strSeconds = String(format: "%02d", seconds)
let strFraction = String(format: "%02d", fraction)
//concatenate minuets, seconds and milliseconds as assign it to the UILabel
displayTimeLabel.text = "\(strMinutes):\(strSeconds):\(strFraction)"
}
func start(sender: AnyObject) {
alarmTime = NSDate()
if (!timer.isValid) {
let Selector : Selector = #selector(ViewController.updateTime)
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: Selector, userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate
}
}
func stop(sender: AnyObject) {
endTime = NSDate()
saveSleepAnalysis()
retrieveSleepAnalysis()
timer.invalidate()
}
func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
}
Replace your code with this
import UIKit
import HealthKit
let healthStore = HKHealthStore()
class ViewController: UIViewController {
#IBOutlet var displayTimeLabel: UILabel!
var startTime = TimeInterval()
var timer:Timer = Timer()
var endTime: NSDate!
var alarmTime: NSDate!
func saveSleepAnalysis() {
//1. startTime(alarmTime) and endTime are NSDate Objects//
if let sleepType = HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier.sleepAnalysis) {
//we create a new object that we want to add into our Health app(This is our INBED object)//
let object1 = HKCategorySample(type:sleepType, value: HKCategoryValueSleepAnalysis.inBed.rawValue, start: self.alarmTime as Date, end: self.endTime as Date)
// Time to save the object//
healthStore.save(object1, withCompletion: { (success, errpr) -> Void in
if errpr != nil
{
return
}
if success {
print("My new data was saved in HealthKit")
} else {
//something happened again//
}
})
//This our ASLEEP object//
let object2 = HKCategorySample(type:sleepType, value: HKCategoryValueSleepAnalysis.asleep.rawValue, start: self.alarmTime as Date, end: self.endTime as Date)
//now we save our objects to our mainLibrary known as HealthStore
healthStore.save(object2, withCompletion: { (success, error) -> Void in
if error != nil {
//Something went wrong//
return
if success {
print("My new data (2: Asleep data) was saved into HealthKit")
} else {
//something happened again//
}
}
}
)}
}
func retrieveSleepAnalysis() {
//first, define our object type that we watn again in BOOLEAN FORMAT//
if let sleepType = HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier.sleepAnalysis) {
//use sortDescriptor to get teh recent data first: so from MostRecentData to PastData//
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
//we create our query with a block completion to execute
let query = HKSampleQuery(sampleType: sleepType, predicate: nil, limit:30, sortDescriptors: [sortDescriptor]) { (query, tmpResult, error) -> Void in
if error != nil {
//something happends//
return
}
if let result = tmpResult {
//then i want the computer to do something with my data//
for item in result {
if let sample = item as? HKCategorySample {
let value = (sample.value == HKCategoryValueSleepAnalysis.inBed.rawValue) ? "InBed" : "Asleep"
print("Healthkit sleep: \(sample.startDate) \(sample.endDate) = value: \(value)")
}
}
}
}
//finally, we execute our query: Print out our output file //
healthStore.execute(query)
}
}
override func viewDidLoad() {
super.viewDidLoad()
let typestoRead = Set([
HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier.sleepAnalysis)!
])
let typestoShare = Set([
HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier.sleepAnalysis)!
])
healthStore.requestAuthorization(toShare: typestoShare, read: typestoRead) { (success, error) -> Void in
if success == false {
NSLog(" Display not allowed")
}
}
}
func updateTime() {
let currentTime = NSDate.timeIntervalSinceReferenceDate
//Find the difference between current time and start time.
var elapsedTime: TimeInterval = currentTime - startTime
//calculate the minutes in elapsed time.
let minutes = UInt8(elapsedTime / 60.0)
elapsedTime -= (TimeInterval(minutes) * 60)
//calculate the seconds in elapsed time.
let seconds = UInt8(elapsedTime)
elapsedTime -= TimeInterval(seconds)
//find out the fraction of milliseconds to be displayed.
let fraction = UInt8(elapsedTime * 100)
//add the leading zero for minutes, seconds and millseconds and store them as string constants
let strMinutes = String(format: "%02d", minutes)
let strSeconds = String(format: "%02d", seconds)
let strFraction = String(format: "%02d", fraction)
//concatenate minuets, seconds and milliseconds as assign it to the UILabel
displayTimeLabel.text = "\(strMinutes):\(strSeconds):\(strFraction)"
}
func start(sender: AnyObject) {
alarmTime = NSDate()
if (!timer.isValid) {
let Selector : Selector = #selector(ViewController.updateTime)
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: Selector, userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate
}
}
func stop(sender: AnyObject) {
endTime = NSDate()
saveSleepAnalysis()
retrieveSleepAnalysis()
timer.invalidate()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
im having a real nightmare with my project where i need to save cell contents to an object, for each object in an array. I cant get this to work by looping through table cells adn array objects and trying to match them all up.
So my next idea was to add didFinishEditing related functions into the cellForRowAt function?
Im not sure this would work either, but this is what i have:
Each row here has a label for the set, a picker for the reps that can be scrolled to a number, and a textfield to put a weight. Then i save each row as an object storing the set, rep and weight.
Issue is when editing this, how can i save these again overwriting the old values? Hence my plan above to use didFinishEditing methods.
My previous plan was the code below, but i cant figure out the annotated part. So i was hoping someone had guidance on how i can approach saying when editing rather than this save button function that doesnt work!
func saveUserExerciseSets() {
if userExercise == nil {
print("CREATING A FRESH SET OF SETS FOR THE NEW EXERCISE")
for cell in self.customSetsTable.visibleCells as! Array<NewExerciseTableViewCell> {
print("SAVING THESE CELLS \(customSetsTable.visibleCells)")
let newUserExerciseSet = UserExerciseSet(context: self.managedObjectContext)
newUserExerciseSet.setPosition = Int64(cell.setNumber.text!)!
newUserExerciseSet.setReps = Int64(cell.repsPicker.selectedRow(inComponent: 0))
newUserExerciseSet.parentExerciseName = self.userExerciseName.text
if self.localeIdentifier == "en_GB" {
let kgWeight = Measurement(value: Double(cell.userExerciseWeight.text!)!, unit: UnitMass.kilograms)
newUserExerciseSet.setWeight = kgWeight as NSObject?
newUserExerciseSet.initialMetricSystem = self.localeIdentifier
} else if self.localeIdentifier == "en_US" {
let lbsWeight = Measurement(value: Double(cell.userExerciseWeight.text!)!, unit: UnitMass.pounds)
newUserExerciseSet.setWeight = lbsWeight as NSObject?
newUserExerciseSet.initialMetricSystem = self.localeIdentifier
}
let fetchRequest: NSFetchRequest<UserExercise> = UserExercise.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "name == %#", self.exerciseNameToAddTo!)
do {
let parentExercise = try self.managedObjectContext.fetch(fetchRequest).first
parentExercise?.addToExercisesets(newUserExerciseSet)
print("SET ADDED TO EXERCISE")
} catch {
print("Fetching Routine Failed")
}
}
} else if self.userExercise != nil {
print("UPDATING EXISTING SETS FOR THE EXISTING EXERCISE")
let cells = self.customSetsTable.visibleCells as! Array<NewExerciseTableViewCell>
for cell in cells {
let exerciseSets = self.userExercise?.exercisesets?.allObjects as! [UserExerciseSet]
let sortedexerciseSets = exerciseSets.sorted { ($0.setPosition < $1.setPosition) }
let cellsSet = sortedexerciseSets //match the sortedexerciseSets set object to the cell index positions
cellsSet.setPosition = Int64(setsCell.setNumber.text!)!
cellsSet.setReps = Int64(setsCell.repsPicker.selectedRow(inComponent: 0))
if self.localeIdentifier == "en_GB" {
let kgWeight = Measurement(value: Double(cell.userExerciseWeight.text!)!, unit: UnitMass.kilograms)
cellsSet.setWeight = kgWeight as NSObject?
} else if self.localeIdentifier == "en_US" {
let lbsWeight = Measurement(value: Double(cell.userExerciseWeight.text!)!, unit: UnitMass.pounds)
cellsSet.setWeight = lbsWeight as NSObject?
}
cellsSet.parentExerciseName = self.userExerciseName.text
}
}
do {
try self.managedObjectContext.save()
print("THE SET HAS BEEN SAVED")
} catch {
fatalError("Failure to save context: \(error)")
}
delegate?.didFinishEditing()
self.dismiss(animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? NewExerciseTableViewCell
else {
fatalError("Unexpected Index Path")
}
cell.backgroundColor = UIColor.customBackgroundGraphite()
cell.textLabel?.textColor = UIColor.white
cell.repsPicker.dataSource = self
cell.repsPicker.delegate = self
configure(cell, at: indexPath)
return cell
}
func configure(_ cell: NewExerciseTableViewCell, at indexPath: IndexPath) {
// configuring cells when theres a loaded exercise causes the issues --------------------
if self.userExercise != nil {
print("RESTORING CELLS FOR THE EXISTING EXERCISE")
let unsortedExerciseSets = self.userExercise?.exercisesets?.allObjects as! [UserExerciseSet]
let exerciseSets = unsortedExerciseSets.sorted { ($0.setPosition < $1.setPosition) }
let cellsSet = exerciseSets[indexPath.row]
cell.setNumber.text = String((indexPath.row) + 1)
let indexRow = Int(cellsSet.setReps)
print("INDEX ROW INT IS \(indexRow)")
cell.repsPicker.selectRow(indexRow, inComponent: 0, animated: true) //fix this crashing issue!
let localeIdentifier = Locale(identifier: UserDefaults.standard.object(forKey: "locale") as! String)
let setWeight = cellsSet.setWeight as! Measurement<UnitMass>
let formatter = MassFormatter()
formatter.numberFormatter.locale = localeIdentifier
formatter.numberFormatter.maximumFractionDigits = 2
if localeIdentifier.usesMetricSystem {
let kgWeight = setWeight.converted(to: .kilograms)
let finalKgWeight = formatter.string(fromValue: kgWeight.value, unit: .kilogram)
let NumericKgResult = finalKgWeight.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.").inverted)
cell.userExerciseWeight.text = NumericKgResult
} else {
let lbsWeight = setWeight.converted(to: .pounds)
let finalLbWeight = formatter.string(fromValue: lbsWeight.value, unit: .pound)
let NumericLbResult = finalLbWeight.trimmingCharacters(in: CharacterSet(charactersIn: "0123456789.").inverted)
cell.userExerciseWeight.text = NumericLbResult
}
} else if self.userExercise == nil {
print("NEW SET CELL ADDED FOR FRESH EXERCISE")
cell.setNumber.text = String((indexPath.row) + 1)
}
}
Try something like this to match the setIds correctly. That's where I think the issue is.
for x in sortedexerciseSets {
if x.setPosition == Int64(setsCell.setNumber.text!)! {
//save
}
}
Proper way to do it would be to have an array of those sets (I guess, since you tagged core-data, they are instances of NSManagedObject?). When user does ANY change in the cell (write new value in the text field or scroll to another value for reps) you need to update the approproate object in your array immediately. Then you could call save on NSManagedObjectContext when you're sure you want to save changes, or just call rollback on the context to cancel all changes.
In my project I am creating HKStatisticsCollectionQueries for a series of HKQuantityTypes. The resultsHandler then adds this data to an date-ordered array of objects. I want to do another operation only when the entire series of HKStatisticsCollectionQueries have been processed and the results appended to my array.
I have tried to do this by putting the task inside of a subclass of NSOperation, but the dependent block is fired before any of the samples are added to the array. According to the HKStatisticsCollectionQuery documentation "This method runs the query on an anonymous background queue. When the query is complete, it executes the results handler on the same background queue"
Is there a way to use HKStatisticsCollectionQuery's initialResultsHandler and statisticsUpdateHandler with NSOperation?
when I run this I get the following output:
cycleOperation start
cycleOperation CompletionBlock
dependentOperation start
dependentOperation CompletionBlock
SumStatistics addSamplesToArray: cycle: 96 samples added
SumStatistics main complete: 96 samples added
func getCycleKm(){
let sampleType = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierDistanceCycling)
let hkUnit = HKUnit.meterUnitWithMetricPrefix(.Kilo)
println("cycleOperation start")
let cycleOperation = SumStatistics(quantityType: sampleType, startDate: startingDate, heathDataArray: self.healthDataArray)
cycleOperation.completionBlock = {println("cycleOperation CompletionBlock ")}
let dependentOperation = NSBlockOperation{()->Void in println("dependentOperation start")}
dependentOperation.completionBlock = {println("dependentOperation CompletionBlock")}
dependentOperation.addDependency(cycleOperation)
self.operationQueue.addOperation(cycleOperation)
self.operationQueue.addOperation(dependentOperation)
}
class SumStatistics:NSOperation{
let healthKitStore:HKHealthStore = HKHealthStore()
private let quantityType:HKQuantityType
private let startDate:NSDate
private let endDate: NSDate
private let statsOption: HKStatisticsOptions
var healthDataArray:[HealthData]
required init(quantityType:HKQuantityType, startDate:NSDate, heathDataArray:[HealthData]){
self.quantityType = quantityType
self.startDate = startDate
let startOfToday = NSDate().getStartOfDate()
self.endDate = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: startOfToday, options: nil)!
self.statsOption = HKStatisticsOptions.CumulativeSum
self.healthDataArray = heathDataArray
super.init()
}
override func main() {
getSumStatistics { (hkSamples, statsError) -> Void in
self.addSamplesToArray(hkSamples)
println("SumStatistics main complete: \(hkSamples.count) samples added")
}
}
func addSamplesToArray(newSamples:[HKQuantitySample]){
var samples = newSamples
samples.sort({$0.startDate.timeIntervalSinceNow > $1.startDate.timeIntervalSinceNow})
if samples.count == 0{
println("SumStatistics addSamplesToArray: no samples!")
return
}
var ctr = 0
var typeString = ""
for healthDataDate in self.healthDataArray{
while healthDataDate.date.isSameDate(samples[ctr].startDate) && ctr < samples.count - 1{
switch samples[ctr].quantityType.identifier {
case HKQuantityTypeIdentifierBodyMass:
healthDataDate.weight = samples[ctr].quantity
typeString = "weight"
case HKQuantityTypeIdentifierDietaryEnergyConsumed:
healthDataDate.dietCalories = samples[ctr].quantity
typeString = "diet"
case HKQuantityTypeIdentifierActiveEnergyBurned:
healthDataDate.activeCalories = samples[ctr].quantity
typeString = "active"
case HKQuantityTypeIdentifierBasalEnergyBurned:
healthDataDate.basalCalories = samples[ctr].quantity
typeString = "basal"
case HKQuantityTypeIdentifierStepCount:
healthDataDate.steps = samples[ctr].quantity
typeString = "steps"
case HKQuantityTypeIdentifierDistanceCycling:
healthDataDate.cycleKM = samples[ctr].quantity
typeString = "cycle"
case HKQuantityTypeIdentifierDistanceWalkingRunning:
healthDataDate.runWalkKM = samples[ctr].quantity
typeString = "runWalk"
default:
println("SumStatistics addSamplesToArray type not found -> \(samples[ctr].quantityType)")
}
if ctr < samples.count - 1{
ctr += 1
}else{
break
}
}
}
println("SumStatistics addSamplesToArray: \(typeString): \(newSamples.count) samples added")
}
func getSumStatistics(completionHandler:([HKQuantitySample], NSError!)->Void){
let dayStart = NSCalendar.currentCalendar().startOfDayForDate(startDate)
let addDay = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: endDate, options:nil)
let dayEnd = NSCalendar.currentCalendar().startOfDayForDate(addDay!) //add one day
let interval = NSDateComponents()
interval.day = 1
let predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: HKQueryOptions.None)
let newQuery = HKStatisticsCollectionQuery(quantityType: quantityType,
quantitySamplePredicate: predicate,
options: statsOption,
anchorDate: dayStart,
intervalComponents: interval)
newQuery.initialResultsHandler = {
query, statisticsCollection, error in
var resultsArray = [HKQuantitySample]()
if error != nil {
println("*** An error occurred while calculating the statistics: \(error.localizedDescription) ***")
}else{
statisticsCollection.enumerateStatisticsFromDate(self.startDate, toDate: self.endDate, withBlock: { (statistics, stop) -> Void in
if let statisticsQuantity = statistics.sumQuantity() {
let startD = NSCalendar.currentCalendar().startOfDayForDate(statistics.startDate)
let endD = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: startD, options: nil)
let qSample = HKQuantitySample(type: self.quantityType, quantity: statisticsQuantity, startDate: startD, endDate: endD)
resultsArray.append(qSample)
}
})
}
completionHandler(resultsArray,error)
}
newQuery.statisticsUpdateHandler = {
query, statistics, statisticsCollection, error in
println("*** updateHandler fired")
var resultsArray = [HKQuantitySample]()
if error != nil {
println("*** An error occurred while calculating the statistics: \(error.localizedDescription) ***")
}else{
statisticsCollection.enumerateStatisticsFromDate(self.startDate, toDate: self.endDate, withBlock: { (statistics, stop) -> Void in
if let statisticsQuantity = statistics.sumQuantity() {
let startD = NSCalendar.currentCalendar().startOfDayForDate(statistics.startDate)
let endD = NSCalendar.currentCalendar().dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: startD, options: nil)
let qSample = HKQuantitySample(type: self.quantityType, quantity: statisticsQuantity, startDate: startD, endDate: endD)
resultsArray.append(qSample)
}
})
}
completionHandler(resultsArray,error)
}
self.healthKitStore.executeQuery(newQuery)
}
}
The short answer was RTFM! (more carefully). I am leaving my original question and code as is, and adding the solution here.
Thanks to this post for helping me figure this out: http://szulctomasz.com/ios-second-try-to-nsoperation-and-long-running-tasks/ And of course, as I found, there is no substitute for a careful reading of the Reference: https://developer.apple.com/library/ios/documentation/Cocoa/Reference/NSOperation_class/
The problem was that I was not subclassing NSOperation properly for running concurrent operations. One must add the concurrent operations to start() rather than main() and then use KVO to update the finished and executing properties, which are what signals that an operation is complete.
I needed to modify the class above to include:
private var _executing = false
private var _finished = false
override var executing:Bool{return _executing}
override var finished:Bool{return _finished}
override func cancel() {
super.cancel()
finish()
}
override func start() {
if cancelled{
finish()
return
}
willChangeValueForKey("isExecuting")
_executing = true
didChangeValueForKey("isExecuting")
getSumStatistics { (hkSamples, statsError) -> Void in
println("LoadStatistics getStatistics completion: \(hkSamples.count) samples")
self.addSamplesToArray(hkSamples, completionHandler: { (success) -> Void in
println("LoadStatistics addSamplesToArray completion")
self.finish()
})
self.completion(true)
}
main()
}
func finish(){
willChangeValueForKey("isExecuting")
willChangeValueForKey("isFinished")
_executing = false
_finished = true
didChangeValueForKey("isExecuting")
didChangeValueForKey("isFinished")
}
override func main() {
if cancelled == true && _finished != false{
finish()
return
}
}
Now I get!
cycleOperation start
LoadStatistics getStatistics completion: 97 samples
LoadStatistics addSamplesToArray completion
cycleOperation CompletionBlock
dependentOperation start
dependentOperation CompletionBlock