Callback is not working when button tapped - ios

I want to trigger an action on button tap with my callback. Also I have presenter and coordinator. But nothing happenes. My code is not working in this closure:
startViewController.output = { [weak self] action in
switch action {
case .registrationButtonTapped:
self?.showRegistrationViewController()
case .loginButtonTapped:
self?.showLoginViewController()
}
}
In my ViewController I have enum:
enum StartViewControllerButton {
case registrationButtonTapped
case loginButtonTapped
}
callback:
var output: ((StartViewControllerButton) -> Void)?
and selectors:
#objc func registrationButtonPressed() {
startModulPresenter.openNextScreen()
self.output?(.registrationButtonTapped)
}
#objc func loginButtonPressed() {
startModulPresenter.openNextScreen()
self.output?(.loginButtonTapped)
}
My Presenter
class StartModulPresenter: StartModulPresenterProtocol {
var navigationController: UINavigationController
var coordinator: CoordinatorProtocol?
//Init
init(navigationController: UINavigationController) {
self.navigationController = navigationController
coordinator = AuthorizationCoordinator(navigationController: navigationController)
}
//Functions
func openNextScreen() {
coordinator?.start()
}
}
My Coordinator:
class AuthorizationCoordinator: RegistrationCoordinatorProtocol {
var presenter: PresenterProtocol?
var navigationController: UINavigationController
var childCoordinators: [CoordinatorProtocol] = []
//Init
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
presenter = StartModulPresenter(navigationController: navigationController)
let startViewController = StartViewController(startModulPresenter: presenter as! StartModulPresenter)
startViewController.output = { [weak self] action in
switch action {
case .registrationButtonTapped:
self?.showRegistrationViewController()
case .loginButtonTapped:
self?.showLoginViewController()
}
}
}
private func showRegistrationViewController() {
let registrationViewController = RegistrationViewController()
registrationViewController.view.backgroundColor = .orange
self.navigationController.pushViewController(registrationViewController, animated: true)
}
private func showLoginViewController() {
let loginViewController = LoginViewController()
loginViewController.view.backgroundColor = .orange
self.navigationController.pushViewController(loginViewController, animated: true)
}
}

Could you check if startViewController is pushed/presented or not?
func start() {
presenter = StartModulPresenter(navigationController: navigationController)
let startViewController = StartViewController(startModulPresenter: presenter as! StartModulPresenter)
startViewController.output = { [weak self] action in
switch action {
case .registrationButtonTapped:
self?.showRegistrationViewController()
case .loginButtonTapped:
self?.showLoginViewController()
}
}
}
And, is self.output is nil or not? If it is nil please check your assignment call, it needed to be called before you use this variable.
#objc func loginButtonPressed() {
startModulPresenter.openNextScreen()
self.output?(.loginButtonTapped)
}
Honestly, I don't recommend you to use this design pattern, just a simple thing but the real result is too complicated.
Just use protocol-based MVC. View communicate with Controller via protocol/closure or Reactive-based with Combine (PassthroughSubject/CurrentValueSubject)

Related

How to fix problem with opening ViewController by action from Coordinator in Swift?

