I have created protocol as updateTime protocol to update timer to other view controllers conform to updateTime delegates but the timer not updating to other controllers.
Please refer below code and pin point where I did wrong?
protocol UpdateTime: class {
func updateTIme(count: Int)
}
class ViewController: UIViewController, UpdateTime {
#IBOutlet var timeLabel: UILabel!
let secVC = SecondViewController()
override func viewDidLoad() {
super.viewDidLoad()
secVC.delegate = self
// Do any additional setup after loading the view.
}
// must be internal or public.
func updateTIme(count: Int){
print("firstVC: \(count)")
DispatchQueue.main.async {
self.timeLabel.text = "\(count)"
}
}
}
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) -> Int{
count += amount
print(count)
return count
}
func reset() {
count = 0
}
}
This is seconviewcontroller:
class SecondViewController: UIViewController {
#IBOutlet var label: UILabel!
weak var delegate: UpdateTime?
var counter = Counter()
var timer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
DispatchQueue.main.async {
self.timer.invalidate()
self.timer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(self.countDown), userInfo: nil, repeats: true)
}
}
#objc func countDown() -> Int{
// counter increments
counter.increment()
let count = counter.increment(by: 5)
DispatchQueue.main.async {
self.label.text = String(describing: count)
}
self.delegate?.updateTIme(count: count)
return count
}
}
The label in view controller not updating count down.
This looks like a lifecycle issue. Though you are creating an instance of SecondViewController, the timer from the second view controller only gets added to the run loop when that view controller's viewDidLoad method is called. Since you are programmatically creating it in the first view controller, there is nowhere in your example that would cause the second view controller's view to get loaded, so the timer would not be added to the runloop.
A better way of accomplishing this issue is to create the timer on the first view controller directly, or create a secondary object (not a second view controller) that manages the timer and have it push updates to the first view controller
Related
I am trying to use MVVM. I am going to VC2 from VC1. I am updating the viewModel.fromVC = 1, but the value is not updating in the VC2.
Here is what I mean:
There is a viewModel, in it there is a var fromVC = Int(). Now, in vc1, I am calling the viewModel as
let viewModel = viewModel().
Now, on the tap of button, I am updating the viewModel.fromVC = 8. And, moving to the next screen. In the next screen, when I print fromVC then I get the value as 0 instead of 8.
This is how the VC2 looks like
class VC2 {
let viewModel = viewModel()
func abc() {
print(viewModel.fromVC)
}
}
Now, I am calling abc() in viewDidLoad and the fromVC is printed as 0 instead of 8. Any help?
For the MVVM pattern you need to understand that it's a layer split in 2 different parts: Inputs & Outputs.
Int terms of inputs, your viewModel needs to catch every event from the viewController, and for the Outputs, this is the way were the viewModel will send data (correctly formatted) to the viewController.
So basically, if we have a viewController like this:
final class HomeViewController: UIViewController {
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
titleLabel.text = "toto"
}
}
We need to extract the responsibilities to a viewModel, since the viewController is handling the touchUp event, and owning the data to bring to th label.
By Extracting this, you will keep the responsibility correctly decided and after all, you'll be able to test your viewModel correctly 🙌
So how to do it? Easy, let's take a look to our futur viewModel:
final class HomeViewModel {
// MARK: - Private properties
private let title: String
// MARK: - Initializer
init(title: String) {
self.title = title
}
// MARK: - Outputs
var titleText: ((String) -> Void)?
// MARK: - Inputs
func viewDidLoad() {
titleText?("")
}
func buttonDidPress() {
titleText?(title)
}
}
So now, by doing this, you are keeping safe the different responsibilities, let's see how to bind our viewModel to our previous viewController :
final class HomeViewController: UIViewController {
// MARK: - public var
var viewModel: HomeViewModel!
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
// MARK: - Private func
private func bind(to viewModel: HomeViewModel) {
viewModel.titleText = { [weak self] title in
self?.titleLabel.text = title
}
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
viewModel.buttonDidPress()
}
}
So one thing is missing, you'll asking me "but how to initialise our viewModel inside the viewController?"
Basically you should once again extract responsibilities, you could have a Screens layer which would have the responsibility to create the view like this:
final class Screens {
// MARK: - Properties
private let storyboard = UIStoryboard(name: StoryboardName, bundle: Bundle(for: Screens.self))
// MARK: - Home View Controller
func createHomeViewController(with title: String) -> HomeViewController {
let viewModel = HomeViewModel(title: title)
let viewController = storyboard.instantiateViewController(withIdentifier: "Home") as! HomeViewController
viewController.viewModel = viewModel
return viewController
}
}
And finally do something like this:
let screens = Screens()
let homeViewController = screens.createHomeViewController(with: "Toto")
But the main subject was to bring the possibility to test it correctly, so how to do it? very easy!
import XCTest
#testable import mvvmApp
final class HomeViewModelTests: XCTestCase {
func testGivenAHomeViewModel_WhenViewDidLoad_titleLabelTextIsEmpty() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
viewModel.titleText = { title in
XCTAssertEqual(title, "")
expectation.fulfill()
}
viewModel.viewDidLoad()
waitForExpectations(timeout: 1.0, handler: nil)
}
func testGivenAHomeViewModel_WhenButtonDidPress_titleLabelTextIsCorrectlyReturned() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
var counter = 0
viewModel.titleText = { title in
if counter == 1 {
XCTAssertEqual(title, "toto")
expectation.fulfill()
}
counter += 1
}
viewModel.viewDidLoad()
viewModel.buttonDidPress()
waitForExpectations(timeout: 1.0, handler: nil)
}
}
And that's it 💪
I have 3 UIViewControllers. ContainerVC which contains 2 ContainerViews. First Container View is DashboardVC and second one is SidebarVC. The DashboardVC covers the entire screen, while the SidebarVC is outside.
I have a leading constraint for the SidebarVC that should be animated and the SidebarVC should slide in (from the left side). On the DashboardVC I have a UIBarButtonItem and when it's pressed it should perform the animation. The problem is that I'm doing something wrong with the delegate and when the ContainerVC conforms to the protocol, nothing happens.
PS: I have very hard time understanding protocols/delegates despite having watch a bunch of different videos on this concept. Here's the code:
DashboardVC
protocol SideBarDelegate {
func showMenu()
func hideMenu()
}
class DashboardVC: UIViewController {
var delegate: SideBarDelegate?
var isSideMenuOpen = true
#IBAction func menuButtonPressed(_ sender: UIBarButtonItem) {
if isSideMenuOpen {
delegate?.showMenu()
isSideMenuOpen = false
}
else {
delegate?.hideMenu()
isSideMenuOpen = true
}
}
}
ContainerVC
class ContainerVC: UIViewController {
#IBOutlet weak var sideBarMenuLeadingConstraint: NSLayoutConstraint!
}
extension ContainerVC : SideBarDelegate {
func showMenu() {
sideBarMenuLeadingConstraint.constant = -290
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
func hideMenu() {
sideBarMenuLeadingConstraint.constant = 0
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
}
You use the delegate only on classes. To prevent memory leaks, do those two things:
Change:
protocol SideBarDelegate {
func showMenu()
func hideMenu()
}
to:
protocol SideBarDelegate: class {
func showMenu()
func hideMenu()
}
Now, rename delegate property to:
weak var delegate: SideBarDelegate?
Weak does not increase the reference counting. This is important to prevent memory leaks.
Your instance of ContainerVC must have some sort of reference to an instance of DashboardVC (or make the delegate static but I have never seen something like that). Then, in your viewDidLoad method of ContainterVC, set this:
myInstanceReferenceToDashboardVC.delegate = self
Im developing a quiz app and there is a second view controller that appears after the initial view controller where you are asked to answer a question. On the second view controller you the user must press a button to return to the initial view controller to be asked another question. However when I segue back from the second view controller I believe a new instance of the initial view controller is being created and the user is asked questions they have already answered. The code in the swift file for my initial view controller is constructed that when once a user is asked a question once that question is removed from an array by it's index. How could I make it to where a new instance of the initial view controller isn't created from segueing from the second view controller? Or is there a way the second view controller can access the same methods as the initial view controller?
Here is my code for the initial view controller:
import UIKit
class ViewController: UIViewController {
var questionList = [String]()
func updateCounter() {
counter -= 1
questionTimer.text = String(counter)
if counter == 0 {
timer.invalidate()
wrongSeg()
counter = 15
}
}
func randomQuestion() {
//random question
if questionList.isEmpty {
questionList = Array(QADictionary.keys)
questionTimer.text = String(counter)
}
let rand = Int(arc4random_uniform(UInt32(questionList.count)))
questionLabel.text = questionList[rand]
//matching answer values to go with question keys
var choices = QADictionary[questionList[rand]]!
questionList.remove(at: rand)
//create button
var button:UIButton = UIButton()
//variables
var x = 1
rightAnswerBox = arc4random_uniform(4)+1
for index in 1...4
{
button = view.viewWithTag(index) as! UIButton
if (index == Int(rightAnswerBox))
{
button.setTitle(choices[0], for: .normal)
}
else {
button.setTitle(choices[x], for: .normal)
x += 1
}
randomImage()
}
}
let QADictionary = ["Who is Thor's brother?" : ["Atum", "Loki", "Red Norvell", "Kevin Masterson"], "What is the name of Thor's hammer?" : ["Mjolinr", "Uru", "Stormbreaker", "Thundara"], "Who is the father of Thor?" : ["Odin", "Sif", "Heimdall", "Balder"]]
//wrong view segue
func wrongSeg() {
performSegue(withIdentifier: "incorrectSeg", sender: self)
}
//proceed screen
func rightSeg() {
performSegue(withIdentifier: "correctSeg", sender: self)
}
//variables
var rightAnswerBox:UInt32 = 0
var index = 0
//Question Label
#IBOutlet weak var questionLabel: UILabel!
//Answer Button
#IBAction func buttonAction(_ sender: AnyObject) {
if (sender.tag == Int(rightAnswerBox))
{
rightSeg()
print ("Correct!")
}
if counter != 0 {
counter = 15
questionTimer.text = String(counter)
}
else if (sender.tag != Int(rightAnswerBox)) {
wrongSeg()
print ("Wrong!")
timer.invalidate()
questionList = []
}
randomQuestion()
}
override func viewDidAppear(_ animated: Bool)
{
randomQuestion()
}
//variables
var counter = 15
var timer = Timer()
#IBOutlet weak var questionTimer: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
timer = Timer.scheduledTimer(timeInterval: 1, target:self, selector: #selector(ViewController.updateCounter), userInfo: nil, repeats: true)
}
Here's the code to the second view controller:
import UIKit
class ContinueScreen: UIViewController {
//correct answer label
#IBOutlet weak var correctLbl: UILabel!
//background photo
#IBOutlet weak var backgroundImage: UIImageView!
func backToQuiz() {
if let nav = self.navigationController {
nav.popViewController(animated: true)
}
else {
self.dismiss(animated: true, completion: nil)
}
}
#IBAction func `continue`(_ sender: Any) {
backToQuiz()
}
What you need, sir is a delegate or an unwind segue. I far prefer delegates because they're easier to understand and I think a bit more powerful.
In your ContinueScreen view controller define a protocol similar to this:
protocol QuizCompletedDelegate {
func completedQuiz()
func canceledQuiz()
}
Also in your ContinueScreen view controller you need to declare an optional delegate of type QuizCompletedDelegate
var delegate: QuizCompletedDelegate?
... and use that delegate to call the functions when appropriate:
#IBAction func didPressContinue(_ sender: Any) {
if allQuestionsAnswered == true {
delegate.completedQuiz()
} else {
delegate.cancelledQuiz()
}
}
In your initial view controller is where you implement the functions for the protocol:
extension ViewController: QuizCompletedDelegate {
func completedQuiz() {
//Handle quiz complete
}
func cancelledQuiz() {
//Handle quiz cancelled
}
}
Then the last thing you need to do is set the delegate of your ContinueScreen view controller in the prepareForSegue function of your initial view controller.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showContinueScreen" {
let continueVC = segue.destination as! ContinueScreen
continueVC.delegate = self
}
}
Remove the call to randomQuestion() on your initial view controller's ViewDidAppear, and you're in business!
It shouldn't create a new View Controller when you go back. If it does - it's very bad - and you should re-architect the flow of your app... To pass data forward, we should use Dependency Injection. To pass data to the previous view controller - use the iOS Delegation Pattern. I recommend to spend 30-40 mins to read (and understand this article), and it will make things more clear for you in the future: http://matteomanferdini.com/how-ios-view-controllers-communicate-with-each-other/.
I am developing a quiz app where the user is asked a question at random and must answer it on the initial view controller. If the user picks correctly, a second view controller appears which contains a button that pops the view controller off the navigation stack and goes back to the initial view controller to finish the other questions. However, I have a timer that I want to reset (start at 15s) every time that second view controller is popped and the initial view controller appears with the next question. How would I accomplish this task? I already have the countdown timer code in my swift file. I just need to know how to get it to start from scratch every time the second view controller is popped/removed.
Here's my code for the initial view controller:
import UIKit
extension ViewController: QuizCompletedDelegate {
func continueQuiz() {
randomQuestion()
}
}
class ViewController: UIViewController {
var questionList = [String]()
func updateCounter() {
counter -= 1
questionTimer.text = String(counter)
if counter == 0 {
timer.invalidate()
wrongSeg()
counter = 15
}
}
func randomQuestion() {
//random question
if questionList.isEmpty {
questionList = Array(QADictionary.keys)
}
let rand = Int(arc4random_uniform(UInt32(questionList.count)))
questionLabel.text = questionList[rand]
//matching answer values to go with question keys
var choices = QADictionary[questionList[rand]]!
questionList.remove(at: rand)
//create button
var button:UIButton = UIButton()
//variables
var x = 1
rightAnswerBox = arc4random_uniform(4)+1
for index in 1...4 {
button = view.viewWithTag(index) as! UIButton
if (index == Int(rightAnswerBox)) {
button.setTitle(choices[0], for: .normal)
} else {
button.setTitle(choices[x], for: .normal)
x += 1
}
randomImage()
}
}
let QADictionary = ["Who is Thor's brother?" : ["Atum", "Loki", "Red Norvell", "Kevin Masterson"], "What is the name of Thor's hammer?" : ["Mjolinr", "Uru", "Stormbreaker", "Thundara"], "Who is the father of Thor?" : ["Odin", "Sif", "Heimdall", "Balder"]]
//wrong view segue
func wrongSeg() {
performSegue(withIdentifier: "incorrectSeg", sender: self)
}
//proceed screen
func rightSeg() {
performSegue(withIdentifier: "correctSeg", sender: self)
}
//variables
var rightAnswerBox:UInt32 = 0
var index = 0
//Question Label
#IBOutlet weak var questionLabel: UILabel!
//Answer Button
#IBAction func buttonAction(_ sender: AnyObject) {
if (sender.tag == Int(rightAnswerBox)) {
rightSeg()
print ("Correct!")
}
if counter != 0 {
counter = 15
questionTimer.text = String(counter)
} else if (sender.tag != Int(rightAnswerBox)) {
wrongSeg()
print ("Wrong!")
timer.invalidate()
questionList = []
}
}
override func viewDidAppear(_ animated: Bool) {
randomQuestion()
}
//variables
var counter = 15
var timer = Timer()
#IBOutlet weak var questionTimer: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
timer = Timer.scheduledTimer(timeInterval: 1, target:self, selector: #selector(ViewController.updateCounter), userInfo: nil, repeats: true)
}
}
Here's the code to the second view controller:
class ContinueScreen: UIViewController {
var delegate: QuizCompletedDelegate?
//correct answer label
#IBOutlet weak var correctLbl: UILabel!
//background photo
#IBOutlet weak var backgroundImage: UIImageView!
func backToQuiz() {
delegate?.continueQuiz()
if let nav = self.navigationController {
nav.popViewController(animated: true)
} else {
self.dismiss(animated: true, completion: nil)
}
}
#IBAction func `continue`(_ sender: Any) {
backToQuiz()
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
How to make countdown timer reset after button push on different view
controller?
So if you can get the reference of yourView controller where you have
setup the timer then you can access counter variable from previous
view controller inside your second viewController and when pushed the
button just reset to 0.
If above approach is not possible for you then another way is create
the static variable and access it anywhere and reset like this:-
static var counter = 0
if you are popping/removing the 2nd view controller and thus returning to the first, you could reset the timer in your viewDidAppearfunc in your 1st view controller. this way the timer restarts every time the view appears - the first time, and whenever the 2nd pops.
If one ViewController inherits from another, how do I update stuff in the child ViewController as the variable changes in the parent ViewController?
class ViewControllerOne: UIViewController {
var timer = Timer()
var number: Int = 0
func updateNumber() {
number += 1
}
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateNumber), userInfo: nil, repeats: true)
}
}
class ViewControllerTwo: ViewControllerOne {
// So in this class I want to print to the console when number is 3.
// How do I check for that from this class?
}
Override updateNumber in ViewControllerTwo.
override func updateNumber() {
super.updateNumber()
if number == 3 {
// do something
}
}