How to load tab bar controller from login view - Swift - swift

So I made a view controller for login user with Parse. when the user login by entering the log in button, tab bar view controller load. The issue is if the user open the app and he already login, I don't want him to go without entering his login. I want the signing view controller send him to tab bar view controller.
The initial view controller is Tab bar view controller, I tried many ways to deal with this problem, but nothing seems good.
waiting for all you thoughts.

There are probably a lot of different ways you can achieve this, but one way I've dealt with this type of situation in the past is to create a custom container ViewController. This VC doesn't have a UIView of its own, but instead presents other ViewControllers as its "Views".
Here's a link to some Apple documentation that describes creating a controller of this type.
One of the benefits of this architecture is that I always have a VC in-scope that can take action to reroute my user based on events related to their account status (not logged in, offline limit reached, account suspended, first-time user, logout, etc.).
EDIT: Example Container Controller
Here's an example of how to implement a custom container controller. I'm sure there are some better ways to do a few of the features shown here, but hopefully this gives you a good start.
import UIKit
class ApplicationContainerController: UIViewController {
//MARK: - View Controller Routing Properties
private var _currentClientView:UIView? = nil
private var _currentViewController: UIViewController? = nil
//MARK: - Initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
//MARK: - UIViewController Members
override func viewDidLoad() {
super.viewDidLoad()
}
override var shouldAutomaticallyForwardAppearanceMethods : Bool {
return true
}
override func viewWillAppear(_ animated: Bool) {
//Get the user and route to the appropriate VC
let yourUserObject: AnyObject? = YourDataSource.TryToGetAUser()
DispatchQueue.main.async {
self.routeUser(yourUserObject)
}
}
//MARK: - Your Custom Routing Logic
func routeUser(_ yourUserObject: AnyObject?) {
//make sure we have an existing user, or else we send them to login
guard let user = yourUserObject
else {
self.displayContentController(YourLoginViewController())
return
}
var destinationViewController:UIViewController
//please use an enum or something (instead of strings) in your code
switch user.signInStatus {
case "loginActive":
let mainMenuViewController = YourMainMenuViewController()
mainMenuViewController.user = user
destinationViewController = mainMenuViewController
case "firstLogin":
let firstLoginViewController = YourFirstLoginViewController()
firstLoginViewController.user = user
destinationViewController = firstLoginViewController
case "giveUsMoney":
let weWantMoneyViewController = YourOtherViewController()
weWantMoneyViewController.user = user
destinationViewController = weWantMoneyViewController
default:
//loginFailed or some other status we don't know how to handle
destinationViewController = YourLoginViewController()
}
if let activeViewController = self._currentViewController,
type(of: activeViewController) !== type(of: destinationViewController) {
//we have an active viewController that is not the destination, cycle
self.cycleFromCurrentViewControllerToViewController(destinationViewController)
} else {
//no active viewControllers
self.displayContentController(destinationViewController)
}
}
//MARK: - Custom Content Controller Routing Methods
private func frameForContentController() -> CGRect {
return self.view.frame
}
private func newViewStartFrame() -> CGRect {
return CGRect(x: self.view.frame.origin.x,
y: self.view.frame.origin.y + self.view.frame.size.width,
width: self.view.frame.size.width,
height: self.view.frame.size.height)
}
private func oldViewEndFrame() -> CGRect {
return CGRect(x: self.view.frame.origin.x,
y: self.view.frame.origin.y - self.view.frame.size.width,
width: self.view.frame.size.width,
height: self.view.frame.size.height)
}
/**
Transitions viewControllers, adds-to/removes-from context, and animates views on/off screen.
*/
private func cycleFromCurrentViewControllerToViewController(_ newViewController: UIViewController) {
if let currentViewController = self._currentViewController {
self.cycleFromViewController(currentViewController, toViewController: newViewController)
}
}
private func cycleFromViewController(_ oldViewController:UIViewController, toViewController newViewController:UIViewController) {
let endFrame = self.oldViewEndFrame()
oldViewController.willMove(toParentViewController: nil)
self.addChildViewController(newViewController)
newViewController.view.frame = self.newViewStartFrame()
self.transition(from: oldViewController, to: newViewController,
duration: 0.5,
options: [],
animations: { () -> Void in
newViewController.view.frame = oldViewController.view.frame
oldViewController.view.frame = endFrame
}) { (finished:Bool) -> Void in
self.hideContentController(oldViewController)
self.displayContentController(newViewController)
}
}
/**
Adds a view controller to the hierarchy and displays its view
*/
private func displayContentController(_ contentController: UIViewController) {
self.addChildViewController(contentController)
contentController.view.frame = self.frameForContentController()
self._currentClientView = contentController.view
self.view.addSubview(self._currentClientView!)
self._currentViewController = contentController
contentController.didMove(toParentViewController: self)
}
/**
Removes a previously added view controller from the hierarchy
*/
private func hideContentController(_ contentController: UIViewController) {
contentController.willMove(toParentViewController: nil)
if (self._currentViewController == contentController) {
self._currentViewController = nil
}
contentController.view.removeFromSuperview()
contentController.removeFromParentViewController()
}
}

