Function Calling After ViewController Is Dismissed - swift

I have a func that calls itself over and over again for a simple little slideshow.
func animateImage(no:Int)
{
println("ANIMATE")
var imgNumber:Int = no
let t:NSTimeInterval = 1;
let t1:NSTimeInterval = 0;
let url = NSURL(string: self.itemss[imgNumber][0])
let data = NSData(contentsOfURL: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check
imgView!.alpha = 0.4
self.imgView?.image = UIImage(data: data!)
//code to animate bg with delay 2 and after completion it recursively calling animateImage method
UIView.animateWithDuration(2.0, delay: 0, options:UIViewAnimationOptions.CurveEaseOut, animations: {() in
self.imgView!.alpha = 1.0;
},
completion: {(Bool) in
imgNumber++;
if imgNumber>self.itemss.count-1 //only for 4 image
{
imgNumber = 0
}
self.animateImage(imgNumber);
})
}
The problem I'm having is that the func seems to continue to call and doesn't seem to be stopping when i dismiss the view controller ? Why is this func continuing to call after the view controller is dismissed.
#IBAction func dismissMe(sender: AnyObject) {
if((self.presentingViewController) != nil){
self.dismissViewControllerAnimated(false, completion: nil)
println("done")
}
}

The reason that your function is continuing to be called after the view controller is dismissed is because there's no logic that should prevent it from discontinuing to call itself once it is dismissed.
A dismissed view controller should cease to have a parent view controller, however, so we could put an end to this loop by checking if there's a parent view controller at the top of the method:
if self.parentViewController == nil {
return;
}
If this is the first thing your method does, then the method should only continue to execute (and line up another call to itself) if the view controller still has a parent.

Related

NotificationCenter obersever not called

I have UITabBarController and two ViewControllers.
In the first vc I have a video, after it ends the data transfer to another controller is triggered. But unfortunately nothing happens in the second controller.
func fetchCaloriesData() {
refWorkout = workout!.title
Database.database().reference().child("programs").child(refWorkout).child("calories").observeSingleEvent(of: .value) { (snapshot) in
self.caloriesData = snapshot.value as! String
print(self.caloriesData)
self.dictionary["calories"] = self.caloriesData
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "sendCalories"), object: nil, userInfo: self.dictionary)
print("Заполнили словарь")
print(self.dictionary["calories"])
//self.caloriesDelegate?.update(newText: caloriesData)
// self.dietVC.BurnedLabel.text? = caloriesData
}
}
Everything is work. When I try print I can see: Optional ("...")
In second vc in viewDidLoad I have next code.
NotificationCenter.default.addObserver(self, selector: #selector(gotNotification), name: NSNotification.Name("sendCalories"), object: nil)
and next
#objc func gotNotification(notification: Notification) {
guard let userInfo = notification.userInfo else {return}
guard let calories = userInfo["calories"] as? String else {return}
print("Наблюдатель")
print(calories)
BurnedLabel.text! = calories
}
Nothing happens. What am I doing wrong ?
NotificationCenter.default.addObserver(self, selector: #selector(gotNotification)
this will be called only if view is loaded before notification is triggered,
so you need to pass data in different way when you tapped to open second view controller. After that observer is in on state, so he is monitoring changes. Basically if you downloading something asynchronous and that is not finished while you open second view controller, after that if downloading is done, your second view will be updated, be sure to update UI on main thread. In your case make variable with calories object in second controller, then from first controller update main tab bar controller(add calories property there as well) with delegate method(make protocol to pass calories) so you don't shoot notification everywhere and from there update second controller.
That is how you will have fresh data always.
guard let firstController = controllers[0] as? FirstController else {return}
firstController.caloriesDelegate = self
guard let secondViewController = controllers[0] as? SecondController else {return}
secondViewController.calories = self.calories // in main tab bar after updating

Autolayout images inside cells in tableview. Correct layout but only once scrolling down and back up?

Im trying to get tableview cells with auto resizing images to work. Basically I want the image width in the cell to always be the same, and the height to change in accordance with the aspect ratio of the image.
I have created a cell class, which only has outlets for a label, imageView and a NSLayoutConstraint for the height of the image. I have some async methods to download an image and set it as the image for the cell imageView. Then the completion handle gets called and I run the following code to adjust the height constraint to the correct height:
cell.cellPhoto.loadImageFromURL(url: photos[indexPath.row].thumbnailURL, completion: {
// Set imageView height to the width
let imageSize = cell.cellPhoto.image?.size
let maxHeight = ((self.tableView.frame.width-30.0)*imageSize!.height) / imageSize!.width
cell.cellPhotoHeight.constant = maxHeight
cell.layoutIfNeeded()
})
return cell
And here is the UIImageView extension I wrote which loads images:
func loadImageFromURL(url: String, completion: #escaping () -> Void) {
let url = URL(string: url)
makeDataRequest(url: url!, completion: { data in
DispatchQueue.main.async {
self.image = UIImage(data: data!)
completion()
}
})
}
And the makeDataRequest function which it calls:
func makeDataRequest(url: URL, completion: #escaping (Data?) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: url, completionHandler: { data, response, error in
if error == nil {
let response = response as? HTTPURLResponse
switch response?.statusCode {
case 200:
completion(data)
case 404:
print("Invalid URL for request")
default:
print("Something else went wrong in the data request")
}
} else {
print(error?.localizedDescription ?? "Error")
}
})
task.resume()
}
This works for all the cells out of frame, but the imageviews in the cells in the frame are small. Only when I scroll down and then back up again do they correctly size. How do I fix this? I know other people have had this issue but trying their fixes did nothing.
I had to sorta recreate the problem to understand what was going on. Basically you need to reload the tableview. I would do this when a picture finishes downloading.
In the view controller that has the table view var. Add this to the viewDidLoad() function.
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
//Create a notification so we can update the list from anywhere in the app. Good if you are calling this from an other class.
NotificationCenter.default.addObserver(self, selector: #selector(loadList), name: NSNotification.Name(rawValue: "loadList"), object: nil)
}
//This function updates the cells in the table view
#objc func loadList(){
//load data here
self.tableView.reloadData()
}
Now, when the photo is done downloading, you can notify the viewcontroller to reload the table view by using the following,
func loadImageFromURL(url: String, completion: #escaping () -> Void) {
let url = URL(string: url)
makeDataRequest(url: url!, completion: { data in
DispatchQueue.main.async {
self.image = UIImage(data: data!)
completion()
//This isn't the best way to do this as, if you have 25+ pictures,
//the list will pretty much freeze up every time the list has to be reloaded.
//What you could do is have a flag to check if the first 'n' number of cells
//have been loaded, and if so then don't reload the tableview.
//Basically what I'm saying is, if the cells are off the screen who cares.
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "loadList"), object: nil)
}
})
}
Heres something I did to have better Async, see below.
My code as follows, I didn't do the resizing ratio thing like you did but the same idea applies. It's how you go about reloading the table view. Also, I personally don't like writing my own download code, with status code and everything. It isn't fun, why reinvent the wheel when someone else has done it?
Podfile
pod 'SDWebImage', '~> 5.0'
mCell.swift
class mCell: UITableViewCell {
//This keeps track to see if the cell has been already resized. This is only needed once.
var flag = false
#IBOutlet weak var cellLabel: UILabel!
#IBOutlet weak var cell_IV: UIImageView!
override func awakeFromNib() { super.awakeFromNib() }
}
viewController.swift (Click to see full code)
I'm just going to give the highlights of the code here.
//Set the image based on a url
//Remember this is all done with Async...In the backgorund, on a custom thread.
mCell.cell_IV.sd_setImage(with: URL(string: ViewController.cell_pic_url[row])) { (image, error, cache, urls) in
// If failed to load image
if (error != nil) {
//Set to defult
mCell.cell_IV.image = UIImage(named: "redx.png")
}
//Else we got the image from the web.
else {
//Set the cell image to the one we downloaded
mCell.cell_IV.image = image
//This is a flag to reload the tableview once the image is done downloading. I set a var in the cell class, this is to make sure the this is ONLY CALLED once. Otherwise the app will get stuck in an infinite loop.
if (mCell.flag != true){
DispatchQueue.main.asyncAfter(deadline: .now() + 0.025){ //Nothing wrong with a little lag.
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "loadList"), object: nil)
mCell.flag = true
}
}
}
}

