My problem that I'm facing right now is that whenever user loads up the app. The singleton object will run
Singleton design
import SocketIO
class SocketIOManager: NSObject {
static let sharedInstance = SocketIOManager()
var socket: SocketIOClient!
override init() {
socket = SocketIOClient(socketURL: URL(string: mainURL)!, .connectParams(["token": getToken()])])
super.init()
}
func establishConnection() {
socket.connect()
}
func closeConnection() {
socket.disconnect()
}
func getToken() -> String {
if let token = keychain["token"] {
return token
}
return ""
}
}
Take a look at init() and the .connectParams, in order for the user to connect to the server, token must be present thus the getToken() being passed.
If the token is not there it will initialize the socket object without the token. I run the establishConnection at the applicationDidBecomeActive
func applicationDidBecomeActive(_ application: UIApplication) {
SocketIOManager.sharedInstance.establishConnection()
}
The token will only be there after the user logs in.
The main question is, is there any way to reinitialized the socket object? or do i use didSet or willSet method?
Maybe something like this?
var socket: SocketIOClient! {
didSet {
oldValue.closeConnection()
}
}
It looks like you could probably get rid of the ! too if you want, since you're setting it in your init, assuming SocketIOClient.init returns a non-optional instance.
It is simple, You just need to declare a method in your class:
func resetConnection() {
socket.disconnect()
socket = SocketIOClient(socketURL: URL(string: mainURL)!, .connectParams(["token": getToken()])])
socket.connect()
}
and use in the following
SocketIOManager.sharedInstance.resetConnection()
let socket =
SocketIOManager.sharedInstance.socket // this will be the newer
One way to to do that is to create a public method inside SocketIOManager, and use that method to initialize the socket:
func initializeSocket() {
socket = SocketIOClient(socketURL: URL(string: mainURL)!, .connectParams(["token": getToken()])])
}
And call this method after the user has logged in.
But the way, your initializer must be private in order to implement the Singleton design pattern properly.
Another note is that the initialization of static variables in Swift happens lazily, which means that they only get initialized the first time they are used. Check this answer and the Swift documentation on this topic for more information
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!
Related
I'm calling a Firestore query that does come back, but I need to ensure completion before moving on with the rest of the code. So I need a completion handler...but for the life of me I can't seem to code it.
// get user info from db
func getUser() async {
self.db.collection("userSetting").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let userTrust = document.data()["userTrust"] as! String
let userGrade = document.data()["userGrade"] as! String
let userDisclaimer = document.data()["userDisclaimer"] as! String
var row = [String]()
row.append(userTrust)
row.append(userGrade)
row.append(userDisclaimer)
self.userArray.append(row)
// set google firebase analytics user info
self.userTrustInfo = userTrust
self.userGradeInfo = userGrade
}
}
}
}
Called by:
internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
db = Firestore.firestore()
Database.database().isPersistenceEnabled = true
Task {
do {
let userInfo = await getUser()
}
} return true }
I used a Task as didFinishLauncingWithOptions is synchronous and not asynchronous
However, the getUser() still isn't completed before didFinishLauncingWithOptions moves on.
I need the data from getUser as the very next step uses the data in the array, and without it I get an 'out of bounds exception' as the array is still empty.
Also tried using dispatch group within the func getUser(). Again with no joy.
Finally tried a completion handler:
func getUser(completion: #escaping (Bool) -> Void) {
self.db.collection("userSetting").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let userTrust = document.data()["userTrust"] as! String
let userGrade = document.data()["userGrade"] as! String
let userDisclaimer = document.data()["userDisclaimer"] as! String
var row = [String]()
row.append(userTrust)
row.append(userGrade)
row.append(userDisclaimer)
self.userArray.append(row)
// set google firebase analytics user info
self.userTrustInfo = userTrust
self.userGradeInfo = userGrade
completion(true)
}
}
}
}
Nothing works. The getUser call isn't completed before the code moves on. Can someone please help. I've searched multiple times, looked at all linked answers but I can not make this work.I'm clearly missing something easy, please help
read this post: Waiting for data to be loaded on app startup.
It explains why you should never wait for data before returning from
function application(_:didFinishLaunchingWithOptions).
To achieve what you need, you could use your first ViewController as a sort of splashscreen (that only shows an image or an activity indicator) and call the function getUser(completion:) in the viewDidLoad() method the ViewController.
Example:
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
MyFirestoreDatabaseManager.shared.getUser() { success in
if success {
//TODO: Navigate to another ViewController
} else {
//TODO: Show an error
}
}
}
}
Where obviously MyFirestoreDatabaseManager.shared is the object on which you defined the getUser(completion:) method.
(In your example, I think that you defined that function in the AppDelegate. In that case, you should mark your getUser(completion:) method and all related variables as static. Then replace MyFirestoreDatabaseManager.shared with AppDelegate).
Not 100% sure what you would like to accomplish as I can't see all your code, but try something similar to this, replacing Objects for what you are trying to return from the documents.
You don't want your user's data spread across multiple documents. With Firebase you pay for every document you have to get. Ideally you want all your user's settings within one firebase document. Then create a UserInfo struct that you can decode to using the library CodeableFirebase or the decoder of your choice.
// Create user struct
struct UserInfo: Codable {
var userId: String
var userTrust: String
var userGrade: String
var userDisclaimer: String
}
// get user info from db and decode using CodableFirebase
func getUser() async throws -> UserInfo {
let doc = try await self.db.collection("users").document("userIdHere")
let userInfo = try FirestoreDecoder().decode(UserInfo.self, from: doc.data())
return UserInfo
}
Then you can do this...
Task {
do {
let userInfo = try await getUser()
let userTrust = userInfo.userTrust
let userGrade = userInfo.userGrade
let userDisclaimer = userInfo.userDisclaimer
}
}
I've got the below code in sceneDidDisconnect Delegate, letting the backend know that the user is no longer online when the app is being closed. The print statement gets executed but not the api call. I don't get why that is or how / where else I would alternatively call the api prior to the app closing down.
func sceneDidDisconnect(_ scene: UIScene) {
print("************************sceneDidDisconnect***********************************")
// set offline
guard let userId = UserDefaults.standard.string(forKey: "userId") else { return }
self.apiService.setOnline(online: false, userId: userId) { (result) in
// handle result
}
}
I am using AlamoreFire and checking for network reachability in two of the app's view controllers in the viewDidAppear method of both. But sometimes when the network is found the view controller collection view loads twice.
I am guessing that maybe the Reachability should be put in one place only in the entire app.
What is the best and cleanest way to implement Reachability when you have multiple view controllers to check?
I would like to use the NetworkReachabilityManager from AlamoFire.
Normally with Reachability you'd have some sort of error view on top of the screen, don't worry if the background is trying to load or not.
Create a container view and in viewDidLoad()
if Reachability.isConnectedToNetwork() == true {
self.errorView.isHidden = true
} else {
self.errorView.isHidden = false
}
That solves your problem and helps with UX.
I have my method below. To be more direct to your question though, I would not check for connection in either.
Example: I get all the way to the apps first network call and then turn my wifi on.
Problem: My app fails even though nothing that needed wifi in the App was used.
My Method: I can understand checking for a network connection on the login page, but besides that, I would put it into your network. If a network call fails, check the connection and then relate to the user whether the call failed because of the server or because of the network.
This is what I use for Reachability:
import Foundation
/// This class is designed to check if the network is connected.
open class Reachability {
/// This function will hit two urls that should never be totally down to see if the device is connected
/// If either url connects, this returns true
///
/// - Parameter resultHandler: returns the result of the connection existing as a Bool in a resultHandler
class func isConnectedToNetwork(_ resultHandler: #escaping (_ isTrue: Bool) -> Void) {
let urlA = URL(string: "https://google.com/")
let urlB = URL(string: "https://baidu.com/")
Reachability.fetchURL(urlA!) { isTrue in
if isTrue {
print("NETWORK STATUS: \(isTrue)")
resultHandler(isTrue)
} else {
Reachability.fetchURL(urlB!, resultHandler: resultHandler)
}
}
}
/// Hits a URL in order to see if the device can connect to it
///
/// - Parameters:
/// - url: the url to request
/// - resultHandler: returns whether the url was successfully retrieved or not
class func fetchURL(_ url:URL, resultHandler: #escaping (_ isTrue: Bool) -> Void) {
var request = URLRequest(url: url)
request.httpMethod = "HEAD"
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData
request.timeoutInterval = 10.0
let session = URLSession(configuration: .default)
let dataTask = session.dataTask(with: request) { data, response, error in
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode == 200 {
resultHandler(true)
} else {
resultHandler(false)
}
} else {
resultHandler(false)
}
}
dataTask.resume()
}
}
Then anywhere in code you can call it like so:
Reachability.isConnectedToNetwork() { isConnected in
if isConnected {
//Do something when connected
} else {
//Do something when not connected
}
}
I want to execute a function when there is an socket connection. But the methods can be fired immediately when there is an connection. The connection must be made when there isn't one.
What is an nice and proper way to solve this?
import SocketIO
class SocketIOManager: NSObject {
static let sharedInstance = SocketIOManager()
var socket: SocketIOClient = SocketIOClient(socketURL: NSURL(string: "http://192.168.1.59:3000")! as URL)
var connectionMade = false;
override init() {
super.init()
}
func establishConnection(completionHandler: (() -> Void)!) {
if(!connectionMade){
socket.connect()
connectionMade = true;
}
completionHandler();
}
func connectToRoom(roomNumber: String){
establishConnection {
self.socket.emit("connectToRoom", roomNumber);
}
}
}
Is this an good setup? And yes I have to set the bool to false when the connection is closed:)
I ask this because I have a problem with my code. I call this at the app delegate to made an connection:
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.establishConnection {
}
}
And this at my view controller:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
SocketIOManager.sharedInstance.connectToRoom(self.roomNumber);
}
But the server is never getting the connectToRoom 'message'. It works when I push on a button with this code in it:
SocketIOManager.sharedInstance.connectToRoom(self.roomNumber);
So it looks like the socket connection isnt made at the viewdidload. But why does it work when I push the button? Because i'm waiting for an callback at the at the connectTo Room function from the connection at the SocketIOManger class.
just add a listener on connect like below:
socket.on(clientEvent: .connect) { [unowned self] data, ack in
print("socket connected")
print(data)
if !self.HasConnected {
// JOIN YOUR ROOM
self.HasConnected = true
}
}
What I am trying to accomplish is posting a notification through NSNotificationCenter's default center. This is being done within a closure block after making a network call using Alamofire. The problem I am having is that a class that should be responding to a posted notification isn't receiving such notification.
My ViewController simply creates a First object that get's things moving:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let first = First()
}
}
My First class creates and instance of a Second class and adds itself as an observer to my NSNotificationCenter. This is the class that can't seem to get the notification when the notification is posted.
class First : NSObject {
let second = Second()
override init(){
super.init()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(First.gotDownloadNotification(_:)), name: "test", object: nil)
second.sendRequest()
}
// NOT REACHING THIS CODE
func gotDownloadNotification(notification: NSNotification){
print("Successfully received download notification from Second")
}
}
My Second class is what makes the network call through my NetworkService class and posts a notification in a closure once the request is successful and complete.
class Second : NSObject {
func sendRequest(){
let networkService = NetworkService()
networkService.downloadFile() { statusCode in
if let statusCode = statusCode {
print("Successfully got a status code")
// Post notification
NSNotificationCenter.defaultCenter().postNotificationName("test", object: nil)
}
}
}
}
Finally, my NetworkService class is what makes a network call using Alamofire and returns the status code from the response through a closure.
class NetworkService : NSObject {
func downloadFile(completionHandler: (Int?) -> ()){
Alamofire.download(.GET, "https://www.google.com") { temporaryURL, response in
let fileManager = NSFileManager.defaultManager()
let directoryURL = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
let pathComponent = response.suggestedFilename
return directoryURL.URLByAppendingPathComponent(pathComponent!)
}
.response { (request, response, _, error) in
if let error = error {
print("File download failed with error: \(error.localizedDescription)")
completionHandler(nil)
} else if let response = response{
print("File downloaded successfully")
// Pass status code through completionHandler to Second
completionHandler(response.statusCode)
}
}
}
}
The output after execution is:
File downloaded successfully
Successfully got a status code
From this output I know the download was successful and Second got the status code from the closure and posted a notification right after.
I believe that I have tried resolving most other suggestions on Stack Overflow related to not receiving notifications such as objects not being instantiated before notification is posted or syntax of either adding an observer or posting a notification.
Does anyone have any idea why the posted notification is not being received in the First class?
Since there is a direct relationship between First and Second the protocol/delegate pattern is the better way to notify. Even better with this pattern and you don't have to take care of unregistering the observer. NSNotificationCenter is supposed to be used only if there is no relationship between sender and receiver.
And basically the thread doesn't matter either.
protocol SecondDelegate {
func gotDownloadNotification()
}
class Second : NSObject {
var delegate : SecondDelegate?
init(delegate : SecondDelegate?) {
self.delegate = delegate
}
func sendRequest(){
let networkService = NetworkService()
networkService.downloadFile() { statusCode in
if let statusCode = statusCode {
print("Successfully got a status code")
// Post notification
self.delegate?.gotDownloadNotification()
}
}
}
}
class First : NSObject, SecondDelegate {
let second : Second
override init(){
super.init()
second = Second(delegate:self)
second.sendRequest()
}
func gotDownloadNotification(){
print("Successfully received download notification from Second")
}
}