Done button in phone pad (Swift 4) - ios

I'm new to Swift and I was creating a registration form. I have a phone text field and when I open the phone pad, I can not see the done or return button so I can dismiss the on screen keyboard with resignfirstresponder().
I used this link : https://gist.github.com/jplazcano87/8b5d3bc89c3578e45c3e
And now, I get the Done button but on pressing Done, my app is crashing because of the selector which says is unidentified. Can anyone please help or guide me with another way?
Thanks in advance
Here is the code
import UIKit
class ViewController: UIViewController, UITextFieldDelegate, UIPickerViewDelegate, UIPickerViewDataSource {
#IBOutlet var registerTF: [KaustabhTF]!
// MARK:- App LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK:- User Defined
func isValidEmail(email: String) -> Bool {
let emailRegEx = "^(?:(?:(?:(?: )*(?:(?:(?:\\t| )*\\r\\n)?(?:\\t| )+))+(?: )*)|(?: )+)?(?:(?:(?:[-A-Za-z0-9!#$%&’*+/=?^_'{|}~]+(?:\\.[-A-Za-z0-9!#$%&’*+/=?^_'{|}~]+)*)|(?:\"(?:(?:(?:(?: )*(?:(?:[!#-Z^-~]|\\[|\\])|(?:\\\\(?:\\t|[ -~]))))+(?: )*)|(?: )+)\"))(?:#)(?:(?:(?:[A-Za-z0-9](?:[-A-Za-z0-9]{0,61}[A-Za-z0-9])?)(?:\\.[A-Za-z0-9](?:[-A-Za-z0-9]{0,61}[A-Za-z0-9])?)*)|(?:\\[(?:(?:(?:(?:(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9][0-9])|(?:2[0-4][0-9])|(?:25[0-5]))\\.){3}(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9][0-9])|(?:2[0-4][0-9])|(?:25[0-5]))))|(?:(?:(?: )*[!-Z^-~])*(?: )*)|(?:[Vv][0-9A-Fa-f]+\\.[-A-Za-z0-9._~!$&'()*+,;=:]+))\\])))(?:(?:(?:(?: )*(?:(?:(?:\\t| )*\\r\\n)?(?:\\t| )+))+(?: )*)|(?: )+)?$"
let emailTest = NSPredicate(format:"SELF MATCHES %#", emailRegEx)
let result = emailTest.evaluate(with: email)
return result
}
func isValidPassword(pass:String?) -> Bool {
let passwordTest = NSPredicate(format: "SELF MATCHES %#", "(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{8,}")
return passwordTest.evaluate(with: pass)
}
func displayAlert(alertMessage: String) {
let alertController = UIAlertController(title: "Alert!!", message: alertMessage, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel,handler: nil))
self.present(alertController, animated: true, completion: nil)
}
func addDoneButtonOnKeyboard()
{
let doneToolbar: UIToolbar = UIToolbar()
doneToolbar.barStyle = UIBarStyle.default
let flexSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.flexibleSpace, target: nil, action: nil)
let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.done, target: self, action: Selector(("doneButtonAction")))
var items = [UIBarButtonItem]()
items.append(flexSpace)
items.append(done)
doneToolbar.items = items
doneToolbar.sizeToFit()
self.registerTF[5].inputAccessoryView = doneToolbar
}
func doneButtonAction()
{
self.registerTF[5].resignFirstResponder()
}
func isValidPhone(value: String) -> Bool {
let PHONE_REGEX = "^[0-9]{10}$"
let phoneTest = NSPredicate(format: "SELF MATCHES %#", PHONE_REGEX)
let result = phoneTest.evaluate(with: value)
return result
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true) //This will hide the keyboard
}
let myPickerData = [String](arrayLiteral: "+91", "+971", "+1", "+121", "+80", "+00")
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView( _ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return myPickerData.count
}
func pickerView( _ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return myPickerData[row]
}
func pickerView( _ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
registerTF[4].text = myPickerData[row]
}
func nextTextFieldToFirstResponder(textField: KaustabhTF) {
if textField.absoluteCount() == 0
{
displayAlert(alertMessage: "Please enter the apt value.")
}
let senderTag = textField.tag
if senderTag == 0 // Name
{
registerTF[senderTag + 1].becomeFirstResponder()
}
else if senderTag == 1 // Email
{
if !isValidEmail(email: textField.text!)
{
displayAlert(alertMessage: "Please input your valid email address")
}
else
{
registerTF[senderTag + 1].becomeFirstResponder()
}
}
else if senderTag == 2 // Password
{
if !isValidPassword(pass: textField.text)
{
displayAlert(alertMessage: "Please input a password with minimum of 8 characters including an upper case character, a lower case character and a digit.")
}
else
{
registerTF[senderTag + 1].becomeFirstResponder()
}
}
else if senderTag == 3 // Confirm Password
{
if !isValidPassword(pass: textField.text)
{
displayAlert(alertMessage: "Please input a password with minimum of 8 characters including an upper case character, a lower case character and a digit.")
}
else if registerTF[senderTag-1].text != registerTF[senderTag].text
{
displayAlert(alertMessage: "Password and confirm password dont match. Please try again")
}
else
{
textField.resignFirstResponder()
}
}
else if senderTag == 4 //Country Code
{
let thePicker = UIPickerView()
registerTF[4].inputView = thePicker
thePicker.delegate = self
}
else if senderTag == 5 // Phone Number
{
self.addDoneButtonOnKeyboard()
if !isValidPhone(value: textField.text!)
{
displayAlert(alertMessage: "Please make sure that the phone number is correct")
}
doneButtonAction()
}
}

