Selector in UIBarButtonItem not calling - ios

I have a selector on my UIBarButton referencing a function to segue to another view controller but the function never gets called when clicked on. Through testing breakpoints I can see the function, segueToCartViewController, never gets called.
Thanks in advance!
UIBarButtonItem init
private let reuseIdentifier = "ItemCell"
private let SegueCartIdentifier = "CatalogToCart"
final class CatalogViewController: UICollectionViewController {
//MARK: -properties
var brand: Brand!
var cart: [Item]!
fileprivate let itemsPerRow:CGFloat = 3
fileprivate let sectionInsets = UIEdgeInsets(top: 30, left: 20, bottom: 30, right: 20)
private var cartItem: UIBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "Cart"), style: .plain, target: self, action: #selector(segueToCartViewController(_:)))
var selectedItemIndexPath: IndexPath?{
didSet{
var indexPaths = [IndexPath]()
if let selectedItemIndexPath = selectedItemIndexPath{
indexPaths.append(selectedItemIndexPath)
}
if let oldValue = oldValue{
indexPaths.append(oldValue)
}
collectionView?.performBatchUpdates({
self.collectionView?.reloadItems(at: indexPaths)
}) { completed in
if let selectedItemIndexPath = self.selectedItemIndexPath{
self.collectionView?.scrollToItem(at: selectedItemIndexPath, at: .centeredVertically, animated: true)
}
}
}
}
override func viewDidAppear(_ animated: Bool) {
self.navigationController?.setToolbarHidden(false, animated: false)
let flex = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
self.navigationController?.toolbar.items = [flex,cartItem,flex]
}
}
call for segue
//MARK: CartNavigation
extension CatalogViewController: CartDelegate{
func segueToCartViewController(_ sender: AnyObject){
super.performSegue(withIdentifier: SegueCartIdentifier, sender: sender)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destination = segue.destination as? UINavigationController else{
return
}
cartVC.delegate = self
}
func closeModallyPresentedViewController() {
dismiss(animated: true, completion: nil)
}
}

The target of your UIBarButtonItem is nil because self is nil during it's initialization.
You can initialize it like this instead
final class CatalogViewController: UICollectionViewController {
lazy final private var cartItem: UIBarButtonItem = { [unowned self] in
return UIBarButtonItem(image: #imageLiteral(resourceName: <#T##String#>), style: .plain, target: self, action: #selector(segueToCartViewController(_:)))
}()
override function viewDidAppear(_ animated: Bool) {
//blah blah, the rest of your code
}
}
See here for a good explanation about the value of self during initialization of properties.

Related

DatePicker goes under UINavigationBar on iPhone, works well on iPad

I have the following code for the DatePickerController:
import UIKit
#objc protocol DatePickerControllerDelegate: AnyObject {
func datePicker(controller: DatePickerController, didSelect date: Date)
func datepickerDidDismiss(controller: DatePickerController)
}
final class DatePickerController: UIViewController {
#objc weak var delegate: DatePickerControllerDelegate?
#objc public var date: Date {
get {
return datePicker.date
}
set(value) {
datePicker.setDate(value, animated: false)
}
}
#objc public lazy var datePicker: UIDatePicker = {
let v = UIDatePicker()
v.datePickerMode = .date
return v
}()
override var preferredContentSize: CGSize {
get {
return datePicker.frame.size
}
set(newValue) {
super.preferredContentSize = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(datePicker)
datePicker.autoresizingMask = [.flexibleTopMargin, .flexibleRightMargin, .flexibleLeftMargin, .flexibleBottomMargin]
view.backgroundColor = .white
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done,
target: self,
action: #selector(DatePickerController.doneButtonDidTap))
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel,
target: self,
action: #selector(DatePickerController.cancelButtonDidTap))
}
#objc private func doneButtonDidTap() {
delegate?.datePicker(controller: self, didSelect: date)
}
#objc private func cancelButtonDidTap() {
dismiss(animated: true, completion: nil)
delegate?.datepickerDidDismiss(controller: self)
}
}
It works well on iPad:
However, on iPhone the picker slides up under the UINavigationBar:
The code used to show the DatePickerController is absolutely the same, the style is UIModalPresentationStylePopover in both cases.
Update: code to present DatePickerController:
#objc func presentDatePicker() {
let picker = DatePickerController()
let navC = UINavigationController(rootViewController: picker)
present(navC, animated: true, completion: nil)
}
Fixed using AutoLayout:
private func configureLayout() {
datePicker.translatesAutoresizingMaskIntoConstraints = false
[datePicker.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
datePicker.leadingAnchor.constraint(equalTo: view.leadingAnchor),
datePicker.trailingAnchor.constraint(equalTo: view.trailingAnchor),]
.forEach{$0.isActive = true}
}

Cannot dynamically add custom cell from 2nd data source