Related

GCVirtualController not displaying with SKScene

The Virtual controllers appear but are then obscured by objects added to the view. I've also tried adding the virtual controllers in the UIViewController but this doesn't work either.
Is it possible to use GCVirtualController directly with SKScene?
class GameScene: SKScene {
private var _virtualController: Any?
public var virtualController: GCVirtualController? {
get { return self._virtualController as? GCVirtualController }
set { self._virtualController = newValue }
}
override func didMove(to view: SKView) {
let background = SKSpriteNode(imageNamed: ".jpg")
background.zPosition = -1
addChild(background)
let virtualConfig = GCVirtualController.Configuration()
virtualConfig.elements = [GCInputLeftThumbstick, GCInputRightThumbstick, GCInputButtonA, GCInputButtonB]
virtualController = GCVirtualController(configuration: virtualConfig)
virtualController?.connect()
}
}
It appears the issue only occurs when pushing from one ViewController to the GameViewController.
When launching to the GameViewController the issue does not occur.
The Virtual Game Controller can run on any view controller running iOS 15 At least but for the purpose of work it is best viewed in landscape but you need to complete the codes as a physical gamepad.
For the virtual game controller to appear you need to register a physical game controller and apply the functions of notification when connect and disconnect a controller as you do with physical controller exactly.
Here is a code to setup and register a virtual and physical game controller I use and it works for me.
1st you need to import the Game Controller Library
import GameController
Then you define the Virtual Controller Under your Controller Class
class GameViewController: UIViewController {
// Virtual Onscreen Controller
private var _virtualController: Any?
#available(iOS 15.0, *)
public var virtualController: GCVirtualController? {
get { return self._virtualController as? GCVirtualController }
set { self._virtualController = newValue }
}
And then you call the setupGameController Function In Your ViewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
//your code
setupGameController()
}
and here is the main function to setup your Virtual and physical game controller
func setupGameController() {
NotificationCenter.default.addObserver(
self, selector: #selector(self.handleControllerDidConnect),
name: NSNotification.Name.GCControllerDidBecomeCurrent, object: nil)
NotificationCenter.default.addObserver(
self, selector: #selector(self.handleControllerDidDisconnect),
name: NSNotification.Name.GCControllerDidStopBeingCurrent, object: nil)
if #available(iOS 15.0, *)
{
let virtualConfiguration = GCVirtualController.Configuration()
virtualConfiguration.elements = [GCInputLeftThumbstick,
GCInputRightThumbstick,
GCInputButtonA,
GCInputButtonB]
virtualController = GCVirtualController(configuration: virtualConfiguration)
// Connect to the virtual controller if no physical controllers are available.
if GCController.controllers().isEmpty {
virtualController?.connect()
}
}
guard let controller = GCController.controllers().first else {
return
}
registerGameController(controller)
}
Then to act with the virtual or physical gamepad actions you need to assign the connect and register for the game controller
as
func handleControllerDidConnect(_ notification: Notification) {
guard let gameController = notification.object as? GCController else
{
return
}
unregisterGameController()
if #available(iOS 15.0, *)
{
if gameController != virtualController?.controller
{
virtualController?.disconnect()
}
}
registerGameController(gameController)
}
func handleControllerDidDisconnect(_ notification: Notification) {
unregisterGameController()
if #available(iOS 15.0, *) {
if GCController.controllers().isEmpty
{
virtualController?.connect()
}
}
}
func registerGameController(_ gameController: GCController) {
var buttonA: GCControllerButtonInput?
var buttonB: GCControllerButtonInput?
if let gamepad = gameController.extendedGamepad
{
buttonA = gamepad.buttonA
buttonB = gamepad.buttonB
}
buttonA?.valueChangedHandler = {(_ button: GCControllerButtonInput, _ value: Float, _ pressed: Bool) -> Void in
// Put here the codes to run when button A clicked
print("Button A Pressed")
}
buttonB?.valueChangedHandler = {(_ button: GCControllerButtonInput, _ value: Float, _ pressed: Bool) -> Void in
// Put here the codes to run when button B clicked
print("Button B Pressed")
}
}
func unregisterGameController()
{
}
And Here Is A Result of the code on a very basic ship sample of Xcode
I've encountered the same problem, presenting UIViewController using UINavigationController's present method(no storyboards used).
navigationController.present(
GameViewController(),
animated: false
)
I fixed it by setting UINavigationController's viewControllers property to needed view controllers, instead of pushing.
navigationController.viewControllers = [
UIViewController(),
GameViewController()
]

