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

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

Related

How can I check popularProduct.xib file loaded into HomeViewController

In my HomeViewController have four section:
Banner
Popular Product
New Product
Old Product
All are .xib files and they are working fine.
Now I want to add ActivityIndicator in my HomeViewController
So now I want to show ActivityIndicator on HomeViewController until all the .xib's file not fully loaded in HomeViewController as .xib's ActivityIndicator should hide.
Now, the problem for me is that how can I get the confirmation about .xib's loaded to HomeViewController or not?
As a pretty simple direct solution, you could follow the delegation approach. So, when "Popular Product" View complete the needed process, you should fire a delegate method which will be handled by the view controller.
Example:
consider that in PopularProduct class you are implementing a method called doSomething() which need to get called and finish its work to hide the activity indicator from the view controller and it should send Data instance to the view controller. You could do it like this:
protocol PopularProductDelegate: class {
func popularProduct(popularProduct: PopularProduct, doSomethingDidPerformWith data: Data)
}
class PopularProduct: UIView {
// ...
weak var delegate: PopularProductDelegate?
func doSomething() {
// consider that there is much work to be done here
// which generates a data instance:
let data = Data(base64Encoded: "...")!
// therefore:
delegate?.popularProduct(popularProduct: self, doSomethingDidPerformWith: data)
}
// ...
}
Therefore, in ViewController:
class ViewController: UIViewController {
// ...
var popularProduct: PopularProduct!
override func viewDidLoad() {
super.viewDidLoad()
// show indicator
popularProduct.doSomething()
}
// ...
}
extension ViewController: PopularProductDelegate {
func popularProduct(popularProduct: PopularProduct, doSomethingDidPerformWith data: Data) {
print(data)
// hide indicator
}
}
Furthermore, you could check my answer for better understanding for delegates.

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

Using a UISegmentedControl like a UISwitch

Is it possible to use a UISegmentedControl with 3 segments as if it was a three-way UISwitch? I tried to use one as a currency selector in the settings section of my app with no luck, it keeps reseting to the first segment when I switch views and that creates a big mess.
I proceeded like that:
IBAction func currencySelection(_ sender: Any) {
switch segmentedControl.selectedSegmentIndex {
case 0:
WalletViewController.currencyUSD = true
WalletViewController.currencyEUR = false
WalletViewController.currencyGBP = false
MainViewController().refreshPrices()
print(0)
case 1:
WalletViewController.currencyUSD = false
WalletViewController.currencyEUR = true
WalletViewController.currencyGBP = false
MainViewController().refreshPrices()
print(1)
case 2:
WalletViewController.currencyUSD = false
WalletViewController.currencyEUR = false
WalletViewController.currencyGBP = true
MainViewController().refreshPrices()
print(2)
default:
break
}
}
The UISegmentedControl is implemented in the
SettingsViewController of the app to choose between currencies to
display in the MainViewController.
(Taken from a comment in #pacification's answer.)
This was the missing piece I was looking for. It provides a lot of context.
TL;DR;
Yes, you can use a three segment UISegmentedControl as a three-way switch. The only real requirement is that you can have only one value or state selected.
But I wasn't grasping why your code referred to two view controllers and some of switching views resulting in resetting the segment. One very good way to do what you want is to:
Have MainViewController present SettingsViewController. Presenting it modally means the user is only doing one thing at a time. When they are making setting changes, you do not want them adding new currency values.
Create a delegate protocol in SettingsViewController and make MainViewController conform to it. This tightly-couples changes made to the settings to the view controller interested in what those changes are.
Here's a template for what I'm talking about:
SettingsViewController:
protocol SettingsVCDelegate {
func currencyChanged(sender: SettingsViewController)
}
class SettingsViewController : UIViewController {
var delegate:SettingsVCDelegate! = nil
var currency:Int = 0
#IBAction func valueChanged(_ sender: UISegmentedControl) {
currency = sender.selectSegmentIndex
delegate.currencyChanged(sender:self)
}
}
MainViewController:
class MainViewController: UIViewController, SettingsVCDelegate {
var currency:Int = 0
let settingsVC = SettingsViewController()
override func viewDidLoad() {
super.viewDidLoad()
settingsVC.delegate = self
}
func presentSettings() {
present(settingsVC, animated: true, completion: nil)
}
func currencyChanged(sender:SettingsViewController) {
currency = sender.currency
}
}
You can also create an enum of type Int to make your code more readable, naming each value as currencyUSD, currencyEUR, and currencyGBP. I'll leave that to you as a learning exercise.
it keeps reseting to the first segment when I switch views
yes, it is. to avoid this situation you should set the correct switch value to the segmentedControl.selectedSegmentIndex every time when you load your view with UISegmentedControl.
UPD
Ok, the behavior of MainViewController can be similar to this:
final class MainViewController: UIViewController {
private var savedValue = 0
override func viewDidLoad() {
super.viewDidLoad()
}
func openSettingsController() {
let viewController = SettingsController.instantiate() // simplify code a bit. use the full controller initialization
viewController.configure(value: savedValue, onValueChanged: { [unowned self] value in
self.savedValue = value
})
navigationController?.pushViewController(viewController, animated: true)
}
}
And the SettingsViewController:
final class SettingsViewController: UIViewController {
#IBOutlet weak var segmentedControl: UISegmentedControl!
private var value: Int = 0
var onValueChanged: ((Int) -> Void)?
func configure(value: Int, onValueChanged: #escaping ((Int) -> Void)) {
self.value = value
self.onValueChanged = onValueChanged
}
override func viewDidLoad() {
super.viewDidLoad()
segmentedControl.selectedSegmentIndex = value
}
#IBAction func valueChanged(_ sender: UISegmentedControl) {
onValueChanged?(sender.selectedSegmentIndex)
}
}
The main idea that you should keep your selected value if you moving from SettingsViewController. For this thing you can create closure
var onValueChanged: ((Int) -> Void)?
that pass back to MainViewController the selected UISegmentedControl value. And in future when you will open the SettingsViewController again you just configure() this value and set it to UI.

