My company builds an application using the External Accessory framework to communicate with our hardware. We have followed the documentation and example here and use the stream delegate pattern for scheduling the handling of the EASession's InputStream and OutputStream: https://developer.apple.com/library/archive/featuredarticles/ExternalAccessoryPT/Articles/Connecting.html
Our application works, however we have had some issues that cause us to doubt our implementation of the Stream handling for our EASession.
All the examples I can find for how to set up this RunLoop based implementation for managing and using the streams associated with the EASession seem to use RunLoop.current
to schedule the InputStream and OutputStream. What is not clear to me is what thread the processing of these streams is actually getting scheduled upon.
We have occasionally observed our app "freezing" when our connected accessory disconnects, which makes me worry that we have our Stream processing on the main thread of the application. We want these streams to be processed on a background thread and never cause problems locking up our main thread or UI.
How exactly do we achieve this? If we are indeed supposed to only use RunLoop.current, how can we make sure we're opening the EASession and scheduling its streams on a non-main thread?
On what thread will we receive EAAccessoryDidConnect and EAAccessoryDidDisconnect notifications? Is it safe to schedule streams using RunLoop.current from that thread? What about when the app returns from the background, how are we meant to reconnect to an accessory that the iOS device is already connected to?
Hopefully someone here can help guide us and shed some light on how to achieve our desired behavior here.
After writing the above I decided to try this for myself and… sheesh… the details are a quite persnickety. So, pasted in below is a concrete example of the process I’m talking about.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
import Foundation
final class RunLoopThread: NSObject {
static let shared: RunLoopThread = RunLoopThread(name: "RunLoopThread.shared")
init(name: String) {
// We have to call `super` before initialising `thread` because
// initialising it requires that `self` be fully initialised. To make
// this work we make `thread` an optional, initialise it to `nil`, and
// then reinitialise it with our actual thread.
self.thread = nil
super.init()
let thread = Thread(
target: self,
selector: #selector(threadEntryPoint),
object: nil
)
thread.name = name
thread.start()
self.thread = thread
}
private var thread: Thread?
@objc
private func threadEntryPoint() {
// We need an initial run loop source to prevent the run loop from
// stopping. We use a timer that fires roughly once a day.
Timer.scheduledTimer(withTimeInterval: 100_000, repeats: true, block: { _ in })
RunLoop.current.run()
fatalError()
}
func run(_ body: @escaping () -> Void) {
self.perform(
#selector(runBody(_:)),
on: self.thread!,
with: body,
waitUntilDone: false,
modes: [RunLoop.Mode.default.rawValue]
)
}
@objc
private func runBody(_ body: Any) {
let body = body as! (() -> Void)
body()
}
}
func main() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
RunLoopThread.shared.run {
Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { _ in
print("timer did fire, thread name: \(Thread.current.name ?? "-")")
}
}
}
dispatchMain()
}
main()