I have two UILabels with two UITapGestureRecognizers in a UITableViewCell.
cell.Username.tag = indexPath.row
cell.SharedUser.tag = indexPath.row
let tapGestureRecognizer2 = UITapGestureRecognizer(target:self, action:"GoToProfil:")
let tapGestureRecognizer3 = UITapGestureRecognizer(target:self, action:"GoToProfil:")
cell.Username.userInteractionEnabled = true
cell.Username.addGestureRecognizer(tapGestureRecognizer2)
cell.SharedUser.userInteractionEnabled = true
cell.SharedUser.addGestureRecognizer(tapGestureRecognizer3)
func GoToProfil (sender: AnyObject!) {
self.performSegueWithIdentifier("GoToProfilSegue", sender: sender)
}
I'm using a Segue to push another UIViewController, and I'm overriding the PrepareSegue function to send the needed information corresponding to the Sender tag.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
let ProfilView = segue.destinationViewController as! Profil
ProfilView.hidesBottomBarWhenPushed = true
ProfilView.title = posts[sender.view!.tag].User?.objectForKey("Name") as? String
ProfilView.User = posts[sender.view!.tag].User
}
My problem is that I want to know which UILabel was pressed, knowing that I'm already using tag.
Your GoToProfile: function should be written properly. The parameter isn't the "sender", it's the gesture recognizer.
func GoToProfil (gestureRecognizer: UITapGestureRecognizer) {
}
From there, you can determine the label by using the view property of the gesture recognizer.
But you seem to have two conflicting requirements. You want to know which of the two labels was tapped and you want to know which row the label is in.
Normally you would use the label's tag to know which of the two labels was tapped. But you are using their tags to track the row.
The solution I recommend is to use the tag to differentiate the two labels. Then you can calculate the row based on the frame of the label.
See the following answer for sample code that translates the frame of a cell's subview to the cell's indexPath.
Making the following assumptions:
You are trying to uniquely identify the label using UIView.tag
You want different behaviour for Username & SharedUser
I recommend the following, first define your tags below your #imports
#define kUsername 1
#define kSharedUser 2
Then assign them to your views
cell.Username.tag = kUsername
cell.SharedUser.tag = kSharedUser
Then in your prepareSegue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
int tag = [sender.view!.tag]
if (tag == kUsername) {
//Username logic
} else if(tag == kSharedUser) {
//Shared User Logic
}
}
This way you can easily and simply determine tap, Note this might have different results if you have more then 1 Username & SharedUser labels. Then you will either need more #defines or change how you generate your tags.
You can add a property to UILabel to track the label's type. (I used an enum since there's just 2 cases, but it could be a string, etc.)
enum LabelDest : String
{
case Username = "Username"
case SharedUser = "SharedUser"
}
extension UILabel
{
struct Static {
static var key = "labelDest"
}
var labelDest:LabelDest? {
set { objc_setAssociatedObject( self, &Static.key, newValue?.rawValue, .OBJC_ASSOCIATION_COPY_NONATOMIC )
}
get {
guard let val = objc_getAssociatedObject( self, &Static.key ) as? String else { return nil }
return LabelDest( rawValue:val )
}
}
}
Now you can just do this:
let label = UILabel()
label.labelDest = .Username
Later:
switch label.labelDest
{
case .Some(.Username):
// handle user name
break
...
If you want to use the .tag field on your labels you can use a different technique to find the table row associated with a label: (again using class extensions)
extension UIView
{
var enclosingTableViewCell:UITableViewCell? {
return superview?.enclosingTableViewCell
}
var enclosingTableView:UITableView? {
return superview?.enclosingTableView
}
}
extension UITableViewCell
{
var enclosingTableViewCell:UITableViewCell? {
return self
}
}
extension UITableView
{
var enclosingTableView:UITableView? {
return self
}
}
extension UIView {
var tableRow:Int? {
guard let cell = self.enclosingTableViewCell else { return nil }
return self.enclosingTableView?.indexPathForCell( cell )?.row
}
}
Now, from your gesture recognizer action:
func goToProfil( sender:UIGestureRecognizer! )
{
guard let tappedRow = sender.view?.tableRow else { return }
// handle tap here...
}
You can access the sender data, and read the tag of the object that send you, like in this sample code.
To uniquely identify each row and each label, you can use something like this:
cell.Username.tag = (indexPath.row*2)
cell.SharedUser.tag = (indexPath.row*2)+1
With this, if you have a even tag, its the Username, odd will be the SharedUser. Dividing by the floor of 2 you can have the row back.
#IBOutlet weak var test1: UILabel!
#IBOutlet weak var test2: UILabel!
override func viewWillAppear(animated: Bool) {
test1.tag = 1
test2.tag = 2
test1.userInteractionEnabled = true
test2.userInteractionEnabled = true
self.test1.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "handleSingleTap:"))
self.test2.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "handleSingleTap:"))
}
func handleSingleTap(sender: UITapGestureRecognizer) {
print(sender.view?.tag)
}
Related
I have many textfields to enter values for calculation.
For each textfield I also added a LongPressGestureRecognizer so that I can update my calculations with interim results that I store in the placeholders.
#IBAction func lTap1(_ sender: UILongPressGestureRecognizer) {
if sender.state == .began && lTap1.placeholder!.isNumeric
{
lTap1.text = lTap1.placeholder
Calculation()
}
}
Is there a more convenient way with less code to add the long tap function to each text field instead of repeating the #IBAction function for lTap2, lTap3, etc.?
First assign tag to all Textfields
then declare the array of TextFields there are two ways to declare
#IBOutlet var textFields: [UITextField]!
and
let textFields = [lTap1, lTap2, lTap3,...]
then
#objc func textFeildLongPressed(_ sender: UILongPressGestureRecognizer) {
guard let tag = sender.view?.tag else { return }
guard let textField = textFields[tag] else {
return
}
if sender.state == .began && textField.placeholder!.isNumeric {
textField.text = textField.placeholder
Calculation()
}
}
Now assign gesture to all textFields in viewDidLoad function
textFields.forEach {
let tap = UILongPressGestureRecognizer(target: self, action: #selector(textFeildLongPressed(_:)))
tap.view?.tag = $0.tag
$0.isUserInteractionEnabled = true
$0.addGestureRecognizer(tap)
}
Thanks.
In the last viewcontroller of my app I wrote a code which creates a number of labels which are filled with random characters from a set
Under the labels textfields appear where the user should match the characters from the labels. When he hits the button, his answer is checked. If the answer is right the score is updated.
After that the score label should update, and new text for the labels with characters should be generated and shown, but that's where I get stuck... My instinct would be to add a loop to the code in the viewDidLoad, but as the code to check the answer is in the buttonAction function outside of the viewDidLoad, I don't know how to do this...
Here is some code from my app which might clarify things. I deleted quite a lot of straightforward code and put a comment line in just for this example.
override func viewDidLoad()
{
super.viewDidLoad()
// variables are declared, the number of labels is calculated based on the frame size
// the text for the labels is calculated based on a function in a separate class I wrote
var a = 0
while a < numberOfLabels {
// the UILabels are made and filled
// the corresponding UITextFields are made
a += 1
labelX += labelWidth
}
// then here the button is coded, the last lines are:
myButton.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
view.addSubview(myButton)
}
#objc func buttonAction(sender: UIButton!) {
var a = 0
var rightAnswer = false
var userInput: [String] = Array()
while a < numberOfLabels.shared.category! {
if let theLabel = self.view.viewWithTag(a) as? UITextField {
let tekstInput = theLabel.text
userInput.insert(tekstInput!, at:a-1)
}
a = a + 1
}
let controle = Controle()
rightAnswer = controle.checkAnswer(userAnswer: userInput)
if rightAnswer {
if var score = PassScore.shared.category {
score += 1
PassScore.shared.category = score
}
else {
var score = 1
PassScore.shared.category = score
}
}
return
}
Have your initialization code in a function
func setupLabels() {
var a = 0
while a < numberOfLabels {
// the UILabels are made and filled
// the corresponding UITextFields are made
a += 1
labelX += labelWidth
}
}
And call it whenever you need it:
override func viewDidLoad()
{
super.viewDidLoad()
setupLabels()
}
#objc func buttonAction(sender: UIButton!) {
//...
setupLabels()
}
I have the following settings tableView:
I want to perform a segue from each of the cells (as indicated by the disclosures) to another view controller. The problem is I don't want to make six different view controllers, especially since many of them will be almost identical, containing one or two text fields and a label. Is there any way I can just make one view controller and change it upon which cell is clicked?
The problem is I don't want to make six different view controllers,
especially since many of them will be almost identical, containing one
or two text fields and a label.
That's definitely true. You can achieve this by adding only one segue -without the need of adding six different segues- from the settings ViewController to the next one (details ViewController); Based on which row -in which section- has been selected, you can perform to your segue and sending the desired data.
1- Adding Segue:
you need to add a segue from the settings ViewController to the details ViewController. Make sure that the segue has been connected from the settings ViewController itself, but not from any of table view cell. After adding the segue on the storyboard, you need to add an identifier for it, I'll call it -in my code snippet example- "toDetails".
If you don't know how to add an identifier for the segue, you might want to check this answer.
2- Sending Desired Data:
For the purpose of simplifying, I assumed that the data you want to send is just a single string variable, called -in my code snippet example- dataToSend.
It goes as follows:
Settings ViewController:
class SettingsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
//...
private var dataToSend = ""
//...
//...
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "toDetails", sender: self)
}
//...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// tableView should be connected as an IBOutlet
guard let selectedIndexPath = tableView.indexPathForSelectedRow else {
print("Something went wrong when selected a row!")
return
}
if selectedIndexPath.section == 0 { // Account Section
if selectedIndexPath.row == 0 { // Name Row
dataToSend = "name"
} else if selectedIndexPath.row == 1 { // School Row
dataToSend = "school"
} else if selectedIndexPath.row == 2 { // Grade Row
dataToSend = "grade"
}
} else if selectedIndexPath.section == 1 { // Private Section
if selectedIndexPath.row == 0 { // Email Row
dataToSend = "email"
} else if selectedIndexPath.row == 1 { // Password Row
dataToSend = "password"
} else if selectedIndexPath.row == 2 { // Phone Number Row
dataToSend = "phone"
}
}
let detailsViewController = segue.destination as! DetailsViewController
detailsViewController.receivedData = dataToSend
}
//...
}
Take it Further:
It is a good practice when working with such a case to use enums instead of checking rows numbers, that leads to more readable code:
enum Sections:Int {
case
account = 0,
privacy = 1
}
enum AccountRows:Int {
case
name = 0,
school = 1,
grade = 2
}
enum PrivacyRows:Int {
case
email = 0,
password = 1,
phone = 2
}
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
//...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// tableView should be connected as an IBOutlet
guard let selectedIndexPath = tableView.indexPathForSelectedRow else {
print("Something went wrong when selected a row!")
return
}
// tuple of (section, row)
switch (selectedIndexPath.section, selectedIndexPath.row) {
// Accounts Section
case (Sections.account.rawValue, AccountRows.name.rawValue):
dataToSend = "name"
case (Sections.account.rawValue, AccountRows.school.rawValue):
dataToSend = "school"
case (Sections.account.rawValue, AccountRows.grade.rawValue):
dataToSend = "grade"
// Privacy Section
case (Sections.privacy.rawValue, PrivacyRows.email.rawValue):
dataToSend = "email"
case (Sections.privacy.rawValue, PrivacyRows.password.rawValue):
dataToSend = "password"
case (Sections.privacy.rawValue, PrivacyRows.phone.rawValue):
dataToSend = "phone"
default:
print("Something went wrong when checking Section and Rows!")
}
let detailsViewController = segue.destination as! DetailsViewController
detailsViewController.receivedData = dataToSend
}
//...
}
Cheers up! Hope this helped.
Create a new View Controller i am naming it as "VController"
Create enum in "VController" above the view didload :
public enum VCType:Int {
case name = 1
case school = 2
case grade = 3
}
var selectedVc:VCType!
In viewdidload of "VController":
if selectedVc == .name {
// do whatever you want
}
else if selectedVc == .school {
// do whatever you want
}
else if
..........
..........
Code For First ViewController :
above the view didload :
var selectedType:VController.VCType!
// in didSelectRowAtIndexPath
selectedType = .school // if selecting school
perform segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "mySegue"{
let nextScene = segue.destination as? VController
nextScene?.selectedVc = selectedType
}
}
i tried to add gesture recognizer in my UIImageView
let rc = UITapGestureRecognizer(target: self, action: "foo:")
rc.numberOfTapsRequired = 1
rc.numberOfTouchesRequired = 1
cell.bar.tag = indexPath.row
cell.bar.addGestureRecognizer(rc)
but didn't call my foo function
func foo(sender: UIImageView! ) {
self.performSegueWithIdentifier("VC", sender: sender)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "VC" {
let vc = segue.destinationViewController as! VC
vc.item = items[sender.tag]
}
}
So, as I mentioned before your problem is UIImageView by default has property userInteractionEnabled set to false. You can change this in you storyboard, or add line cell.bar.userInteractionEnabled = true.
Next your problem is in your foo: method implementation: you specify sender as UIImageView!, but it should be UITapGestureRecognizer. This is why it crashes - it cannot be UIImageView, so when it unwraps (!) it is nil.
Solution: change your foo method declaration to foo(recognizer: UITapGestureRecognizer). If you need access your imageView inside this method you can use code below:
if let imageView = recognizer.view as? UIImageView {
...
}
or with new guard keyword (Swift 2.0)
guard let imageView = recognizer.view as? UIImageView
else { return }
...
You can change
foo(recognizer: UITapGestureRecognizer) {
}
I use the "Next" value for the "Return Key" to get the Next button in place of the Done button, but (obviously) pressing it doesn't automatically move to the next UITextField in my view.
What's the right way to do this? I have seen many answers, but anyone have a swift solution?
Make sure your text fields have their delegate set and implement the textFieldShouldReturn method. This is the method that is called when the user taps the return key (no matter what it looks like).
The method might look something like this:
func textFieldShouldReturn(textField: UITextField) -> Bool {
if textField == self.field1 {
self.field2.becomeFirstResponder()
}
return true
}
The actual logic in here might vary. There are numerous approaches, and I'd definitely advise against a massive if/else chain if you have lots of text fields, but the gist here is to determine what view is currently active in order to determine what view should become active. Once you've determined which view should become active, call that view's becomeFirstResponder method.
For some code cleanliness, you might consider a UITextField extension that looks something like this:
private var kAssociationKeyNextField: UInt8 = 0
extension UITextField {
var nextField: UITextField? {
get {
return objc_getAssociatedObject(self, &kAssociationKeyNextField) as? UITextField
}
set(newField) {
objc_setAssociatedObject(self, &kAssociationKeyNextField, newField, .OBJC_ASSOCIATION_RETAIN)
}
}
}
And then change our textFieldShouldReturn method to look like this:
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.nextField?.becomeFirstResponder()
return true
}
Once you've done this, it should simply be a matter of setting each text field's new nextField property in viewDidLoad:
self.field1.nextField = self.field2
self.field2.nextField = self.field3
self.field3.nextField = self.field4
self.field4.nextField = self.field1
Although if we really wanted, we could prefix the property with #IBOutlet, and that would allow us to hook up our "nextField" property right in interface builder.
Change the extension to look like this:
private var kAssociationKeyNextField: UInt8 = 0
extension UITextField {
#IBOutlet var nextField: UITextField? {
get {
return objc_getAssociatedObject(self, &kAssociationKeyNextField) as? UITextField
}
set(newField) {
objc_setAssociatedObject(self, &kAssociationKeyNextField, newField, .OBJC_ASSOCIATION_RETAIN)
}
}
}
And now hook up the nextField property in interface builder:
(Set up your delegate while you're here too.)
And of course, if the nextField property returns nil, the keyboard just hides.
Here is an example in Swift:
I created a screen with 6 UITextFields. I assigned them the tags 1 through 6 in Interface Builder. I also changed the Return key to Next in IB. Then I implemented the following:
import UIKit
// Make your ViewController a UITextFieldDelegate
class ViewController: UIViewController, UITextFieldDelegate {
// Use a dictionary to define text field order 1 goes to 2, 2 goes to 3, etc.
let nextField = [1:2, 2:3, 3:4, 4:5, 5:6, 6:1]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Make ourselves the delegate of the text fields so that textFieldShouldReturn
// will be called when the user hits the Next/Return key
for i in 1...6 {
if let textField = self.view.viewWithTag(i) as? UITextField {
textField.delegate = self
}
}
}
// This is called when the user hits the Next/Return key
func textFieldShouldReturn(textField: UITextField) -> Bool {
// Consult our dictionary to find the next field
if let nextTag = nextField[textField.tag] {
if let nextResponder = textField.superview?.viewWithTag(nextTag) {
// Have the next field become the first responder
nextResponder.becomeFirstResponder()
}
}
// Return false here to avoid Next/Return key doing anything
return false
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
There is nothing wrong with the other answers, this is just a different approach with the benefit of being more focused on OOP - imho (although this is a bit more work up front, it can be reused). In the storyboard, I start off adding tags with a distinct range (e.g 800-810) that define the specific order of the fields I want to move between. This has the benefit of working across all subviews in the main view so that one can navigate between UITextField's and UITextView's (and any other control) as needed.
Generally - I typically try to have view controllers message between views and custom event handler objects. So I use a message (aka, NSNotification) passed back to the view controller from a custom delegate class.
(TextField Delegate Handler)
Note: In AppDelegate.swift: let defaultCenter = NSNotificationCenter.defaultCenter()
//Globally scoped
struct MNGTextFieldEvents {
static let NextButtonTappedForTextField = "MNGTextFieldHandler.NextButtonTappedForTextField"
}
class MNGTextFieldHandler: NSObject, UITextFieldDelegate {
var fields:[UITextField]? = []
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
return true
}
func textFieldDidBeginEditing(textField: UITextField) {
textField.backgroundColor = UIColor.yellowColor()
}
func textFieldDidEndEditing(textField: UITextField) {
textField.backgroundColor = UIColor.whiteColor()
}
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
return true
}
func textFieldShouldClear(textField: UITextField) -> Bool {
return false
}
func textFieldShouldEndEditing(textField: UITextField) -> Bool {
return true
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
//passes the message and the textField (with tag) calling the method
defaultCenter.postNotification(NSNotification(name: MNGTextFieldEvents.NextButtonTappedForTextField, object: textField))
return false
}
}
This allows my view controller to remain focused on it's main job of handling the messaging between objects, model and view.
(View Controller receives a message from the delegate and passes instructions using the advanceToNextField function)
Note: In my storyboard my custom handler classes are defined using an NSObject and that object is linked into the storyboard as a delegate for the controls that I need monitored. Which causes the custom handler class to be initialized automatically.
class MyViewController: UIViewController {
#IBOutlet weak var tagsField: UITextField! { didSet {
(tagsField.delegate as? MNGTextFieldHandler)!.fields?.append(tagsField)
}
}
#IBOutlet weak var titleField: UITextField!{ didSet {
(titleField.delegate as? MNGTextFieldHandler)!.fields?.append(titleField)
}
}
#IBOutlet weak var textView: UITextView! { didSet {
(textView.delegate as? MNGTextViewHandler)!.fields?.append(textView)
}
}
private struct Constants {
static let SelectorAdvanceToNextField = Selector("advanceToNextField:")
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
registerEventObservers()
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
deRegisterEventObservers()
}
func advanceToNextField(notification:NSNotification) {
let currentTag = (notification.object as! UIView).tag
for aView in self.view.subviews {
if aView.tag == currentTag + 1 {
aView.becomeFirstResponder()
}
}
}
func registerEventObservers () {
defaultCenter.addObserver(self, selector: Constants.SelectorAdvanceToNextField, name: MNGTextFieldEvents.NextButtonTappedForTextField, object: nil)
}
func deRegisterEventObservers() {
defaultCenter.removeObserver(self, name: MNGTextFieldEvents.NextButtonTappedForTextField, object: nil)
}
....
}
Just another way to achieve the result that I found helpful. My app had 11 text fields followed by a text view. I needed to be able to cycle through all fields using the next key and then resign the keyboard following the textview (i.e. other notes).
In the storyboard, I set the tag on all of the fields (both text and textview) starting with 1 through 12, 12 being the textview.
I'm sure there are other ways to do it and this method isn't perfect, but hopefully it helps someone.
In code, I wrote the following:
func textFieldShouldReturn(textField: UITextField) -> Bool {
let nextTag = textField.tag + 1
//Handle Textview transition, Textfield programmatically
if textField.tag == 11 {
//Current tag is 11, next field is a textview
self.OtherNotes.becomeFirstResponder()
} else if nextTag > 11 {
//12 is the end, close keyboard
textField.resignFirstResponder()
} else {
//Between 1 and 11 cycle through using next button
let nextResponder = self.view.viewWithTag(nextTag) as? UITextField
nextResponder?.becomeFirstResponder()
}
return false
}
func textFieldDidEndEditing(textField: UITextField) {
textField.resignFirstResponder()
}
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
//Remove keyboard when clicking Done on keyboard
if(text == "\n") {
textView.resignFirstResponder()
return false
}
return true
}
Another approach, if you're using storyboards, you can change the textfield's attribute for Return Key.
Currently you have the following options: Default (Return), Go, Google, Join, Next, Route, Search, Send, Yahoo, Done, Emergency Call, Continue