Complication won't update - swift

I have a very simple complication with a random number.
But my number won't update. Everytime I look on my watch it's the same. Only if I reinstall the complication (reinstalling apple watch app) I'm getting a new number.
I have set update to 1 second. Anyone an idea what could be wrong?
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
handler(CLKComplicationTimelineEntry(date: NSDate(), complicationTemplate: getTemplateForComplication(family: complication.family)!))
}
func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
handler(NSDate(timeIntervalSinceNow: 1))
}
func getPlaceholderTemplateForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTemplate?) -> Void) {
handler(getTemplateForComplication(family: complication.family))
}
func getTemplateForComplication(family family: CLKComplicationFamily) -> CLKComplicationTemplate? {
let bitcoinPrice = Double(arc4random_uniform(400))
switch family {
case .ModularSmall:
let template = CLKComplicationTemplateModularSmallSimpleText()
template.textProvider = CLKSimpleTextProvider(text: String(format: "%.2f", bitcoinPrice))
return template
case .ModularLarge:
let template = CLKComplicationTemplateModularLargeTallBody()
template.headerTextProvider = CLKSimpleTextProvider(text: "Bitcoin")
template.bodyTextProvider = CLKSimpleTextProvider(text: String(format: "%.2f €", bitcoinPrice))
return template
case .UtilitarianSmall:
let template = CLKComplicationTemplateUtilitarianSmallFlat()
template.textProvider = CLKSimpleTextProvider(text: String(format: "%.2f", bitcoinPrice))
return template
case .UtilitarianLarge:
let template = CLKComplicationTemplateUtilitarianLargeFlat()
template.textProvider = CLKSimpleTextProvider(text: String(format: " %.2f €", bitcoinPrice))
return template
default:
return nil
}
}

Your data source is missing the methods that are responsible for handling the scheduled update.
At the start of a scheduled update, ClockKit calls either the requestedUpdateDidBegin or requestedUpdateBudgetExhausted method, depending on the state of your complication’s time budget. You must implement one or both of those methods if you want to add data to your timeline. Your implementation of those methods should extend or reload the timeline of your complication as needed. When you do that, ClockKit requests the new timeline entries from your data source. If you do not extend or reload your timeline, ClockKit does not ask for any new timeline entries.
Here's how you can reload the timeline once your scheduled update occurs:
// MARK: - Responding to Scheduled Updates
func requestedUpdateDidBegin() {
let server=CLKComplicationServer.sharedInstance()
for complication in server.activeComplications {
server.reloadTimelineForComplication(complication)
}
}
You should also implement requestedUpdateBudgetExhausted().
Keep in mind that scheduled updates can only occur every 10 minutes; it's not possible to update your complication every second. Also consider that updating too often might exhaust your update budget:
Specify a date as far into the future as you can manage. Do not ask the system to update your complication within minutes. Instead, provide data to last for many hours or for an entire day. If your budget is exhausted, the next scheduled update does not occur until after your budget is replenished.

Related

Limit API Calls to 40 per minute (Swift)

I have a limit of 40 URL Session calls to my API per minute.
I have timed the number of calls in any 60s and when 40 calls have been reached I introduced sleep(x). Where x is 60 - seconds remaining before new minute start. This works fine and the calls don’t go over 40 in any given minute. However the limit is still exceeded as there might be more calls towards the end of the minute and more at the beginning of the next 60s count. Resulting in an API error.
I could add a:
usleep(x)
Where x would be 60/40 in milliseconds. However as some large data returns take much longer than simple queries that are instant. This would increase the overall download time significantly.
Is there a way to track the actual rate to see by how much to slow the function down?
Might not be the neatest approach, but it works perfectly. Simply storing the time of each call and comparing it to see if new calls can be made and if not, the delay required.
Using previously suggested approach of delay before each API call of 60/40 = 1.5s (Minute / CallsPerMinute), as each call takes a different time to produce response, total time taken to make 500 calls was 15min 22s. Using the below approach time taken: 11min 52s as no unnecessary delay has been introduced.
Call before each API Request:
API.calls.addCall()
Call in function before executing new API task:
let limit = API.calls.isOverLimit()
if limit.isOver {
sleep(limit.waitTime)
}
Background Support Code:
var globalApiCalls: [Date] = []
public class API {
let limitePerMinute = 40 // Set API limit per minute
let margin = 2 // Margin in case you issue more than one request at a time
static let calls = API()
func addCall() {
globalApiCalls.append(Date())
}
func isOverLimit() -> (isOver: Bool, waitTime: UInt32)
{
let callInLast60s = globalApiCalls.filter({ $0 > date60sAgo() })
if callInLast60s.count > limitePerMinute - margin {
if let firstCallInSequence = callInLast60s.sorted(by: { $0 > $1 }).dropLast(2).last {
let seconds = Date().timeIntervalSince1970 - firstCallInSequence.timeIntervalSince1970
if seconds < 60 { return (true, UInt32(60 + margin) - UInt32(seconds.rounded(.up))) }
}
}
return (false, 0)
}
private func date60sAgo() -> Date
{
var dayComponent = DateComponents(); dayComponent.second = -60
return Calendar.current.date(byAdding: dayComponent, to: Date())!
}
}
Instead of using sleep have a counter. You can do this with a Semaphore (it is a counter for threads, on x amount of threads allowed at a time).
So if you only allow 40 threads at a time you will never have more. New threads will be blocked. This is much more efficient than calling sleep because it will interactively account for long calls and short calls.
The trick here is that you would call a function like this every sixty second. That would make a new semaphore every minute that would only allow 40 calls. Each semaphore would not affect one another but only it's own threads.
func uploadImages() {
let uploadQueue = DispatchQueue.global(qos: .userInitiated)
let uploadGroup = DispatchGroup()
let uploadSemaphore = DispatchSemaphore(value: 40)
uploadQueue.async(group: uploadGroup) { [weak self] in
guard let self = self else { return }
for (_, image) in images.enumerated() {
uploadGroup.enter()
uploadSemaphore.wait()
self.callAPIUploadImage(image: image) { (success, error) in
uploadGroup.leave()
uploadSemaphore.signal()
}
}
}
uploadGroup.notify(queue: .main) {
// completion
}
}

Swift combine publishers where one hasn't sent a value yet

I have a publisher which would need re-evaluating on day change, but should continue to emit values at any other time.
As such, I thought I could use a NotificationCenter publisher for the UIApplication.significantTimeChangeNotification notification and combine it with my publisher such that the combine emission process would re-run on either on data change or day change and hence re-evaluate the map filter. See a rough outline of that code below.
The problem is that there is no published event by NotificationCenter at the point in time that this is setup and hence, none of the following map etc calls actually evaluate. merge(with:) won't work as the two publishers publish different types, but combineLatest(_:) and zip(_:) both won't emit events until both publishers have emitted a single event.
I can validate that my code operates as expected by adding NotificationCenter.default.post(name: UIApplication.significantTimeChangeNotification, object: nil) after this code, but that is undesirable due to it potentially signalling other areas of the app that an actual time change has occurred when it hasn't
private func todaysDate() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "YYYY-MM-dd"
return formatter.string(from: Date())
}
#Published var entities: [MyEntity]
let dayChangePublisher = NotificationCenter.default
.publisher(for: UIApplication.significantTimeChangeNotification)
$entities.combineLatest(dayChangePublisher)
.map(\.0) // Only pass on the entity for further operations
.map { entities -> MyEntity? in
let today = todaysDate()
return entities?.first(where: { $0.id == today })
}
...remainder of combine code
Can this combination of publishers and evaluation of events occur with the current Swift combine framework? Like the behaviour I'd expect from merge(with:) but where the publishers emit two different types.
edit:
I found one solution where I map the notification publisher to a nil array
let dayChangePublisher = NotificationCenter.default
.publisher(for: UIApplication.significantTimeChangeNotification)
.map { _ ➝ [MyEntity]? in
return nil
}
And then use merge and compactMap to avoid passing any nil values on
let mergedPub = repo.$entities
.merge(with: dayChangePublisher)
.compactMap { entity -> MyEntity? in
let today = todaysDate()
return entities?.first { $0.id == today }
}
.share()
It works, but maybe a bit cumbersome if anyone has a better solution?
If I understood your question, you need a combineLatest that is not blocked by not having an initial value from one of the publishers.
You can achieve that with .prepend(value) operator. In this case, since you don't care about the actual value, map first to Void, then prepend a Void. It would work like so:
let dayChangePublisher = NotificationCenter.default
.publisher(for: UIApplication.significantTimeChangeNotification)
$entities.combineLatest(
dayChangePublisher
.map { _ in }
.prepend(()) // make sure to prepend a () value
)
.map(\.0) // Only pass on the entity for further operations
.map { entities -> MyEntity? in
let today = todaysDate()
return entities?.first(where: { $0.id == today })
}
//...

