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.
}
}
Related
I have a countdown Timer that shows seconds and milliseconds. The user can start/stop recording multiple times until the timer hits zero. The user can also delete a previous recording at which point I have to re-add that deleted time back into the initial 20 secs. There are 2 issues.
The first issue is when the timer is stopped, the remaining time that shows on the timer label doesn't match the time culmination of the recordings. From my understanding this might be a RunLoop issue and I don't think there is anything that I can do about the inaccuracies.
let initialTime = 20.0
var cumulativeTimeForAllAssests = 0.0
for asset in arrOfAssets {
let assetDuration = CMTimeGetSeconds(asset.duration)
print("assetDuration: ", assetDuration)
cumulativeTimeForAllAssests += assetDuration
}
print("\ncumulativeTimeForAllAssests: ", cumulativeTimeForAllAssests)
After starting/stopping 5 times, the remaining time on the timer label says 16.5 but the culmination of the assets time is 4.196666.... The timer label should say 15.8, it's 0.7 milli off. The more I start/stop the recording, the more inaccurate/further off the culmination time - the initial time and the timer label time is.
assetDuration: 0.7666666666666667
assetDuration: 0.9666666666666667
assetDuration: 0.7983333333333333
assetDuration: 0.7333333333333333
assetDuration: 0.9316666666666666
cumulativeTimeForAllAssests: 4.196666666666667
The second issue is because I'm using seconds and milliseconds in my timerLabel, when I add re-add the subtracted time back in via deleteAssetAndUpdateTimer(...), I use the parts of modf() to update the seconds and milliseconds. I couldn't think of another way to update the timer. I know there has to be a more accurate way to do it.
Timer code:
weak var timer: Timer?
var seconds = 20
var milliseconds = 0
let initialTime = 20.0
func startTimer() {
invalidateTimer()
if seconds == Int(initalTime) && milliseconds == 0 {
timerIsRunning()
}
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] _ in
self?.timerIsRunning()
})
}
func timerIsRunning() {
updateTimerLabel()
if milliseconds == 0 {
seconds -= 1
}
milliseconds -= 1
if milliseconds < 0 {
milliseconds = 9
}
if seconds == 0 && milliseconds == 0 {
invalidateTimer()
updateTimerLabel()
}
}
func invalidateTimer() {
timer?.invalidate()
timer = nil
}
func updateTimerLabel() {
let milisecStr = "\(milliseconds)"
let secondsStr = seconds > 9 ? "\(seconds)" : "0\(seconds)"
timerLabel.text = "\(secondsStr).\(milisecStr)"
}
Delete asset and update timer code:
// the timer is stopped when this is called
func deleteAssetAndUpdateTimer(_ assetToDelete: AVURLAsset) {
var cumulativeTimeForAllAssests = 0.0
for asset in arrOfAssets {
let assetDuration = CMTimeGetSeconds(asset.duration)
cumulativeTimeForAllAssests += assetDuration
}
let timeFromAssetToDelete = CMTimeGetSeconds(assetToDelete.duration)
let remainingTime = self.initialTime - cumulativeTimeForAllAssests
let updatedTime = remainingTime + timeFromAssetToDelete
let mod = modf(updatedTime)
self.seconds = Int(mod.0)
self.milliseconds = Int(mod.1 * 10)
updateTimerLabel()
// remove assetToDelete from array
}
The big issue here was I was using a Timer to countdown which was incorrect. Following #LeoDabus' comments, I instead used CACurrentMediaTime():
let timerLabel = UILabel()
let maxRecordingTime = 30.0
lazy var elapsedTime = maxRecordingTime
var startTime: CFTimeInterval?
var endTime: CFTimeInterval?
weak var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
updateTimerLabel(with: Int(maxRecordingTime))
}
#IBAction func recordButtonPressed(_ sender: UIButton) {
if startTime == nil {
startTimer()
} else {
stopTimer(updateElapsed: true)
}
}
func startTimer() {
if elapsedTime == 0 { return }
stopTimer()
startTime = CACurrentMediaTime()
endTime = startTime! + elapsedTime
print("startTime: \(startTime!) | endTime: \(endTime!)")
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in
self?.timerIsRunning()
}
}
func timerIsRunning() {
guard let startTime = startTime, let endTime = endTime else { return }
let currentTime = CACurrentMediaTime()
let remainingTime = currentTime - startTime
print("%2d %.3lf", elapsedTime, remainingTime)
if currentTime >= endTime {
print("stopped at - currentTime: \(currentTime) | endTime: \(endTime)")
stopTimer(updateElapsed: true, currentTime: currentTime)
return
}
let countDownTime: Double = elapsedTime - remainingTime
let seconds = Int(countDownTime)
updateTimerLabel(with: seconds)
}
func updateTimerLabel(with seconds: Int) {
let secondsStr = seconds > 9 ? "\(seconds)" : "0\(seconds)"
timerLabel.text = secondsStr
}
func stopTimer(updateElapsed: Bool = false, currentTime: Double? = nil) {
timer?.invalidate()
timer = nil
if updateElapsed {
updateElapsedTime(using: currentTime)
}
startTime = nil
endTime = nil
}
func updateElapsedTime(using currentTime: Double? = nil) {
guard let startTime = startTime else { return }
var timeNow = CACurrentMediaTime()
if let currentTime = currentTime {
timeNow = currentTime
}
var updatedTime = elapsedTime - (timeNow - startTime)
if updatedTime < 0 {
updatedTime = 0
}
elapsedTime = updatedTime
}
func resetElapsedTime() { // This is for a resetButton not shown here
elapsedTime = maxRecordingTime
}
I'm populating a twodimensional array with HealthKit data. It's working fine as long as the function loops through the 30 days. outside of that function the array is empty because HealthKit data is loaded in background and everything else executes before.
How can I wait until the data is loaded so I can access the data inside the arrays?
Here's what's happening inside my ViewController
print("function call starting") // 1
// Populate 2DArrays
self.hkManager.getTotalCalories(forDay: 30) {caloriesValue, date in
//check if caloriesValue is nil, only do something if it's not
guard let caloriesValue = caloriesValue else { return }
let dateValue = DateFormatter.localizedString(from: date!, dateStyle: .short, timeStyle: .none)
print("will append") // 7 (31x)
self.caloriesArray.append([(dateValue,caloriesValue)])
}
print("function call done") // 2
print(self.caloriesArray.count) // 3
hkManager.getWeightData(forDay: 30) {bodyMassValue, date in
// Check if bodyMassValue is nil, only do something, if it's not
guard let bodyMassValue = bodyMassValue else { return }
let dateValue = DateFormatter.localizedString(from: date!, dateStyle: .short, timeStyle: .none)
self.bodyMassArray.append([(dateValue,bodyMassValue)])
}
do {
self.age = try hkManager.getAge()
} catch let error {
print("Error calculating age: \(error)")
}
print(bodyMassArray) // 4
print(caloriesArray) // 5
print(age!) // 6
}
I've added numbers behind the print statements to make clear what get's executed when.
The functions I'm calling look like this:
func getTotalCalories(forDay days: Int, completion: #escaping ((_ calories: Int?, _ date: Date?) -> Void)) {
// Getting quantityType as .dietaryCaloriesConsumed
guard let calories = HKObjectType.quantityType(forIdentifier: .dietaryEnergyConsumed) else {
print("*** Unable to create a dietaryEnergyConsumed type ***")
return
}
let now = Date()
let startDate = Calendar.current.date(byAdding: DateComponents(day: -days), to: now)!
var interval = DateComponents()
interval.day = 1
var anchorComponents = Calendar.current.dateComponents([.day, .month, .year], from: now)
anchorComponents.hour = 0
let anchorDate = Calendar.current.date(from: anchorComponents)!
let query = HKStatisticsCollectionQuery(quantityType: calories,
quantitySamplePredicate: nil,
options: [.cumulativeSum],
anchorDate: anchorDate,
intervalComponents: interval)
query.initialResultsHandler = { _, results, error in
guard let results = results else {
print("ERROR")
return
}
results.enumerateStatistics(from: startDate, to: now) { statistics, _ in
DispatchQueue.main.async {
if let sum = statistics.sumQuantity() {
let calories = Int(sum.doubleValue(for: HKUnit.kilocalorie()).rounded())
completion(calories, statistics.startDate)
return
}
}
}
}
healthStore.execute(query)
}
Anyone any ideas what I need to do?
Thanks! :)
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.
I am quite new to Swift and already learned a lot using the questions here.
In one of my first projects I try to write a soccer playtime timer app. The first timer is counting up after the whistle button is pressed showing the minutes played and the second timer is counting down to zero showing the minutes left to play. This works so far.
Now both timers should stop automatically when the halftime is over, so that I can start a third overtime timer. So far the invalidate statement of the timer is not working - both timers keep running. There seems to be something wrong with my if-statements, but at the moment I have no clue what. So any help would be very appreciated.
var countUpClock: Timer?
var countDownClock: Timer?
private var formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [.minute, .second]
formatter.zeroFormattingBehavior = .pad
return formatter
}()
func runPlaytimeClocks() {
let startTime = Date()
let countTime = Date() + 2700 //45min of playtime
if startTime <= countTime {
countUpClock = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.timePlayed.text = self?.formatter.string(from: startTime, to: Date())
}
}
else {
countUpClock?.invalidate()
}
if startTime <= countTime {
countDownClock = Timer.scheduledTimer(withTimeInterval: -1.0, repeats: true) { [weak self] _ in
self?.timetoPlay.text = self?.formatter.string(from: Date(), to: countTime)
}
}
else {
countDownClock?.invalidate()
}
Thank you very much for the replies.
I found exactly what I was looking for here: http://ioscake.com/swift-nstimer-in-background.html
I adapted the solution to my for clocks (CountUpClock, CountDownClock, OvertimeClock, HalftimeClock).
Do you have any suggestions what would be the best solution to start the second halftime of the soccer game?
So far the CountUpClock starts again at 0:00 when I press the whistle button after the halftime break. But it should keep running from minute 45:00 to 90:00 - while the CountDownClock should counting down from 45:00 to 0:00 again.
What would be the best solution for such a behaviour?
import UIKit
import UserNotifications
private let stopTimeKey = "stopTimeKey"
class ViewController: UIViewController {
//Outlets
#IBOutlet weak var timePlayed: UILabel!
#IBOutlet weak var timeToPlay: UILabel!
#IBOutlet weak var overtime: UILabel!
#IBOutlet weak var halftime: UILabel!
#IBOutlet weak var halftimeButton: UIButton!
private var stopTime: Date?
override func viewDidLoad() {
super.viewDidLoad()
registerForLocalNotifications()
stopTime = UserDefaults.standard.object(forKey: stopTimeKey) as? Date
if let time = stopTime {
if time > Date() {
startTimers(time, includeNotification: false)
} else {
notifyTimerCompleted()
}
}
}
private func registerForLocalNotifications() {
if #available(iOS 10, *) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
guard granted && error == nil else {
// display error
print("\(String(describing: error))")
return
}
}
} else {
let types: UIUserNotificationType = [.badge, .sound, .alert]
let settings = UIUserNotificationSettings(types: types, categories: nil)
UIApplication.shared.registerUserNotificationSettings(settings)
}
}
//Actions
#IBAction func whistleButtonTapped(_ sender: UIButton) {
overtimeClock?.invalidate()
overtimeClock = nil
halftimeClock?.invalidate()
halftimeClock = nil
overtime.text = "00:00"
halftime.text = "00:00"
halftimeButton.isHidden = true
//add 10 seconds per halftime to try out
let time = Date() + 10
if time > Date() {
startTimers(time)
} else {
timeToPlay.text = "error"
}
}
#IBAction func halftimeButton(_ sender: UIButton) {
halftimeButtoPressed()
}
// Code for different Timers
private var countDownClock: Timer?
private var countUpClock: Timer?
var overtimeClock: Timer?
var halftimeClock: Timer?
private func startTimers(_ stopTime: Date, includeNotification: Bool = true) {
// save `stopTime` in case app is terminated
UserDefaults.standard.set(stopTime, forKey: stopTimeKey)
self.stopTime = stopTime
// start Timer
countDownClock = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(handleCountDownTimer(_:)), userInfo: nil, repeats: true)
countUpClock = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(handleCountUpTimer(_:)), userInfo: nil, repeats: true)
guard includeNotification else { return }
// start local notification (so we're notified if timer expires while app is not running)
if #available(iOS 10, *) {
let content = UNMutableNotificationContent()
content.title = "Overtime is starting soon"
content.body = "In 5 seconds the overtime will start"
content.sound = UNNotificationSound.default()
//5 seconds warning before overtime starts
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: stopTime.timeIntervalSinceNow - 5, repeats: false)
let notification = UNNotificationRequest(identifier: "timer", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(notification)
} else {
let notification = UILocalNotification()
//5 seconds warning before overtime starts
notification.fireDate = stopTime - 5
notification.alertBody = "Overtime is starting soon"
UIApplication.shared.scheduleLocalNotification(notification)
}
}
private func stopTimer() {
countDownClock?.invalidate()
countDownClock = nil
countUpClock?.invalidate()
countUpClock = nil
}
private func halftimeButtoPressed() {
overtimeClock?.invalidate()
overtimeClock = nil
startHalftimeClock()
halftimeButton.isHidden = true
}
private let dateComponentsFormatter: DateComponentsFormatter = {
let _formatter = DateComponentsFormatter()
_formatter.allowedUnits = [.minute, .second]
_formatter.unitsStyle = .positional
_formatter.zeroFormattingBehavior = .pad
return _formatter
}()
#objc func handleCountDownTimer(_ timer: Timer) {
let now = Date()
if stopTime! > now {
timeToPlay.text = dateComponentsFormatter.string(from: now, to: stopTime!)
} else {
stopTimer()
notifyTimerCompleted()
startOvertimeClock()
halftimeButton.isHidden = false
}
}
#objc func handleCountUpTimer(_ timer: Timer) {
//add 10 seconds per halftime to try out
let now = Date() + 10
if now > stopTime! {
timePlayed.text = dateComponentsFormatter.string(from: stopTime!, to: now)
} else {
stopTimer()
notifyTimerCompleted()
}
}
//Overtime Clock
#objc func startOvertimeClock() {
let startOvertime = Date()
overtimeClock = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
self?.overtime.text = self?.dateComponentsFormatter.string(from: startOvertime, to: Date())
}
}
//Halftime Clock
#objc func startHalftimeClock() {
let startHalftime = Date()
halftimeClock = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
self?.halftime.text = self?.dateComponentsFormatter.string(from: startHalftime, to: Date())
}
}
private func notifyTimerCompleted() {
timeToPlay.text = "End"
timePlayed.text = "End"
}
}
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