Replace
Selector(("doneButtonAction"))
with
#selector(ViewController.doneButtonAction)
And complete code will be:
let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.done, target: self, action: #selector(ViewController.doneButtonAction))
And add #objc before your doneButtonAction method and final code will be:
#objc func doneButtonAction()
{
self.registerTF[5].resignFirstResponder()
}
For more info refer THIS.

In swift 3.0 and 4.0 you can used this code.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var txtNumber: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.addDoneButtonOnKeyboard()
}
func addDoneButtonOnKeyboard()
{
let doneToolbar: UIToolbar = UIToolbar(frame: CGRect.init(x: 0, y: 0, width: 320, height: 50))
doneToolbar.barStyle = UIBarStyle.default
let flexSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.flexibleSpace, target: nil, action: nil)
let done: UIBarButtonItem = UIBarButtonItem(title: "Done", style: UIBarButtonItemStyle.done, target: self, action: #selector(doneButtonAction))
var items = [UIBarButtonItem]()
items.append(flexSpace)
items.append(done)
doneToolbar.items = items
doneToolbar.sizeToFit()
self.txtNumber.inputAccessoryView = doneToolbar
}
**//button action generate following two way both are working great but use any one**
#objc func doneButtonAction()
{
self.txtNumber.resignFirstResponder()
}
#IBAction func doneButtonAction(_ sender: UIButton) {
self.txtNumber.resignFirstResponder()
}
}

Possible Duplicate of this post: How to show "Done" button on iPhone number pad
I have tried to add a duplicate flag, However I could not do it for some reason. Hope it will be helpful for others.

Related

How can I disable a UIButton until multiple TextFields are filled in with data (numbers)?

