LiveQuery stops and does not restart in the background

While I am in the app, everything works great. I put the app in the background and come back even 30 seconds later and it’s no longer updating etc. I am fairly new at parse and even newer at LiveQuery so I am not sure what is needed to post but this is my basic setup:

ParseSwift Version: 4.0.1

import ParseSwift
 
var queryM = Message.query()
let subscriptionM = queryM.subscribeCallback!

 struct MessagingInterface: View {

...

 .onAppear(perform: {

    let delegate = LiveQueryDelegate()
                    if let socket = ParseLiveQuery.getDefault() {
                        socket.receiveDelegate = delegate
                    }

  queryM = queryM.where("chatKey" == finalChatID)

 subscriptionM.handleSubscribe { subscribedQuery, isNew in
                            //: You can check this subscription is for this query
                            if isNew {
                                print("Successfully subscribed to messages query \(subscribedQuery)")
                                subscribedQuery.find { result in
                                    switch result {
                                    case .success(let messages):
                                        self.messages = messages
                                    case .failure(let error):
                                        print(error)
                                    }
                                }
                            } else {
                                print("Successfully updated subscription to messages query \(subscribedQuery)")
                            }

subscriptionM.handleEvent { newQuery, event in
                            newQuery.find { result in
                                switch result {
                                case .success(let messages):
                                    self.messages = messages
                                case .failure(let error):
                                    print(error)
                                }
                            }
                            
                            switch event {

                            case .entered(let object):
                                print("Entered: \(object)")
                            case .left(let object):
                                print("Left: \(object)")
                            case .created(let object):
                                messages?.append(object)
                                print("Created: \(object)")
                            case .updated(let object):
                                
                                let lastMessage = messages?.last
                                
                                if lastMessage?.id == object.id {
                                    if messages?.count ?? 0 > 0 {
                                        messages?[messages!.count - 1] = object
                                        print(messages)
                                    }
                                }
                                print("Updated: \(object)")
                            case .deleted(let object):
                                print("Deleted: \(object)")
                            }

})

}

You should look at the second link in the comment referring to requesting permissions as this is on the developer end of any Apple OS app and not the Swift SDK:

I looked at this, but I don’t really need the app to get data in the background, let me explain a bit better.

What I am expecting:

  1. Livequery loads the messages for a given chat
  2. I switch apps to anything else
  3. Come back (even 30 seconds later) and it keeps loading where we left off

What is happening:

  1. Livequery loads the messages for a given chat
  2. I switch to anything else
  3. Come back, it no longer updates at all without reloading the entire app/view.

So I am not sure what I am doing wrong. Currently, I have both background fetch and remote notifications. Would I still need to do background refresh and how/what would I do to keep the connection alive (if I even need to)

The Swift SDK can only do what the OS allows it to do, so if the OS cuts communication based on what your app is authorized to do, there’s nothing the SDK can do. You can look online for iOS restricting apps when the app leaves the foreground. You can also add print statements to see when the OS cuts access and allows it. You can also look at the following thread:

I don’t mind the OS cutting communication, I guess a better question would be is there a way to resume the connection one you return?

So for example lets say when the app goes to the background I unsubscribe anyways, is there a way to resubscribe?

.onChange(of: scenePhase, perform: { newPhase in
            if newPhase == .background {
                do {
                    try queryM.unsubscribe()
                } catch {
                    print(error)
                }
            } else {
                //Resubscribe?
            }
            
        
        })

You should be able to do quertM.subscribe! and it should subscribe to all previous subscriptions assuming the OS didn’t discard them from memory:

You can also use open, similar to sendPing in the Playgrounds:

1 Like

I think I solved the issue, originally I was subscribing etc within the views .onAppear. When the app goes to the background I have no idea what happens, but something happens to the view that permanently breaks the connection.

I switched it over to a ViewModel (to follow MVVM, as I should have done from the beginning). So yea folks don’t be lazy like me, make a ViewModel and ParseSwift will work as it should, here’s my code if anyone is interested:

import Foundation
import ParseSwift

class MessageViewModel: ObservableObject {
    
    @Published var messages = [Message]()
    @Published var queryM = Message.query()
    
    
    func loadMessages() {
        
        //Set up the delegate
        
        let delegate = LiveQueryDelegate()
        if let socket = ParseLiveQuery.getDefault() {
            socket.receiveDelegate = delegate
        }
        
        
        //Begin subscribing
        
        let subscriptionM = queryM.subscribeCallback!
        
        subscriptionM.handleSubscribe { subscribedQuery, isNew in
            //: You can check this subscription is for this query
            if isNew {
                print("Successfully subscribed to messages query \(subscribedQuery)")
                subscribedQuery.find { result in
                    switch result {
                    case .success(let messages):
                        self.messages = messages
                    case .failure(let error):
                        print(error)
                    }
                }
            } else {
                print("Successfully updated subscription to messages query \(subscribedQuery)")
            }
        }
        
        
        //Handle Subscription Events
        
        subscriptionM.handleEvent { newQuery, event in
            newQuery.find { result in
                switch result {
                case .success(let messages):
                    self.messages = messages
                case .failure(let error):
                    print(error)
                }
            }
            
            switch event {

            case .entered(let object):
                print("Entered: \(object)")
            case .left(let object):
                print("Left: \(object)")
            case .created(let object):
                self.messages.append(object)
                print("Created: \(object)")
            case .updated(let object):
                
                let lastMessage = self.messages.last
                
                if lastMessage?.id == object.id {
                    if self.messages.count ?? 0 > 0 {
                        self.messages[self.messages.count - 1] = object
                        print(self.messages)
                    }
                }
                print("Updated: \(object)")
            case .deleted(let object):
                print("Deleted: \(object)")
            }
        }
    }
    
    
    func closeConnection() {
        do {
            try queryM.unsubscribe()
        } catch {
            print(error)
        }
    }
    
}

The Swift SDK is designed for SwiftUI and MVVM out-of-the-box. A LiveQuery Subscription is already a View Model as discussed in the documentation, so using subscribeCallback in the manner you are using it is the long way:

Cloud Functions and every Query can be used directly as a View Model without any additional code as well. The Playgrounds shows how to use a LiveQuery as a View Model directly:

I discuss and show examples on how to make sophisticated View Models leveraging subscriptions and standard queries by conforming and subclassing to what the Swift SDK provides:

I realized the issue actually has not been fixed, when testing on a physical device the same issue happens.

If the app goes to the background for even 30 seconds, the livequery will unsubscribe and cannot be resubscribed to.

I have rewritten my file and even directly used what was used in the playground example to the exact same result. No matter what I attempt after restoring the app I get

“Unsubscribed from query!”

query.subscribe seems to do nothing. How do I resubscribe to a query once the app is no longer in the background/inactive.

I even made a quick test app here: https://github.com/EliteTechnicalCare/ParseSwift-Example

Exact same problem,

  1. Open App, runs perfect livequery works 100%
  2. Switch apps for 30 seconds
  3. Go back to app, livequery no longer works, no apparent way of restoring the connection other than restarting the app

When you reopen you app, you should check isSubscribed() and isPendingSubscription, if your query isn’t in either it means the OS cleared the memory of your app as I stated in:

If your query isn’t present in either, it is your responsibility to save the state of your app before the OS terminates it, not the SDK (there’s a ton of info about this online). If the query is present, but the connection is disconnected, you need to make the request by calling open or resubscribing. To learn more about how the OS makes decisions you should read here:

You can also check isSocketEstablished, isConnected, and isConnecting when reopening your app to get the real connection status of LiveQuery.

The OS does not clear the memory within 2 seconds of going home then going back to the app.

As I keep asking how? There is nothing in the documentation nor can I find anything detailing what to do when your app has momentarily been switched from and back to.

I already attempted query.subscribe! xcode throws the following warning:

Expression of type 'Subscription<GameScore>' is unused

I posted my example project above but here is the entire class so you can see:

import SwiftUI
import ParseSwift

//: Create a query just as you normally would.
var query = GameScore.query()

//: To use subscriptions inside of SwiftUI
struct ContentView: View {

    //: A LiveQuery subscription can be used as a view model in SwiftUI
    @ObservedObject var subscription = query.subscribe!
    @Environment(\.scenePhase) var scenePhase

    var body: some View {
        VStack {

            if subscription.subscribed != nil {
                Text("Subscribed to query!")
            } else if subscription.unsubscribed != nil {
                Text("Unsubscribed from query!")
            } else if let event = subscription.event {

                //: This is how you register to receive notifications of events related to your LiveQuery.
                switch event.event {

                case .entered(let object):
                    Text("Entered with points: \(String(describing: object.points))")
                case .left(let object):
                    Text("Left with points: \(String(describing: object.points))")
                case .created(let object):
                    Text("Created with points: \(String(describing: object.points))")
                case .updated(let object):
                    Text("Updated with points: \(String(describing: object.points))")
                case .deleted(let object):
                    Text("Deleted with points: \(String(describing: object.points))")
                }
            } else {
                Text("Not subscribed to a query")
            }

            Text("Update GameScore in Parse Dashboard to see changes here:")

            Button(action: {
                try? query.unsubscribe()
            }, label: {
                Text("Unsubscribe")
                    .font(.headline)
                    .background(Color.red)
                    .foregroundColor(.white)
                    .padding()
                    .cornerRadius(20.0)
            })
            Spacer()
                .onChange(of: scenePhase, perform: { newPhase in
                    if newPhase == .background {
                        print("in background")
                    } else {
                        query.subscribe!
                     //   print(test?.results)
                    }
                })
                
                
            
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I even made a video highlighting the issue:

Its important to note this only seems to occur on a physical device, on simulator it works perfectly.

After losing about half my hair I think I got it:

.onChange(of: scenePhase, perform: { newPhase in
                    if newPhase == .background {
                        print("in background")
                    } else {
                        
                        ParseLiveQuery.getDefault()?.open(completion: { error in
                            if let error = error {
                                print(error.localizedDescription)
                            }
                        })
                        
                        //   print(test?.results)
                    }
                })

In my test app this now works correctly. Is there anyway this can be the default behavior of the SDK. When the app leaves it disconnects, when it comes back it re-establishes the connection. Seems like a very easy thing to impliment

If you believe you have an improvement, feel free to open a PR with the added changes with the reasons for your changes and supporting test cases.

By some miracle, it stopped working again, this time with an error message:

in background
2022-02-08 21:38:48.165700-0500 ParseSwift Example[431:9397] [connection] nw_read_request_report [C1] Receive failed with error "Software caused connection abort"
2022-02-08 21:38:48.166207-0500 ParseSwift Example[431:9397] Connection 1: received failure notification
2022-02-08 21:38:48.166277-0500 ParseSwift Example[431:9397] [websocket] Read completed with an error Software caused connection abort
2022-02-08 21:38:48.167144-0500 ParseSwift Example[431:9400] [connection] nw_flow_add_write_request [C1.1 2600:1f18:448b:6f01:f542:e8c6:5ea:5357.80 failed channel-flow (satisfied (Path is satisfied), viable, interface: en0, ipv4, ipv6, dns)] cannot accept write requests
2022-02-08 21:38:48.167222-0500 ParseSwift Example[431:9400] [connection] nw_write_request_report [C1] Send failed with error "Socket is not connected"
2022-02-08 21:39:00.047973-0500 ParseSwift Example[431:9397] Task <F9F05D33-A4F0-40C5-BC17-A5148DF0EA89>.<1> finished with error [53] Error Domain=kNWErrorDomainPOSIX Code=53 "Software caused connection abort" UserInfo={NSDescription=Software caused connection abort, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalWebSocketTask <F9F05D33-A4F0-40C5-BC17-A5148DF0EA89>.<1>"
), _NSURLErrorFailingURLSessionTaskErrorKey=LocalWebSocketTask <F9F05D33-A4F0-40C5-BC17-A5148DF0EA89>.<1>}

However this is what I get when I check ParseLiveQuery:

Is Subscribed: Optional(true)
Is Pending Subscription: Optional(false)
Is Socket Established: Optional(true)
Is Connected: Optional(true)
Is Connecting: Optional(false)

I am attempting to use open but I have been unable to resubscribe in any way.

It looks like however you setup your app, you created a weird situation the OS breaks the connection and didn’t report the broken connection back to ParseLiveQuery.

You can probably address that by: closing the connection first, then opening the connection, a simple, “off/on”.

At any rate, the issues you are facing are related to you not properly handling your apps live cycle and managing state (why you need to off/on and others) as I’ve mentioned a few times already:

In addition, some of your issues from the code you provided are SwiftUI based and not Parse SwiftUI based. I recommend looking more into SwiftUI on https://www.hackingwithswift.com or on StackOverFlow along with looking into understanding more about the iOS app life cycle: