How to limit the times a BGProcessingTaskRequest is called? - ios

Using the code below I'm attempting to limit the times that a BGProcessingTasRequest is called. I don't need it to run multiple times a day, only once and I want to be respectful to the user's battery life. However in testing the code, it never runs (even when substituting one hour for one day). If I comment out the guard statement then the task gets scheduled several times an hour. Is there a logical error here, or something I am not thinking of? (I know that when the processing task fires my Date is saving to UserDefaults properly).
private func schedulePersonalRecordsProcessingTask() {
let request = BGProcessingTaskRequest(identifier: "com.myndarc.personalRecordsProcessing")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false
request.earliestBeginDate = Date(timeIntervalSinceNow: 60)
//Only update PRs no more than once a day
let now = Date()
let oneDay = TimeInterval(24 * 60 * 60)
let lastBGTaskUpdateDate = defaults.value(forKey: backgroundTaskForPRLastDateKey) as? Date ?? .distantPast
guard now > (lastBGTaskUpdateDate + oneDay) else {
return
}
do {
try BGTaskScheduler.shared.submit(request)
}
catch {
print("Could not schedule app refresh: \(error)")
}
}

Related

Synchronous start of the event

Is there a way to run the code simultaneously on different devices? Let's say I want that when I click on a button on one of the devices, the function starts simultaneously on both the first and the second device? I tried to use a timer with a time check for 3 seconds ahead, but the function is triggered with a delay of 0.5 seconds on the second device
func getEventTime() -> UInt64{
let now = Date()
let interval = now.timeIntervalSince1970
let result = (UInt64(interval) + (3)) * 1000
return result
}
func getCurrentTime() -> UInt64{
let now = Date()
let interval = now.timeIntervalSince1970
let result = UInt64(interval * 1000)
return result
}
func startTimer(time : UInt64){
Timer.scheduledTimer(withTimeInterval: 0.0001, repeats: true) { timer in
switch getCurrentTime() {
case time - 1000 :
DispatchQueue.main.async {
countdownImageTimer.image = UIImage(named: "Start")
}
break
case time :
DispatchQueue.main.async {
countdownImageTimer.removeFromSuperview()
}
self.setShips()
timer.invalidate()
break
default:
break
}
}
}

How to Display Best Time in an iOS Game with 3 Significant Digits Decimal Value?

I am saving the best time in my iOS Game using the following functions:-
func format(timeInterval: TimeInterval) -> String {
let interval = Int(timeInterval)
let seconds = interval % 60
let minutes = (interval / 60) % 60
let milliseconds = Int(timeInterval * 1000) % 1000
return String(format: "%02d:%02d.%03d", minutes, seconds, milliseconds)
}
func setBestTime(with time: Int){
let defaults = UserDefaults.standard
let previousBestTime = defaults.integer(forKey: "bestTime")
defaults.set(time > previousBestTime ? time : previousBestTime, forKey: "bestTime")
}
func getBestTime(){
self.bestTimeLabel.text = "\(UserDefaults.standard.integer(forKey: "bestTime"))"
}
func gameOver() {
stopGame()
setBestTime(with: Int(elapsedTime))
}
But, it displays the best time in integer. I want the best time to be displayed in decimal with 3 significant figures. Could anyone please let me know how can I do that? Thanks for the help!
You aren't calling your format function. You simply need to pass the Int value retrieve from UserDefault to format before displaying it on your label.
You should also use UserDefaults.double if you want to store a TimeInterval rather than an Int.
let defaults = UserDefaults.standard
let bestTimeKey = "bestTime"
func format(timeInterval: TimeInterval) -> String {
let interval = Int(timeInterval)
let seconds = interval % 60
let minutes = (interval / 60) % 60
let milliseconds = Int(timeInterval * 1000) % 1000
return String(format: "%02d:%02d.%03d", minutes, seconds, milliseconds)
}
func setBestTime(with time: TimeInterval){
let previousBestTime = defaults.double(forKey: bestTimeKey)
defaults.set(max(time, previousBestTime), forKey: bestTimeKey)
}
func getBestTime() {
let bestTime = defaults.double(forKey: bestTimeKey)
let formattedBestTime = format(timeInterval: bestTime)
bestTimeLabel.text = formattedBestTime
}
func gameOver() {
stopGame()
setBestTime(with: elapsedTime)
}

How to differentiate sources with HealthKit sleep query

