Async completion blocks with singleton class swift - swift

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
}
}

Related

Swift: Ignored request to start a new background task because RunningBoard has already started the expiration timer

I have a series of tasks that is triggered by a silent push notification. Upon receiving the push notification, it wakes the iOS up in the background and performs the following tasks:
Opens up a WebViewController that contains a WKWebview
Goes to a webpage, and clicks some buttons automated by javascript injection
Once completed, dismisses the WebViewController
I have added selected BackgroundTasks handlers to manage it by following this tutorial but the console is flooded with the following warning.
[ProcessSuspension] 0x280486080 - WKProcessAssertionBackgroundTaskManager: Ignored request to start a new background task because RunningBoard has already started the expiration timer
Note that the tasks that needs to be done are still performed correctly.
class WebViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
lazy var webView: WKWebView = {
let v = WKWebView()
v.translatesAutoresizingMaskIntoConstraints = false
v.navigationDelegate = self
return v
}()
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
//Remove BG task when not needed
deinit {
NotificationCenter.default.removeObserver(self)
endBackgroundTask()
}
override func viewDidLoad() {
super.viewDidLoad()
//Register notification for background task
NotificationCenter.default.addObserver(self,
selector: #selector(reinstateBackgroundTask),
name: UIApplication.didBecomeActiveNotification,
object: nil)
registerBackgroundTask()
//Load webview with URL
if let url = url {
let request = URLRequest(url: url)
webView.load(request)
}
}
//MARK:- Handle BG Tasks
func registerBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
self?.endBackgroundTask()
}
}
func endBackgroundTask() {
Log("Background task ended.")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
#objc func reinstateBackgroundTask() {
if backgroundTask == .invalid {
registerBackgroundTask()
}
}
func endBackgroundTaskIfNotInvalid() {
if backgroundTask != .invalid {
self.endBackgroundTask()
}
}
//This is the final task that needs to be done
fileprivate func updateScheduler(visitedPlace: VisitedPlace) {
if navigator == .scheduler {
if let jobId = jobId {
let data = [
"status": "scheduled",
"completedOn": Date()
] as [String : Any]
///Do some work here...
//Dismiss controller after completing
self.dismiss(animated: true) {
self.endBackgroundTaskIfNotInvalid()
}
}
} else {
self.endBackgroundTaskIfNotInvalid()
}
}
}
What is triggering all these warnings and how do I silence it?
I'm having the same console flood. For me it turned out to be adMob that was the cause.
This happens to me when I run unit tests that wait for test expectations to be filled. I was hoping that it was just a simulator issue, since I don't see it in production, but it sounds like that's not the case.
I fixed the flood by removing the AdMob banner from the view hierarchy on app suspension:
self.bannerView?.removeFromSuperview()
AdMob would still try infrequently which would generate a single log message.

Losing reference to variable while using SocketIO in swift 4

I'm new to Swift and have always been a bit messy when it comes to developing for iOS so bear with me.
So in my AppDelegate I have a variable like such
var manager = Manager()
and in didFinishLaunchingWithOptions I've got
let controller = ChatRoomViewController()
self.manager.delegate = controller
self.manager.setup()
self.manager.attemptToIdentify(user:username);
In the Manager I've got
var connectionManager = SocketManager(socketURL: URL(string: "http://0.0.0.0:8080")!)
var usersName = ""
var socket:SocketIOClient!
func setup(){
connectionManager.reconnects = true
socket = connectionManager.defaultSocket;
self.setSocketEvents();
socket.connect();
}
This works and I'm able to open up a socket which displays the username on the node.js server. Now when I navigate away from the main view to the chat controller and call
appDelegate.manager.attemptMessage(msg: message);
the console tells me that I'm no longer connected. Best I can tell, I'm losing the reference to one of my variables.
I don't think you should declare your variable in the AppDelegate.
I am using Socket.IO too in one of my app and I prefer use it from a shared instance class. I don't know if you are familar with it, but it is a common architecture in iOS development.
It is based on a singleton instance and allows you to keep instance of variables in memory during all the life of the app.
For you case, you can do the following for example:
private let _managerSharedInstance = Manager()
class Manager() {
private var connectionManager = SocketManager(socketURL: URL(string: "http://0.0.0.0:8080")!)
private var usersName = ""
private var socket:SocketIOClient!
var delegate: UIViewController?
class var shared: Manager {
return _managerSharedInstance
}
init() {
connectionManager.reconnects = true
socket = connectionManager.defaultSocket;
setSocketEvents();
socket.connect();
}
private func setSocketEvents() {
// Your socket events logic
}
func attemptToIdentify(user username: String) {
socket.emit("identify", ["username": username])
}
func attemptToSend(message: String) {
socket.emit("message", ["username": username, "message": message])
}
}
So, in your AppDelegate.didFinishLaunchingWithOptions, you can now call:
Manager.shared.attemptToIdentify(user: username)
You can call this from everywhere in your code, the shared class variable will return the instance of _managerSharedInstance.
By the way, you should set your delegate of the controller in the viewWillAppear of that controller and not in the AppDelegate.
func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Manager.shared.delegate = self
}
You set controller variable's object in didFinishLaunchingWithOptions method to your manager's delegate. But when you navigate ChatRoomViewController on UI, you open another ChatRoomViewController object.
You should set manager's delegate in your ChatRoomViewController class' viewDidLoad method like this
class ChatRoomViewController: UIViewController{
override func viewDidLoad(){
super.viewDidLoad()
appDelegate.manager.delegate = self
}
}

How do I reinitialized a property in a singleton class?

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!

XMPP Stream Delegate Not Called? Swift 3