Access the presenting view controller from the presented view controller?

I have a view controller (containing my menu) presented on top of another view controller (my application).
I would need to access the presenting view controller (below my menu) from the presented view controller (my menu), for example to access some variables or make the presenting view controller perform one of its segues.
However, I just can't figure out how to do it.
I'm aware of the "presentingViewController" and "presentedViewController" variables but I didn't manage to use them successfully.
Any Idea ?
Code (from the presented VC, which as a reference to the AppDelegate in which the window is referenced) :
if let presentingViewController = self.appDelegate.window?.rootViewController?.presentingViewController {
presentingViewController.performSegue(withIdentifier: "nameOfMySegue", sender: self)
}
Here is a use of the delegation Design pattern to talk back to the presenting view controller.
First Declare a protocol, that list out all the variables and methods a delegate is expected to respond to.
protocol SomeProtocol {
var someVariable : String {get set}
func doSomething()
}
Next : Make your presenting view controller conform to the protocol.
Set your presenting VC as the delegate
class MainVC: UIViewController, SomeProtocol {
var someVariable: String = ""
func doSomething() {
// Implementation of do
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Your code goes here.
if let destVC = segue.destination as? SubVC{
destVC.delegate = self
}
}
}
Finally, when you are ready to call a method on the presenting VC (Delegate).
class SubVC: UIViewController {
var delegate : SomeProtocol?
func whenSomeEventHappens() {
// For eg : When a menu item is selected
// Set some Variable
delegate?.someVariable = "Some Value"
// Call a method on the deleate
delegate?.doSomething()
}
}
Assuming that VCApplication is presenting VCMenu, in VCMenu you can access VCApplication with:
weak let vcApplication = self.presentingViewController as? VCApplicationType
Your example self.appDelegate.window?.rootViewController?.presentingViewController is looking for the ViewController that presented the rootViewController - it will be nil.
EDIT
Per TheAppMentor I've added weak so there are no retain cycles.