How to add `toggleSidebar` NSToolbarItem in Catalyst?

In my app, I added a toggleSidebar item to the NSToolbar.
#if targetEnvironment(macCatalyst)
extension SceneDelegate: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.toggleSidebar, NSToolbarItem.Identifier.flexibleSpace, AddRestaurantButtonToolbarIdentifier]
}
}
#endif
However, when I compile my app to Catalyst, the button is disabled. Does anybody know what else I need to do to hook it up?
If you look at the documentation for .toggleSidebar/NSToolbarToggleSidebarItemIdentifier you will see:
The standard toolbar item identifier for a sidebar. It sends toggleSidebar: to firstResponder.
Adding that method to your view controller will enable the button in the toolbar:
Swift:
#objc func toggleSidebar(_ sender: Any) {
}
Objective-C:
- (void)toggleSidebar:(id)sender {
}
Your implementation will need to do whatever you want to do when the user taps the button in the toolbar.
Normally, under a real macOS app using an NSSplitViewController, this method is handled automatically by the split view controller and you don't need to add your own implementation of toggleSidebar:.
The target needs changed to self, this is shown in this Apple sample where it is done for the print item but can easily be changed to the toggle split item as I did after the comment.
/** This is an optional delegate function, called when a new item is about to be added to the toolbar.
This is a good spot to set up initial state information for toolbar items, particularly items
that you don't directly control yourself (like with NSToolbarPrintItemIdentifier).
The notification's object is the toolbar, and the "item" key in the userInfo is the toolbar item
being added.
*/
func toolbarWillAddItem(_ notification: Notification) {
let userInfo = notification.userInfo!
if let addedItem = userInfo["item"] as? NSToolbarItem {
let itemIdentifier = addedItem.itemIdentifier
if itemIdentifier == .print {
addedItem.toolTip = NSLocalizedString("print string", comment: "")
addedItem.target = self
}
// added code
else if itemIdentifier == .toggleSidebar {
addedItem.target = self
}
}
}
And then add the action to the scene delegate by adding the Swift equivalent of this:
- (IBAction)toggleSidebar:(id)sender{
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
[UIView animateWithDuration:0.2 animations:^{
splitViewController.preferredDisplayMode = (splitViewController.preferredDisplayMode != UISplitViewControllerDisplayModePrimaryHidden ? UISplitViewControllerDisplayModePrimaryHidden : UISplitViewControllerDisplayModeAllVisible);
}];
}
When configuring your UISplitViewController, set the primaryBackgroundStyle to .sidebar
let splitVC: UISplitViewController = //your application's split view controller
splitVC.primaryBackgroundStyle = .sidebar
This will enable your NSToolbarItem with the system identifier .toggleSidebar and it will work automatically with the UISplitViewController in Mac Catalyst without setting any target / action code.
This answer is mainly converting #malhal's answer to the latest Swift version
You will need to return [.toggleSidebar] in toolbarDefaultItemIdentifiers.
In toolbarWillAddItem you will write the following (just like the previous answer suggested):
func toolbarWillAddItem(_ notification: Notification) {
let userInfo = notification.userInfo!
if let addedItem = userInfo["item"] as? NSToolbarItem {
let itemIdentifier = addedItem.itemIdentifier
if itemIdentifier == .toggleSidebar {
addedItem.target = self
addedItem.action = #selector(toggleSidebar)
}
}
}
Finally, you will add your toggleSidebar method.
#objc func toggleSidebar() {
let splitController = self.window?.rootViewController as? MainSplitController
UIView.animate(withDuration: 0.2) {
splitController?.preferredDisplayMode = (splitController?.preferredDisplayMode != .primaryHidden ? .primaryHidden : .allVisible)
}
}
A few resources that might help:
Integrating a Toolbar and Touch Bar into Your App
Mac Catalyst: Adding a Toolbar
The easiest way to use the toggleSidebar toolbar item is to set primaryBackgroundStyle to .sidebar, as answered by #Craig Scrogie.
That has the side effect of enabling the toolbar item and hiding/showing the sidebar.
If you don't want to use the .sidebar background style, you have to implement toggling the sidebar and validating the toolbar item in methods on a class in your responder chain. I put these in a subclass of UISplitViewController.
#objc func toggleSidebar(_ sender: Any?) {
UIView.animate(withDuration: 0.2, animations: {
self.preferredDisplayMode =
(self.displayMode == .secondaryOnly) ?
.oneBesideSecondary : .secondaryOnly
})
}
#objc func validateToolbarItem(_ item: NSToolbarItem)
-> Bool {
if item.action == #selector(toggleSidebar) {
return true
}
return false
}

Cannot convert value of type '(SwipeableTabBarController).Type' to expected argument type 'UIView'

I want to add Tabbar to my application. But when I try to add it, it gives the error in the header. How do I activate the Tabbar function?
public extension UIViewController {
public func setTabBarSwipe(enabled: Bool) {
if let swipeTabBarController = tabBarController as? SwipeableTabBarController {
swipeTabBarController.isSwipeEnabled = enabled
}
}
}
class MainTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(SwipeableTabBarController)
}
You can't add a user defined type SwipeableTabBarController tab as a subview here
view.addSubview(SwipeableTabBarController)
you need to add an instance like
let vc = SwipeableTabBarController()
self.addChild(vc)
vc.view.frame = self.view.bounds
view.addSubview(vc.view)
vc.willMove(toParent:self)
if your tabBarController is the root view controller of the app, use this, please.
if let window = UIApplication.shared.keyWindow,
let tabBar = window.rootViewController as? SwipeableTabBarController {
tabBar.isSwipeEnabled = enabled
}

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)

Swift - How to use a closure to fire a function in a view model?

I am watching the video series
Swift Talk #5
Connecting View Controllers
url: https://talk.objc.io/episodes/S01E05-connecting-view-controllers
In this video series they remove all the prepareForSegue and use an App class to handle the connection between different view controllers.
I want to replicate this, but specifically only in my current view model; but what I don't get is how to connect view controllers through a view model (or even if you're meant to)
In their code, at github: https://github.com/objcio/S01E05-connecting-view-controllers/blob/master/Example/AppDelegate.swift
They use do this within their view controller
var didSelect: (Episode) -> () = { _ in }
This runs;
func showEpisode(episode: Episode) {
let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
detailVC.episode = episode
navigationController.pushViewController(detailVC, animated: true)
}
In the same way, I want to use my ViewController to use my ViewModel for a menu button press (relying on tag).
My code follows;
struct MainMenuViewModel {
enum MainMenuTag: Int {
case newGameTag = 0
}
func menuButtonPressed(tag: Int) {
guard let tagSelected = MainMenuTag.init(rawValue: tag) else {
return
}
switch tagSelected {
case .newGameTag:
print ("Pressed new game btn")
break
}
}
func menuBtnDidPress(tag: Int) {
print ("You pressed: \(tag)")
// Do a switch here
// Go to the next view controller? Should the view model even know about navigation controllers, pushing, etc?
}
}
class MainMenuViewController: UIViewController {
#IBOutlet var mainMenuBtnOutletCollection: [UIButton]!
var didSelect: (Int) -> () = { _ in }
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func mainMenuBtnPressed(_ sender: UIButton) {
let tag = (sender).tag
self.didSelect(tag)
}
}
What I don't understand is how do I connect the command
self.didSelect(tag)
to the function
func menuButtonPressed(tag: Int)
within my ViewModel
As I understand it, according to the swift talk video is that the idea is that the view controller are "plain" and that the view model handles all the major stuff, like menu button presses and then moving to different view controllers as necessary.
How do I connect the didSelect item to my viewModel function?
Thank you.
You should set didSelect property for your controller like here:
func showEpisode(episode: Episode) {
let detailVC = storyboard.instantiateViewControllerWithIdentifier("Detail") as! DetailViewController
detailVC.episode = episode
detailVC.didSelect = { episode in
// do whatever you need
// for example dismiss detailVC
self.navigationController.popViewController(animated: true)
// or call the model methods
self.model.menuButtonPressed(episode)
}
navigationController.pushViewController(detailVC, animated: true)
}