I've spent 20+ hours searching/trying different solutions just for this specific problem and cannot make it work. I'm just starting to learn Swift, so be gentile... I made sure to exhaust all options before asking for help.
I have 3 view controllers (VC1: form with 5 text fields, VC2: tableview for displaying gathered data, VC3: 2nd form with 3 text fields and an image picker)
screen shot of storyboard
VC1 gathers the text field data, pass to VC2, and use custom cell #1 to add to the tableview. I used the exact same methods to do the same for VC3 (changing the cell identifier, and using if/else to determine which section to add to) but cannot get any results. At first I thought my data wasn't being passed, but that checks out ("Finihed & Send" button set to alert and prints variable's text) next I thought it was my logic, but custom cell #1 works... I've been staring it this code for so long, I have horrible dreams about it. I feel like this should be obtainable with the techniques I'm applying but wonder if I am I wandering into Core Data territory but just don't know it.
tableview with only 1 custom cell
My intent is to have VC1 add a cell (custom cell 1) at indexPath.row 0 and VC3 to add (custom cell 2) indexPath.row >= 1. Everything works like I want it to with the exception of adding a second custom cell to the tableview.
addWorkViewController
import UIKit
import MapKit
import CoreLocation
class mainVC: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, CLLocationManagerDelegate, UITextFieldDelegate {
#IBOutlet weak var scrollviewWORK: UIScrollView!
#IBOutlet weak var typeWORK: UISegmentedControl!
#IBOutlet weak var locationWORK: UITextField!
#IBOutlet weak var positionWORK: UISegmentedControl!
#IBOutlet weak var priceWORK: UITextField!
#IBOutlet weak var photo1WORK: UIImageView!
#IBOutlet weak var descriptionWORK: UITextView!
/// Prepare Segues
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.destination {
case is WorkOverview:
let workDest: WorkOverview = segue.destination as! WorkOverview
var cost = ""
var snap = UIImage()
if (priceWORK.text == nil) {
cost = ""
} else {
cost = priceWORK.text!
}
if (photo1WORK.image == nil) {
snap = UIImage()
} else {
snap = photo1WORK.image!
}
workDest.workLocation = locationWORK.text!
workDest.workDescription = descriptionWORK.text!
workDest.workPrice = cost
workDest.workPhoto = snap
case is PlantList:
let plantDest: PlantList = segue.destination as! PlantList
plantDest.placeholder = ""
default:
break
}
}
/// END Segue Preparation
/// Save to List Button
#IBAction func saveToListBTN(_ sender: UIButton) {
performSegue(withIdentifier: "unwindToList", sender: self)
}
/// END Save to List Button
/// Insert Plant
#IBAction func insertPlant(_ sender: UIButton) {
performSegue(withIdentifier: "toPlantListSegue", sender: self)
}
var addedPlant: String? = ""
/// END Insert Plant
/// Clear All Button
#IBAction func clearAllBTN(_ sender: UIButton) {
}
/// END Clear All Button
/// Segmented Controller - Work Type
#IBAction func positionChanged(_ sender: UISegmentedControl) {
switch positionWORK.selectedSegmentIndex {
case 0:
locationWORK.text? += " - Front"
case 1:
locationWORK.text? += " - Back"
case 2:
locationWORK.text? += " - Side"
default:
break
}
}
#IBAction func indexChanged(_ sender: UISegmentedControl) {
switch typeWORK.selectedSegmentIndex {
case 0:
descriptionWORK.text = "Provide and install "
case 1:
descriptionWORK.text = "Replace "
case 2:
descriptionWORK.text = "Remove and dispose of "
default:
break
}
}
/// END Segmented Controller - Work Type
/// ScrollView Keyboard Adjust
func textFieldDidBeginEditing(_ textField: UITextField) {
if (textField == priceWORK){
scrollviewWORK.setContentOffset(CGPoint(x: 0, y: 205), animated: true)
} else {}
}
func textFieldDidEndEditing(_ textField: UITextField) {
scrollviewWORK.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
/// END Scrollview Keyboard Adjust
/// VIEWDIDLOAD
override func viewDidLoad() {
super.viewDidLoad()
// Toolbar
let toolBar = UIToolbar()
toolBar.sizeToFit()
let backArrow = UIBarButtonItem.init(image: #imageLiteral(resourceName: "backArrow"), style: .plain, target: nil, action: #selector(backArrowClicked))
let spacerA = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let workDoneBtn = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneBtnClicked))
let spacerB = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let nextArrow = UIBarButtonItem.init(image: #imageLiteral(resourceName: "nextArrow"), style: .plain, target: nil, action: #selector(nextArrowClicked))
toolBar.setItems([backArrow, spacerA, workDoneBtn, spacerB, nextArrow], animated: false)
toolBar.setItems([backArrow, spacerA, workDoneBtn, spacerB, nextArrow], animated: false)
locationWORK.inputAccessoryView = toolBar
priceWORK.inputAccessoryView = toolBar
descriptionWORK.inputAccessoryView = toolBar
}
/// END VIEWDIDLOAD
/// Toolbar - Done Button
#objc func doneBtnClicked() {
view.endEditing(true)
}
/// END Toolbar - Done Button
/// Arrow to Next TextField
#objc func nextArrowClicked() {
if (locationWORK.isFirstResponder) {
descriptionWORK.becomeFirstResponder()
} else if (descriptionWORK.isFirstResponder) {
priceWORK.becomeFirstResponder()
} else if (priceWORK.isFirstResponder) {
view.endEditing(true)
scrollviewWORK.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
}
/// END Arrow to Next TextField
/// Arrow to Previous TextField
#objc func backArrowClicked() {
if (locationWORK.isFirstResponder) {
view.endEditing(true)
scrollviewWORK.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
} else if (descriptionWORK.isFirstResponder) {
locationWORK.becomeFirstResponder()
} else if (priceWORK.isFirstResponder) {
descriptionWORK.becomeFirstResponder()
}
}
/// END Arrow to Previous TextField
/// Image Select from Library & Camera
#IBAction func takePhotoONE(_ sender: UIButton) {
let imagePickerController = UIImagePickerController()
imagePickerController.delegate = self
let actionSheet = UIAlertController(title: "Want to add a photo?", message: "Please choose a source.", preferredStyle: .actionSheet)
actionSheet.addAction(UIAlertAction(title: "Camera", style: .default, handler: { (action:UIAlertAction) in
if UIImagePickerController.isSourceTypeAvailable(.camera) {
imagePickerController.sourceType = .camera
self.present(imagePickerController, animated: true, completion: nil)
}else{
print("Camera is not available")
}
}))
actionSheet.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: { (action:UIAlertAction) in imagePickerController.sourceType = .photoLibrary
self.present(imagePickerController, animated: true, completion: nil)
}))
actionSheet.addAction(UIAlertAction(title: "Remove Photo", style: .destructive, handler: { (action:UIAlertAction) in self.photo1WORK.image = nil}))
actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.present(actionSheet, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
let image = info[UIImagePickerControllerOriginalImage] as! UIImage
photo1WORK.image = image
picker.dismiss(animated: true, completion: nil)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)
}
/// END Image Select from Library & Camera
/// GPS Location
let addressManager = CLLocationManager()
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let streetAddress = locations[0]
CLGeocoder().reverseGeocodeLocation(streetAddress) { (placemark, error) in
if error != nil
{
print ("Sorry, there has been an error.")
}
else
{
if let place = placemark?[0]
{
if place.subThoroughfare != nil
{
self.locationWORK.text = "\(place.subThoroughfare!) \(place.thoroughfare!)"
}
}
}
}
}
#IBAction func getGPS(_ sender: UIButton) {
// Address
addressManager.delegate = self
addressManager.desiredAccuracy = kCLLocationAccuracyBest
addressManager.requestWhenInUseAuthorization()
addressManager.startUpdatingLocation()
}
/// END GPS Location
}
TableViewController
import UIKit
class WorkOverview: UIViewController {
#IBOutlet weak var listTableView: UITableView!
#IBAction func addWorkBTN(_ sender: UIButton) {
performSegue(withIdentifier: "overviewToWorkSegue", sender: self)
}
#IBAction func unwindToList(segue:UIStoryboardSegue) { }
#IBAction func finishedBTN(_ sender: UIButton) {
let alertController = UIAlertController(title: "Data Pass Test", message:
workLocation, preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default,handler: nil))
self.present(alertController, animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
property = createArray()
workPJs = workArray()
listTableView.delegate = self
listTableView.dataSource = self
}
var propForm = String()
var propName = String()
var propCity = String()
var propDate = String()
var propDue = String()
var propRep = String()
var workLocation = String()
var workDescription = String()
var workPrice = String()
var workPhoto = UIImage()
var workPJs: [WorkManager] = []
var property: [TaskManager] = []
func workArray() -> [WorkManager] {
var tempWork: [WorkManager] = []
let work1 = WorkManager(location: workLocation, description: workDescription, price: workPrice, photo: workPhoto)
tempWork.append(work1)
return tempWork
}
func createArray() -> [TaskManager] {
var tempProperty: [TaskManager] = []
let prop1 = TaskManager(title: propForm, property: propName, city: propCity, date: propDate, due: propDue, rep: propRep)
tempProperty.append(prop1)
return tempProperty
}
}
extension WorkOverview: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return property.count
} else {
return workPJs.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let prop = property[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "PropertyCell", for: indexPath) as! PropertyCell
cell.setProperty(prop: prop)
return cell
} else if indexPath.section == 1 {
let wrk = workPJs[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "WorkCell", for: indexPath) as! WorkCell
cell.setWork(wrk: wrk)
return cell
}
return UITableViewCell()
}
}
WorkCell class
import UIKit
class WorkCell: UITableViewCell {
#IBOutlet weak var theLocation: UILabel!
#IBOutlet weak var theDescription: UILabel!
#IBOutlet weak var thePrice: UILabel!
#IBOutlet weak var thePhoto: UIImageView!
func setWork(wrk: WorkManager) {
theLocation.text = wrk.location
theDescription.text = wrk.description
thePrice.text = wrk.price
thePhoto.image = wrk.photo
}
}
WorkManager class
import UIKit
class WorkManager {
var location: String
var description: String
var price: String
var photo: UIImage
init(location: String, description: String, price: String, photo: UIImage){
self.location = location
self.description = description
self.price = price
self.photo = photo
}
}
in ViewDidLoad add this code according to names of your cell
tableView.registerNib(UINib(nibName: "cell xib", bundle: nil), forCellReuseIdentifier: "cell name")
in your case you have to register both nib of your custom cell.
i have feelings your section if else condition is wrong. better to debug.
Here, you need to define the second TableViewCell class but in your code only WorkCell class define. so, i hope below link is help you.
UITableview with more than One Custom Cells with Swift
I changed the if statement in cellForRowAt func to a switch case (don't know if it helped or not) but I know adding .reloadData() to my tableview in the viewDidAppear func got it working. Still needs some debugging but that's life.

VC with UISlider to change textfield on VC2

I have a View controller that has a tableView embedded in a ContainerView and just below the ContainerView I have a UISlider. I have code on a view controller for the UISlider and code on another view controller that controls the table view.
Properties of the UISlider are set based on the selected text field - this section of code works. I am struggling to create a function/feature that will change the textField value when the UISlider is move. I think the UISlider Action needs to on the code that controls the UISlider, but I cannot determine how to cast the value of the UISlider.setvalue between the two viewController as the slider is moved to update the textField located in a tableCell. Hopefully makes some sense.
// UISlider ViewController
override func viewDidLoad() {
super.viewDidLoad()
sliderOutlet.isContinuous = true
sliderOutlet.tintColor = UIColor.green
self.refreshSlider()
}
#objc func refreshSlider() {
sliderOutlet.minimumValue = Float(GlobalSliderValues.minimumValue)
sliderOutlet.maximumValue = Float(GlobalSliderValues.maximumValue)
sliderOutlet.value = Float(GlobalSliderValues.sliderValue)
// if let chgIntValue = Int(GlobalSliderValues.changeValue)
// { sliderOutlet.setValue(Float(Double(chgIntValue)), animated: true)
// }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(self.refreshSlider), name: Notification.Name("refreshSlider"), object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self, name: Notification.Name("refreshSlider"), object: nil)
}
TableView Controller
override func viewDidLoad() {
super.viewDidLoad()
mortgageAmount.addTarget(self, action: #selector(chgTextFieldDidChange(textField:)), for: UIControlEvents.editingChanged)
}
#objc func chgTextFieldDidChange(textField: UITextField)
{
if let chgStringValue = mortgageAmount.text
{
if Double(chgStringValue) ?? 1 > 10000 {
let alert = UIAlertController(title: "Input Error", message: "Value cannot be greater than 10000", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
return
}
GlobalSliderValues.minimumValue = 10
GlobalSliderValues.maximumValue = 10000
GlobalSliderValues.sliderValue = Int(mortgageAmount.text!)!
GlobalSliderValues.mortageAmountValue = Float(Int(mortgageAmount.text!)!)
NotificationCenter.default.post(name:Notification.Name("refreshSlider"), object: nil)
if let chgIntValue = Int(chgStringValue)
{ GlobalSliderValues.changeValue.setValue(Float(Double(chgIntValue)), animated: true)
}
}
}
#IBAction func valueChanged(_ sender: UISlider) {
mortgageAmount.text = String(format: "%.2f",Double(sender.value))
}
struct GlobalSliderValues {
static var minimumValue = Int()
static var maximumValue = Int()
static var lowerValue = Int()
static var UpperValue = Int()
static var locationValue = Int()
static var sliderValue = Int()
static var sliderChgValue = ""
}
We don't need notification center to deal with refreshing the slider. Since we are using constants to store value your code can be changed as follows
class ViewController: UIViewController {
#IBOutlet weak var sliderOutlet: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
sliderOutlet.isContinuous = true
sliderOutlet.tintColor = UIColor.green
}
#objc func refreshSlider() {
DispatchQueue.main.async {
self.sliderOutlet.minimumValue = Float(GlobalSliderValues.minimumValue)
self.sliderOutlet.maximumValue = Float(GlobalSliderValues.maximumValue)
self.sliderOutlet.value = Float(GlobalSliderValues.sliderValue)
print(self.sliderOutlet.minimumValue)
print(self.sliderOutlet.maximumValue)
print(self.sliderOutlet.value)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.refreshSlider()
}
}
I entered mortgage amount as so 1098 so slider look like below