I'm trying to open another controller by tapping on the cell of my tableView. I'm coding with MVVM and Coordinator pattern.
In the beginning we see this screen - it is declarated in the method start()
let service = Service()
private(set) weak var navigationController: UINavigationController?
func start() -> UINavigationController {
let vm = ContinentsViewModel(service: service)
let vc = ContinentsViewController(viewModel: vm)
let navigationController = UINavigationController()
self.navigationController = navigationController
navigationController.setViewControllers([vc], animated: false)
bindContinentsViewModel(viewModel: vm)
return navigationController
}
Later, my goal is to open all list of countries of the continent, but now l just need to open empty ViewController by tap on the cell (ex. Africa or Antarctica). Here is my methods for it, but they don't work.
private func showCountries() {
let vc = ViewController()
navigationController?.pushViewController(vc, animated: true)
}
private func bindContinentsViewModel(viewModel: ContinentsViewModel) {
viewModel
.flow
.bind { [weak self] flow in
switch flow {
case .onContinentTap:
self?.showCountries() // don't work
// print("show \(continent)") // work - continent is a param of .onContinentTap, which prints an geo-id of the continent, just if you need to know.
}
}
.disposed(by: viewModel.bag)
}
Thank you so much!
The following works as expected. What are you doing differently?
#main
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var viewModel: ViewModel?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
viewModel = ViewModel()
let controller = viewModel?.start()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = controller
window?.makeKeyAndVisible()
return true
}
}
final class ViewModel {
private(set) weak var navigationController: UINavigationController?
func start() -> UINavigationController {
let vm = ContinentsViewModel()
let vc = ContinentsViewController(viewModel: vm)
let navigationController = UINavigationController()
self.navigationController = navigationController
navigationController.setViewControllers([vc], animated: false)
bindContinentsViewModel(viewModel: vm)
return navigationController
}
private func showCountries() {
let vc = UIViewController()
vc.view.backgroundColor = .blue
navigationController?.pushViewController(vc, animated: true)
}
private func bindContinentsViewModel(viewModel: ContinentsViewModel) {
viewModel.flow
.bind { [weak self] flow in
switch flow {
case .onContinentTap:
self?.showCountries()
}
}
.disposed(by: viewModel.bag)
}
}
final class ContinentsViewModel {
enum Flow {
case onContinentTap
}
let flow: Observable<Flow>
let bag = DisposeBag()
init() {
flow = .just(.onContinentTap)
.delay(.seconds(3), scheduler: MainScheduler.instance)
}
}
final class ContinentsViewController: UIViewController {
var viewModel: ContinentsViewModel
init(viewModel: ContinentsViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
}
}

use popToRootViewController and pass Data

I'm applying for a junior developer position and I've got a very specific task, that already took me 3 days to complete. Sounds easy - pass data to rootViewController.
That's what I've done:
1)
private func userDefaultsToRootController() {
let input = textField.text!
defaults.set(input, forKey: "SavedLabel")
navigationController?.popViewController(animated: true)
}
private func segueToRootViewController() {
let destinationVC = MainScreen1()
let input = textField.text!
if input == "" { self.navigationController?.popToRootViewController(animated: true) }
destinationVC.input = input
navigationController?.pushViewController(destinationVC, animated: true)
}
private func popToNavigationController() {
let input = textField.text!
if let rootVC = navigationController?.viewControllers.first as? MainScreen1 {
rootVC.input = input
}
navigationController?.popToRootViewController(animated: true)
}
I've used CoreData
But here is the difficult part - I've got an email, that all these methods are not good enough and I need to use delegate and closure. I've done delegation and closures before, but when I popToRootViewController delegate method passes nil. Could you at least point where to find info about this?
** ADDED **
There are 2 View Controllers: Initial and Second one.
That's what I have in the Initial View Controller:
var secondVC = MainScreen2()
override func viewDidLoad() {
super.viewDidLoad()
secondVC.delegate = self
}
That's how I push SecondViewController
#objc private func buttonTapped(_ sender: CustomButton) {
let nextViewController = MainScreen2()
navigationController?.pushViewController(nextViewController, animated: true)
}
In SecondViewController I've got this protocol
protocol PassData {
func transferData(text: String)
}
Also a delegate:
var delegate: PassData?
This is how I go back to initial view controller
#objc private func buttonTapped(_ sender: CustomButton) {
if let input = textField.text {
print(input)
self.delegate?.transferData(text: input)
self.navigationController?.popToRootViewController(animated: true)
}
}
Back to the Initial view controller where I've implemented delegate method
extension MainScreen1: PassData {
func transferData(text: String) {
print("delegate called")
label.text = text
}
}
Delegate doesn't get called.
BASED ON YOUR EDIT:
You must set the delegate in buttonTapped
#objc private func buttonTapped(_ sender: CustomButton) {
let nextViewController = MainScreen2()
nextViewController.delegate = self // HERE WHERE YOU SET THE DELEGATE
navigationController?.pushViewController(nextViewController, animated: true)
}
You can delete the second instance and your code in viewDidLoad. That's not the instance you push.
This should point you in the right direction to use delegation and completion handler.
protocol YourDelegateName {
func passData(data:YourDataType)
}
class SecondViewController: UIViewController {
var delegate: YourDelegateName?
func passDataFromSecondViewController(){
YourCoreDataClass.shared.getCoreData { (yourStringsArray) in
self.delegate?.passData(data: yourStringsArray)
self.navigationController?.popToRootViewController(animated: true)
}
}
class InitialViewController: UIViewController, YourDelegateName {
override func viewDidLoad() {
super.viewDidLoad()
// or whenever you instantiate your SecondViewController
let secondViewController = SecondViewController()
secondViewController.delegate = self //VERY IMPORTANT, MANY MISS THIS
self.navigationController?.pushViewController(createVC, animated: true)
}
func passData(data:YourDataType){
//user your data
}
}
class YourCoreDataClass: NSObject {
static let shared = YourCoreDataClass()
func getCoreData (completion: ([String]) -> ()){
........... your code
let yourStringsArray = [String]() // let's use as example an array of strings
//when you got the data your want to pass
completion(yourStringsArray)
}
}

Manage Delegate out UIViewController class

I would like to understand what would be the best way to implement a delegate out UIViewController class
How can I manage the delegate using controller: UIViewController parameter of my function in AuthManager?
These are the two classes I'm working with .. I show you small examples to make you understand
class StartController: UIViewController {
#objc private func presentAuthFacebookController() {
AuthManager.signInWithFacebook(controller: self)
}
}
class AuthManager {
static func signInWithFacebook(controller: UIViewController) {
let loginManager = LoginManager()
loginManager.logIn(permissions: [.publicProfile, .email], viewController: controller) { (result) in
switch result {
case .cancelled : print("\n AuthFacebook: operazione annullata dall'utente \n")
case .failed(let error) : print("\n AuthFacebook: \(error) \n")
case .success(granted: _, declined: let declinedPermission, token: _):
let authVC = ExistingEmailController()
authVC.delegate = // ?????? (controller)
UIApplication.shared.windows.first?.rootViewController?.present(authVC, animated: true, completion: nil)
}
}
}
}
I personally don't think StartController should know about/conform to ExistingEmailControllerDelegate. But if you really want, you can declare controller as a composition type:
static func signInWithFacebook(controller: UIViewController & ExistingEmailControllerDelegate) {
...
authVC.delegate = controller
In my opinion, the whole point of having a AuthManager is to create a layer of abstraction on top of ExistingEmailController, and to encapsulate the logic of authentication. Therefore, StartController shouldn't know, or care, about ExistingEmailControllerDelegate. It only knows about AuthManager.
AuthManager should be the delegate of ExistingEmailController, which implies that signInWithFacebook should not be static, and AuthManager can have an AuthManagerDelegate that StartController conforms to:
class AuthManager : ExistingEmailControllerDelegate {
weak var delegate: AuthManagerDelegate?
func signInWithFacebook(controller: UIViewController) {
...
let authVC = ExistingEmailController()
authVC.delegate = self
UIApplication.shared.windows.first?.rootViewController?.present(authVC, animated: true, completion: nil)
}
func someMethodFromExistingEmailControllerDelegate() {
delegate?.someMethod() // delegating it self.delegate, which StartController conforms to
}
}
protocol AuthManagerDelegate : class {
func someMethod()
}
class StartController: UIViewController, AuthManagerDelegate {
var authManager: AuthManager!
override func viewDidLoad() {
authManager = AuthManager()
authManager.delegate = self
}
#objc private func presentAuthFacebookController() {
authManager.signInWithFacebook(controller: self)
}
func someMethod() {
// write here the code that you would have written in someMethodFromExistingEmailControllerDelegate
}
}