I'm currently using the following code to query for the number of hours the user was asleep in the last 24 hours:
func getHealthKitSleep() {
let healthStore = HKHealthStore()
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
// Get all samples from the last 24 hours
let endDate = Date()
let startDate = endDate.addingTimeInterval(-1.0 * 60.0 * 60.0 * 24.0)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
// Sleep query
let sleepQuery = HKSampleQuery(
sampleType: HKObjectType.categoryType(forIdentifier: HKCategoryTypeIdentifier.sleepAnalysis)!,
predicate: predicate,
limit: 0,
sortDescriptors: [sortDescriptor]){ (query, results, error) -> Void in
if error != nil {return}
// Sum the sleep time
var minutesSleepAggr = 0.0
if let result = results {
for item in result {
if let sample = item as? HKCategorySample {
if sample.value == HKCategoryValueSleepAnalysis.asleep.rawValue && sample.startDate >= startDate {
let sleepTime = sample.endDate.timeIntervalSince(sample.startDate)
let minutesInAnHour = 60.0
let minutesBetweenDates = sleepTime / minutesInAnHour
minutesSleepAggr += minutesBetweenDates
}
}
}
self.sleep = Double(String(format: "%.1f", minutesSleepAggr / 60))!
print("HOURS: \(String(describing: self.sleep))")
}
}
// Execute our query
healthStore.execute(sleepQuery)
}
This works great if the user has only one sleep app as the source for the data. The problem is if the user is using 2 sleep apps, for example, as sources, the data will be doubled. How can I differentiate the sources? If able to differentiate the sources, I would like to either only grab data from one source, or maybe take the average of the sources.
When you're looping over the samples, you can access information about the source for each. I only accept a single source, so I just keep a variable of the source name and if the current sample has a different source name I continue looping without processing the data from that sample, but you could combine the data in other ways if you wanted to.
Here's how to access the source info:
if let sample = item as? HKCategorySample {
let name = sample.sourceRevision.source.name
let id = sample.sourceRevision.source.bundleIdentifier
}
There's some more info on the HKSourceRevision object in the docs here.

SwiftUI State updates in Xcode 11 Playground

I have not been able to find anything through the standard Google search, but is there any reason why the ContentView is not updating through the ObservableObject? Feel like I am missing something but I am not quite sure what.
import SwiftUI
import PlaygroundSupport
let start = Date()
let seconds = 10.0 * 60.0
func timeRemaining(minutes: Int, seconds: Int) -> String {
return "\(minutes) minutes \(seconds) seconds"
}
class ViewData : ObservableObject {
#Published var timeRemaining: String = "Loading..."
}
// View
struct ContentView: View {
#ObservedObject var viewData: ViewData = ViewData()
var body: some View {
VStack {
Text(viewData.timeRemaining)
}
}
}
let contentView = ContentView()
let viewData = contentView.viewData
let hosting = UIHostingController(rootView: contentView)
// Timer
let timer = DispatchSource.makeTimerSource()
timer.schedule(deadline: .now(), repeating: .seconds(1))
timer.setEventHandler {
let diff = -start.timeIntervalSinceNow
let remaining = seconds - diff
let mins = Int(remaining / 60.0)
let secs = Int(remaining) % 60
let timeRemaning = timeRemaining(minutes: mins, seconds: secs)
viewData.timeRemaining = timeRemaning
print(timeRemaning)
}
timer.resume()
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
timer.cancel()
PlaygroundPage.current.finishExecution()
}
PlaygroundPage.current.setLiveView(contentView)
PlaygroundPage.current.needsIndefiniteExecution = true
The reason is that GCD based timer works on own queue, so here is the fix - view model have to be updated on main, UI, queue as below
DispatchQueue.main.async {
viewData.timeRemaining = timeRemaning
}
The main utility of GCD timers over standard timers is that they can run on a background queue. Like Asperi said, you can dispatch the updates to the main queue if your GCD timer isn’t using the main queue, itself.
But, you might as well just schedule your GCD timer on the main queue from the get go, and then you don’t have to manually dispatch to the main queue at all:
let timer = DispatchSource.makeTimerSource(queue: .main)
But, if you’re going to run this on the main thread, you could just use a Timer, or, in SwiftUI projects, you might prefer Combine’s TimerPublisher:
import Combine
...
var timer: AnyCancellable? = nil
timer = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { _ in
let remaining = start.addingTimeInterval(seconds).timeIntervalSince(Date())
guard remaining >= 0 else {
viewData.timeRemaining = "done!"
timer?.cancel()
return
}
let mins = Int(remaining / 60.0)
let secs = Int(remaining) % 60
viewData.timeRemaining = timeRemaining(minutes: mins, seconds: secs)
}
When you incorporate your timer within your SwiftUI code (rather than a global like here), it’s nice to stay within the Combine Publisher paradigm.
I also think it’s probably cleaner to cancel the timer when it expires in the timer handler, rather than doing a separate asyncAfter.
Unrelated, but you might consider using DateComponentsFormatter in your timeRemaining function, e.g.:
let formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.unitsStyle = .full
return formatter
}()
func timeRemaining(_ timeInterval: TimeInterval) -> String {
formatter.string(from: timeInterval) ?? "Error"
}
Then,
It gets you out of the business of calculating minutes and seconds yourself;
The string will be localized; and
It will ensure grammatically correct wording; e.g. when there are 61 seconds left and the existing routine will report a grammatically incorrect “1 minutes, 1 seconds”.
DateComponentsFormatter gets you out of the weeds of handling these sorts of edge cases, where you want singular instead of plural or languages other than English.

Solve memory issue with migrating a big bunch of data

