How can I get an updated value from UserDefaults? - swift

I have an Onboarding controller, when a user has completed Onboarding I am writing a value in UserDefaults and then dismissing the OnboardingViewController.
My base viewcontroller however is pushing them back to the Onboarding flow as UserDefaults.standard.bool(forKey: "ONBOARDING|COMPLETE") is returning false.
If I restart my app however is will return true and they are pushed to the correct VC.
My Base ViewController
class ViewController: UIViewController {
var onboardingComplete: Bool {
get {
return UserDefaults.standard.bool(forKey: "ONBOARDING|COMPLETE")
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(UserDefaults.standard.bool(forKey: "ONBOARDING|COMPLETE"))
print(onboardingComplete)
setBaseViewController()
}
fileprivate func setBaseViewController() {
if onboardingComplete {
print("has completed onboarding")
} else {
let layout = UICollectionViewFlowLayout()
navigationController?.present(OnboardingController(collectionViewLayout: layout), animated: true, completion: { })
}
}
}
My Onboarding controller has a method in which I am using to set the value as complete
#objc func handleCompleteOnboarding() {
dismiss(animated: true) {
DispatchQueue.main.async {
UserDefaults.standard.set(true, forKey: "ONBOARDING|COMPLETE")
}
}
}

You are setting the value in the completion handler.
Move this outside of dismiss.
#objc func handleCompleteOnboarding() {
UserDefaults.standard.set(true, forKey: "ONBOARDING|COMPLETE")
dismiss(animated: true)
}
You have essentially created a race condition between you setting the value and your viewDidAppear method being called.

Related

how can i dismiss a viewcontroller whilst at the same time segue onto a different one

I am currently unable to dismiss a viewcontroller and at the same time make the application move to a different controller without experiencing a few bugs. What my code does currently is when the timer 'secondsRemaining' hits zero it will dismiss the current viewcontroller it is on and will show the previous viewController that is displayed underneath the current viewcontroller being the 'homeScreenViewController'. Because of this it will not call the code- self.performSegue(withIdentifier: "gameScreenToHighscore", sender: self).
Current code:
#objc func updateTimer() {
if secondsRemaining > 0 {
secondsRemaining -= 1
timerLabel.text = "Timer:\(secondsRemaining)"
print(secondsRemaining)
} else {
self.dismiss(animated: true, completion: nil)
self.performSegue(withIdentifier: "gameScreenToHighscore", sender: self)
}
}
I think the best approach is that you handle the navigation in the parent ViewController, You can achieve this by making delegate.
In the ChildViewController:
#protocol ModalVCDelegate {
func navigateToTheSecondVC()
}
class ModalViewController {
weak delegate: ModalVCDelegate!
}
and you should call the delegate method when the timer has done.
delegate?.navigateToTheSecondVC()
And Also in the ParentViewController, you should set the delegate:
childVC.delegate = self
present(childVC, animated: true, completion: nil)
and then:
extension MainViewController: ModalVCDelegate {
func navigateToTheSecondVC {
dismiss(animated: true, completion: {
self.performSegue(withIdentifier: "gameScreenToHighscore", sender: self)
})
}
}

blank collection view in user login for the first time

Good day everyone,
I have root view controller set up as my HomeViewController, in this project i am using token based authentication ( which i am storing in user defaults ) and i am using token for all my API calls.
I have a check in my viewWillAppear method to check if there is access token present and then i make the api call in viewDidAppear to populate the collection view, and this works perfectly fine at all times except the first time.
If I log in for the first time it hits the viewWillAppear, viewDidAppear and then login screen pops up and once i authenticate the user and save it in the UserDefaults, dismiss the login screen and in the HomeViewController all i get is a spinner ( which means that viewDidAppear is also been called ) but if i close the app and open it again it all works fine.
What can i change in my code to make it work in the first time please and thank you!!
class HomeViewController: UIViewController {
// MARK: - Properties
let refreshControl = UIRefreshControl()
var publishedReportList: [ReportListDetail] = []
private let reportsCollectionView: UICollectionView = {
let viewLayout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: viewLayout)
collectionView.register(ReportsCollectionViewCell.self, forCellWithReuseIdentifier: ReportsCollectionViewCell.identifier)
collectionView.backgroundColor = .systemBackground
return collectionView
}()
// MARK: - Initialisation
override func viewDidLoad() {
super.viewDidLoad()
reportsCollectionView.delegate = self
reportsCollectionView.dataSource = self
print(publishedReportList.count)
refreshControl.tintColor = .blue
refreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
reportsCollectionView.addSubview(refreshControl)
reportsCollectionView.alwaysBounceVertical = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// check auth status
handleNotAuthenticated()
}
override func viewDidAppear(_ animated: Bool) {
// call for reports
getReportUserLayout()
}
// MARK: - Handlers
#objc func pullToRefresh() {
// Code to refresh table view
getReportUserLayout()
}
fileprivate func getReportUserLayout() {
// publishedReportList.removeAll()
whySuchEmptyLabel.isHidden = true
spinner.show(in: view)
spinner.textLabel.text = "Loading Reports.."
DispatchQueue.main.async {
ReportsManager.shared.getReportData { [weak self] (listOfReports) in
guard let strongSelf = self else { return }
strongSelf.publishedReportList = listOfReports
if listOfReports.count == 0 {
strongSelf.whySuchEmptyLabel.isHidden = false
}
strongSelf.reportsCollectionView.reloadData()
strongSelf.spinner.dismiss()
strongSelf.refreshControl.endRefreshing()
}
}
}
private func handleNotAuthenticated() {
if UserDefaults.standard.string(forKey: "accessToken") == nil {
// show login view controller
let loginVC = LoginViewController()
loginVC.modalPresentationStyle = .overCurrentContext
present(loginVC, animated: false)
}
}
}
You are in the HomeViewController and presenting loginVC, so it will not trigger viewDidAppear or viewWillAppear because it is not disappeared from the app. You have to use closure or delegate or notification to communicate back to the HomeViewController. You can also use the Combine framework and save the state. Here is an example of using delegate.
// Add protocol
protocol ViewControllerDelegate {
func loggedIn()
}
class HomeViewController: UIViewController, ViewControllerDelegate {
private func handleNotAuthenticated() {
if UserDefaults.standard.string(forKey: "accessToken") == nil {
let loginVC = LoginViewController()
loginVC.modalPresentationStyle = .overCurrentContext
loginVC.viewDelegate = self
present(loginVC, animated: false)
}
}
}
class LoginViewController: UIViewController {
var viewDelegate: ViewControllerDelegate? = nil
func userLoggedIn() {
self.viewDelegate?.loggedIn()
}
}

How to push controller after dismiss presented controller in swift?

Hey I am showing a controller (A) in main controller with presentation style (not push), and I want to button tapped and push another controller (B) after dismiss this (A) controller, this situation occurred in main controller. I am using protocol for this situation. Any idea for that ? Code like below.`
//this is dismiss button action
var segueDelegate: segueFromController?
#objc func dismissController() {
self.dismiss(animated: true) {
self.segueDelegate?.segueFromController()
}
//and this one is protocol function in main controller
func segueFromController() {
let contProfile = ContViewController(collectionViewLayout: UICollectionViewFlowLayout())
navigationController?.pushViewController(contProfile, animated: true)
}
// and I am making "self" this protocol in main controller's didload
let aCont = AController()
override func viewDidLoad() {
super.viewDidLoad()
AController.segueDelegate = self
}
// protocol
protocol segueFromController {
func segueFromController()
}
// this is presenting (A) controller code in main page
func openController() {
let preController = AController()
preController.modalPresentationStyle = .fullScreen
self.present(preController, animated: true, completion: nil)
}
First you need to make this segueDelegate weak
protocol segueFromController : class {
func segueFromController()
}
weak var segueDelegate: segueFromController?
func openController() {
let preController = AController()
preController.segueDelegate = self
preController.modalPresentationStyle = .fullScreen
self.present(preController, animated: true, completion: nil)
}
Try to dismiss without animation
self.dismiss(animated: false) {
self.segueDelegate?.segueFromController()
}

Return control to function when modal viewcontroller dismissed

I need to present a modal VC that sets a property in my presenting VC, and then I need to do something with that value back in the presenting VC. I have to be able to pass pointers to different properties to this function, so that it's reusable. I have the code below (KeyPickerTableViewController is the modal VC).
It should work, except not, because the line after present(picker... gets executed immediately after the picker is presented.
How do I get my presenting VC to "wait" until the modal VC is dismissed?
#objc func fromKeyTapped(_ button: UIBarButtonItem) {
print("from tapped")
setKey(for: &sourceKey, presentingFrom: button)
}
#objc func toKeyTapped(_ button: UIBarButtonItem) {
print("from tapped")
setKey(for: &destKey, presentingFrom: button)
}
fileprivate func setKey(for key: inout Key!, presentingFrom buttonItem: UIBarButtonItem) {
let picker = KeyPickerTableViewController()
picker.delegate = self
picker.modalPresentationStyle = .popover
picker.popoverPresentationController?.barButtonItem = buttonItem
present(picker, animated: true, completion: nil)
if let delKey = delegatedKey {
key = delKey
}
}
You could use delegate pattern or closure.
I would do the following
1. I would not use inout pattern, I would first call the popover and then separately update what is needed to be updated
2. In KeyPickerTableViewController define property var actionOnDismiss: (()->())? and setting this action to what we need after initialisation of KeyPickerTableViewController
I could show it in code, but the abstract you've shown is not clear enough to come up with specific amendments. Please refer the illustration below.
import UIKit
class FirstVC: UIViewController {
var key = 0
#IBAction func buttonPressed(_ sender: Any) {
let vc = SecondVC()
vc.action = {
print(self.key)
self.key += 1
print(self.key)
}
present(vc, animated: true, completion: nil)
}
}
class SecondVC: UIViewController {
var action: (()->())?
override func viewDidLoad() {
onDismiss()
}
func onDismiss() {
action?()
}
}
While presenting VC, add dismissing modal VC action in its completion handler, so that Viewcontroller will be presented after dismissal is completed
present(picker, animated: true, completion: { (action) in
//dismissal action
if let delKey = delegatedKey {
key = delKey
}
})

Swift, core data, add item from modally presented view

I am updating an app into Swift and have a tableView to which items can be added from a modally presented view. This works fine when the Save button is pressed, but when cancel is pressed the table view is presented with a blank line at the top, showing that an empty item was added. What am I missing?
The AddViewController protocol is:
protocol NewGaugeDelegate {
func didFinish(viewController:AddGaugeViewController, didSave:Bool)
}
and the didSave function is:
extension AddGaugeViewController {
#IBAction func cancelButtonWasTapped(_ sender: AnyObject) {
delegate?.didFinish(viewController: self, didSave: false)
}
#IBAction func saveButtonWasTapped(_ sender: AnyObject) {
addGauge()
delegate?.didFinish(viewController: self, didSave: true)
}
}
in the tableView controller, the protocol is accessed like this:
extension GaugeTableViewController: NewGaugeDelegate {
func didFinish(viewController: AddGaugeViewController, didSave: Bool) {
guard didSave,
let context = viewController.context,
context.hasChanges else {
dismiss(animated: true)
return
}
context.perform {
do {
try context.save()
} catch let error as NSError {
fatalError("Error: \(error.localizedDescription)")
}
self.coreDataStack.saveContext()
}
dismiss(animated: true)
}
}
Suggestions as to why the cancel function is not working properly would be appreciated.