How to reload Container A after update Container B?

I have a ViewController containing a Segmented Control of 3 ContainerViews. Each Container is a ViewController containing a tableView. In CV2, I can choose a row. This row is deleted from CV2 and should be added in CV1.
Before use Segmented Controller, I used tab and table view on each tab were correctly updated. Now, with ContainerView, I don't know how to call the reload. This is my action on segment :
#IBAction func bSegment(_ sender: UISegmentedControl) {
if sender.selectedSegmentIndex == 0 {
UIView.animate(withDuration: 0.0, animations: {
self.listBOutlet.alpha = 1
self.addBOutlet.alpha = 0
self.delBOutlet.alpha = 0
})
} else {
if sender.selectedSegmentIndex == 1 {
UIView.animate(withDuration: 0.0, animations: {
self.listBOutlet.alpha = 0
self.addBOutlet.alpha = 1
self.delBOutlet.alpha = 0
})
} else {
if sender.selectedSegmentIndex == 2 {
UIView.animate(withDuration: 0.0, animations: {
self.listBOutlet.alpha = 0
self.addBOutlet.alpha = 0
self.delBOutlet.alpha = 1
})
}
}
}
}
I tried many things like this to access to viewWillAppear in CV2 :
print(self.children)
var myClass : ListeBEtb = self.????
myClass.ListeB.reloadData()
myClass.viewWillAppear(false)
But I don't know how to call ListeBEtb (CV1) to update it.
In my CV2 :
override func viewWillAppear(_ animated: Bool) {
let recupListeBAddModel = RecupListeBAddModel()
recupListeBAddModel.delegate = self
recupListeBAddModel.downloadItems(id: idEtablissement)
self.ListeB.reloadData()
}
Do you any idea about it ?
Thanks
You have a couple of ways of making this work, with the simplest one using the notification center for start.
Delegate pattern is the other, RxSwift is also an alternative, etc.
For using the notification center, first, define an extension for easier notification handling:
extension Notification.Name {
static let rowDeleted = Notification.Name("rowDeleted")
}
After that, subscribe the the newly created notification in CV1:
NotificationCenter.default.addObserver(self, selector: #selector(rowDeleted), name: .rowDeleted, object: nil)
Note that you need a method called rowDeleted in CV1, or however you want to call it.
Also, make sure to remove yourself as an observer on deinit of CV1:
deinit {
NotificationCenter.default.removeObserver(self)
}
Then, when you make an update in CV2, use something like this:
NotificationCenter.default.post(name: .rowDeleted, object: nil)
This is all assuming you don't need to pass the actual object, but just need an event notification. Object passing is a bit different and better done with delegate pattern, but this is a decent start.
If you're trying to call some logic that exists in the viewWillAppear(_:) function, why not extract that into its own function, call that new function from viewWillAppear(_:), and then you can call that function again when you transition your container views.
So in your container view, it would now look something like this.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
reloadDataModel()
}
func reloadDataModel() {
let recupListeBAddModel = RecupListeBAddModel()
recupListeBAddModel.delegate = self
recupListeBAddModel.downloadItems(id: idEtablissement)
self.ListeB.reloadData()
}
And then, in your transition code, perhaps just before your animation code, you can call listBOutlet.reloadDataModel(), so it can do the operation as the animation is in progress.

Segmented Control, present childview B from childview A

I'm stuck on how I can show child B after a successful API call from child A. I am using a container view with embedded segues to the 2 views.
The parent view has a segment control that I'm using like this:
#IBAction func segmentsPressed(_ sender: AwardSegmentedControl) {
if sender.selectedSegmentIndex == 0 {
container.segueIdentifierReceivedFromParent("A")
} else {
container.segueIdentifierReceivedFromParent("B")
}
}
I added a function on the parent view called changeView which I'm calling from a successful API call from A.
func changeSubView() {
let oldController = self.storyboard?.instantiateViewController(withIdentifier: "A") as! AwardStoreVC
oldController.willMove(toParentViewController: nil)
oldController.view.removeFromSuperview()
oldController.removeFromParentViewController()
let controller = self.storyboard?.instantiateViewController(withIdentifier: "B") as! AwardsForUserVC
addChildViewController(controller)
view.addSubview(controller.view)
controller.didMove(toParentViewController: self)
}
But nothing happens. Is this the right approach?

After my completion handler completes still unable to re-load tableView data swift 4

In my model i've got a function to read in data from firebase.
I call completionHandler(true) when that's done.
This is my viewDidLoad function in my controller that extends UITableViewController.
override func viewDidLoad()
{
super.viewDidLoad()
let activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.whiteLarge)
tableView.backgroundView = activityIndicatorView
self.activityIndicatorView = activityIndicatorView
activityIndicatorView.startAnimating()
model.readInFirebaseData { (success) in
print("data read in")
activityIndicatorView.stopAnimating()
dataArray = model.firebaseDataArray
self.tableView.reloadData()
}
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
tableView.tableHeaderView = searchController.searchBar
}
But the table remains empty and for whatever reason self.tableView.reloadData() isn't populating the table as i'd like but if I segue from the TableViewController and come back the list is populated.
I can't see exactly where i'm going wrong.
Thanks.
Update:
I still couldn't get it working so instead of a completionHandler i used a delegate. What i did was:
Singleton:
protocol Refresh{
func refreshData()
}
var delegate:Refresh?
func readInFirebaseData()
{
self.ref.child("users").observe(DataEventType.childAdded, with: { (snapshot) in
user.name = value?["name"] as? String ?? ""
self.dict.updateValue(user, forKey: uid)
DispatchQueue.main.async {
self.delegate?.refreshData()
print("main thread dispatch")
}
})
}
The TableViewController:
class ListController: TableViewController, Refresh{
viewDidLoad()
{
model.delegate = self
}
func refreshData() {
print("called")
array = model.array
self.tableView.reloadData()
}
That all works. The only issue really that I don't know the answer to is the DispatchQueue.main.async is getting called everytime firebase reads in a "user". But I put it at the end of the readInFirebase function and nothing was populated on the list. But in any case it works at the moment.
Probably readInFirebaseData method is asynchronous and the callback runs on a thread different from the main one.
Remember that all the UIKit related calls must be run on the main thread.
Try with:
model.readInFirebaseData { (success) in
print("data read in")
dataArray = model.firebaseDataArray
DispatchQueue.main.async {
activityIndicatorView.stopAnimating()
self.tableView.reloadData()
}
}
Be sure to set delegate and dataSource:
tableView.delegate = self
tableView.dataSource = self
And make sure you call reloadData() from Main Thread:
DispatchQueue.main.async {
self.tableView.reloadData()
}
Calling it from background threads would typically not lead to your table reloaded, since this operation is UI-related. Any UI related operations should be performed from main thread.