In my previous version of my app I stored all user data into .dat files (with NSKeyedArchiver), but in my new version I want to upgrade to a real(m) database.
I'm trying to import all of this data (and that can be a LOT) into Realm. But it's taking so much memory that the debugger eventually kill my app before the migration has finished. The 'strange' thing is that the data on hard disk is only 1.5 mb big, but it's taking memory for more than 1gb so I'm doing something wrong.
I also tried to work with multiple threads, but that didn't help. Well it speeded up the migration process (which is good), but it also took the same amount of memory.
Who can help me out? See my code below for more information..
FYI Async can be found here https://github.com/duemunk/Async
import Async
let startDate = NSDate(timeIntervalSince1970: 1388534400).startOfDay // Start from 2014 jan 1st
let endDate = NSDate().dateByAddingTimeInterval(172800).startOfDay // 2 days = 3600 * 24 * 2 = 172.800
var pathDate = startDate
let calendar = NSCalendar.currentCalendar()
let group = AsyncGroup()
var allPaths = [(Int, Int)]()
while calendar.compareDate(pathDate, toDate: endDate, toUnitGranularity: .Month) != .OrderedDescending {
// Components
let currentMonth = calendar.component(.Month, fromDate: pathDate)
let currentYear = calendar.component(.Year, fromDate: pathDate)
allPaths.append((currentYear, currentMonth))
// Advance by one month
pathDate = calendar.dateByAddingUnit(.Month, value: 1, toDate: pathDate, options: [])!
}
for path in allPaths {
group.background {
// Prepare path
let currentYear = path.0
let currentMonth = path.1
let path = (Path.Documents as NSString).stringByAppendingPathComponent("Stats_\(currentMonth)_\(currentYear).dat")
print(path)
if NSFileManager.defaultManager().fileExistsAtPath(path) {
NSKeyedUnarchiver.setClass(_OldStatisticsDataModel.self, forClassName: "StatisticsDataModel")
if let statistics = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? [_OldStatisticsDataModel] {
// Loop through days
for i in 1...31 {
let dateComponents = NSDateComponents()
dateComponents.year = currentYear
dateComponents.month = currentMonth
dateComponents.day = i
dateComponents.hour = 0
dateComponents.minute = 0
// Create date from components
let userCalendar = NSCalendar.currentCalendar() // user calendar
guard let date = userCalendar.dateFromComponents(dateComponents) else {
continue
}
// Search for order items
let filtered = statistics.filter {
if let date = $0.date {
let dateSince1970 = date.timeIntervalSince1970
return date.startOfDay.timeIntervalSince1970 <= dateSince1970 && date.endOfDay.timeIntervalSince1970 >= dateSince1970
}
return false
}
if filtered.isEmpty == false {
// Create order
let transaction = Transaction()
transaction.employee = Account.API().administratorEmployee()
let order = Order()
order.status = PayableStatus.Paid
order.createdDate = date.timeIntervalSince1970
order.paidDate = date.timeIntervalSince1970
// Loop through all found items
for item in filtered {
// Values
let price = (item.price?.doubleValue ?? 0.0) * 100.0
let tax = (item.tax?.doubleValue ?? 0.0) * 100.0
// Update transaction
transaction.amount += Int(price)
// Prepare order item
let orderItem = OrderItemm()
orderItem.amount = item.amount
orderItem.price = Int(price)
orderItem.taxPercentage = Int(tax)
orderItem.name = item.name ?? ""
orderItem.product = Product.API().productForName(orderItem.name, price: orderItem.price, tax: orderItem.taxPercentage)
// Add order item to order
order.orderItems.append(orderItem)
}
if order.orderItems.isEmpty == false {
print("\(date): \(order.orderItems.count) order items")
// Set transaction for order
order.transactions.append(transaction)
// Save the order
Order.API().saveOrders([order])
}
}
}
}
}
}
}
group.wait()
As in the comments of my question was suggested by #bdash, autoreleasepool did the trick.
I use AsyncSwift as syntactic sugar for grand central dispatch, but when using block groups the group keeps a reference to the block which caused that the memory wasn't released. I still make use of a group now but I leave the group after it's finished.
I provided my code example below, to make things clearer. Using multiple threads gave me incredibly (like 10 times faster) more performance while memory won't go above 150MB. Previously app was crashing at 1,3GB due to memory pressure.
let group = AsyncGroup()
var allPaths = [(Int, Int)]()
// Some logic to fill the paths -> not interesting
for path in allPaths {
group.enter() // The following block will be added to the group
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
autoreleasepool { // Creating an autoreleasepool to free up memory for the loaded statistics
// Stripped unnecessary stuff
if NSFileManager.defaultManager().fileExistsAtPath(path) {
// Load the statistics from .dat files
NSKeyedUnarchiver.setClass(_OldStatisticsDataModel.self, forClassName: "StatisticsDataModel")
if let statistics = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? [_OldStatisticsDataModel] {
// Loop through days
for i in 1...31 {
autoreleasepool {
// Do the heavy stuff here
}
}
}
}
}
group.leave() // Block has finished, now leave the group
}
}
group.wait()

Resources