Logout functionality in swift

I am trying to provide in my app the logout functionality, I would like to know if this way is a good approach to continue.Classes involved are described below:
the first one is the AuthViewCoordinator, which class redirects to the user to auth screens
protocol AuthViewCoordinatorDelegate: class {
func authCompleted(coordinator: AuthViewCoordinator)
}
class AuthViewCoordinator: Coordinator {
weak var fromViewController: UIViewController?
weak var navigationController: UINavigationController?
weak var delegate: AuthViewCoordinatorDelegate?
init(fromViewController: UIViewController, delegate: AuthViewCoordinatorDelegate) {
self.fromViewController = fromViewController
self.delegate = delegate
}
func start() {
let authViewController = UIStoryboard.main.instantiateViewController(withIdentifier: "AuthViewController") as! AuthViewController
authViewController.coordinator = self
let navigationController = NavigationController(rootViewController: authViewController)
navigationController.navigationBar.isHidden = true
fromViewController?.present(navigationController, animated: true, completion: nil)
self.navigationController = navigationController
}
func userDidSelectLogin() {
let loginViewController = UIStoryboard.main.instantiateViewController(withIdentifier: "LoginViewController") as! LoginViewController
loginViewController.viewModel.coordinator = self
self.navigationController?.pushViewController(loginViewController, animated: true)
}
func userDidSelectSignUp() {
let signupViewController = UIStoryboard.main.instantiateViewController(withIdentifier: "SignUpViewController") as! SignUpViewController
signupViewController.viewModel.coordinator = self
self.navigationController?.pushViewController(signupViewController, animated: true)
}
func userDidLogin() {
navigationController?.dismiss(animated: true, completion: nil)
self.delegate?.authCompleted(coordinator: self)
}
func userDidSignUp() {
navigationController?.dismiss(animated: true, completion: nil)
self.delegate?.authCompleted(coordinator: self)
}
}
And the 2nd one is an external class called SessionController. In this class I'm trying to call AuthViewCoordinator().start() inside the function logout immediately after the tokens have been removed to show again the auth screen to the user, but the output is
Use of unresolved identifier 'AuthViewCoordinator'
public class SessionController{
public enum SessionState {
case anonymous
case authenticated
case notAuthenticated
}
let service: Service
let sessionProvider: SessionProvider
convenience public init() {
self.init(service: Service.instance, sessionProvider: SessionProvider.instance)
}
init(service: Service, sessionProvider: SessionProvider) {
self.service = service
self.sessionProvider = sessionProvider
}
public func getMe(completion: #escaping (Error?) -> ()){
service.execute(resource: Login.getMe()) { (result) in
if let error = result.error {
completion(error)
} else if let session = result.value {
print("\n session \(session)\n")
completion(nil)
}
}
}
public func logout() {
self.sessionProvider.removeUserToken()
self.sessionProvider.removeInstanceToken()
self.sessionProvider.removeAnonymousToken()
AuthViewController().start()
}
public func state() -> SessionState {
if let _ = sessionProvider.getUserToken() {
print("###### authenticated #########")
return .authenticated
} else if let _ = sessionProvider.getAnonymousToken() {
print("###### anonymous #########")
return .anonymous
} else {
print("###### notAuthenticated #########")
return .notAuthenticated
}
}
}

Swapping centreViewControllers with FloatingDrawers

