Now facing some challenges using CoreBlueTooth L2CAP channel. In order to better understand how things work. I have taken the L2CapDemo (master) (https://github.com/paulw11/L2CapDemo) from GitHub and tried to experiment with it. Here is what I have done, along with one question.
In have replaced the sendTextTapped function, with this one :
#IBAction func sendTextTapped(_ sender: UIButton) {
guard let ostream = self.channel?.outputStream else {
return
}
var lngStr = "1234567890"
for _ in 1...10 {lngStr = lngStr + lngStr}
let data = lngStr.data(using: .utf8)!
let bytesWritten = data.withUnsafeBytes { ostream.write($0, maxLength: data.count) }
print("bytesWritten = \(bytesWritten)")
print("WR = \(bytesWritten) / \(data.count)")
}
And the execution result is:
bytesWritten = 8192
WR = 8192 / 10240
That allows me to see what happens in the case where bytesWritten < data.count.
In other words, all the bytes cannot be sent over in one chunk.
Now comes the question. The problem is I see nothing, the bytes left over seems to be just ignored.
I want to know what to do if I do not want to ignore those bytes. What is the way to care about the rest of the bytes? There will be cases where we will need to transfer tens of thousands or even hundreds of thousands of bytes.
You simply need to make a note of how many characters were sent, remove those from the data instance and then when you get a delegate callback indicating space is available in the output stream, send some more.
For example, you could add a couple of properties to hold the queued data and a serial dispatch queue to ensure thread-safe access to that queue:
private var queueQueue = DispatchQueue(label: "queue queue", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem, target: nil)
private var outputData = Data()
Now, in the sendTextTapped function you can just add the new data to the output queue:
#IBAction func sendTextTapped(_ sender: UIButton) {
var lngStr = "1234567890"
for _ in 1...10 {lngStr = lngStr + lngStr}
let data = lngStr.data(using: .utf8)!
self.queue(data:data)
}
the queue(data:) function adds the data to the outputData object in a thread-safe manner and calls send()
private func queue(data: Data) {
queueQueue.sync {
self.outputData.append(data)
}
self.send()
}
send() ensures that the stream is connected, there is data to send and there is space available in the output stream. If all is OK then it sends as many bytes as it can. The sent bytes are then removed from output data (again in a thread safe manner).
private func send() {
guard let ostream = self.channel?.outputStream, !self.outputData.isEmpty, ostream.hasSpaceAvailable else{
return
}
let bytesWritten = outputData.withUnsafeBytes { ostream.write($0, maxLength: self.outputData.count) }
print("bytesWritten = \(bytesWritten)")
queueQueue.sync {
if bytesWritten < outputData.count {
outputData = outputData.advanced(by: bytesWritten)
} else {
outputData.removeAll()
}
}
}
The final change is to call send() in response to a .hasSpaceAvailable stream event:
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
switch eventCode {
case Stream.Event.openCompleted:
print("Stream is open")
case Stream.Event.endEncountered:
print("End Encountered")
case Stream.Event.hasBytesAvailable:
print("Bytes are available")
case Stream.Event.hasSpaceAvailable:
print("Space is available")
self.send()
case Stream.Event.errorOccurred:
print("Stream error")
default:
print("Unknown stream event")
}
}
You can see the modified code in the largedata branch of the example
Related
In a personal project of mine, I have created an API caller to retrieve a user's saved tracks from the Spotify API. The Spotify endpoint which I am using has a limit (maximum of 50 tracks per request) as well as an offset (starting index of first track in request), which is why I decided to use a FOR loop to get a series of track pages (each 50 tracks) and append them to a global array. The data is loaded from the main thread, and while the data is being requested, I display a child view controller with a spinner view. Once the data request has completed, I remove the spinner view, and transition to another view controller (passing the data as a property).
I have tried many things, but the array of tracks is always empty following the API request. I have a feeling it has to do with the synchronicity of my request, or maybe its possible that I'm not handling it correctly. Ideally, I would like to wait until the request from my API finishes, then append the result to the array. Do you have any suggestions on how I could solve this? Any help is much appreciated!
func createSpinnerView() {
let loadViewController = LoadViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
add(asChildViewController: loadViewController)
DispatchQueue.main.async { [weak self] in
if (self?.dropdownButton.dropdownLabel.text == "My saved music") {
self?.fetchSavedMusic() { tracksArray in
self?.tracksArray = tracksArray
}
}
...
self?.remove(asChildViewController: loadViewController)
self?.navigateToFilterScreen(tracksArray: self!.tracksArray)
}
}
private func fetchSavedMusic(completion: #escaping ([Tracks]) -> ()) {
let limit = 50
var offset = 0
var total = 200
for _ in stride(from: 0, to: total, by: limit) {
getSavedTracks(limit: limit, offset: offset) { tracks in
//total = tracks.total
self.tracksArray.append(tracks)
}
print(offset, limit)
offset = offset + 50
}
completion(tracksArray)
}
private func getSavedTracks(limit: Int, offset: Int, completion: #escaping (Tracks) -> ()) {
APICaller.shared.getUsersSavedTracks(limit: limit, offset: offset) { (result) in
switch result {
case .success(let model):
completion(model)
print("success")
case .failure(let error):
print("Error retrieving saved tracks: \(error.localizedDescription)")
print(error)
}
}
}
private func navigateToFilterScreen(tracksArray: [Tracks]) {
let vc = FilterViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
vc.paginatedTracks = tracksArray
show(vc, sender: self)
}
First you need to call completion when all of your data is loaded. In your case you call completion(tracksArray) before any of the getSavedTracks return.
For this part I suggest you to recursively accumulate tracks by going through all pages. There are multiple better tools to do so but I will give a raw example of it:
class TracksModel {
static func fetchAllPages(completion: #escaping ((_ tracks: [Track]?) -> Void)) {
var offset: Int = 0
let limit: Int = 50
var allTracks: [Track] = []
func appendPage() {
fetchSavedMusicPage(offset: offset, limit: limit) { tracks in
guard let tracks = tracks else {
completion(allTracks) // Most likely an error should be handled here
return
}
if tracks.count < limit {
// This was the last page because we got less than limit (50) tracks
completion(allTracks+tracks)
} else {
// Expecting another page to be loaded
offset += limit // Next page
allTracks += tracks
appendPage() // Recursively call for next page
}
}
}
appendPage() // Load first page
}
private static func fetchSavedMusicPage(offset: Int, limit: Int, completion: #escaping ((_ tracks: [Track]?) -> Void)) {
APICaller.shared.getUsersSavedTracks(limit: limit, offset: offset) { result in
switch result {
case .success(let model):
completion(model)
case .failure(let error):
print(error)
completion(nil) // Error also needs to call a completion
}
}
}
}
I hope comments will clear some things out. But the point being is that I nested an appendPage function which is called recursively until server stops sending data. In the end either an error occurs or the last page returns fewer tracks than provided limit.
Naturally it would be nicer to also forward an error but I did not include it for simplicity.
In any case you can now anywhere TracksModel.fetchAllPages { } and receive all tracks.
When you load and show your data (createSpinnerView) you also need to wait for data to be received before continuing. For instance:
func createSpinnerView() {
let loadViewController = LoadViewController.instantiateFromAppStoryboard(appStoryboard: .OrganizeScreen)
add(asChildViewController: loadViewController)
TracksModel.fetchAllPages { tracks in
DispatchQueue.main.async {
self.tracksArray = tracks
self.remove(asChildViewController: loadViewController)
self.navigateToFilterScreen(tracksArray: tracks)
}
}
}
A few components may have been removed but I hope you see the point. The method should be called on main thread already. But you are unsure what thread the API call returned on. So you need to use DispatchQueue.main.async within the completion closure, not outside of it. And also call to navigate within this closure because this is when things are actually complete.
Adding situation for fixed number of requests
For fixed number of requests you can do all your requests in parallel. You already did that in your code.
The biggest problem is that you can not guarantee that responses will come back in same order than your requests started. For instance if you perform two request A and B it can easily happen due to networking or any other reason that B will return before A. So you need to be a bit more sneaky. Look at the following code:
private func loadPage(pageIndex: Int, perPage: Int, completion: #escaping ((_ items: [Any]?, _ error: Error?) -> Void)) {
// TODO: logic here to return a page from server
completion(nil, nil)
}
func load(maximumNumberOfItems: Int, perPage: Int, completion: #escaping ((_ items: [Any], _ error: Error?) -> Void)) {
let pageStartIndicesToRetrieve: [Int] = {
var startIndex = 0
var toReturn: [Int] = []
while startIndex < maximumNumberOfItems {
toReturn.append(startIndex)
startIndex += perPage
}
return toReturn
}()
guard pageStartIndicesToRetrieve.isEmpty == false else {
// This happens if maximumNumberOfItems == 0
completion([], nil)
return
}
enum Response {
case success(items: [Any])
case failure(error: Error)
}
// Doing requests in parallel
// Note that responses may return in any order time-wise (we can not say that first page will come first, maybe the order will be [2, 1, 5, 3...])
var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count) { // Start with all nil
didSet {
// Yes, Swift can do this :D How amazing!
guard responses.contains(where: { $0 == nil }) == false else {
// Still waiting for others to complete
return
}
let aggregatedResponse: (items: [Any], errors: [Error]) = responses.reduce((items: [], errors: [])) { partialResult, response in
switch response {
case .failure(let error): return (partialResult.items, partialResult.errors + [error])
case .success(let items): return (partialResult.items + [items], partialResult.errors)
case .none: return (partialResult.items, partialResult.errors)
}
}
let error: Error? = {
let errors = aggregatedResponse.errors
if errors.isEmpty {
return nil // No error
} else {
// There was an error.
return NSError(domain: "Something more meaningful", code: 500, userInfo: ["all_errors": errors]) // Or whatever you wish. Perhaps just "errors.first!"
}
}()
completion(aggregatedResponse.items, error)
}
}
pageStartIndicesToRetrieve.enumerated().forEach { requestIndex, startIndex in
loadPage(pageIndex: requestIndex, perPage: perPage) { items, error in
responses[requestIndex] = {
if let error = error {
return .failure(error: error)
} else {
return .success(items: items ?? [])
}
}()
}
}
}
The first method is not interesting. It just loads a single page. The second method now collects all the data.
First thing that happens is we calculate all possible requests. We need a start index and per-page. So the pageStartIndicesToRetrieve for case of 145 items using 50 per page will return [0, 50, 100]. (I later found out we only need count 3 in this case but that depends on the API, so let's stick with it). We expect 3 requests starting with item indices [0, 50, 100].
Next we create placeholders for our responses using
var responses: [Response?] = .init(repeating: nil, count: pageStartIndicesToRetrieve.count)
for our example of 145 items and using 50 per page this means it creates an array as [nil, nil, nil]. And when all of the values in this array turn to not-nil then all requests have returned and we can process all of the data. This is done by overriding the setter didSet for a local variable. I hope the content of it speaks for itself.
Now all that is left is to execute all requests at once and fill the array. Everything else should just resolve by itself.
The code is not the easiest and again; there are tools that can make things much easier. But for academical purposes I hope this approach explains what needs to be done to accomplish your task correctly.
I'm using multipeer connectivity to synchronize between several devices connected through Wifi the movement of nodes in a 3D SceneKit environnement.
The node movement is calculated on the master device and sent to the slave devices which then set the node to the position received. The movement is sent via the renderer loop of the SceneView of the master, and through a data stream opened with each slave and managed with the MultipeerConnectivity framework.
The video below shows the result, and the issue which is the jittering of the ball on the slave (right) due to a regular pause every 0.6-0.7 second in the reception of the data through the stream. The counter on the upper left shows that no packet is lost. There is also no issue with the integrity of the data received.
This very regular pause on the slaves is not present in the simulator but only when it runs on real devices, and whatever the devices (iPhone or Ipad, old or recent).
Is there a way to find out what can cause this regular pause on the slaves devices ?
Would it make sense to have the input stream on the slaves executed on a dedicated thread/runloop instead of the runloop of the main thread ?
Below the implementation
Master Multipeer Connectivity initialization
PeerID = MCPeerID(displayName: "Master (" + UIDevice.current.name + ")")
MPCSession = MCSession(peer: PeerID, securityIdentity: nil, encryptionPreference: .none)
MPCSession.delegate = self
ServiceAdvertiser = MCNearbyServiceAdvertiser(peer: PeerID, discoveryInfo: nil, serviceType: "ARMvt")
ServiceAdvertiser.delegate = self
ServiceAdvertiser.startAdvertisingPeer()
Slave Multipeer Connectivity initialization
PeerID = MCPeerID(displayName: "Slave (" + UIDevice.current.name + ")")
MPCSession = MCSession(peer: PeerID, securityIdentity: nil, encryptionPreference: .none)
MPCSession.delegate = self
ServiceBrowser = MCNearbyServiceBrowser(peer: PeerID, serviceType: "ARMvt")
ServiceBrowser.delegate = self
ServiceBrowser.startBrowsingForPeers()
Function that sends the data, called in the renderer function
func MPCSendData(VCRef: GameViewController, DataToSend: Dictionary<String, Any>, ViaStream: Bool = false)
{
var DataFilledToSend = DataToSend
var DataConverted = try! NSKeyedArchiver.archivedData(withRootObject: DataFilledToSend, requiringSecureCoding: true)
var TailleData: Int = 0
var NewTailleData: Int = 0
if ViaStream // Through the stream
{
// Filling data to have a constant size packet
// kSizeDataPack is set to 2048. The bigger it is the worst is the jittering.
VCRef.Compteur = VCRef.Compteur + 1
VCRef.Message.SetText(Text: String(VCRef.Compteur))
DataFilledToSend[eTypeData.Compteur.rawValue] = VCRef.Compteur
DataFilledToSend[eTypeData.FillingData.rawValue] = "A"
TailleData = DataConverted.count
DataFilledToSend[eTypeData.FillingData.rawValue] = String(repeating: "A", count: kSizeDataPack - TailleData)
DataConverted = try! NSKeyedArchiver.archivedData(withRootObject: DataFilledToSend, requiringSecureCoding: false)
NewTailleData = DataConverted.count
DataFilledToSend[eTypeData.FillingData.rawValue] = String(repeating: "A", count: kSizeDataPack - TailleData - (NewTailleData - kSizeDataPack))
DataConverted = try! NSKeyedArchiver.archivedData(withRootObject: DataFilledToSend, requiringSecureCoding: false)
if VCRef.OutStream!.hasSpaceAvailable
{
let bytesWritten = DataConverted.withUnsafeBytes { VCRef.OutStream!.write($0, maxLength: DataConverted.count) }
if bytesWritten == -1 { print("Erreur send stream") }
} else { print("No space in stream") }
}
else // Not through the stream
{
let Peer = VCRef.MPCSession.connectedPeers.first!
try! VCRef.MPCSession.send(DataConverted, toPeers: [Peer], with: .reliable)
}
}
Function that is called when data is received through the stream on the slave
func stream(_ aStream: Stream, handle eventCode: Stream.Event)
{
DispatchQueue.main.async
{
switch(eventCode)
{
case Stream.Event.hasBytesAvailable:
let InputStream = aStream as! InputStream
var Buffer = [UInt8](repeating: 0, count: kSizeDataPack)
let NumberBytes = InputStream.read(&Buffer, maxLength: kSizeDataPack)
let DataString = NSData(bytes: &Buffer, length: NumberBytes)
if let _ = NSKeyedUnarchiver.unarchiveObject(with: DataString as Data) as? [String:Any] //deserializing the NSData
{
ProcessMPCDataReceived(VCRef: self, RawData: DataString as Data)
}
case Stream.Event.hasSpaceAvailable:
break
case Stream.Event.errorOccurred:
print("ErrorOccurred: \(String(describing: aStream.streamError?.localizedDescription))")
default:
break
}
}
}
Function that processes the data received
func ProcessMPCDataReceived(VCRef: GameViewController, RawData: Data)
{
let DataReceived: Dictionary = (try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(RawData) as! [String : Any])
switch DataReceived[eTypeData.EventType.rawValue] as! String
{
case eTypeEvent.SetMovement.rawValue:
VCRef.CurrentMovement = eTypeMovement(rawValue: DataReceived[eTypeData.Movement.rawValue] as! String)!
case eTypeEvent.SetPosition.rawValue:
VCRef.Ball.position = DataReceived[eTypeData.Position.rawValue] as! SCNVector3
default:
break
}
}
Looks like you're dispatching work on the main thread. While I wouldn't expect unpacking data to really cause these regular pauses, it's possible that you're running in to a data processing bottleneck. In other words, the time it's taking you to receive, unpack, and reposition nodes (which also incurs implicit SceneKit transactions) may be just enough to make the device slow down. It seems like your code is compact enough to allow you to dispatch the entire thing to a queue. I recommend trying that to see if you get any new behavior. Try DispatchQueue.global(), or better, make your own with DispatchQueue(label:"StreamReceiver", qos: .userInteractive). I think the async dispatch is perfectly fine here.
EDIT: Actually, looking at more, I think this may be related to SceneKit transactions. It looks like you're not really 'pausing', but decelerating. I mentioned that implicit transaction - when you position the node, explicitly start, set the animation time for, and end a SCNTransaction. A snippit I've been using is:
func sceneTransaction(_ duration: Int? = nil,
_ operation: () -> Void) {
SCNTransaction.begin()
SCNTransaction.animationDuration =
duration.map{ CFTimeInterval($0) }
?? SCNTransaction.animationDuration
operation()
SCNTransaction.commit()
}
Try calling this with your repositioning code, or just sticking the transaction start/animationTime/end around your block. Good luck!
EDIT 2: Ok, one more thing. If it makes sense for your use case, make sure you've stopped browsing and advertising for peers. It's an expensive bit of networking, and it may be bogging down the entire subsystem.
I have also encountered this issue, which seems to depend on the environment in which the device is located.
Here are the results of my attempts: on two devices connected by multipeer connectivity, the sender sends data 60 times per second, and the receiver prints log. Sometimes there is a slight pause of 0.1-0.2 seconds every second, but sometimes it works fine.
In the end, I replaced the multipeer connectivity framework with the corebluetooth framework, which does not seem to have this issue.
I'm doing a multiplayer game based on the MPC framework for the data communication between the devices of the players (and spritekit for the game).
The logic is that one of the device is the master, does all the game logic and calculations, and sends the positions of the various sprites to the slave devices that just have to update the sprites positions.
The master opens streams between him and all the slaves and sends the various data through these streams. Data are stored in dictionaries of type (String, Any).
The issue is twofold :
little by little I see a difference between the number of time the master send data and the number of time a slave received data
the more the master increase the number of data sent the more the slave receive empty/partial data
I have put various checks and controls, I don't have errors thrown by the send or receive functions, just lost data or empty/partial data.
Here is below some relevant parts of the code. Let me know if more is needed
Thx
J.
Stream initialization between master and slaves
Executed by the master
func InitMPCStream()
{
for Peer in Session.connectedPeers
{
if Peer.displayName != gMasterPeerName
{
let IndexPeer = gtListPlayerMulti.index(where: { $0.Name == Peer.displayName } )
try! gtListPlayerMulti[IndexPeer!].Stream = Session.startStream(withName: "Stream " + Peer.displayName, toPeer: Peer)
gtListPlayerMulti[IndexPeer!].Stream!.delegate = self
gtListPlayerMulti[IndexPeer!].Stream!.schedule(in: RunLoop.main, forMode:RunLoopMode.defaultRunLoopMode)
gtListPlayerMulti[IndexPeer!].Stream!.open()
}
}
}
Executed by the slaves
func session(_ session: MCSession, didReceive InStream: InputStream, withName StreamName: String, fromPeer PeerID: MCPeerID)
{
InputStream = InStream
InputStream.delegate = self
InputStream.schedule(in: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
InputStream.open()
}
Function that sends the data to the slaves
This function is executed on the master side in the update loop of the SKScene, several times per update, so very often.
The logic is that for each update cycle several events occur (between 10 to 100). Each event generates a call to this function. The quantity of data sent is not very important : a dictionary of (String, Any) of 3 to 10 lines where Any can be a string, number (Int, CGFloat...) or bool.
func SendStreamData(IDPeer: Int = 0, DataToSend: Dictionary<String, Any>)
{
let DataConverted = NSKeyedArchiver.archivedData(withRootObject: DataToSend)
if IDPeer == 0
{
for Peer in Session.connectedPeers
{
let IndexPeer = gtListPlayerMulti.index(where: { $0.Name == Peer.displayName } )
if gtListPlayerMulti[IndexPeer!].Stream!.hasSpaceAvailable
{
let bytesWritten = DataConverted.withUnsafeBytes { gtListPlayerMulti[IndexPeer!].Stream!.write($0, maxLength: DataConverted.count) }
if bytesWritten == -1 { print("Erreur send stream") } else { gSendingNumber = gSendingNumber + 1 }
} else { print("No space in stream") }
}
}
else
{
let IndexPeer = gtListPlayerMulti.index(where: { $0.ID == IDPeer } )
let bytesWritten = DataConverted.withUnsafeBytes { gtListPlayerMulti[IndexPeer!].Stream!.write($0, maxLength: DataConverted.count) }
}
}
Function called when data is received on the slave side
func stream(_ aStream: Stream, handle eventCode: Stream.Event)
{
switch(eventCode)
{
case Stream.Event.hasBytesAvailable:
gSendingNumber = gSendingNumber + 1
let InputStream = aStream as! InputStream
var Buffer = [UInt8](repeating: 0, count: 1024)
let NumberBytes = InputStream.read(&Buffer, maxLength:1024)
let DataString = NSData(bytes: &Buffer, length: NumberBytes)
if let Message = NSKeyedUnarchiver.unarchiveObject(with: DataString as Data) as? [String:Any] //deserializing the NSData
{ ProcessMPCDataReceived(VCMain: self, PeerID: PeerID, RawData: DataString as Data) }
else { print("Empty Data") }
case Stream.Event.hasSpaceAvailable:
break
case Stream.Event.errorOccurred:
print("ErrorOccurred: \(aStream.streamError?.localizedDescription)")
default:
break
}
}
The issue here is on the NSKeyedUnarchiver.unarchiveObject line which return nil because DataString does not conform to the expected dictionary
I have an app that pulls data from a socket using the Stream class.
Everything appears to work as expected, I am receiving data in the delegate method:
func stream(_ Stream: Stream, handle eventCode: Stream.Event)
The challenge I am having is converting the data into useful information. There is an Android app that does what I am trying to accomplish using a ByteBuffer.
This ByteBuffer iterates through the data at intervals of 4 and parses the data into the required data types using methods like getFloat and getInt.
Basically I want to loop through the InputStream data and retrieve all the values in the stream. Each value is always 4 bytes in length.
How can I do this using Swift 3?
I got it working. If anyone else needs to do something similar, here is my code:
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
if aStream === inputStream {
switch eventCode {
case Stream.Event.errorOccurred:
print("input: ErrorOccurred: \(String(describing: aStream.streamError))")
case Stream.Event.openCompleted:
print("input: OpenCompleted")
case Stream.Event.hasBytesAvailable:
print("input: HasBytesAvailable")
var index = 0 //this index is used to map the value from bytes below. For example, 0 is date
var buffer = [UInt8](repeating: 0, count: 4) //this breaks the bytes into groups of 4 bytes each
while (inputStream?.hasBytesAvailable)!{
if let length = inputStream?.read(&buffer, maxLength: buffer.count) {
if(length > 0){
let data = Data(bytes: buffer)
let UINT32 = UInt32(littleEndian: data.withUnsafeBytes { $0.pointee })
let float = data.withUnsafeBytes { $0.pointee } as Float
let int = data.withUnsafeBytes { $0.pointee } as Int
//Do something with the values, map to an object, etc
index += 1 //increase the index to know what the next 4 bytes should be mapped as
}
}
}
break
default:
break
}
} ...
I am receiving up to four push notifications for each event I am subscribed to. I have gone through everything related to my CloudKit subscriptions and notification registry and I am convinced this is an Apple problem. I have instead turned my attention toward correctly processing the notifications no matter how many I receive. Here is a simplified version of what I am doing:
func recievePrivatePush(_ pushInfo: [String:NSObject], completion: #escaping ()->Void) {
let notification = CKNotification(fromRemoteNotificationDictionary: pushInfo)
let alertBody = notification.alertBody
if let queryNotification = notification as? CKQueryNotification {
let recordID = queryNotification.recordID
guard let body = queryNotification.alertBody else {
return
}
if recordID != nil {
switch body {
case "Notification Type":
let id = queryNotification.recordID
switch queryNotification.queryNotificationReason {
case .recordCreated:
DataCoordinatorInterface.sharedInstance.fetchDataItem(id!.recordName, completion: {
//
})
break
default:
break
}
}
}
}
}
The fetching code looks something like this:
func fetchDataItem(_ id: String, completion: #escaping ()-> Void) {
if entityExistsInCoreData(id) {return}
let db = CKContainer.default().privateCloudDatabase
let recordID = CKRecordID(recordName: id)
db.fetch(withRecordID: recordID) { (record, error) in
if let topic = record {
//Here I create and save the object to core data.
}
completion()
}
}
All of my code works, the problem I am having is that when I receive multiple notifications, multiple fetch requests are started before the first core data entity is created, resulting in redundant core data objects.
What I would like to do is find a way to add the fetch requests to a serial queue so they are processed one at a time. I can put my request calls in a serial queue, but the callbacks always run asynchronously, so multiple fetch requests are still make before the first data object is persisted.
I have tried using semaphores and dispatch groups with a pattern that looks like this:
let semaphore = DispatchSemaphore(value: 1)
func recievePrivatePush(_ pushInfo: [String:NSObject], completion: #escaping ()->Void) {
_ = semaphore.wait(timeout: .distantFuture)
let notification = CKNotification(fromRemoteNotificationDictionary: pushInfo)
let alertBody = notification.alertBody
if let queryNotification = notification as? CKQueryNotification {
let recordID = queryNotification.recordID
guard let body = queryNotification.alertBody else {
return
}
if recordID != nil {
switch body {
case "Notification Type":
let id = queryNotification.recordID
switch queryNotification.queryNotificationReason {
case .recordCreated:
DataCoordinatorInterface.sharedInstance.fetchDataItem(id!.recordName, completion: {
semaphore.signal()
})
break
default:
break
}
}
}
}
}
Once the above function is called for the second time, and semaphore.wait is called, the execution of the first network request pauses, resulting in a frozen app.
Again, what I would like to accomplish it adding the asynchronous network requests to a queue so that they are made only one at a time i.e. the first network call is completed before the second request is started.
Carl,
Perhaps you'll find your solutions with dispatch groups, a few key expressions to look into.
let group = DispatchGroup()
group.enter()
... code ...
group.leave
group.wait()
I use them to limit the number of http requests I send out in a batch, to wait for the response. Perhaps you could use them together with the suggestion in my comment. Watch this video too, dispatch groups in here, I think more.
https://developer.apple.com/videos/play/wwdc2016/720/
These simple classes helped me solve the problem.
class PushQueue {
internal var pushArray: Array<String> = [String]()
internal let pushQueue = DispatchQueue(label: "com.example.pushNotifications")
public func addPush(_ push: Push) {
pushQueue.sync {
if pushArray.contains(push.id) {
return
} else {
pushArray.append(push.id)
processNotification(push: push)
}
}
}
internal func processNotification(push: Push) {
PushInterface.sharedInstance.recievePrivatePush(push.userInfo as! [String: NSObject])
}
}
class CKPush: Equatable {
init(userInfo: [AnyHashable: Any]) {
let ck = userInfo["ck"] as? NSDictionary
let id = ck?["nid"] as? String
self.id = id!
self.userInfo = userInfo
}
var id: String
var userInfo: [AnyHashable:Any]
public static func ==(lhs: CKPush, rhs: CKPush) -> Bool {
return lhs.id == rhs.id ? true : false
}
}
Please ignore the sloppy force unwraps. They need to be cleaned up.