I have suspected there is something wrong with my personal Health account for a while but while working on a new app that queries HealthKit I'm pretty certain.
I've developed a very simple health app where you can request auth to read steps and then runs a statistic collection query to get a tally of steps you take.
This test app runs on the simulator fine. But when I run it on a device using my Apple ID the query just hangs. I don't get the callback for the initial result or the updated results.
Looking in Xcode, the memory graph just steadily increases until you terminate the app. And it appears there are millions of statistics objects being created.
Can anyone suggest anything to prevent this from happening? Do I need to reset my Health data? I would rather there was another solution.
I've included the code for this dummy app below. You will need to add HealthKit capabilities and the Privacy: Health Sharing info key for it to run.
import SwiftUI
import HealthKit
@main
struct StepCountApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class ViewModel: ObservableObject {
let healthStore = HKHealthStore()
var query: HKStatisticsCollectionQuery?
@Published var isQuerying = false
@Published var authStatus: HKAuthorizationStatus = .notDetermined
@Published var stepCount: Int = 0
init() {
}
func updateAuthStatus() {
authStatus = healthStore.authorizationStatus(for: .quantityType(forIdentifier: .stepCount)!)
}
func requestAuth() {
let sharing = Set<HKSampleType>()
let reading = Set<HKObjectType>([.quantityType(forIdentifier: .stepCount)!])
healthStore.requestAuthorization(toShare: sharing, read: reading) { [weak self] result, error in
if result {
DispatchQueue.main.async {
self?.updateAuthStatus()
}
} else if let error = error {
print("Auth failed: \(error.localizedDescription)")
} else {
fatalError("How did we get here?")
}
}
}
func stopQuery() {
guard let query = query else { return }
healthStore.stop(query)
self.query = nil
isQuerying = false
}
func startQuery() {
let stepsType = HKObjectType.quantityType(forIdentifier: .stepCount)!
let intervalComponents = DateComponents(day: 1)
query = HKStatisticsCollectionQuery(
quantityType: stepsType,
quantitySamplePredicate: nil,
options: [.cumulativeSum],
anchorDate: Date(),
intervalComponents: intervalComponents
)
query!.initialResultsHandler = { [weak self] query, results, error in
print("Initial result")
if let error = error {
print("Error: \(error.localizedDescription)")
return
}
guard let statisticsCollection = results else {
print("Error: No stats collection")
return
}
guard let statistics = statisticsCollection.statistics(for: Date()) else {
print("Error: No stats for date")
return
}
let sum = statistics.sumQuantity()?.doubleValue(for: .count()) ?? 0
DispatchQueue.main.async {
self?.stepCount = Int(sum)
}
}
query!.statisticsUpdateHandler = { [weak self] query, statistics, results, error in
print("Updated result")
if let error = error {
print("Error: \(error.localizedDescription)")
return
}
guard let statisticsCollection = results else {
print("Error: No stats collection")
return
}
guard let statistics = statisticsCollection.statistics(for: Date()) else {
print("Error: No stats for date")
return
}
guard let sum = statistics.sumQuantity()?.doubleValue(for: .count()) else {
print("Error: Statistics have no data?")
return
}
DispatchQueue.main.async {
DispatchQueue.main.async {
self?.stepCount = Int(sum)
}
}
}
healthStore.execute(query!)
isQuerying = true
}
var authStatusString: String {
switch authStatus {
case .notDetermined: return "Not determined"
case .sharingAuthorized: return "Sharing authorised"
case .sharingDenied: return "Sharing denied"
@unknown default: fatalError()
}
}
}
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
Form {
Section {
HStack {
Text("Auth Status")
Spacer()
Text("\(viewModel.authStatusString)")
}
}
Section {
Button("Request Auth") {
viewModel.requestAuth()
}
}
Section {
if viewModel.isQuerying {
Button("Stop query") {
viewModel.stopQuery()
}
} else {
Button("Start query") {
viewModel.startQuery()
}
}
}
Section {
HStack {
Text("Step count")
Spacer()
Text("\(viewModel.stepCount)")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Post
Replies
Boosts
Views
Activity
In previous versions of Xcode you were able to pick a simulator pair (iOS + watchOS) when running a watch app. In Xcode 12, I cannot find the option for this.
In fact, the only way I seem to be able to test connectivity between my iOS and watchOS is to build and run the iOS app. Then on the simulator open the "Watch" app, navigate to my app name and enable the Show App on Apple Watch option.
This is clearly not a sustainable debug cycle.
How am I able to launch both apps from Xcode without jumping through the above hoops so that I can test the connectivity between the two apps?
Below is a simple example. If you tap and release the first cell, it deselects with the animation as you would expect.
If you tap and release the second cell, you may not notice anything happen at all. You may even need to hold the cell to see the selection, and when you release it reverts back to the unselected state immediately.
This is obviously a contrived example but it shows the essence of the issue. If I wanted to delay the deselection until something had happened I would face the same issue.
Why does this happen and is there any way around it?
import UIKit
class ViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = indexPath.row == 0 ? "Regular deselection" : "Async deselection"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
tableView.deselectRow(at: indexPath, animated: true)
} else {
DispatchQueue.main.async {
tableView.deselectRow(at: indexPath, animated: true)
}
}
}
}
I'm updating a timer app that is written in regular Swift. After being built using watchOS 8 the display stays on (Hooray!) but none of the text elements update (Boo!).
I've read the documentation at:
https://developer.apple.com/documentation/swiftui/text
But this suggests that the only way to get the Always On display to update is to user SwiftUI.
Please tell me this is not the case.
I have the need to show and hide a toolbar (bottomBar) when certain conditions are met. I am able to do this via:
func toolbar(_ visibility: Visibility, for bars: ToolbarPlacement...) -> some View
However, changes to state result in the toolbar appearing immediately rather than sliding in the from the bottom of the screen like I, and iOS users in general, are used to when using:
self.navigationController.setToolbarHidden(false, animated: true)
I've tried adding a .transition() and .animation() view modifiers to the ToolbarItemGroup but no such API exists.
What do I need to do?