I have 3 UITextFields in my app and I need a user to fill in before he/she can see the UIButton which will calculate the result. I have tried a lot of different methods to resolve the issue but they don't seem to work and I can't quite understand some of them. I am sure there is a simple way to do that. P.S.: I don't use a storyboard
I have already tried to include UITextFieldDelegate, then writing a function, also used other ways, etc...
import UIKit
let Label: UILabel = {
let f = UILabel()
f.text = "Label"
return f
}()
let textFiled1: UITextField = {
let fw = UITextField()
fw.keyboardType = UIKeyboardType.decimalPad
return fw
}()
let textField2: UITextField = {
let fh = UITextField()
fh.keyboardType = UIKeyboardType.decimalPad
return fh
}()
let textField3: UITextField = {
let fa = UITextField()
fa.keyboardType = UIKeyboardType.numberPad
return fa
}()
class ViewController: UIViewController {
let calculateButton: UIButton = {
let c = UIButton()
c.setTitle("Calculate", for: .normal)
return c
}()
override func viewDidLoad() {
super.viewDidLoad()
setupLabel()
setupTextFieldComponents()
setupCalculateButton()
}
fileprivate func setupLabel() {
view.addSubview(fLabel)
}
fileprivate func setupTextFieldComponents() {
setupTextField1()
setupTextField2()
setupTexrField3()
}
fileprivate func setupTextField1() {
view.addSubview(textField1)
}
fileprivate func setupTextField2() {
view.addSubview(textField2)
}
fileprivate func setupTextField3() {
view.addSubview(textField3)
}
fileprivate func setupCalculateButton() {
view.addSubview(calculateButton)
}
#objc func calculateButtonPressed(sender: UIButton){
let textfield1 = Float(textField1.text!)
let textfield2 = Float(textField2.text!)
let textfield3 = Float(textField3.text!)
femaleMetaLabel.text = String(111 + (1.1 * textfield1!) + (1.1 * textfield2!) - (1.1 * textfield3!))
}
}
User is supposed to enter numeric data in all three text fields in order to calculate the result. Is there a way to make an ERROR message pop out without an app crashing?
When I am doing validation on text input that enables a button, I usually do something like this:
#IBOutlet weak var usernameField: UITextField!
#IBOutlet weak var passwordField: UITextField!
#IBOutlet weak var submitButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
submitButton.isEnabled = false
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self,
selector: #selector(validateInput(_:)),
name: UITextField.textDidChangeNotification,
object: usernameField)
NotificationCenter.default.addObserver(self,
selector: #selector(validateInput(_:)),
name: UITextField.textDidChangeNotification,
object: passwordField)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self)
}
#objc func validateInput(_ sender: Any?) {
submitButton.isEnabled = false
if let username = usernameField.text,
let password = passwordField.text {
// your validation rules will be more complex than this :D
if username.count > 0 && password.count > 0 {
submitButton.isEnabled = true
}
}
}
If what you need to do is pop an alert when the inputs are incorrect, simply substitute a call to a method like the one below, where you vaildate the user's input.
func showAlert() {
let alert = UIAlertController(title: "Nope",
message: "Your inputs were incorrect.",
preferredStyle: .alert)
alert.addAction( UIAlertAction(title: "Try again",
style: .default,
handler: nil) )
self.present(alert, animated: true, completion: nil)
}
#Maxim you are almost there. Let me add a simple delegate in your code to achieve your goal.
Add code in your textFieldDidEndEditing delegate -
func textFieldDidEndEditing(textField: UITextField) {
if (textField1.text?.count)! > 0 && (textField2.text?.count)! > 0 && (textField3.text?.count)! > 0 {
calculateButton.isEnabled = true
}else{
calculateButton.isEnabled = false
}
}
Now time to calculate the result:
#objc func calculateButtonPressed(sender: UIButton){
if let inputOne = Float(textField1.text!), let inputTwo = Float(textField2.text!), let inputThree = Float(textField3.text!) {
femaleMetaLabel.text = String(111+(1.1*inputOne)+(1.1*inputTwo)-(1.1*inputThree))
}else{
let alert = UIAlertController(title: "", message: "You entered invalid inputs", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
self.present(alert, animated: true)
}
}
Hope it will help you. Let me know if you are still having any issue.

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.

How to add an independent UIView on button press in swift?

