I’ve recently started looking at retrieving ECG data in HealthKit, as currently recorded by my Watch (mine’s on 8.7). I already have a database of ECG recordings from my Polar H10 strap and want to incorporate the Watch data (from a different lead position).
i note that HealthKit now has an HKElectrocardiogram sample [https://developer.apple.com/documentation/healthkit/hkelectrocardiogram), which has a property for classification e.g. “atrialFibrillation”. So, we should be able, now, to do an HKQuery to retrieve AFib classifications with or without the voltage data. I haven’t tried yet, but soon will.
I hope this helps.
Regards, Michaela
PS I’ve developed an ECG Viewer with SwiftUI Canvas for the voltage time series ( currently Polar H10 data) and will be adding the capability for plotting the Watch data.
Post
Replies
Boosts
Views
Activity
I had similar problems with an expenses management app, where I have a hierarchy of Groupings (master) with Transactions (detail) against the lowest Grouping in the hierarchy. The app uses CoreData with CloudKit sync and is multi-platform., both of which add complexity to the design approach.
My solution is this:
In the CoreData model there are two entities, Grouping and Transaction: with a one-to-many relationship from Grouping to Transaction and one-to-one reciprocal from Transaction to Grouping. A Fetch of a Grouping (master), or set of Groupings, therefore has the transactions (detail) as an NSManaged NSSet on each Grouping (master) record. In the Xcode editor of the CoreData model I use Manual Codegen to put the NSManagedObject entity, properties and accessors in my project. This allows for easier coding of extensions to create computed properties on the CoreData entities, e.g. summing transactions for a Grouping.
I have a DataStore ObservableObject class singleton that handles all CoreData / CloudKit setup and data processing, e.g. all Fetches, insertions, deletions etc. For example, there is a DataStore method to fetch all Groupings (master records) “getAllGroupings” which returns an array of CoreData Grouping objects, each of which has its own set of Transaction records (detail). So, in a SwiftUI View, I have a @ObservedObject var dataStore = DataStore.shared, which means that I can therefore refer to the Groupings array in a List by using List(dataStore.getAllGroupings()) ….. To get the list of Transactions in another (detail) view, I pass the required Grouping (master record) to the detail view as a var, then use List(grouping.transactionsArray) - where “transactionsArray” is a computed property on my Grouping CoreData extension that turns an NSSet into a Swift Array. **** This particular solution doesn't need datastore to be an ObservedObject, but I use that for other reasons in the app.
To delete a transaction in detail view, I call a method on dataStore e.g. deleteTransaction(transaction: Transaction) which does a context.delete(transaction) and then a context save. This deletes the transaction and its entry in the Grouping’s transaction NSSet (according to the delete rules I set in CoreData).
HOWEVER: this delete method (or add a transaction) does not update the view, because the changes are made to the CoreData context, which is not directly observable by SwiftUI. So, in DataStore I have a Combine property to send a notification public let transactionDeleted = PassthroughSubject<(String), Never>() then in the “deleteTransaction” method, I use transactionDeleted.send(“txn deleted”) - you can set up the PassthroughSubject to send whatever you want.
Either in your Master View or DetailView (depends on your processing - need to experiment), have
.onReceive((dataStore.transactionDeleted), perform: { transaction in
self.statusTime = Date()
})
This updates a @State var statusTime which, unfortunately, needs to be used somewhere in the view: this triggers a View refresh and, voila, the deletion no longer appears in the View.
NOTE: I use public for my methods and properties in DataStore because this is a Multiplatform app and DataStore is therefore in a framework. This is also why I use Manual CodeGen in Xcode's CoreData models, i.e. to use in a framework.
I also use the PassthroughSubject in non-CoreData apps where there’s complex data processing, which if ObservedObjects are used can cause unnecessary view refreshes. In these cases, once all processing has completed I then issue the PassthroughSubject send to update the view.
I hope this helps. Regards, Michaela
I recently wrote an HTTP Listener (i.e. TCP based) app in Swift for MacOS and at first I didn't know how to go about it, so I posted a question here on the Forum.. I ended up using the Network framework (3rd answer in the post), which is based on a WWDC video (as referenced in the post). My code and that video should help get you started.
Regards, Michaela
OMG, the solution is very easy - at least in a prototype state. Now let's see how it performs in reality, with code for handling state changes.
import Foundation
import Network
class HTTPServer {
static let shared = HTTPServer()
var listener : NWListener
var queue : DispatchQueue
var connected : Bool = false
let nwParms = NWParameters.tcp
init() {
queue = DispatchQueue(label: "HTTP Server Queue")
listener = try! NWListener(using: nwParms, on: 80)
listener.newConnectionHandler = { [weak self] (newConnection ) in
print("**** New Connection added")
if let strongSelf = self {
newConnection.start(queue: strongSelf.queue)
strongSelf.receive(on: newConnection)
}
}
listener.stateUpdateHandler = { (newState) in
print("**** Listener changed state to \(newState)")
}
listener.start(queue: queue)
}
func receive(on connection: NWConnection) {
connection.receive(minimumIncompleteLength: 0, maximumLength: 4096) { (content, context, isComplete, error) in
guard let receivedData = content else {
print("**** content is nil")
return
}
let dataString = String(decoding: receivedData, as: UTF8.self)
print("**** received data = \(dataString)")
connection.cancel()
}
}
}
This solution is a modification of a Bonjour Listener demo app in a WWDC presentation.
As I said in my original question, this is for listening for infrequent HTTP packets on an internal, secure network. I wouldn't dare use it as a fully open web-server. For my purposes, the information in the incoming HTTP command is all I need: it contains the new state of the WiFi switch that is on the local network.
Regards, Michaela
If your data are static, or change only infrequently, putting them into the app is fine. If the data change, and you are distributing the app to others, you will need to replace the app’s data file, recompile, and re-release the app. There’s a way of using files from the Documents folder of iOS, or “external” files, but this can be a bit of a learning curve.
Assuming that your data are static, then here’s what to do:
Place your term/phrase csv file into your App’s project folder using MacOS’ Finder, preferably using Copy (not Move), so as to keep a backup copy.
In Xcode, select File -> Add Files to…. From the Top Menu and a pop-up screen will appear
Select your file from the list (others should be greyed out) and make sure that the Destination - copy files if needed box is ticked and the Add to targets box - YourAppName is also ticked
Press the Add button (far right, bottom of pop-up)
Back in Xcode’s main screen, replace the getData function in DataModel with:
func getData() {
// DatFrame(contentsOfCSVFile) needs a URL reference, so we need to get the app's data (Bundle resource) reference as a URL
guard let fileURL = Bundle.main.url(forResource: "TermPhrases", withExtension: "csv") else {
// CAUTION - withExtension is case sensitive, so if your CSV file was created as TermPhrases.CSV then you need to use uppercase in the parameter
print("**** error getting file url")
return
}
let csvOptions = CSVReadingOptions(hasHeaderRow: true, ignoresEmptyLines: true, delimiter: ",")
do {
dataTable = try DataFrame(contentsOfCSVFile: fileURL,columns: nil, rows: nil, types: ["Term":CSVType.string,"Phrase":CSVType.string], options: csvOptions)
} catch {
print("Failed to load datatable from CSV \(error)")
return
}
print("**** Loaded \(dataTable.rows.count) Terms") // check to make sure terms got loaded
}
I haven't tested this with iOS (just on the Mac), but it should work - assuming that you created a new iOS project or multi-platform project with Shared resources.
Best wishes and regards, Michaela
Further to my comment on my previous answer, a computed property (realDate) cannot be used as an NSSortDescriptor key (my mistake, sorry) but FetchedResults can be sorted, upon return, using Swift array's .sorted(by: {$0.realDate! > $1.realDate!}) and then used in your table (or whatever), but you have to be careful in converting the String date to a system date, or in dealing with nil values coming from the conversion.
In my sample code for realDate, the dateFormat should be df.dateFormat = "MMM dd, yyyy" not df.dateFormat = "MMM d, yyyy". Maybe check your date strings for variations to the string format, just to be sure? I made a couple of typos when creating test data and that messed the solution up!!
If you do have variations, you can test for them in the reaDate extension code and convert accordingly.
Regards, Michaela
Only the last iteration of thing persists because it is recreated on each iteration of i. Change the initialisation of thing to be str, i.e. to "SOME SEARCH TEXT" and the reference to str.map to be thing.map and it will work:
let str = "SOME SEARCH TEXT"
var thing = str
let letters = ["A","E","S"]
let replacements = ["a", "e", "s"]
letters.enumerated().forEach { (i,r) in
thing = String(thing.map {$0 == Character(String(r)) ? Character(String(replacements[i])) : $0});
print(thing)
}
If it's OK to have str mutate, then you can change str to a var and then use this code:
var str = "SOME SEARCH TEXT"
let letters = ["A","E","S"]
let replacements = ["a", "e", "s"]
letters.enumerated().forEach { (i,r) in
str = String(str.map {$0 == Character(String(r)) ? Character(String(replacements[i])) : $0});
print(str)
}
Best wishes and regards, Michaela
There is a way of fixing the problem without restructuring your CoreData model and then recreating the CoreData records. However, this can be a bit tricky if you haven't had experience with manually generating class definitions for CoreData entities. In essence the solution is this:
In Xcode's CoreData Model editor, click on the Entity you need to fix, make sure that the Inspector panel is open (far right of Xcode screen) then change the Codegen option to Manual/None (it's probably set at "Class definition").
Then (still in the Model editor) on the Xcode menu bar (far top of screen) select Editor, CreateNSManagedObject Subclass and follow the prompts to create Swift files in your project for the CoreData Entity classes.
Create a new Swift file for an extension to your Entity. For example, if your Entity is called "Commitment", create a file called "Commitment_Developer" and then use code like this:
import Foundation
extension Commitment {
var realDate : Date? {
guard let strDate = strAttribute else { // where "strAttribute" is the name of your CoreData date-as-string attribute
return nil
}
let df = DateFormatter()
df.timeZone = NSTimeZone.local
df.dateFormat = "MMM d, yyyy"
guard let rDate = df.date(from:strDate) else {
return nil
}
return rDate
}
}
Then, in your NSSortDescriptor use the "realDate" property that was created in the extension. Apart from that, your code remains the same as now - without any refactoring of your CoreData model or actual data. You also have the ability to use the "realDate" elsewhere whenever you need access to a system date, as opposed to a string.
Note that "realDate" is Optional, so needs to be unwrapped upon use. It could be defined as non-optional, if you're really confident in the integrity of the string-dates, but then there's a problem if you later try to use iCloud syncing with non-optionals.
I hope this helps. best wishes and regards, Michaela
Here's a revised Example App for your situation, where there's now a Search Bar and only search results are shown in the View - not the full set of Terms and Phrases.
ContentView
import SwiftUI
import TabularData
struct ContentView: View {
@ObservedObject var model = DataModel.shared
@State private var searchText = ""
var body: some View {
NavigationView{
Text("Searching for \(searchText)")
.searchable(text: $searchText)
List(model.searchResults.rows,id:\.index) { row in
HStack{
Text(row["Term"] as! String)
Text(String(row["Phrase"] as! String))
}
}
}
.onChange(of: searchText) { srchText in
model.searchResults = model.findTerm(srchText)
}
}
}
Data Model
import Foundation
import TabularData
class DataModel : ObservableObject {
static let shared = DataModel()
var dataTable = DataFrame()
@Published var searchResults = DataFrame()
init() {
getData()
}
func getData() {
var url: URL?
do {
url = try FileManager.default.url(for: FileManager.SearchPathDirectory.downloadsDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true)
} catch {
print("Failed to get Downsloads URL \(error)")
return
}
let csvOptions = CSVReadingOptions(hasHeaderRow: true, ignoresEmptyLines: true, delimiter: ",")
let fileURL = url?.appendingPathComponent("TermPhrases.csv")
do {
dataTable = try DataFrame(contentsOfCSVFile: fileURL!,columns: nil, rows: nil, types: ["Term":CSVType.string,"Phrase":CSVType.string], options: csvOptions)
} catch {
print("Failed to get load datatable from CSV \(error)")
return
}
}
func findTerm(_ searchTerm: String) -> DataFrame {
let results = dataTable.filter ({ row in
guard let term = row["Term"] as? String else {
return false
}
if term == searchTerm {
return true
}
return false
})
return DataFrame(results)
}
}
extension DataFrame.Rows : RandomAccessCollection {
}
Caution!!
The findTerm func checks for an exact match of the Term, i.e. case sensitive and full match. If you need case insensitivity and partial matches, you need to change the findTerm function.
If your purpose is only to look up phrases for a Term, then this is pretty much a complete solution, although if you've started an Xcode project with View Controllers (Storyboard) then to use my solution you'll need to create a new project with SwiftUI Interface.
Best wishes and regards, Michaela
In SwiftUI, ContentView is the main user interface, equivalent to Main ViewController in UIKit. The List command (actually a struct in SwiftUI) produces a Table on the UI.
List(model.dataTable.rows,id:\.index) { row in // this creates a Table of entries from dataTable (i.e. Terms and phrases
HStack{ // this contains formatting for the rows of the table
Text(row["Term"] as! String) // this is the Term "cell", although it's not a cell like in UIKit
Text(String(row["Phrase"] as! String)) // this is the Phrase cell, although it's not a cell like in UIKit
}
}
That's all that's needed in SwiftUI to create a working UI for your Term/Phrase data. However, in Xcode (when creating a project) you have to make sure that the Interface option for a new project specifies SwiftUI not Storyboard.
With UIKit, processing of data for a View is normally done within the ViewController of the View. With SwiftUI, the data processing is preferably done in a Data Model with results made available to SwiftUI views via (for example) the var model = DataModel.shared statement. So, for the Search Bar, the query text would be provided to the Data Model, which runs the query, then provides the result(s) back to the SwiftUI View.
If I get time later today (it's 8:30am now here in Oz) I'll extend my sample code to include searching.
Best wishes and regards, Michaela
The answers to your questions depend what you want to do with the "Dictionary", how volatile it is (i.e. updates, additions, deletions) and how large it is. A few hundred Terms that never change could be handled without a database (i.e. without an indexed, structured permanent store such as SQLite or CoreData). From my understanding of what you're looking for, I'd suggest creating your terms/phrases in a spreadsheet (e.g. Excel) and then exporting the table as a CSV file for use with code like in the SwiftUI sample below:
ContentView
import SwiftUI
import TabularData
struct ContentView: View {
var model = DataModel.shared
var body: some View {
List(model.dataTable.rows,id:\.index) { row in
HStack{
Text(row["Term"] as! String)
Text(String(row["Phrase"] as! String))
}
}
}
}
Data Model - this is not a database: it's where the importing and processing of the terms takes place within the app, and the data have to be loaded into the app again when next run.
import Foundation
import TabularData
class DataModel {
static let shared = DataModel()
@Published var dataTable = DataFrame()
init() {
getData()
let searchTerm = "Nine"
print("there are \(findTerm(searchTerm).rows.count) terms for \(searchTerm)")
}
func getData() {
var url: URL?
do {
url = try FileManager.default.url(for: FileManager.SearchPathDirectory.downloadsDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true)
} catch {
print("Failed to get Downsloads URL \(error)")
return
}
let csvOptions = CSVReadingOptions(hasHeaderRow: true, ignoresEmptyLines: true, delimiter: ",")
let fileURL = url?.appendingPathComponent("TermPhrases.csv")
do {
dataTable = try DataFrame(contentsOfCSVFile: fileURL!,columns: nil, rows: nil, types: ["Term":CSVType.string,"Phrase":CSVType.string], options: csvOptions)
} catch {
print("Failed to load datatable from CSV \(error)")
return
}
}
func findTerm(_ searchTerm: String) -> DataFrame.Slice {
let results = dataTable.filter ({ row in
guard let term = row["Term"] as? String else {
return false
}
if term == searchTerm {
return true
}
return false
})
return results
}
}
extension DataFrame.Rows : RandomAccessCollection {
}
Thiis sample uses the TabularData Framework which has many features for importing and manipulating data from CSV files (spreadsheet output), but also for Machine Learning. This sample app was written for MacOS using csv input from the Downloads folder:
Sample Data in a file called TermPhrases.csv
Term Phrase
Nine A stitch in time saves these
Two A bird in the hand is worth these in the bush
Seven Pillars of wisdom
The findTerm function in the DataModel shows how to filter (Search) the Data Table for a term. DataFrame filtering is not like normal Swift array filtering.
Hopefully this gets you started, then you can decide what other processing you'd like to do on the Data Table (i.e. your dictionary)
Best wishes and regards, Michaela
My understanding is that CMAmbientPressureData is an internal class, which is then used by CMAltimeter to calculate relative altitude based on pressure changes. The data from CMAltimeter.startRelativeAltitudeUpdates has a pressure property that provides the atmospheric pressure, as an NSNumber of kilopascals, of the reported relative altitude.
As of iOS 15 there is a CMAbsoluteAltitudeData class that provides the absolute altitude (and changes thereof), but I can't see a pressure property available via the updates handler.
I've been using CMAltimeter data on a regular basis for a couple of years in my running app and the update handler consistently provides an update every second, irrespective of an altitude (or pressure) change or not. Most of my runs start and end at the same location, so I often see a discrepancy of 2 to 5 metres between the start and end relative altitudes - which coincide with the atmospheric pressure change at that location as recorded by the Bureau of Meteorology.
I haven't accessed the pressure property of CMAltitudeData but, from the foregoing, I conclude that it would be a fairly accurate recording of the current ambient pressure of a location. For my purposes, the CMAltimeter data are far more accurate and consistent than the altitude data from CoreLocation.
I hope this helps. Best wishes and regards, Michaela
I haven't used UIKit since SwiftUI arrived, but the below code should work.
Create a DataModel class for use as a singleton and put your items array into it:
import Foundation
class DataModel {
static let shared = DataModel()
var items: [String] = []
}
In your FirstViewController add a reference to the DataModel singleton, then use the items array as needed, e.g. in your UITableView
let dataModel = DataModel.shared
Use dataModel.items in the UITableView
In the SecondViewController also add a reference to the DataModel singleton, then use this to append to items:
class SecondViewController: UIViewController {
let dataModel = DataModel.shared
let textField: UITextField = UITextField()
let saveButton: UIButton = UIButton()
@objc func saveButtonTapped() {
dataModel.items.append(textField.text)
}
}
However, using this method you then, presumably, need to force a reload of the FirstViewController's tableview. This can be done via delegation (which could also be used to send the new item for appending in the FirstViewController) or via Combine. If using Combine, the DataModel becomes:
import Foundation
import Combine
class DataModel {
static let shared = DataModel()
let itemAdded = PassthroughSubject<(String), Never>()
var items: [String] = [] {
didSet {
itemAdded.send("ItemAdded")
}
}
}
and the FirstViewController needs to include Combine and to have
var itemSink : AnyCancellable? = nil
itemSink = dataModel.itemAdded.sink(receiveValue: { message in
// you don't need to use message, but can be useful if you want to know about additions, deletions etc by sending different messages
tableView.reloadData()
})
at an appropriate place in your code, i.e. to force the Table Reload.
I haven't tested this with a real UIKit app and tableview, but it should work.
Regards, Michaela
I'm not sure that I fully understand what you're doing and how you're doing it. However, I've a couple of apps that have numerous variables in my Data Store (backend?) updated by async methods (e.g. BLE devices) and prefer not to overload my SwiftUI Views with extraneous updates. I therefore use the Combine Framework to publish an object when it's ready for display e.g. in my Data Store public let transactionAdded = PassthroughSubject<(Transaction), Never>(). Then when appropriate, in my Data Store code, I publish the object e.g. transactionAdded.send(newTransaction)
In my SwiftUI View I use .onReceive to listen for the published object and do whatever is necessary e.g. .onReceive((dataStore.transactionAdded), perform: { transaction in .......}. The perform usually needs to trigger a View refresh, e.g. by setting/updating a @State variable.
That said, Combine can update values in the store without triggering view updates, and/or chain async results before then informing views.
Dunno if this helps. Cheers, Michaela
I assume that you want to show the location's coordinates as a place name: if so, you need to use CoreLocation's CLGeocoder class, which returns an array of Placemarks. i.e. name and address. Usually there will be only one place mark for a CLLocation (i.e. coordinates), but sometimes there can be several, e.g. when a location has a number of businesses.
There are plenty of examples on the Web of how to use CLGeocoder, but if you get stuck I can provide some sample code.
Best wishes and regards, Michaela