Update UITabBarController bar item from NSObject class

I have NSObject class listening for a specific event from my server.
When this specific event happens, I would like to update the badge value of an existing tabBar item from my UITabBarController called TabBarController.
How can I access it from the NSObject class?
Below is the NSOBject class listening for the event.
The function connectedToSocketIo() is launched when the application is launched.
The print("Event is working") is displayed in the terminal so everything is working.
The only thing I need now is to be able to update the badge of a specific bar item.
import Foundation
import UIKit
import SwiftyJSON
class SocketIOManager: NSObject{
func connectedToSocketIo(){
socket.on("post-channel:App\\Events\\contact\\newContactRequest"){ (data, ack) -> Void in
let json = JSON(data)
if json[0]["id"].string! == self.defaults.stringForKey("user_id")! {
print("event is working")
// I want to change the tab bar item badge here
} else {
print("no event")
}
}
}
}
You should try to get a reference to the UITabBarController in your SocketIOManager class. Once you have a reference the tab bar controller you can change the badge value of the desired UITabBarItem.
import Foundation
import UIKit
import SwiftyJSON
class SocketIOManager: NSObject {
/*
var tabBarController: UITabBarController!
*/
// When the tabBarController gets set the connectedToSocketIO function gets automatically called.
var tabBarController: UITabBarController! {
didSet {
connectedToSocketIO()
}
}
init() {
super.init()
}
// Either call this function
init(tabBarController: UITabBarController) {
super.init()
self.tabBarController = tabBarController
connectedToSocketIO()
}
// Or create a setter
func setTabBarController(tabBarController: UITabBarController) {
self.tabBarController = tabBarController
}
func connectedToSocketIo() {
socket.on("post-channel:App\\Events\\contact\\newContactRequest"){ (data, ack) -> Void in
let json = JSON(data)
if json[0]["id"].string! == self.defaults.stringForKey("user_id")! {
print("event is working")
// Set the desired tab bar item to a given value
tabBarController!.tabBar.items![0].badgeValue = "1"
} else {
print("no event")
}
}
}
}
EDIT
class CustomTabBarController: UITabBarController {
var socketIOManager: SocketIOManager!
viewDidLoad() {
super.viewDidLoad()
socketIOManager = SocketIOManager(tabBarController: self)
}
}
Hope this helps!
#Jessy Naus
I removed:
the socket connection from the app delegate,
the override init function inside the socketIOManager so the init(UITabBarController)
and added the socket.connect() (from socket.io library) function inside the init function linked to the tab bar controller as follow:
init(tabBarController: UITabBarController) {
super.init()
self.tabBarController = tabBarController
socket.connect()
self.listeningToSocketEvent()
}
I have replaced "self.connectedToSocketIo()" by "listeningToSocketEvent()" has the meaning of this function is more clear.
All together following your instructions mentioned above = Works perfectly. So I put your answer as the good one.
Really not easy concept. Will still need some time to assimilate it and apply it to other components of the UI.
Thanks a lot for your help on this!
actually, I found another way which avoid touching my socket.io instance.
Source:
Swift 2: How to Load UITabBarController after Showing LoginViewController
I just make the link with my tab bar controller as follow:
In my SocketIOManager
//"MainTabBarController" being the UITabBarController identifier I have set in the storyboard.
//TabBarController being my custom UITabBarController class.
// receivedNotification() being a method defined in my custom TabBarController class
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarController: TabBarController = mainStoryboard.instantiateViewControllerWithIdentifier("MainTabBarController") as! TabBarController
tabBarController.receivedNotification(1)
In my TabBarController class:
func receivedNotification(barItem: Int){
if let actualValue = self.tabBar.items![barItem].badgeValue {
let currentValue = Int(actualValue)
self.tabBar.items![barItem].badgeValue = String(currentValue! + 1)
} else {
self.tabBar.items![barItem].badgeValue = "1"
}
// Reload tab bar item
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window?.rootViewController = self
}