I am using a third party pod KGFloatingDrawer which is great because it achieves this:
and is a reimplementation of JVFloatingDrawer. I used their sample code and the sliding drawers are working great!
BUT
When I first run my app I call one centreViewController with no drawers (Login). Then after login I call a new centreViewController with
appDelegate.centerViewController = appDelegate.navigationBarController()
which only works if I restart the app. Am I missing something?
The logout seems fine though
appDelegate.centerViewController = appDelegate.drawerSettingsViewController()
which puzzles me a bit because then I think I'm on the right track?
Am I supposed to only use normal segues and such first and then only call the drawerViewController?
Here is the other code when setting up the floating drawers :
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.rootViewController = drawerViewController
window?.makeKeyAndVisible()
return true
}
private var _drawerViewController: KGDrawerViewController?
var drawerViewController: KGDrawerViewController {
get {
if let viewController = _drawerViewController {
return viewController
}
return prepareDrawerViewController()
}
}
func prepareDrawerViewController() -> KGDrawerViewController {
let drawerViewController = KGDrawerViewController()
drawerViewController.centerViewController = drawerSettingsViewController()
drawerViewController.leftViewController = leftViewController()
drawerViewController.rightViewController = rightViewController()
drawerViewController.backgroundImage = UIImage(named: "sky3")
_drawerViewController = drawerViewController
return drawerViewController
}
private func drawerStoryboard() -> UIStoryboard {
let storyboard = UIStoryboard(name: StoryboardIDs.MainStoryBoardID , bundle: nil)
return storyboard
}
private func viewControllerForStoryboardId(storyboardId: String) -> UIViewController {
let viewController: UIViewController = drawerStoryboard().instantiateViewControllerWithIdentifier(storyboardId)
return viewController
}
func drawerSettingsViewController() -> UIViewController {
let viewController = viewControllerForStoryboardId(StoryboardIDs.LoginViewConSid)
return viewController
}
func sourcePageViewController() -> UIViewController {
let viewController = viewControllerForStoryboardId(StoryboardIDs.SettingsViewConID)
return viewController
}
func navigationBarController() -> UIViewController{
let viewController = viewControllerForStoryboardId(StoryboardIDs.NavConSid)
return viewController
}
private func leftViewController() -> UIViewController {
let viewController = viewControllerForStoryboardId(StoryboardIDs.LeftViewConID)
return viewController
}
private func rightViewController() -> UIViewController {
let viewController = viewControllerForStoryboardId(StoryboardIDs.RightViewConID)
return viewController
}
func toggleLeftDrawer(sender:AnyObject, animated:Bool) {
_drawerViewController?.toggleDrawer(.Left, animated: animated, complete: { (finished) -> Void in
// do nothing
})
}
func toggleRightDrawer(sender:AnyObject, animated:Bool) {
_drawerViewController?.toggleDrawer(.Right, animated: animated, complete: { (finished) -> Void in
// do nothing
})
}
func closeDrawer(sender:AnyObject, animated:Bool){
_drawerViewController?.closeDrawer(.Left, animated: animated, complete: { (finished) -> Void in
})
}
private var _centerViewController: UIViewController?
var centerViewController: UIViewController {
get {
if let viewController = _centerViewController {
return viewController
}
return drawerSettingsViewController()
}
set {
if let drawerViewController = _drawerViewController {
drawerViewController.closeDrawer(drawerViewController.currentlyOpenedSide, animated: true) { finished in }
if drawerViewController.centerViewController != newValue {
drawerViewController.centerViewController = newValue
}
}
_centerViewController = newValue
}
}
Any help/suggestions would be appreciated :D
Just gonna put this here in case anyone has similar problems.
After a week long struggle to find the problem. I eventually found that whenever I changed the centreViewController with
appDelegate.centerViewController = appDelegate.navigationBarController()
OR
appDelegate.centerViewController = appDelegate.logoutController()
that the methods
deinit {
print("deinit called")
notifCentre.removeObserver(self)
}
were not being called in any of the viewControllers.
So I added the line
self.dismissViewControllerAnimated(false, completion: {})
every time that I change the centreViewController.
Apparently Swift normally deinits automagically but when the using the third party methods there is some confusion with the memory handler and we need to step in. Good to know though as it could be a general swift issue as well.

Resources