First, you are calling this flow from AppDelegate, trouble with this is you depend on this token being present. So what jumps out at me here is that you're missing a method that checks if this token is actually present before initiating the connection, the method should just forgo connecting the socket entirely if you can't produce the token (that is, if your connection is actually token dependent, if it is not then previous answers should help you out).
Since you're right to initialize the socket within the init override of your manager class, it's going against what I think you want, which is to reset a connection once a token does become present if it was not there initially. For this, you should hold back on creating the socket as I mention above.
What I usually do for singletons: I give them a blank "Configure" method, to commit it to memory, usually on AppDelegate's didFinishLaunchin withOptions
. If this method contains anything, it's those methods which check for any values the singleton is dependent on, and to assign a custom internal state to the singleton based on those values (like some enum cases). I would then call up establishConnection like you do here, but establishConnection
should be a generic method which can run at every appDidEnterForeground
method, but without having to worry about altering things, and it should re-establish things that were dropped while your app was backgrounded.
So i'd recommend altering your class to something along the lines of:
import SocketIO
enum SocketIOManagerState {
case invalidURL
case launched
case tokenNotPresent
case manuallyDisconnected
case backgroundedByOS
}
class SocketIOManager: NSObject {
private var state : SocketIOManagerState = SocketIOManagerState.launched
private var staticSocketURL : URL?
static let sharedInstance = SocketIOManager()
var socket: SocketIOClient?
override init() {
super.init()
}
func configure() {
//fetch the url string from wherever and apply it to staticSocketURL
guard let url = URL(string: "The URL from wherever") else {
state = SocketIOManagerState.invalidURL
return
}
if getToken() == nil {
state = .tokenNotPresent
} else {
//only here can we be sure the socket doesn't have any restrictions to connection
staticSocketURL = url
state = SocketIOManagerState.launched
}
}
func evaluateConnection() {
guard let token = getToken() else {
//maybe something went wrong, so make sure the state is updated
if socket != nil {
return evaluateSocketAsNotNil()
}
return closeConnection(true, .tokenNotPresent)
}
switch state {
case .tokenNotPresent, .invalidURL:
closeConnection(true)
break
case .launched:
//means token was present, so attempt a connection
guard socket == nil else {
evaluateSocketAsNotNil()
return
}
guard let url = staticSocketURL else {
//maybe something went wrong with the url? so make sure the state is updated.
if socket != nil {
return closeConnection(true, .invalidURL)
}
return setState(.invalidURL)
}
if socket == nil {
socket = SocketIOClient(socketURL: url, .connectParams(["token": token]))
}
socket?.connect()
default:
//unless you care about the other cases, i find they all fall back on the same logic : we already checked if the token is there, if we get here, it means it is, so should we reconnect?
guard weCanReconnect /*some param or method which you create to determine if you should*/ else {
//you determine you should not, so do nothing
return
}
//you determine you do, so:
}
}
private func evaluateSocketAsNotNil() {
guard let sock = socket else { return }
switch sock.state {
case .notConnected:
//evaluate if it should be connected
establishConnection()
case .disconnected:
evaluateSocketAsNotNil()
case .connecting:
//do nothing perhaps?
case connected:
guard getToken() != nil else {
//token is not present, but the socket is initialized, this can't happen so disconnect and reset the instance
closeConnection(true, .tokenNotPresent)
return
}
break //nothing to do here
}
}
private func establishConnection() {
guard let sock = socket else { return }
sock.connect()
}
func setState(_ to: SocketIOManagerState) {
self.state = to
}
func closeConnection(_ clearMemory: Bool) {
guard let sock = socket else { return }
sock.disconnect()
setState(.launched)
if clearMemory {
socket = nil
}
}
private func closeConnection(_ clearMemory: Bool,_ to: SocketIOManagerState) {
socket?.disconnect()
setState(to)
if clearMemory {
socket = nil
}
}
func getToken() -> String? {
guard let token = keychain["token"] else {
state = .tokenNotPresent
return nil }
return token
}
}
And your AppDelegate would then look like this:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
SocketIOManager.sharedInstance.configure()
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
SocketIOManager.sharedInstance.closeConnection(false, .backgroundedByOS)
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
SocketIOManager.sharedInstance.evaluateConnection()
}
From here, you can always call evaluateConnection()
and closeConnection(_:, _:)
anywhere else in the app, and add more state cases, and more ways to handle those cases logically. Either way, it's up to you to determine how you should connect and reconnect based on the token.
With this structure, if your user logs in, and you set your token properly in your app, you should then be able to connect the socket properly when calling evaluateConnection
during the login process.
There's also alot of comments, and some things might seem generic (apologies), but it's up to you to fill in the blanks for your use-case.
Hope it helps!