How to use Combine to assign the number of elements returned from a Core Data fetch request?

I want my app to periodically fetch new records and stores them in Core Data. I have a label on my UI that should display the number of elements for a particular record and I want that number to be updated as more records are added into the database. As an exercise, I want to use Combine to accomplish it.
I'm able to display the number of elements in the database when the app launches, but the number doesn't get updated when new data enters into the database (I verified that new data was being added by implementing a button that would manual refresh the UI).
Here's the code that displays the correct number of elements on launch but doesn't update when new records are added:
let replayRecordFetchRequest: NSFetchRequest<ReplayRecord> = ReplayRecord.fetchRequest()
_ = try? persistentContainer.viewContext.fetch(replayRecordFetchRequest).publisher.count().map { String(format: Constants.Strings.playsText, $0) }.assign(to: \.text, on: self.playsLabel)
Here's a code snippet from the WWDC 2019 Session 230 talk that I adapted but this doesn't work at all (the subscriber is never fired):
let replayRecordFetchRequest: NSFetchRequest<ReplayRecord> = ReplayRecord.fetchRequest()
if let replayRecords = try? replayRecordFetchRequest.execute() {
_ = replayRecords.publisher.count().map { String(format: Constants.Strings.playsText, $0) }.assign(to: \.text, on: self.playsLabel)
}
So, I didn't know this until now, but not all publishers are infinitely alive.
And the problem was that the NSFetchRequest.publisher is not a long-living publisher. It simply provides a way to iterate through the sequence of elements in the fetch request. As a result, the subscriber will cancel after the elements are iterated. In my case, I was counting the elements published until cancellation then assigning that value onto the UI.
Instead, I should be subscribing to changes to the managed object context and assigning that pipeline to my UI. Here's some example code:
extension NotificationCenter.Publisher {
func context<T>(fetchRequest: NSFetchRequest<T>) -> Publishers.CompactMap<NotificationCenter.Publisher, [T]> {
return compactMap { notification -> [T]? in
let context = notification.object as! NSManagedObjectContext
var results: [T]?
context.performAndWait {
results = try? context.fetch(fetchRequest)
}
return results
}
}
}
let playFetchRequest: NSFetchRequest<ReplayRecord> = ReplayRecord.fetchRequest()
let replayVideoFetchRequest: NSFetchRequest<ReplayVideo> = ReplayVideo.fetchRequest()
let playsPublisher = contextDidSavePublisher.context(fetchRequest: playFetchRequest).map(\.count)
let replayVideoPublisher = contextDidSavePublisher.context(fetchRequest: replayVideoFetchRequest).map(\.count)
playsSubscription = playsPublisher.zip(replayVideoPublisher).map {
String(format: Constants.Strings.playsText, $0, $1)
}.receive(on: RunLoop.main).assign(to: \.text, on: self.playsLabel)

How to measure code block execution time inside DispatchQueue concurrent async in Swift?

Assuming I have this kind of function and DispatchQueue logic. Assume that when synchLatest() gets called it fires "Code Block 1" twice.
How is supposed to be that, during a loop, the execution of "Code Block 1" which is only a retrieve of a string from the grpc response and a store in UserDefaults take me 1.7 seconds and the second it gets executed during the loop it takes 5 seconds?
let synchQueue = DispatchQueue(label: "com.dmapp.synchQueue", qos: .default, attributes: .concurrent)
let synchProcessQueue = DispatchQueue(label: "com.dmapp.processQueue", qos: .default, attributes: .concurrent)
func synchLatest() {
while(someconditions) {
synchQueue.async {
...
let response = try grpcCall.receive()
...
synchProcessQueue.async {
....
measure("Code Block 1", {
if response.data.nickname != "" {
// Store in UserDefaults
}
})
....
}
}
}
}
#discardableResult
static func measure<A>(name: String = "", _ block: () -> A) -> A {
let startTime = CACurrentMediaTime()
let result = block()
let timeElapsed = CACurrentMediaTime() - startTime
print("Time: \(name) - \(timeElapsed)")
return result
}
Am I measuring code execution time here in the wrong way?
CACurrentMediaTime is a fine way of measuring time. It’s low overhead and doesn’t suffer the problems of Date or CFAbsoluteTimeGetCurrent, which “do not guarantee monotonically increasing results.”
A few general guidelines when benchmarking include:
let the app reach quiescence before starting the benchmarking;
do not run independent tests concurrently with respect to each other (because they may be contending for the same resources);
repeat it many times; and
change the order that the benchmarks are done in order to eliminate noise from the measurements.
If you’re looking for an alternative to manually using CACurrentMediaTime, another approach is to use signposts and points of interest, which not only will capture the amount of time it takes, but would you let you filter your instruments timeline to the relevant period of time (and you might be able to diagnose the source of the discrepancy).

Swift - Unable to retrieve CMSensorDataList records

I'm making a Watch app that will record user acceleration. I've used CMSensorRecorder from the CoreMotion Framework to do this.
The flow of the program right now is that the user presses a button on the watch, which triggers acceleration to be recorded for 30 seconds. After this, there is a 6-minute delay (referring to answer here :watchOS2 - CMSensorRecorder, a delay is needed to read the data), and the acceleration and timestamp data is printed to the console.
Right now I'm getting a "response invalid" and "Error occurred" when running the app. I've added a motion usage description to the info.plist file.
I'm fairly new to Swift and app development, and I fear something's wrong with the way I'm trying to access the data. I've attached the console logs and code below.
Can anybody provide some insight into the messages and how to resolve this? I've searched around but haven't found any cases of this issue before. Cheers.
func recordAcceleration(){
if CMSensorRecorder.isAccelerometerRecordingAvailable(){
print("recorder started")
recorder.recordAccelerometer(forDuration: 30) //forDuration controls how many seconds data is recorded for.
print("recording done")
}
}
func getData(){
if let list = recorder.accelerometerData(from: Date(timeIntervalSinceNow: -400), to: Date()){
print("listing data")
for data in list{
if let accData = data as? CMRecordedAccelerometerData{
let accX = accData.acceleration.x
let timestamp = accData.startDate
//Do something here.
print(accX)
print(timestamp)
}
}
}
}
//Send data to iphone after time period.
func sendData(dataBlock:CMSensorDataList){
WCSession.default.transferUserInfo(["Data" : dataBlock])
}
//UI Elements
#IBAction func recordButtonPressed() {
print("button pressed")
recordAcceleration()
//A delay is needed to read the data properly.
print("delaying 6 mins")
perform(#selector(callback), with: nil, afterDelay: 6*60)
}
#objc func callback(){
getData()
}
extension CMSensorDataList: Sequence {
public func makeIterator() -> NSFastEnumerationIterator {
return NSFastEnumerationIterator(self)
}
Console output:
button pressed
recorder started
2019-03-12 12:12:12.568962+1100 app_name WatchKit Extension[233:5614] [Motion] Warning - invoking recordDataType:forDuration: on main may lead to deadlock.
2019-03-12 12:12:13.102712+1100 app_name WatchKit Extension[233:5614] [SensorRecorder] Response invalid.
recording done
delaying 6 mins
2019-03-12 12:18:13.115955+1100 app_name WatchKit Extension[233:5614] [Motion] Warning - invoking sensorDataFromDate:toDate:forType: on main may lead to deadlock.
2019-03-12 12:18:13.162476+1100 app_name WatchKit Extension[233:5753] [SensorRecorder] Error occurred while trying to retrieve accelerometer records!
I ran your code and did not get the "Response invalid" or "Error occurred". I did get the main thread warnings. So I changed to a background thread and it works fine.
Also, I don't think you need to wait six minutes. I changed it to one minute.
I hope this helps.
let recorder = CMSensorRecorder()
#IBAction func recordAcceleration() {
if CMSensorRecorder.isAccelerometerRecordingAvailable() {
print("recorder started")
DispatchQueue.global(qos: .background).async {
self.recorder.recordAccelerometer(forDuration: 30)
}
perform(#selector(callback), with: nil, afterDelay: 1 * 60)
}
}
#objc func callback(){
DispatchQueue.global(qos: .background).async { self.getData() }
}
func getData(){
print("getData started")
if let list = recorder.accelerometerData(from: Date(timeIntervalSinceNow: -60), to: Date()) {
print("listing data")
for data in list{
if let accData = data as? CMRecordedAccelerometerData{
let accX = accData.acceleration.x
let timestamp = accData.startDate
//Do something here.
print(accX)
print(timestamp)
}
}
}
}