I am trying to add UILabels and other UIView elements on button press and have them act as independent elements but I am having no luck. I can successfully add multiple labels and text fields but when i try to delete them using my gesture recognizer it will only delete the latest UIView element. My full code is posted below. There are two main bugs in my implementation as it is: Creating more than one label or text field at a time causes the gestures to not respond and I can only delete the latest UIView element. Any help is greatly appreciated!
My Model:
import Foundation
import UIKit
class QuizItem: UIViewController{
var alert = UIAlertController()
var labelCountStepper = 0
var tfCountStepper = 0
let gestureRecog = myLongPress(target: self, action: #selector(gestureRecognized(sender:)))
let moveGesture = myPanGesture(target: self, action: #selector(userDragged(gesture:)))
func createLabel(){
var randLabel = UILabel(frame: CGRect(x: 200, y: 200, width: 300, height: 20))
randLabel.isUserInteractionEnabled = true
randLabel.textColor = UIColor .black
randLabel.text = "I am a Test Label"
randLabel.tag = labelCountStepper
gestureRecog.quizItem = randLabel
moveGesture.quizItem = randLabel
randLabel.addGestureRecognizer(self.longPressGesture())
randLabel.addGestureRecognizer(self.movePanGesture())
topViewController()?.view.addSubview(randLabel)
labelCountStepper = labelCountStepper+1
}
func createTextField(){
var randTextField = UITextField(frame: CGRect(x: 200, y: 200, width: 300, height: 35))
randTextField.isUserInteractionEnabled = true
randTextField.backgroundColor = UIColor .lightGray
randTextField.placeholder = "Enter your message..."
randTextField.tag = tfCountStepper
gestureRecog.quizItem = randTextField
moveGesture.quizItem = randTextField
randTextField.addGestureRecognizer(self.longPressGesture())
randTextField.addGestureRecognizer(self.movePanGesture())
topViewController()?.view.addSubview(randTextField)
tfCountStepper = tfCountStepper+1
}
func longPressGesture() -> UILongPressGestureRecognizer {
let lpg = UILongPressGestureRecognizer(target: self, action: #selector(gestureRecognized(sender:)))
lpg.minimumPressDuration = 0.5
return lpg
}
func movePanGesture() -> UIPanGestureRecognizer {
let mpg = UIPanGestureRecognizer(target: self, action: #selector(userDragged(gesture:)))
return mpg
}
#objc func gestureRecognized(sender: UILongPressGestureRecognizer){
if(sender.state == .began){
print("Label Tag #: \(labelCountStepper)")
print("Text Field Tag #: \(tfCountStepper)")
alert = UIAlertController(title: "Remove Item?", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { (action) -> Void in
self.gestureRecog.quizItem.removeFromSuperview()
}))
alert.addAction(UIAlertAction(title: "No", style: .cancel){(_) in
})
topViewController()?.present(alert, animated: true, completion: nil)
}
}
#objc func userDragged(gesture: UIPanGestureRecognizer){
let loc = gesture.location(in: self.view)
moveGesture.quizItem.center = loc
}
override func viewDidLoad() {
super.viewDidLoad()
}
func topViewController() -> UIViewController? {
guard var topViewController = UIApplication.shared.keyWindow?.rootViewController else { return nil }
while topViewController.presentedViewController != nil {
topViewController = topViewController.presentedViewController!
}
return topViewController
}
}
My ViewController:
import UIKit
class ViewController: UIViewController {
var labelMaker = QuizItem()
#IBAction func createLabel(_ sender: UIButton) {
labelMaker.createLabel()
}
#IBAction func createTextField(_ sender: UIButton) {
labelMaker.createTextField()
}
override func viewDidLoad() {
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
I also made two subclasses that inherit from UILongPress and UIPan Gesture Recognizers. I'll only post the LongPress because the UIPan is exactly the same - just inherits from UIPan instead of UILongPress.
import Foundation
import UIKit
class myLongPress: UILongPressGestureRecognizer{
var quizItem = UIView()
}
You can achieve that by slighting changing your code as below:
#objc func gestureRecognized(sender: UILongPressGestureRecognizer){
if(sender.state == .began){
print("Label Tag #: \(labelCountStepper)")
print("Text Field Tag #: \(tfCountStepper)")
alert = UIAlertController(title: "Remove Item?", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { (action) -> Void in
let selectedView = sender.view
selectedView?.removeFromSuperview()
}))
alert.addAction(UIAlertAction(title: "No", style: .cancel){(_) in
})
topViewController()?.present(alert, animated: true, completion: nil)
}
}
#objc func userDragged(gesture: UIPanGestureRecognizer){
let loc = gesture.location(in: self.view)
let selectedView = gesture.view
selectedView?.center = loc
}

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.

Selector in UIBarButtonItem not calling

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.

Resources