Memory Leak in ViewController

So im having somewhat of an issue with my ios app. Im facing a memory leak when I enter my view controller that controls the display of my comments. I am using IGListKit so anyone that is familar with that would be a great help to this question but help is needed none the less. This is my newCommentsViewController that handles pulling the comments from firebase and sends them to the datasource.
import UIKit
import IGListKit
import Firebase
class NewCommentsViewController: UIViewController, UITextFieldDelegate,CommentsSectionDelegate,CommentInputAccessoryViewDelegate {
//array of comments which will be loaded by a service function
var comments = [CommentGrabbed]()
var messagesRef: DatabaseReference?
var bottomConstraint: NSLayoutConstraint?
public let addHeader = "addHeader" as ListDiffable
public var eventKey = ""
//This creates a lazily-initialized variable for the IGListAdapter. The initializer requires three parameters:
//1 updater is an object conforming to IGListUpdatingDelegate, which handles row and section updates. IGListAdapterUpdater is a default implementation that is suitable for your usage.
//2 viewController is a UIViewController that houses the adapter. This view controller is later used for navigating to other view controllers.
//3 workingRangeSize is the size of the working range, which allows you to prepare content for sections just outside of the visible frame.
lazy var adapter: ListAdapter = {
return ListAdapter(updater: ListAdapterUpdater(), viewController: self)
}()
// 1 IGListKit uses IGListCollectionView, which is a subclass of UICollectionView, which patches some functionality and prevents others.
let collectionView: UICollectionView = {
// 2 This starts with a zero-sized rect since the view isn’t created yet. It uses the UICollectionViewFlowLayout just as the ClassicFeedViewController did.
let view = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout())
// 3 The background color is set to white
view.backgroundColor = UIColor.white
return view
}()
//will fetch the comments from the database and append them to an array
fileprivate func fetchComments(){
comments.removeAll()
messagesRef = Database.database().reference().child("Comments").child(eventKey)
// print(eventKey)
// print(comments.count)
let query = messagesRef?.queryOrderedByKey()
query?.observe(.value, with: { (snapshot) in
guard let allObjects = snapshot.children.allObjects as? [DataSnapshot] else {
return
}
// print(snapshot)
allObjects.forEach({ (snapshot) in
guard let commentDictionary = snapshot.value as? [String: Any] else{
return
}
guard let uid = commentDictionary["uid"] as? String else{
return
}
UserService.show(forUID: uid, completion: { (user) in
if let user = user {
let commentFetched = CommentGrabbed(user: user, dictionary: commentDictionary)
commentFetched.commentID = snapshot.key
let filteredArr = self.comments.filter { (comment) -> Bool in
return comment.commentID == commentFetched.commentID
}
if filteredArr.count == 0 {
self.comments.append(commentFetched)
}
self.adapter.performUpdates(animated: true)
}else{
print("user is null")
}
self.comments.sort(by: { (comment1, comment2) -> Bool in
return comment1.creationDate.compare(comment2.creationDate) == .orderedAscending
})
self.comments.forEach({ (comments) in
})
})
})
}, withCancel: { (error) in
print("Failed to observe comments")
})
//first lets fetch comments for current event
}
//allows you to gain access to the input accessory view that each view controller has for inputting text
lazy var containerView: CommentInputAccessoryView = {
let frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 50)
let commentInputAccessoryView = CommentInputAccessoryView(frame:frame)
commentInputAccessoryView.delegate = self
return commentInputAccessoryView
}()
#objc func handleSubmit(for comment: String?){
guard let comment = comment, comment.count > 0 else{
return
}
let userText = Comments(content: comment, uid: User.current.uid, profilePic: User.current.profilePic!,eventKey: eventKey)
sendMessage(userText)
// will clear the comment text field
self.containerView.clearCommentTextField()
}
#objc func handleKeyboardNotification(notification: NSNotification){
if let userinfo = notification.userInfo {
if let keyboardFrame = (userinfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue{
self.bottomConstraint?.constant = -(keyboardFrame.height)
let isKeyboardShowing = notification.name == NSNotification.Name.UIKeyboardWillShow
self.bottomConstraint?.constant = isKeyboardShowing ? -(keyboardFrame.height) : 0
if isKeyboardShowing{
let contentInset = UIEdgeInsetsMake(0, 0, (keyboardFrame.height), 0)
collectionView.contentInset = UIEdgeInsetsMake(0, 0, (keyboardFrame.height), 0)
collectionView.scrollIndicatorInsets = contentInset
}else {
let contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
collectionView.scrollIndicatorInsets = contentInset
}
UIView.animate(withDuration: 0, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: { (completion) in
if self.comments.count > 0 && isKeyboardShowing {
let item = self.collectionView.numberOfItems(inSection: self.collectionView.numberOfSections - 1)-1
let lastItemIndex = IndexPath(item: item, section: self.collectionView.numberOfSections - 1)
self.collectionView.scrollToItem(at: lastItemIndex, at: UICollectionViewScrollPosition.top, animated: true)
}
})
}
}
}
override var inputAccessoryView: UIView? {
get {
return containerView
}
}
override var canBecomeFirstResponder: Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.frame = CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height-40)
view.addSubview(collectionView)
collectionView.alwaysBounceVertical = true
adapter.collectionView = collectionView
adapter.dataSource = self
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
collectionView.register(CommentCell.self, forCellWithReuseIdentifier: "CommentCell")
// collectionView.register(CommentHeader.self, forCellWithReuseIdentifier: "HeaderCell")
collectionView.keyboardDismissMode = .onDrag
navigationItem.title = "Comments"
self.navigationItem.hidesBackButton = true
let backButton = UIBarButtonItem(image: UIImage(named: "icons8-Back-64"), style: .plain, target: self, action: #selector(GoBack))
self.navigationItem.leftBarButtonItem = backButton
}
#objc func GoBack(){
print("BACK TAPPED")
self.dismiss(animated: true, completion: nil)
}
//look here
func CommentSectionUpdared(sectionController: CommentsSectionController){
print("like")
self.fetchComments()
self.adapter.performUpdates(animated: true)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchComments()
tabBarController?.tabBar.isHidden = true
//submitButton.isUserInteractionEnabled = true
}
//viewDidLayoutSubviews() is overridden, setting the collectionView frame to match the view bounds.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// collectionView.frame = view.bounds
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension NewCommentsViewController: ListAdapterDataSource {
// 1 objects(for:) returns an array of data objects that should show up in the collection view. loader.entries is provided here as it contains the journal entries.
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
let items:[ListDiffable] = comments
//print("comments = \(comments)")
return items
}
// 2 For each data object, listAdapter(_:sectionControllerFor:) must return a new instance of a section controller. For now you’re returning a plain IGListSectionController to appease the compiler — in a moment, you’ll modify this to return a custom journal section controller.
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
//the comment section controller will be placed here but we don't have it yet so this will be a placeholder
// if let object = object as? ListDiffable, object === addHeader {
// return CommentsHeaderSectionController()
// }
let sectionController = CommentsSectionController()
sectionController.delegate = self
return sectionController
}
// 3 emptyView(for:) returns a view that should be displayed when the list is empty. NASA is in a bit of a time crunch, so they didn’t budget for this feature.
func emptyView(for listAdapter: ListAdapter) -> UIView? {
let view = UIView()
view.backgroundColor = UIColor.white
return view
}
}
extension NewCommentsViewController {
func sendMessage(_ message: Comments) {
ChatService.sendMessage(message, eventKey: eventKey)
}
}
I did some debugging using both instruments and the debugger graph tool and it seems to be pointing me to my commentsSectionController
import UIKit
import IGListKit
import Foundation
import Firebase
protocol CommentsSectionDelegate: class {
func CommentSectionUpdared(sectionController: CommentsSectionController)
}
class CommentsSectionController: ListSectionController,CommentCellDelegate {
weak var delegate: CommentsSectionDelegate? = nil
var comment: CommentGrabbed?
let userProfileController = ProfileeViewController(collectionViewLayout: UICollectionViewFlowLayout())
var eventKey: String?
var dummyCell: CommentCell?
override init() {
super.init()
// supplementaryViewSource = self
//sets the spacing between items in a specfic section controller
inset = UIEdgeInsets(top: 5, left: 0, bottom: 0, right: 0)
}
// MARK: IGListSectionController Overrides
override func numberOfItems() -> Int {
return 1
}
specifically this function here
override func sizeForItem(at index: Int) -> CGSize {
let frame = CGRect(x: 0, y: 0, width: collectionContext!.containerSize.width, height: 50)
dummyCell = CommentCell(frame: frame)
dummyCell?.comment = comment
dummyCell?.layoutIfNeeded()
let targetSize = CGSize(width: collectionContext!.containerSize.width, height: 55)
let estimatedSize = dummyCell?.systemLayoutSizeFitting(targetSize)
let height = max(40+8+8, (estimatedSize?.height)!)
return CGSize(width: collectionContext!.containerSize.width, height: height)
}
override var minimumLineSpacing: CGFloat {
get {
return 0.0
}
set {
self.minimumLineSpacing = 0.0
}
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
guard let cell = collectionContext?.dequeueReusableCell(of: CommentCell.self, for: self, at: index) as? CommentCell else {
fatalError()
}
// print(comment)
cell.comment = comment
cell.delegate = self
return cell
}
override func didUpdate(to object: Any) {
comment = object as? CommentGrabbed
}
override func didSelectItem(at index: Int){
}
func optionsButtonTapped(cell: CommentCell){
print("like")
let comment = self.comment
_ = comment?.uid
// 3
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
// 4
if comment?.uid != User.current.uid {
let flagAction = UIAlertAction(title: "Report as Inappropriate", style: .default) { _ in
ChatService.flag(comment!)
let okAlert = UIAlertController(title: nil, message: "The post has been flagged.", preferredStyle: .alert)
okAlert.addAction(UIAlertAction(title: "Ok", style: .default))
self.viewController?.present(okAlert, animated: true, completion: nil)
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
let replyAction = UIAlertAction(title: "Reply to Comment", style: .default, handler: { (_) in
//do something here later to facilitate reply comment functionality
print("Attempting to reply to user \(comment?.user.username) comment")
})
alertController.addAction(replyAction)
alertController.addAction(cancelAction)
alertController.addAction(flagAction)
}else{
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
let deleteAction = UIAlertAction(title: "Delete Comment", style: .default, handler: { _ in
ChatService.deleteComment(comment!, (comment?.eventKey)!)
let okAlert = UIAlertController(title: nil, message: "Comment Has Been Deleted", preferredStyle: .alert)
okAlert.addAction(UIAlertAction(title: "Ok", style: .default))
self.viewController?.present(okAlert, animated: true, completion: nil)
self.onItemDeleted()
})
alertController.addAction(cancelAction)
alertController.addAction(deleteAction)
}
self.viewController?.present(alertController, animated: true, completion: nil)
}
func onItemDeleted() {
delegate?.CommentSectionUpdared(sectionController: self)
}
func handleProfileTransition(tapGesture: UITapGestureRecognizer){
userProfileController.user = comment?.user
if Auth.auth().currentUser?.uid != comment?.uid{
self.viewController?.present(userProfileController, animated: true, completion: nil)
}else{
//do nothing
}
}
deinit {
print("CommentSectionController class removed from memory")
}
}
Here is a screenshot is what I saw in the debugger graph tool even when i leave the screen and check the debugger tool those blocks are still there.
So my question is does anyone see anything I don't see with the function. I really want to fix this memory leak. In addition to that this memory leak doesn't seem to be evident when I use my phone but when I use my simulator it is a huge memory leak....Any insight is greatly appreciated
Couple of things:
DatabaseReference's query observer that is owned by your ViewController is capturing self (your view controller). This makes a circle of ownerships which never let each other to deinit. This is called retain cycle.
Add [unowned self] or [weak self] to your completion block like so:
query?.observe(.value, with: { [weak self] (snapshot) in
// your code...
}
I used weak here because query is optional.
Remove notification observers when you no longer need them by calling NotificationCenter.default.removeObserver(self).
Shameless plug of my library: Consider Typist for managing keyboard in UIKit, it avoids any interaction with NotificationCenter and is super easy to setup and use.
Remove your notification Observer in viewWillDisappear method
override func viewWillDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
You should use weak or unowned reference in closure, Like below
query?.observe(.value, with: { [unowned self] (snapshot) in
}
I believe you should rewrite this closure with [unowned self] (or weak):
UserService.show(forUID: uid, completion: { [unowned self] user in }
And as #Dhiru mentioned, you should remove all notification observers.

Execute action when back bar button of UINavigationController is pressed

I need to execute an action (emptying an array), when the back button of a UINavigationController is pressed, while the button still causes the previous ViewController on the stack to appear. How could I accomplish this using swift?
Replacing the button to a custom one as suggested on another answer is possibly not a great idea as you will lose the default behavior and style.
One other option you have is to implement the viewWillDisappear method on the View Controller and check for a property named isMovingFromParentViewController. If that property is true, it means the View Controller is disappearing because it's being removed (popped).
Should look something like:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.isMovingFromParentViewController {
// Your code...
}
}
In swift 4.2
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.isMovingFromParent {
// Your code...
}
}
One option would be implementing your own custom back button. You would need to add the following code to your viewDidLoad method:
- (void) viewDidLoad {
[super viewDidLoad];
self.navigationItem.hidesBackButton = YES;
UIBarButtonItem *newBackButton = [[UIBarButtonItem alloc] initWithTitle:#"Back" style:UIBarButtonItemStyleBordered target:self action:#selector(back:)];
self.navigationItem.leftBarButtonItem = newBackButton;
}
- (void) back:(UIBarButtonItem *)sender {
// Perform your custom actions
// ...
// Go back to the previous ViewController
[self.navigationController popViewControllerAnimated:YES];
}
UPDATE:
Here is the version for Swift:
override func viewDidLoad {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true
let newBackButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Bordered, target: self, action: "back:")
self.navigationItem.leftBarButtonItem = newBackButton
}
#objc func back(sender: UIBarButtonItem) {
// Perform your custom actions
// ...
// Go back to the previous ViewController
self.navigationController?.popViewControllerAnimated(true)
}
UPDATE 2:
Here is the version for Swift 3:
override func viewDidLoad {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true
let newBackButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.plain, target: self, action: #selector(YourViewController.back(sender:)))
self.navigationItem.leftBarButtonItem = newBackButton
}
#objc func back(sender: UIBarButtonItem) {
// Perform your custom actions
// ...
// Go back to the previous ViewController
_ = navigationController?.popViewController(animated: true)
}
override func willMove(toParent parent: UIViewController?)
{
super.willMove(toParent: parent)
if parent == nil
{
print("This VC is 'will' be popped. i.e. the back button was pressed.")
}
}
I was able to achieve this with the following :
Swift 3
override func didMoveToParentViewController(parent: UIViewController?) {
super.didMoveToParentViewController(parent)
if parent == nil {
println("Back Button pressed.")
delegate?.goingBack()
}
}
Swift 4
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if parent == nil {
debugPrint("Back Button pressed.")
}
}
No need of custom back button.
If you want to have back button with back arrow you can use an image and code below
backArrow.png backArrow#2x.png backArrow#3x.png
override func viewDidLoad() {
super.viewDidLoad()
let customBackButton = UIBarButtonItem(image: UIImage(named: "backArrow") , style: .plain, target: self, action: #selector(backAction(sender:)))
customBackButton.imageInsets = UIEdgeInsets(top: 2, left: -8, bottom: 0, right: 0)
navigationItem.leftBarButtonItem = customBackButton
}
func backAction(sender: UIBarButtonItem) {
// custom actions here
navigationController?.popViewController(animated: true)
}
I created this (swift) class to create a back button exactly like the regular one, including back arrow. It can create a button with regular text or with an image.
Usage
weak var weakSelf = self
// Assign back button with back arrow and text (exactly like default back button)
navigationItem.leftBarButtonItems = CustomBackButton.createWithText("YourBackButtonTitle", color: UIColor.yourColor(), target: weakSelf, action: #selector(YourViewController.tappedBackButton))
// Assign back button with back arrow and image
navigationItem.leftBarButtonItems = CustomBackButton.createWithImage(UIImage(named: "yourImageName")!, color: UIColor.yourColor(), target: weakSelf, action: #selector(YourViewController.tappedBackButton))
func tappedBackButton() {
// Do your thing
self.navigationController!.popViewControllerAnimated(true)
}
CustomBackButtonClass
(code for drawing the back arrow created with Sketch & Paintcode plugin)
class CustomBackButton: NSObject {
class func createWithText(text: String, color: UIColor, target: AnyObject?, action: Selector) -> [UIBarButtonItem] {
let negativeSpacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FixedSpace, target: nil, action: nil)
negativeSpacer.width = -8
let backArrowImage = imageOfBackArrow(color: color)
let backArrowButton = UIBarButtonItem(image: backArrowImage, style: UIBarButtonItemStyle.Plain, target: target, action: action)
let backTextButton = UIBarButtonItem(title: text, style: UIBarButtonItemStyle.Plain , target: target, action: action)
backTextButton.setTitlePositionAdjustment(UIOffset(horizontal: -12.0, vertical: 0.0), forBarMetrics: UIBarMetrics.Default)
return [negativeSpacer, backArrowButton, backTextButton]
}
class func createWithImage(image: UIImage, color: UIColor, target: AnyObject?, action: Selector) -> [UIBarButtonItem] {
// recommended maximum image height 22 points (i.e. 22 #1x, 44 #2x, 66 #3x)
let negativeSpacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FixedSpace, target: nil, action: nil)
negativeSpacer.width = -8
let backArrowImageView = UIImageView(image: imageOfBackArrow(color: color))
let backImageView = UIImageView(image: image)
let customBarButton = UIButton(frame: CGRectMake(0,0,22 + backImageView.frame.width,22))
backImageView.frame = CGRectMake(22, 0, backImageView.frame.width, backImageView.frame.height)
customBarButton.addSubview(backArrowImageView)
customBarButton.addSubview(backImageView)
customBarButton.addTarget(target, action: action, forControlEvents: .TouchUpInside)
return [negativeSpacer, UIBarButtonItem(customView: customBarButton)]
}
private class func drawBackArrow(frame frame: CGRect = CGRect(x: 0, y: 0, width: 14, height: 22), color: UIColor = UIColor(hue: 0.59, saturation: 0.674, brightness: 0.886, alpha: 1), resizing: ResizingBehavior = .AspectFit) {
/// General Declarations
let context = UIGraphicsGetCurrentContext()!
/// Resize To Frame
CGContextSaveGState(context)
let resizedFrame = resizing.apply(rect: CGRect(x: 0, y: 0, width: 14, height: 22), target: frame)
CGContextTranslateCTM(context, resizedFrame.minX, resizedFrame.minY)
let resizedScale = CGSize(width: resizedFrame.width / 14, height: resizedFrame.height / 22)
CGContextScaleCTM(context, resizedScale.width, resizedScale.height)
/// Line
let line = UIBezierPath()
line.moveToPoint(CGPoint(x: 9, y: 9))
line.addLineToPoint(CGPoint.zero)
CGContextSaveGState(context)
CGContextTranslateCTM(context, 3, 11)
line.lineCapStyle = .Square
line.lineWidth = 3
color.setStroke()
line.stroke()
CGContextRestoreGState(context)
/// Line Copy
let lineCopy = UIBezierPath()
lineCopy.moveToPoint(CGPoint(x: 9, y: 0))
lineCopy.addLineToPoint(CGPoint(x: 0, y: 9))
CGContextSaveGState(context)
CGContextTranslateCTM(context, 3, 2)
lineCopy.lineCapStyle = .Square
lineCopy.lineWidth = 3
color.setStroke()
lineCopy.stroke()
CGContextRestoreGState(context)
CGContextRestoreGState(context)
}
private class func imageOfBackArrow(size size: CGSize = CGSize(width: 14, height: 22), color: UIColor = UIColor(hue: 0.59, saturation: 0.674, brightness: 0.886, alpha: 1), resizing: ResizingBehavior = .AspectFit) -> UIImage {
var image: UIImage
UIGraphicsBeginImageContextWithOptions(size, false, 0)
drawBackArrow(frame: CGRect(origin: CGPoint.zero, size: size), color: color, resizing: resizing)
image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
private enum ResizingBehavior {
case AspectFit /// The content is proportionally resized to fit into the target rectangle.
case AspectFill /// The content is proportionally resized to completely fill the target rectangle.
case Stretch /// The content is stretched to match the entire target rectangle.
case Center /// The content is centered in the target rectangle, but it is NOT resized.
func apply(rect rect: CGRect, target: CGRect) -> CGRect {
if rect == target || target == CGRect.zero {
return rect
}
var scales = CGSize.zero
scales.width = abs(target.width / rect.width)
scales.height = abs(target.height / rect.height)
switch self {
case .AspectFit:
scales.width = min(scales.width, scales.height)
scales.height = scales.width
case .AspectFill:
scales.width = max(scales.width, scales.height)
scales.height = scales.width
case .Stretch:
break
case .Center:
scales.width = 1
scales.height = 1
}
var result = rect.standardized
result.size.width *= scales.width
result.size.height *= scales.height
result.origin.x = target.minX + (target.width - result.width) / 2
result.origin.y = target.minY + (target.height - result.height) / 2
return result
}
}
}
SWIFT 3.0
class CustomBackButton: NSObject {
class func createWithText(text: String, color: UIColor, target: AnyObject?, action: Selector) -> [UIBarButtonItem] {
let negativeSpacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.fixedSpace, target: nil, action: nil)
negativeSpacer.width = -8
let backArrowImage = imageOfBackArrow(color: color)
let backArrowButton = UIBarButtonItem(image: backArrowImage, style: UIBarButtonItemStyle.plain, target: target, action: action)
let backTextButton = UIBarButtonItem(title: text, style: UIBarButtonItemStyle.plain , target: target, action: action)
backTextButton.setTitlePositionAdjustment(UIOffset(horizontal: -12.0, vertical: 0.0), for: UIBarMetrics.default)
return [negativeSpacer, backArrowButton, backTextButton]
}
class func createWithImage(image: UIImage, color: UIColor, target: AnyObject?, action: Selector) -> [UIBarButtonItem] {
// recommended maximum image height 22 points (i.e. 22 #1x, 44 #2x, 66 #3x)
let negativeSpacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.fixedSpace, target: nil, action: nil)
negativeSpacer.width = -8
let backArrowImageView = UIImageView(image: imageOfBackArrow(color: color))
let backImageView = UIImageView(image: image)
let customBarButton = UIButton(frame: CGRect(x: 0, y: 0, width: 22 + backImageView.frame.width, height: 22))
backImageView.frame = CGRect(x: 22, y: 0, width: backImageView.frame.width, height: backImageView.frame.height)
customBarButton.addSubview(backArrowImageView)
customBarButton.addSubview(backImageView)
customBarButton.addTarget(target, action: action, for: .touchUpInside)
return [negativeSpacer, UIBarButtonItem(customView: customBarButton)]
}
private class func drawBackArrow(_ frame: CGRect = CGRect(x: 0, y: 0, width: 14, height: 22), color: UIColor = UIColor(hue: 0.59, saturation: 0.674, brightness: 0.886, alpha: 1), resizing: ResizingBehavior = .AspectFit) {
/// General Declarations
let context = UIGraphicsGetCurrentContext()!
/// Resize To Frame
context.saveGState()
let resizedFrame = resizing.apply(CGRect(x: 0, y: 0, width: 14, height: 22), target: frame)
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
let resizedScale = CGSize(width: resizedFrame.width / 14, height: resizedFrame.height / 22)
context.scaleBy(x: resizedScale.width, y: resizedScale.height)
/// Line
let line = UIBezierPath()
line.move(to: CGPoint(x: 9, y: 9))
line.addLine(to: CGPoint.zero)
context.saveGState()
context.translateBy(x: 3, y: 11)
line.lineCapStyle = .square
line.lineWidth = 3
color.setStroke()
line.stroke()
context.restoreGState()
/// Line Copy
let lineCopy = UIBezierPath()
lineCopy.move(to: CGPoint(x: 9, y: 0))
lineCopy.addLine(to: CGPoint(x: 0, y: 9))
context.saveGState()
context.translateBy(x: 3, y: 2)
lineCopy.lineCapStyle = .square
lineCopy.lineWidth = 3
color.setStroke()
lineCopy.stroke()
context.restoreGState()
context.restoreGState()
}
private class func imageOfBackArrow(_ size: CGSize = CGSize(width: 14, height: 22), color: UIColor = UIColor(hue: 0.59, saturation: 0.674, brightness: 0.886, alpha: 1), resizing: ResizingBehavior = .AspectFit) -> UIImage {
var image: UIImage
UIGraphicsBeginImageContextWithOptions(size, false, 0)
drawBackArrow(CGRect(origin: CGPoint.zero, size: size), color: color, resizing: resizing)
image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
private enum ResizingBehavior {
case AspectFit /// The content is proportionally resized to fit into the target rectangle.
case AspectFill /// The content is proportionally resized to completely fill the target rectangle.
case Stretch /// The content is stretched to match the entire target rectangle.
case Center /// The content is centered in the target rectangle, but it is NOT resized.
func apply(_ rect: CGRect, target: CGRect) -> CGRect {
if rect == target || target == CGRect.zero {
return rect
}
var scales = CGSize.zero
scales.width = abs(target.width / rect.width)
scales.height = abs(target.height / rect.height)
switch self {
case .AspectFit:
scales.width = min(scales.width, scales.height)
scales.height = scales.width
case .AspectFill:
scales.width = max(scales.width, scales.height)
scales.height = scales.width
case .Stretch:
break
case .Center:
scales.width = 1
scales.height = 1
}
var result = rect.standardized
result.size.width *= scales.width
result.size.height *= scales.height
result.origin.x = target.minX + (target.width - result.width) / 2
result.origin.y = target.minY + (target.height - result.height) / 2
return result
}
}
}
In Swift 5 and Xcode 10.2
Please don't add custom bar button item, use this default behaviour.
No need of viewWillDisappear, no need of custom BarButtonItem etc...
It's better to detect when the VC is removed from it's parent.
Use any one of these two functions
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
if parent == nil {
callStatusDelegate?.backButtonClicked()//Here write your code
}
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if parent == nil {
callStatusDelegate?.backButtonClicked()//Here write your code
}
}
If you want stop default behaviour of back button then add custom BarButtonItem.
If you are using navigationController then add the UINavigationControllerDelegate protocol to class and add the delegate method as follows:
class ViewController:UINavigationControllerDelegate {
func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController,
animated: Bool) {
if viewController === self {
// do here what you want
}
}
}
This method is called whenever the navigation controller will slide to a new screen. If the back button was pressed, the new view controller is ViewController itself.
You can subclass UINavigationController and override popViewController(animated: Bool). Beside being able to execute some code there you can also prevent the user from going back altogether, for instance to prompt to save or discard his current work.
Sample implementation where you can set a popHandler that gets set/cleared by pushed controllers.
class NavigationController: UINavigationController
{
var popHandler: (() -> Bool)?
override func popViewController(animated: Bool) -> UIViewController?
{
guard self.popHandler?() != false else
{
return nil
}
self.popHandler = nil
return super.popViewController(animated: animated)
}
}
And sample usage from a pushed controller that tracks unsaved work.
let hasUnsavedWork: Bool = // ...
(self.navigationController as! NavigationController).popHandler = hasUnsavedWork ?
{
// Prompt saving work here with an alert
return false // Prevent pop until as user choses to save or discard
} : nil // No unsaved work, we clear popHandler to let it pop normally
As a nice touch, this will also get called by interactivePopGestureRecognizer when the user tries to go back using a swipe gesture.
NO
override func willMove(toParentViewController parent: UIViewController?) { }
This will get called even if you are segueing to the view controller in which you are overriding this method. In which check if the "parent" is nil of not is not a precise way to be sure of moving back to the correct UIViewController. To determine exactly if the UINavigationController is properly navigating back to the UIViewController that presented this current one, you will need to conform to the UINavigationControllerDelegate protocol.
YES
note: MyViewController is just the name of whatever UIViewController you want to detect going back from.
1) At the top of your file add UINavigationControllerDelegate.
class MyViewController: UIViewController, UINavigationControllerDelegate {
2) Add a property to your class that will keep track of the UIViewController that you are segueing from.
class MyViewController: UIViewController, UINavigationControllerDelegate {
var previousViewController:UIViewController
3) in MyViewController's viewDidLoad method assign self as the delegate for your UINavigationController.
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.delegate = self
}
3) Before you segue, assign the previous UIViewController as this property.
// In previous UIViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "YourSegueID" {
if let nextViewController = segue.destination as? MyViewController {
nextViewController.previousViewController = self
}
}
}
4) And conform to one method in MyViewController of the UINavigationControllerDelegate
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if viewController == self.previousViewController {
// You are going back
}
}
In my case the viewWillDisappear worked best. But in some cases one has to modify the previous view controller. So here is my solution with access to the previous view controller and it works in Swift 4:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParentViewController {
if let viewControllers = self.navigationController?.viewControllers {
if (viewControllers.count >= 1) {
let previousViewController = viewControllers[viewControllers.count-1] as! NameOfDestinationViewController
// whatever you want to do
previousViewController.callOrModifySomething()
}
}
}
}
Before leave current controller I need to show alert. So I did it this way:
Add extention to UINavigationController with UINavigationBarDelegate
Add selector to your controller navigationShouldPopOnBack(completion:)
It's worked)
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
if let items = navigationBar.items, viewControllers.count < items.count {
return true
}
let clientInfoVC = topViewController as? ClientInfoVC
if clientInfoVC?.responds(to: #selector(clientInfoVC?.navigationShouldPopOnBack)) ?? false {
clientInfoVC?.navigationShouldPopOnBack(completion: { isAllowPop in
if isAllowPop {
DispatchQueue.main.async {
self.popViewController(animated: true)
}
}
})
}
DispatchQueue.main.async {
self.popViewController(animated: true)
}
return false
}
}
#objc func navigationShouldPopOnBack(completion: #escaping (Bool) -> ()) {
let ok = UIAlertAction(title: R.string.alert.actionOk(), style: .default) { _ in
completion(true)
}
let cancel = UIAlertAction(title: R.string.alert.actionCancel(), style: .cancel) { _ in
completion(false)
}
let alertController = UIAlertController(title: "", message: R.string.alert.contractMessage(), preferredStyle: .alert)
alertController.addAction(ok)
alertController.addAction(cancel)
present(alertController, animated: true, completion: nil)
}
When back button is pressed, ignore interactive pop with screen edge gesture.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isMovingFromParent, transitionCoordinator?.isInteractive == false {
// code here
}
}
It's not difficult as we thing. Just create a frame for UIButton with clear background color, assign action for the button and place over the navigationbar back button. And finally remove the button after use.
Here is the Swift 3
sample code done with UIImage instead of UIButton
override func viewDidLoad() {
super.viewDidLoad()
let imageView = UIImageView()
imageView.backgroundColor = UIColor.clear
imageView.frame = CGRect(x:0,y:0,width:2*(self.navigationController?.navigationBar.bounds.height)!,height:(self.navigationController?.navigationBar.bounds.height)!)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(back(sender:)))
imageView.isUserInteractionEnabled = true
imageView.addGestureRecognizer(tapGestureRecognizer)
imageView.tag = 1
self.navigationController?.navigationBar.addSubview(imageView)
}
write the code need to be executed
func back(sender: UIBarButtonItem) {
// Perform your custom actions}
_ = self.navigationController?.popViewController(animated: true)
}
Remove the subView after action is performed
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
for view in (self.navigationController?.navigationBar.subviews)!{
if view.tag == 1 {
view.removeFromSuperview()
}
}
This is my solution
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
if let shouldBlock = self.topViewController?.shouldPopFromNavigation() {
return shouldBlock
}
return true
}
}
extension UIViewController {
#objc func shouldPopFromNavigation() -> Bool {
return true
}
}
In your view controller, you can handle like this:
#objc override func shouldPopFromNavigation() -> Bool {
// Your dialog, example UIAlertViewController or whatever you want
return false
}
Swift 4.2:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.isMovingFromParent {
// Your code...
}
}
For Swift 5, we can check it in view will disappear
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.isMovingFromParent {
delegate?.passValue(clickedImage: selectedImage)
}
}
Swift 3:
override func didMove(toParentViewController parent: UIViewController?) {
super.didMove(toParentViewController: parent)
if parent == nil{
print("Back button was clicked")
}
}
just do control + drag the bar item to below func. work like charm
#IBAction func done(sender: AnyObject) {
if((self.presentingViewController) != nil){
self.dismiss(animated: false, completion: nil)
print("done")
}
}
Swift 5 __ Xcode 11.5
In my case I wanted to make an animation, and when it finished, go back.
A way to overwrite the default action of the back button
and call your custom action is this:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setBtnBack()
}
private func setBtnBack() {
for vw in navigationController?.navigationBar.subviews ?? [] where "\(vw.classForCoder)" == "_UINavigationBarContentView" {
print("\(vw.classForCoder)")
for subVw in vw.subviews where "\(subVw.classForCoder)" == "_UIButtonBarButton" {
let ctrl = subVw as! UIControl
ctrl.removeTarget(ctrl.allTargets.first, action: nil, for: .allEvents)
ctrl.addTarget(self, action: #selector(backBarBtnAction), for: .touchUpInside)
}
}
}
#objc func backBarBtnAction() {
doSomethingBeforeBack { [weak self](isEndedOk) in
if isEndedOk {
self?.navigationController?.popViewController(animated: true)
}
}
}
private func doSomethingBeforeBack(completion: #escaping (_ isEndedOk:Bool)->Void ) {
UIView.animate(withDuration: 0.25, animations: { [weak self] in
self?.vwTxt.alpha = 0
}) { (isEnded) in
completion(isEnded)
}
}
Or you can use this method one time to explore the NavigationBar view hierarchy, and get the indexes to access to the _UIButtonBarButton view, cast to UIControl, remove the target-action, and add your custom targets-actions:
private func debug_printSubviews(arrSubviews:[UIView]?, level:Int) {
for (i,subVw) in (arrSubviews ?? []).enumerated() {
var str = ""
for _ in 0...level {
str += "\t"
}
str += String(format: "%2d %#",i, "\(subVw.classForCoder)")
print(str)
debug_printSubviews(arrSubviews: subVw.subviews, level: level + 1)
}
}
// Set directly the indexs
private func setBtnBack_method2() {
// Remove or comment the print lines
debug_printSubviews(arrSubviews: navigationController?.navigationBar.subviews, level: 0)
let ctrl = navigationController?.navigationBar.subviews[1].subviews[0] as! UIControl
print("ctrl.allTargets: \(ctrl.allTargets)")
ctrl.removeTarget(ctrl.allTargets.first, action: nil, for: .allEvents)
print("ctrl.allTargets: \(ctrl.allTargets)")
ctrl.addTarget(self, action: #selector(backBarBtnAction), for: .touchUpInside)
print("ctrl.allTargets: \(ctrl.allTargets)")
}
override public func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.topItem?.title = GlobalVariables.selectedMainIconName
let image = UIImage(named: "back-btn")
image = image?.imageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal)
self.navigationItem.leftBarButtonItem = UIBarButtonItem(image: image, style: UIBarButtonItemStyle.Plain, target: self, action: #selector(Current[enter image description here][1]ViewController.back) )
}
func back() {
self.navigationController?.popToViewController( self.navigationController!.viewControllers[ self.navigationController!.viewControllers.count - 2 ], animated: true)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.isMovingToParent {
//your code backView
}
}
Try this .
self.navigationItem.leftBarButtonItem?.target = "methodname"
func methodname ( ) {
// enter code here
}
Try on this too.
override func viewWillAppear(animated: Bool) {
//empty your array
}
As I understand you want to empty your array as you press your back button and pop to your previous ViewController let your Array which you loaded on this screen is
let settingArray = NSMutableArray()
#IBAction func Back(sender: AnyObject) {
self. settingArray.removeAllObjects()
self.dismissViewControllerAnimated(true, completion: nil)
}
Here is the simplest possible Swift 5 solution that doesn't require you to create a custom back button and give up all that UINavigationController left button functionality you get for free.
As Brandon A recommends above, you need need to implement UINavigationControllerDelegate in the view controller you want to interact with before returning to it. A good way is to create an unwind segue that you can perform manually or automatically and reuse the same code from a custom done button or the back button.
First, make your view controller of interest (the one you want to detect returning to) a delegate of the navigation controller in its viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}
Second, add an extension at the bottom of the file that overrides navigationController(willShow:animated:)
extension PickerTableViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController,
willShow viewController: UIViewController,
animated: Bool) {
if let _ = viewController as? EditComicBookViewController {
let selectedItemRow = itemList.firstIndex(of: selectedItemName)
selectedItemIndex = IndexPath(row: selectedItemRow!, section: 0)
if let selectedCell = tableView.cellForRow(at: selectedItemIndex) {
performSegue(withIdentifier: "PickedItem", sender: selectedCell)
}
}
}
}
Since your question included a UITableViewController, I included a way to get the index path of the row the user tapped.
I accomplished this by calling/overriding viewWillDisappear and then accessing the stack of the navigationController like this:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
let stack = self.navigationController?.viewControllers.count
if stack >= 2 {
// for whatever reason, the last item on the stack is the TaskBuilderViewController (not self), so we only use -1 to access it
if let lastitem = self.navigationController?.viewControllers[stack! - 1] as? theViewControllerYoureTryingToAccess {
// hand over the data via public property or call a public method of theViewControllerYoureTryingToAccess, like
lastitem.emptyArray()
lastitem.value = 5
}
}
}
You can do something in your Viewcontroller like
override func navigationShouldPopOnBackButton() -> Bool {
self.backAction() //Your action you want to perform.
return true
}
For complete answer use
Detecting when the 'back' button is pressed on a navbar
My preference was to override the popViewController in the Navigation Controller. The advantages of this is:
Your app keeps the default Back Button look and animations, and you don't have to manage it. This is particularly helpful if a user has Large Text set on their phone, since the default back button will increase or decrease in size based on the user settings.
You can stop the view from popping altogether, unlike using viewWillDisappear.
First, create a custom Navigation Controller class (and be sure to assign it to the Navigation Controller in your Story Board or wherever your navigation controller is created):
class NavControllerWithBackButtonOverride: UINavigationController {
var backButtonOverride: (() -> Void)? = nil
override func popViewController(animated: Bool) -> UIViewController? {
if backButtonOverride != nil {
//if anything is assigned to the backButtonOverride the override will run
self.backButtonOverride!()
return nil
} else {
//otherwise the default popViewController will run
return super.popViewController(animated: animated)
}
}
}
Then enable/disable the override in your View Controller by assigning a value to the backButtonOverride variable:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.enableCustomBackButton()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.disableCustomBackButton()
}
/**
Custom Back Button
*/
func customBackButtonAction() {
print("DO THIS INSTEAD")
}
func enableCustomBackButton() {
if let nav = self.navigationController as? NavControllerWithBackButtonOverride {
nav.backButtonOverride = { self.customBackButtonAction() }
nav.interactivePopGestureRecognizer?.isEnabled = false
}
}
func disableCustomBackButton() {
if let nav = self.navigationController as? NavControllerWithBackButtonOverride {
nav.backButtonOverride = nil
nav.interactivePopGestureRecognizer?.isEnabled = true
}
}
Note: I also disabled interactivePopGestureRecognizer because it was causing issues with the custom setup.
Swift 5+ (Back button with alert control)
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true
let newBackButton = UIBarButtonItem(title: "<Back", style: UIBarButtonItem.Style.plain, target: self, action: #selector(PGWebViewController.back(sender:)))
self.navigationItem.leftBarButtonItem = newBackButton
}
#objc func back(sender: UIBarButtonItem) {
let alert = UIAlertController(title: "Warning!", message: "Your payment process is not completed yet. Do you want to go back?", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default, handler: { action in
_ = self.navigationController?.popViewController(animated: true)
})
alert.addAction(ok)
let cancel = UIAlertAction(title: "Cancel", style: .default, handler: { action in
})
alert.addAction(cancel)
DispatchQueue.main.async(execute: {
self.present(alert, animated: true)
})}
You can simply remove unnecessary controllers from the stack, something like this:
self.navigationController?.viewControllers.removeAll(where: {
$0 is FirstViewController || $0 is SecondWithPinController
})

Resources