I am trying to connect to an XMPP server in my iOS Application. I am using the XMPPFrameworks and for some reason the XMPP Stream delegate is not being called after I try to connect to the server. I have double checked the login information using a third party XMPP application on my computer so I do not believe it is that. Am I not setting this delegate up correctly? Am I using the wrong syntax? Do I need to set this in the app delegate instead of my view controller? Any help would be much appreciated. Below is my code
import UIKit
import XMPPFramework
class ViewController: UIViewController, XMPPStreamDelegate {
override func viewDidLoad() {
super.viewDidLoad()
connect()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func connect() {
let stream = XMPPStream()
stream?.addDelegate(self, delegateQueue: DispatchQueue.main)
stream?.myJID = XMPPJID.init(string: "XXXXXXXXXXX")
stream?.hostName = "XXXXXXXXX"
stream?.hostPort = 5222
do {
try stream?.connect(withTimeout: XMPPStreamTimeoutNone)
} catch {
print("error connecting")
}
}
func xmppStreamDidConnect(sender: XMPPStream) {
print("connected!")
do {
try sender.authenticate(withPassword: "XXXXXXXXXX")
} catch {
print("error registering")
}
}
}
I think that your delegate method is not right. You can try with the delegate method given below:
#objc func xmppStreamDidConnect(_ sender: XMPPStream!) {
//write your code here.
}
try this
do {
try self.xmppController = XMPPController(hostName: server,
userJIDString: userJID,
password: userPassword)
self.xmppController.xmppStream.addDelegate(self, delegateQueue: DispatchQueue.main)
self.xmppController.connect()
} catch {
sender.showErrorMessage(message: "Something went wrong")
}
and XMPPController
class XMPPController: NSObject {
var xmppStream: XMPPStream
let hostName: String
let userJID: XMPPJID
let hostPort: UInt16
let password: String
init(hostName: String, userJIDString: String, hostPort: UInt16 = 5222, password: String) throws {
guard let userJID = XMPPJID(string: userJIDString) else {
throw XMPPControllerError.wrongUserJID
}
self.hostName = hostName
self.userJID = userJID
self.hostPort = hostPort
self.password = password
// Stream Configuration
self.xmppStream = XMPPStream()
self.xmppStream.hostName = hostName
self.xmppStream.hostPort = hostPort
self.xmppStream.startTLSPolicy = XMPPStreamStartTLSPolicy.allowed
self.xmppStream.myJID = userJID
super.init()
self.xmppStream.addDelegate(self, delegateQueue: DispatchQueue.main)
}
func connect() {
if !self.xmppStream.isDisconnected() {
return
}
try! self.xmppStream.connect(withTimeout: XMPPStreamTimeoutNone)
}}
it works for me. required your attention this line
try self.xmppController = XMPPController(hostName: server,
userJIDString: userJID,
password: userPassword)
I had the same issue. In my case (as I followed some tutorial) the object was not global and the delegate became nil. That's why it was not called. You have to store the object which implements XMPPStreamDelegate globally.

How do I run an asynchronous thread that only runs as long as the view that uses it is presented?

How do I run an asynchronous thread that only runs as long as the view that uses it is presented?
I want the view to run this asynchronous thread. However, as soon as the view disappears, I want that thread to stop running. What's the best way to do this? I'm not sure where to start and might be thinking about this the wrong way. Nevertheless, what I described is how I want it to behave to the user.
You can use NSOperation to achieve what you want, NSOperation and NSOperationQueue are built on top of GCD. As a very general rule, Apple recommends using the highest-level abstraction, and then dropping down to lower levels when measurements show they are needed.
For example, You want to download images asynchronously when the view is loaded and cancel the task when the view is disappeared. First create a ImageDownloader object subclass to NSOperation. Notice that we check if the operation is cancelled twice, this is because the NSOperation has 3 states: isReady -> isExecuting -> isFinish and when the operation starts executing, it won't be cancelled automatically, we need to do it ourself.
class ImageDownloader: NSOperation {
//1
var photoRecord: NSURL = NSURL(string: "fortest")!
//2
init(photoRecord: NSURL) {
self.photoRecord = photoRecord
}
//3
override func main() {
//4
if self.cancelled {
return
}
//5
let imageData = NSData(contentsOfURL:self.photoRecord)
//6
if self.cancelled {
return
}
}
}
Then you can use it like: downloader.cancel(), downloader.start(). Notice that we need to check if the operation is cancelled in the completion block.
import UIKit
class ViewController: UIViewController {
let downloder = ImageDownloader(photoRecord: NSURL(string: "test")!)
override func viewDidLoad() {
super.viewDidLoad()
downloder.completionBlock = {
if self.downloder.cancelled {
return
}
print("image downloaded")
}
//Start the task when the view is loaded
downloder.start()
}
override func viewWillDisappear(animated: Bool) {
//Cancel the task when the view will disappear
downloder.cancel()
}
}
Once DetailViewController is presented, the asyncOperation method will be executed asynchronously.
Note: currently the asyncOperation method is executed every second so if you want the method to be called only once, you must change the repeats property to false.
class DetailViewController: UIViewController {
// timer that will execute
// asynchronously an operation
var timer: NSTimer!
// counter used in the async operation.
var counter = 0
// when view is about to appear
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// setting up the timer
timer = NSTimer.scheduledTimerWithTimeInterval(
1.0,
target: self,
selector: #selector(asyncOperation),
userInfo: nil,
repeats: true //set up false if you don't want the operation repeats its execution.
)
}
// when view is about to disappear
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
// stopping the timer
timer.invalidate()
}
// async operation that will
// be executed
func asyncOperation() {
counter += 1
print("counter: \(counter)")
}
}
Source: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/
Result: