Losing reference to variable while using SocketIO in swift 4 - swift

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

Related

Access NSCache across view controllers in a tab view controller

I want to access NSCache from more than one place in my APP, as I'm using it to cache images from an API endpoint.
For example table view 4 and viewcontroller 6 in the diagram below use the same images, so I do not want to download them twice.
Candidate solutions:
Singleton
class Cache {
private static var sharedCache: NSCache<AnyObject, AnyObject>?
static public func getCache () -> NSCache<AnyObject, AnyObject> {
if sharedCache == nil {
self.sharedCache = NSCache()
}
return sharedCache!
}
}
Seems to work fine, but "Singletons are bad" so...
Store the cache in TabViewController
This will tightly couple the views to the view controller so...
Store in the AppDelegate somehow. But isn't this the same as 1? So...
Use dependency injection. But we're in a tab view controller, so isn't this the same as 2?
I'm not sure the right strategy here, so am asking whether there is another method that can be used here.
What I've done Created an App with an example using a NSCache, and explored a singleton solution. Ive tried to use dependency injection but think that it doesn't make sense. I've looked at Stack overflow and documentation, but for this specific circumstance I have found no potential solutoins.
What I've given A minimal example, with a diagram and tested solution that I'm dissatisfied with.
What is not helpful are answers that say NSCache is incorrect, or to use libraries. I'm trying to use NSCache for my own learning, this is not homework and I want to solve this specific instance of this problem in this App structure.
What the question is How to avoid using a singleton in this instance, view controllers in a tab view controller.
First up. Singletons are not inherantly bad. They can make your code hard to test and they do act as dependancy magnets.
Singletons are good for classes that are tools e.g NSFileManager aka FileManger, i.e something that does not carry state or data around.
A good alternative is dependancy injection but with view controllers and storyboards it can be hard and feel very boilerplate. You end up passing everything down the line in prepareForSegue.
One possible method is to declare a protocol that describes a cache like interface.
protocol CacheProtocol: class {
func doCacheThing()
}
class Cache: CacheProtocol {
func doCacheThing() {
//
}
}
Then declare a protocol that all things that wish to use this cache can use.
protocol CacheConsumer: class {
var cache: CacheProtocol? { get set }
func injectCache(to object: AnyObject)
}
extension CacheConsumer {
func injectCache(to object: AnyObject) {
if let consumer = object as? CacheConsumer {
consumer.cache = cache
}
}
}
Finally create a concrete instance of this cache at the top level.
/// Top most controller
class RootLevelViewController: UIViewController, CacheConsumer {
var cache: CacheProtocol? = Cache()
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
injectCache(to: segue.destination)
}
}
You could pass the cache down the line in prepareForSegue.
Or you can use subtle sub-classing to create conformance.
class MyTabBarController: UITabBarController, CacheConsumer {
var cache: CacheProtocol?
}
Or you can use delegate methods to get the cache object broadcast downhill.
extension RootLevelViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
injectCache(to: viewController)
}
}
You now have a system where any CacheConsumer can use the cache and pass it downhill to any other object.
If you use the coordinator pattern you can save the cache in the coordinator for your navigation flow and access it from there/init with the cache. It also works nicely since when the navigation flow is removed the cache is also removed.
final class SomeCoordinator: NSObject, Coordinator {
var rootViewController: UINavigationController
var myCache = NSCache<AnyObject, AnyObject>()
override init() {
self.rootViewController = UINavigationController()
super.init()
}
func start() {
let vc = VC1(cache: myCache)
vc.coordinator = self
rootViewController.setViewControllers([vc], animated: false)
parentCoordinator?.rootViewController.present(rootViewController, animated: true)
}
func goToVC2() {
let vc = VC2(cache: myCache)
vc.coordinator = self
rootViewController.pushViewController(vc, animated: true)
}
func goToVC3() {
let vc = VC3(cache: myCache)
vc.coordinator = self
rootViewController.present(vc, animated: true)
}
func goToVC4() {
let vc = VC4(cache: myCache)
vc.coordinator = self
rootViewController.present(vc, animated: true)
}
deinit {
print("✅ Deinit SomeCoordinator")
}
}

How to properly implement Navigator pattern

I am following John Sundell's post to implement a Navigator pattern (https://www.swiftbysundell.com/posts/navigation-in-swift). The basic idea is that, in contrast to Coordinator pattern, each view controller could simply call navigator.navigate(to: .someScreen) without having to know other view controllers.
My question is that, since in order to construct a view controller I need a navigator, to construct a navigator I need a navigation controller, but I want to make the view controller the root of the navigation controller, what's the best way to resolve this circular dependency in a way that respects the best practices of dependency injection?
Below is the idea of Navigator pattern as illustrated by Sundell
Navigator
protocol Navigator {
associatedtype Destination
func navigate(to destination: Destination)
}
class LoginNavigator: Navigator {
enum Destination {
case loginCompleted(user: User)
case signup
}
private weak var navigationController: UINavigationController?
private let viewControllerFactory: LoginViewControllerFactory
init(navigationController: UINavigationController,
viewControllerFactory: LoginViewControllerFactory) {
self.navigationController = navigationController
self.viewControllerFactory = viewControllerFactory
}
func navigate(to destination: Destination) {
let viewController = makeViewController(for: destination)
navigationController?.pushViewController(viewController, animated: true)
}
private func makeViewController(for destination: Destination) -> UIViewController {
switch destination {
case .loginCompleted(let user):
return viewControllerFactory.makeWelcomeViewController(forUser: user)
case .signup:
return viewControllerFactory.makeSignUpViewController()
}
}
}
View Controller
class LoginViewController: UIViewController {
private let navigator: LoginNavigator
init(navigator: LoginNavigator) {
self.navigator = navigator
super.init(nibName: nil, bundle: nil)
}
private func handleLoginButtonTap() {
navigator.navigate(to: .loginCompleted(user: user))
}
private func handleSignUpButtonTap() {
navigator.navigate(to: .signup)
}
}
Now in AppDelegate I want to do something like
let factory = LoginViewControllerFactory()
let loginViewController = factory.makeLoginViewController()
let rootNavigationController = UINavigationController(rootViewController: loginViewController)
window?.rootViewController = rootNavigationController
But I somehow have to pass the rootNavigationController into the factory in order for the loginViewController to be properly constructed right? Because it needs a navigator, which needs the navigation controller. How to do that?
I also was recently trying to implement Sundell's Navigator pattern and ran into this same circular dependency. I had to add some additional behavior to the initial Navigator to handle this odd bootstrap issue. I believe subsequent Navigators in your app can perfectly follow the blog's suggestion.
Here is the new initial Navigator code using JGuo's (the OP) example:
class LoginNavigator: Navigator {
enum Destination {
case loginCompleted(user: User)
case signup
}
private var navigationController: UINavigationController?
// This ^ doesn't need to be weak, as we will instantiate it here.
private let viewControllerFactory: LoginViewControllerFactory
// New:
private let appWindow: UIWindow?
private var isBootstrapped = false
// We will use this ^ to know whether or not to set the root VC
init(appWindow: UIWindow?, // Pass in your app's UIWindow from the AppDelegate
viewControllerFactory: LoginViewControllerFactory) {
self.appWindow = appWindow
self.viewControllerFactory = viewControllerFactory
}
func navigate(to destination: Destination) {
let viewController = makeViewController(for: destination)
// We'll either call bootstrap or push depending on
// if this is the first time we've launched the app, indicated by isBootstrapped
if self.isBootstrapped {
self.pushViewController(viewController)
} else {
bootstrap(rootViewController: viewController)
self.isBootstrapped = true
}
}
private func makeViewController(for destination: Destination) -> UIViewController {
switch destination {
case .loginCompleted(let user):
return viewControllerFactory.makeWelcomeViewController(forUser: user)
case .signup:
return viewControllerFactory.makeSignUpViewController()
}
}
// Add these two new helper functions below:
private func bootstrap(rootViewController: UIViewController) {
self.navigationController = UINavigationController(rootViewController: rootViewController)
self.appWindow?.rootViewController = self.navigationController
}
private func pushViewController(_ viewController: UIViewController) {
// Setup navigation look & feel appropriate to your app design...
navigationController?.setNavigationBarHidden(true, animated: false)
self.navigationController?.pushViewController(viewController, animated: true)
}
}
And inside the AppDelegate now:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let factory = LoginViewControllerFactory()
let loginViewController = factory.makeLoginViewController()
loginViewController.navigate(to: .signup) // <- Ideally we wouldn't need to signup on app launch always, but this is the basic idea.
window?.makeKeyAndVisible()
return true
}
...
}
Does this solve it? in AppDelegate:
let factory = LoginViewControllerFactory()
let navController = UINavigationController()
let loginNavigator = LoginNavigator(navigationController: navController, viewControllerFactory: factory)
loginNavigator.navigate(to: .signup) // The example doesn't have a .login Destination, but it can easily be added to the factory, so using .signup instead
window?.rootViewController = navController
Instead of having the rootViewController as a property of the LoginViewControllerFactory, I would suggest to pass it as an argument when calling the 'make' functions:
return viewControllerFactory.makeWelcomeViewController(forUser: user, with: rootViewController)

Pass data using TabBarController

I know this question has been ask a lot on stack overflow. So I have a TabBarController that has 2 NavigationController, which both NavigationController have a TableViewController. I am using firebase to get a user, saving the user into a variable called currentUser. Now my problem starts here, I want to set the 2nd Navigation/Tableview controller title to the user's name. I know how to pass data using the prepare for segue, however there is no segue in TabBarController.
I've found a solution, not sure if its good or bad. What I did was make the first controller to be the delegate of the tab bar. Then I added tabBarController did select method. Here is the code.
class FirstTableVC: UITableViewController, UITabBarControllerDelegate {
var currentUser: User?
override func viewDidLoad() {
super.viewDidLoad()
tabBarController?.delegate = self
}
//Code that saves user
func code() {
...
...
...
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if viewController == tabBarController.viewControllers![1] {
let navController = tabBarController.viewControllers![1] as? UINavigationController
let secondTableVC = navController?.topViewController as! SecondTableVC
secondTableVC.currentUser = currentUser
}
}
}
class SecondTableVC: UITableViewController {
var currentUser: User?
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title =currentUser?.name
}
}
This works but not sure if this is a good way to do. I was wondering if there is a better way or a more efficient way. Thanks :)
Added:
Okay read this article about passing data using tabController. The author says that we should pass data using the state of the app. I am not really sure what he means by this. This is what I though he meant.
Example code:
class Person {
var name: String
var email: String
static var currentPerson: Person?
init(name: String, email: String) {
self.name = name
self.email = email
}
}
Can some please help me clarify . Thanks.
There's nothing wrong with this solution. Another way would be to have an abstracted class responsible for the user login object (instead of FirstTableVC having responsibility) that is accessible from both FirstTableVC and SecondTableVC
Add this in your FirstTableVC
func code() {
...
...
...
self.changeTabbarTitle()
}
func changeTabbarTitle() {
if let items = self.tabBarController?.tabBar.items {
items[1].title = currentUser?.name
}
}
You can use with delegate protocol
create NavigationTitle Protocol in FirstViewController
protocol NavigationTitle{
func setTitle(name:String)
}
class FirstViewController: UITableViewController,UITabBarControllerDelegate{
var delegate: NavigationTitle?
func setCurrentUser(){
delegate.setTitle(name:self.currentUser?.name)
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if viewController == tabBarController.viewControllers![1] {
let navController = tabBarController.viewControllers![1] as? UINavigationController
let secondTableVC = navController?.topViewController as! SecondTableVC
self.delegate = secondTableVC.self
}
}
}
implement protocol in SecondVC
class SecondVC: UITableViewController,NavigationTitle{
func setTitle(name:String){
navigationItem.title = name
}
}

Async completion blocks with singleton class 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
}
}

Get variable value or run function from different Object - Swift

I am trying to: get the value of a few variables, as well as run some functions which live in Object A, all from Object B.
I have tried for hours now to make it work with delegates and protocols. No luck.
I can't do something like this:
var delegate: MyDelegate = ViewController()
Because it seems to create a new instance of ViewController. And I want the values from the instance that is already running.
I also cannot do:
var delegate: MyDelegate?
Because the ViewController object never responds. So I get a nil anytime I call delegate?.somefunction()
I don't want a segue between screens. I just need to start a function from another object.
I bet this is an easy fix. I just can't get it. Thanks for your help.
Some of my code:
class PauseButtonView: NSView{
var delegate: PauseButtonDelegate?
...
var result = delegate?.startFunction()
}
protocol PauseButtonDelegate {
func startFunction() -> String
}
class ViewController: NSViewController, PauseButtonDelegate {
func startFunction() -> String {
let myString = "Hello World!"
return myString
}
}
If you don't want either classes to have a reference to the other, you could use internal notifications to communicate between them:
// in your PauseButtonView
let object:[String:AnyObject] = [ "aParameter" : 42 ]
let startNotification = NSNotification(name: "startFunction:", object: object)
NSNotificationCenter.defaultCenter().postNotification(startNotification)
// in the view controller
func startFunction(notification:NSNotification)
{
let object = notification.object as? [String:AnyObject]
//...
}