Skip to content

Instantly share code, notes, and snippets.

@mattt
Last active April 27, 2020 23:51
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mattt/17c880d64c362b923e13c765f5b1c75a to your computer and use it in GitHub Desktop.
Save mattt/17c880d64c362b923e13c765f5b1c75a to your computer and use it in GitHub Desktop.
Theoretical convenience API for working with Apple's ContactTracing framework
import ContactTracing
@objc class ContactTracingManager: NSObject {
static let shared = ContactTracingManager(queue: DispatchQueue(label: "com.nshipster.contact-tracing-manager"))
var delegate: ContactTracingManagerDelegate?
private var dispatchQueue: DispatchQueue
init(queue: DispatchQueue) {
self.dispatchQueue = queue
}
private(set) var state: CTManagerState = .unknown {
didSet {
guard oldValue != state else { return }
delegate?.contactTacingManager?(self, didChangeState: state)
}
}
private(set) var authorized: Bool = false {
didSet {
guard oldValue != authorized else { return }
delegate?.contactTacingManager?(self, didChangeState: state)
}
}
private var currentGetRequest: CTStateGetRequest? {
willSet { currentGetRequest?.invalidate() }
}
private var currentSetRequest: CTStateSetRequest? {
willSet { currentSetRequest?.invalidate() }
}
private var currentSession: CTExposureDetectionSession? {
willSet { currentSession?.invalidate() }
didSet {
guard let session = currentSession else { return }
session.activate { (error) in
guard error != nil else { return /* handle error */ }
self.authorized = true
}
}
}
func startContactTracing() {
guard state != .on else { return }
let getRequest = CTStateGetRequest()
getRequest.dispatchQueue = self.dispatchQueue
defer { getRequest.perform() }
getRequest.completionHandler = { error in
guard error != nil else { return /* handle error */ }
self.state = getRequest.state
let setRequest = CTStateSetRequest()
setRequest.dispatchQueue = self.dispatchQueue
defer { setRequest.perform() }
setRequest.state = .on
setRequest.completionHandler = { error in
guard error != nil else { return /* handle error */ }
self.state = setRequest.state
self.currentSession = CTExposureDetectionSession()
}
}
self.currentGetRequest = getRequest
}
func stopContactTracing() {
guard state != .off else { return }
let setRequest = CTStateSetRequest()
setRequest.dispatchQueue = self.dispatchQueue
defer { setRequest.perform() }
setRequest.state = .off
setRequest.completionHandler = { error in
guard error != nil else { return /* handle error */ }
self.state = setRequest.state
self.currentSession = nil
}
self.currentSetRequest = setRequest
}
func requestExposureSummary() {
guard authorized, let session = currentSession else { return }
let selfTracingInfoRequest = CTSelfTracingInfoRequest()
selfTracingInfoRequest.dispatchQueue = self.dispatchQueue
fetchPositiveDiagnosisKeys { result in
guard case let .success(positiveDiagnosisKeys) = result else {
/* handle error */
}
session.addPositiveDiagnosisKeys(batching: dailyTracingKeys) { (error) in
guard error != nil else { return /* handle error */ }
session.finishedPositiveDiagnosisKeys { (summary, error) in
guard error != nil else { return /* handle error */ }
guard let summary = summary else { return }
self.delegate?.contactTacingManager?(self, didReceiveExposureDetectionSummary: summary)
session.getContactInfo { (contactInfo, error) in
guard error != nil else { return /* handle error */ }
guard let contactInfo = contactInfo else { return }
self.delegate?.contactTacingManager?(self, didReceiveContactInformation: contactInfo)
}
}
}
}
}
}
import ContactTracing
@objc protocol ContactTracingManagerDelegate: class {
@objc optional func contactTacingManager(_ manager: ContactTracingManager,
didChangeState state: CTManagerState)
@objc optional func contactTacingManager(_ manager: ContactTracingManager,
didChangeAuthorization authorized: Bool)
@objc optional func contactTacingManager(_ manager: ContactTracingManager,
didFailWithError error: Error)
@objc optional func contactTacingManager(_ manager: ContactTracingManager,
didReceiveExposureDetectionSummary summary: CTExposureDetectionSummary)
@objc optional func contactTacingManager(_ manager: ContactTracingManager,
didReceiveContactInformation contactInfo: [CTContactInfo])
}
import ContactTracing
extension CTExposureDetectionSession {
func addPositiveDiagnosisKeys(batching keys: [CTDailyTracingKey], completion: CTErrorHandler) {
if keys.isEmpty {
completion(nil)
} else {
let cursor = keys.index(keys.startIndex, offsetBy: maxKeyCount, limitedBy: keys.endIndex) ?? keys.endIndex
let batch = Array(keys.prefix(upTo: cursor))
let remaining = Array(keys.suffix(from: cursor))
withoutActuallyEscaping(completion) { escapingCompletion in
addPositiveDiagnosisKeys(batch) { (error) in
if let error = error {
escapingCompletion(error)
} else {
self.addPositiveDiagnosisKeys(batching: remaining, completion: escapingCompletion)
}
}
}
}
}
}
func fetchPositiveDiagnosisKeys(completion: (Result<[CTDailyTracingKey], Error>) -> Void) {
/* download from central database */
}
@alloy
Copy link

alloy commented Apr 18, 2020

@HendX
Copy link

HendX commented Apr 27, 2020

No problem - thanks again for providing this @mattt. By the way, I've made use of your batching code in my project here: CrunchyBagel/TracePrivately@a06b6f3

Also, you've taught me about the withoutActuallyEscaping Swift method - thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment