I currently have a view controller that has a container view which functions similar to the bottom drawer in the iOS Maps application. When an image is scanned, the drawer animates onto the screen (it begins hidden off the screen at (0,700)) and displays information that comes from Firebase. Within this container, I have another container that allows me to have a PageViewController setup. This setup allows the user to swipe between 2 different sets of information (all of which needs to be loaded from Firebase).
I don't know how to reset the data within the second PageViewController so that whenever the drawer is dismissed through a button, the PageViewController system resets and I can use my function inside the secondPageViewController to reload the tableView it has inside.
TL;DR / More Information
Scanning an image is what loads the information into the ViewControllers.
I have one independent VC (ViewController) with a container view inside of it (BottomDrawerViewController). The BottomDrawerViewController also has a container inside of it which is a PageViewController with 2 pages (Swipe1ViewController & Swipe2ViewController)
The first page of the PageViewController setup works fine (Most likely because that information is loaded using an NSNotification)
I haven't been able to use an NSNotification to send and refresh this data the same way because this is not the first view controller that shows up after I post my notification.
I can't just load all my information independently in the second PageViewController because I get the pathway to the Firebase information from my original ViewController
Question: How can I reset both this data and the PageViewController so that whenever the user dismisses the bottom drawer the information on the second page is able to be refreshed on the next scan and the PageViewController starts on the first page? I thought maybe by forcing the view to disappear so that ViewDidLoad is called when it appears on screen but I am realizing this isn't possible.
I've attached some of the important code from all of the view controllers and my Firebase code in image and JSON form just in case that helps.
ViewController: -- This is the part that gets information to send to containers and what animates in the main container
ref.child("Directory").child(imageName).observeSingleEvent(of: .value, with: { (snapshot) in
print(snapshot)
let content = ImageInformation(snapshot: snapshot)
// TODO: Deliver Image Metadata here
let imageInfo = ["eventTitle": imageName, "source": content.Source, "date": content.Date] as [String : Any]
NotificationCenter.default.post(name: Notification.Name.drawerVC, object: nil, userInfo: imageInfo)
NotificationCenter.default.post(name: Notification.Name.newsVC, object: nil, userInfo: imageInfo)
})
UIView.animate(withDuration: 0.50) {
self.bottomDrawer.frame = CGRect(x: self.bottomPosition.x, y: self.bottomPosition.y, width: self.bottomDrawer.frame.size.width, height: self.bottomDrawer.frame.size.height)
}
BottomDrawerViewController: -- This gets a notification from ViewController and changes the drawers title. It also sends a notification back to ViewController when the view is pressed so that it can be dismissed.
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(appeared), name: Notification.Name.drawerVC, object: nil)
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
view.addGestureRecognizer(tap)
}
#objc func appeared(_ notification: NSNotification) {
if let eventTitle = notification.userInfo?["eventTitle"] as? String {
self.eventTitleLabel.text = eventTitle
}
}
#objc func handleTap(_ sender: UITapGestureRecognizer) {
NotificationCenter.default.post(name: Notification.Name.mainVC, object: nil, userInfo: nil)
}
PageViewController: --This controls my PageViewController setup. Since everything is the same as a normal setup I did not include. This line allows me to send a variable from the first page to the second.
var eventTitle: String = ""
Swipe1ViewController: -- Receives a notification from ViewController and uses it to populate the Labels on screen. It also sends the event title ("Test Input" in the database) to the PageViewController to be used by the Swipe2ViewController (No code shown)
Swipe2ViewController: -- As of right now it takes the eventTitle variable from the PageViewController and uses it to get data from Firebase but this doesn't work multiple times because viewDidLoad() only runs once when the BottomDrawerViewController is swiped up. I've also tried putting the fetchHeadlines() function in viewDidAppear() but if the user dismisses and scans a completely different picture it won't update.
override func viewDidLoad() {
super.viewDidLoad()
self.newsTableView.delegate = self
self.newsTableView.dataSource = self
// fetchHeadlines()
}
override func viewDidAppear(_ animated: Bool) {
//
fetchHeadlines()
}
override func viewDidDisappear(_ animated: Bool) {
}
func fetchHeadlines() {
let pageVC = self.parent as! DrawerPageViewController
let eventTitle = pageVC.eventTitle
print("Title: " + eventTitle)
ref.child("Directory").child(eventTitle).child("News").observeSingleEvent(of: .value) { (newsSnapshot) in
for news in newsSnapshot.children.allObjects as! [DataSnapshot] {
print("Key: \(news.key)", "Value: \(news.value)")
//self.headlines.append(news)
DispatchQueue.global().async {
DispatchQueue.main.async {
// self.tableView.reloadData()
}
}
}
}
}
You shouldn't be using notifications to propagate data from one view to another. As you see, it doesn't work that well, it can be error prone, and it can be challenging to debug. Instead, you should embrace the relationship between parent and child view controllers. That is, The ViewController should pass the data to the BottomViewController, who passes the data to the PageViewController, and it's the PageViewController's job to set up the data in the sub-pages. This lets you put the data where it belongs when each view controller is put on screen.
Note that a parent view controller can access a child using the childViewControllers array. So ViewController can access the BottomViewController directly using:
if let bottomViewController = childViewControllers.first as? BottomViewController {
// set up the bottomViewController here
}
Related
I have an app that I am developing using Swift 4.0. I have a View Controller on which I am showing some useful information and user can interact with them. Lets call this as BaseViewController.
What I am doing:
The BaseViewController is starting different other ViewControllers, and than user can dismiss those viewControllers and come back to BaseViewController.
What I want:
Now I want that whenever user comes back to BaseViewController it gets itself updated. I know it can be done using Protocols, but I just want a simple way. Like in Android there is onResume method to perform updates whenever Activity comes into active state.
I think there is no need to share code as starting other viewController from one viewcontroller is pretty simple. I just wanted to know the better approach. Thanks in advance
UPDATE: THIS IS HOW I AM CALLING NEXT CONTROLLER OVER BASE CONTROLLER
let dialogRegisterForEventVC = UIStoryboard(name: "Main",bundle: nil).instantiateViewController(withIdentifier: "idDialogRegisterForEventVC") as! DialogRegisterForEventVC
dialogRegisterForEventVC.modalPresentationStyle = .overCurrentContext
dialogRegisterForEventVC.modalTransitionStyle = .crossDissolve
dialogRegisterForEventVC.isUserLogin = isLogin
self.present(dialogRegisterForEventVC, animated: true) {
}
You can always go with viewWillAppear Method
override func viewWillAppear(_ animated: Bool) {
print("This is called when coming back to Base View Controller")
}
Use your refresh code here. This is called when you pop your viewController
There different cases and you need to handle them for example
If you presenting another viewController on top of this you should use
override func viewWillAppear(_ animated: Bool)
If you need to refresh the view if the app comes in foreground state you need such code :
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func willResignActive(_ notification: Notification) {
refreshMyView()
}
Also, you can create delegate that your viewController can call to update this view controller. And one more option is to make your view controller observer of custom notification in Notification center and to push this notification once when you need to refresh the view controller.
If you using the notification center just be sure that your UI changes are on the main thread.
I'm trying to create a timer app. I have a singleton class with a Timer which fires every x minutes. Using custom delegate I pass the data to active view controller and update the value in a label. If the data is when the count is y, I perform push and update the count in another view controller's label.
When application is in foreground I didn't get any problem. If the application is in background state the counter keeps running and label text isn't updated and push isn't performed. Still I'm in first view controller. How to solve this?
like I mentioned in your comment section. when you app is in the background state you shouldn't continuously updating your UI as it is pointless. when user tapped back into your app your view controller will call a function viewDidAppear(animated). In that function you can check timer condition then present second view controller if needed. I'll post a sample code below
class FirstViewController : UIViewController {
var timerExpiration = false
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if timerExpiration {
let vc = SecondViewController()
present(vc, animated: true, completion: nil)
}
}
}
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
You need to wrap all foreground UI tasks in a block like this
DispatchQueue.main.async {
// do your UI stuff here, like
// label.text = "Main thread stuff"
}
Put this in your timer action.
Issue
I have a number of cases on iOS 11 (that do not occur on iOS 10 and below) where .UIKeyboardWillChangeFrame notification is not being fired — specifically when transitioning between view controllers where both view controllers have a UITextfield which is set as the fristResponder.
Since I have UI that needs to animate above the keyboard in response to the keyboard showing, receiving this notification is essential.
On iOS 10 and below, I get the notification on both view controllers (on showing VC A and also when pushing VC B). However, on iOS 11, the notification does not fire when pushing VC B. It's as if the keyboard remained in place.
Does anyone know what the cause of this is?
Details
Both view controllers inherit from a base view controller with the following implementation:
class BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame), name: .UIKeyboardWillChangeFrame, object: nil)
}
// Update layout when the keyboard is shown or hidden
#objc func keyboardWillChangeFrame(notification : Notification) {
// Check if got info
if (notification.userInfo == nil) {
return;
}
// Get resize properties
let dict = notification.userInfo!
let rect = self.view.convert((((dict[UIKeyboardFrameEndUserInfoKey as NSObject] as Any) as AnyObject).cgRectValue)!, from: nil)
let size = self.view.bounds.size.height - rect.origin.y
let duration = ((dict[UIKeyboardAnimationDurationUserInfoKey] as Any) as AnyObject).doubleValue
let curve = UIViewAnimationCurve.init(rawValue: (((dict[UIKeyboardAnimationCurveUserInfoKey] as Any) as AnyObject).intValue)!)
self.keyboardOffset = max(0, size)
// Set animation options
var options : UIViewAnimationOptions
switch (curve!) {
case .easeInOut:
options = UIViewAnimationOptions()
case .easeIn:
options = UIViewAnimationOptions.curveEaseIn
case .easeOut:
options = UIViewAnimationOptions.curveEaseOut
case .linear:
options = UIViewAnimationOptions.curveLinear
}
// Animate the change
UIView.animate(withDuration: duration!, delay: 0, options: options, animations: { () -> Void in
// Relayout
self.relayout()
}, completion: nil)
}
}
Example of a subclass:
class ViewControllerA: BaseViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
passwordField.becomeFirstResponder()
}
func relayout() {
// ... do animations to show stuff above keyboard.
}
}
Note that .keyboardWillShow and .keybaordWillHide are also not fired on the transition to VC B.
You need to add self.view.endEditing(force:Bool) in you first view controller's viewWillDisappear method.
Generally, you can always call endEditing method when you changing the screen.
I've worked around this by saving the keyboard height in a class variable in VC A and then passing that along to VC B.
It's not ideal, as I would prefer my VC's to be independent for those details.
I am working on SWRevealViewController in which I have a scenario in which -
Demo code link - https://www.dropbox.com/s/02fjub838hv95cr/SWRevealVC.zip?dl=0
1) I have to present a ViewController's view*(NotificationVC in my storyboard)* in my FrontVC.
**2)**This childVC's View has a button*(createNotification)* which is connected to another viewController*(createNotificationVC)* via custom segue(SWRevealViewControllerSeguePushController),which has a back button
**3)**Pressing the back button user returns to the frontVC again with some message to be passed to the FrontVC.
For this message passing,I am using notification Pattern.
In my frontVC -
override func viewDidLoad()
{
super.viewDidLoad()
self.revealViewController().delegate = self
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
if self.revealViewController() != nil
{
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
self.view.addGestureRecognizer(self.revealViewController().tapGestureRecognizer())
}
let NtfctnVC = self.storyboard?.instantiateViewControllerWithIdentifier("NtfctnVC")
addChildViewController(NtfctnVC!)
NtfctnVC!.view.frame = CGRectMake(0, 0,self.customView.frame.width, self.customView.frame.height)
customView.addSubview(NtfctnVC!.view)//add the childVC's view
NtfctnVC!.didMoveToParentViewController(self)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "methodOfReceivedNotification:", name:"NotificationIdentifier", object: nil)
}
func methodOfReceivedNotification(notification: NSNotification)
{
//Take Action on Notification
let userInfo = notification.userInfo
print(userInfo!["sentTag"] as? Int)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
In the createNotificationVC's back button action,I have -
#IBAction func bck(sender: AnyObject)
{
let frontVC = self.storyboard?.instantiateViewControllerWithIdentifier("frontNVC") //frontNVC is the navigation controller of frontVC
NSNotificationCenter.defaultCenter().postNotificationName("NotificationIdentifier", object: nil, userInfo: ["sentTag":2])
self.revealViewController().pushFrontViewController(frontVC, animated: true)//here the frontVC is presented with no animation.
}
Problem -
1)The animation while performing custom segue is not been shown.
2)Notifications message are not being passed to the frontVC.
Please help me regarding this as I have tried lot many stuff & googling, but in vain. Thanks.
This shows my SWR connection with front & rear VC
This shows my NotificationVC/childVC's connection. *Mark:*ChildVC button connected via custom segue to createNotificationVC.
1.SWRevealViewController have SW_Front & SW_REAR .
2.To use this you need to implement this hierarchy
->swrevealviewcontroller -->swfront--->viewcontroller
-->swrear---->viewcontroller
3.in your case why that code doesn't work means .
4.From sw_rear you have to use this animation ,then it will work sure.
I have an reference to a managed object called selectedItem, I load the data in on view controller and have a modal segue (over current context) to another view to edit the title property of the selectedItem.
I expect a textLabel to refresh the data when dismissing the modal view but it does not. I have used the same method to add to the table data and it worked, because I use tableView.reloadData but how can I refresh the label data using the modal segue ? or basically have the label change to the new value.
detailsViewController
var selectedItem: Item!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
self.tableView.reloadData()
titleLabel.text = selectedItem.title
}
override func viewDidLoad() {
super.viewDidLoad()
titleLabel.text = selectedItem.title
}
EditViewController
#IBAction func save(sender: AnyObject) {
selectedItem.title = editTitle.text
var error: NSError?
if context!.save(nil){}
context?.save(&error)
self.presentingViewController?.viewWillAppear(true)
self.dismissViewControllerAnimated(true, completion: {});
}
PS: I tried to do a work around by using another segue to go back to the first view but that crashed, does anybody know why ?
You can use a NSNotification, they are pretty handy for this sort of thing, here's an example of some generic usage:
Parent View Controller
In viewDidLoad:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "udpateObject:", name: "udpateObject", object: nil)
In deinit:
NSNotificationCenter.defaultCenter().removeObserver(self, name: "udpateObject", object: nil)
Then you'll make a func that matches the selector of the observer:
func udpateObject(notification: NSNotification) {
// here you'll get the object you update in a different view
if let receivedObject = notification.object as? YOUR_OBJECT_DATA_TYPE {
self.ThisInstanceVariable = receivedObject
// Update any UI elements
}
}
Update View Controller
Wherever you update your data:
NSNotificationCenter.defaultCenter().postNotificationName("udpateObject", object: YOUR_UPDATED_OBJECT)