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
}
}
Related
I'm currently working on a project for an end of the year assignment and I need help to change a UIImage in my first view controller when the user clicks an item in a list view on the second view controller. I'm using a normal show segue to get to the menu when a button is clicked and this to get back:
Code on second view that goes back to the first view
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
dismiss (animated: true, completion: nil)
building = true
print("\(squareType)")
print("BUILDING = \(building)")
}
How do I get this to activate code on my first view to change images. I already have perimeters that run through both views to tell if it's been clicked and what to make based off of it, but I can't update my images
This is the code I want the other view to activate:
code on my first view that I want to trigger after the second view is dismissed
func farmCreator() {
for i in 0...24 {
if allCells[i].material == "Forest" && squareType == "Cabin" {
imageBottom[i].image = UIImage(named:"Farms/2")
}
if allCells[i].material == "Forest" && squareType == "Forest Mine" {
imageBottom[i].image = UIImage(named:"Farms/1") //Change
}
if allCells[i].material == "Rock" && squareType == "Mine" {
imageBottom[i].image = UIImage(named:"Farms/1")
}
if allCells[i].material == "Water" && squareType == "Fishmonger" {
imageBottom[i].image = UIImage(named:"Farms/3")
}
if allCells[i].material == "Water" && squareType == "Water Mine" {
imageBottom[i].image = UIImage(named:"Farms/1") //Change
}
if allCells[i].material == "Plains" && squareType == "Farm"{
imageBottom[i].image = UIImage(named:"Farms/4")
}
if allCells[i].material == "Plains" && squareType == "Plain Mine"{
imageBottom[i].image = UIImage(named:"Farms/1") //Change
}
}
}
Ignore how poorly optimized it is, I'm new
I've tried messing with all of the overrides. These things:
override func viewDidDisappear(_ animated: Bool) {
print("VIEW DID DIS V1")
}
override func viewWillDisappear(_ animated: Bool) {
print("VIEW WILL DIS V1")
}
override func viewDidAppear(_ animated: Bool) {
print("VIEW DID APP V1")
}
override func viewWillAppear(_ animated: Bool) {
print("VIEW WILL APP V1")
}
But they only work on the view that is disappearing with my dismiss and not the view that it moves to. Would I have to use an unwind? How would I implement that??
You need to use delegation here. I can't see all your code so I'll have to come up with different names for some things.
Step 1: Create a protocol on top of your ViewController 2 class
protocol ViewController2Protocol {
func didTapRow(squareType: String, material: String)
}
class ViewController2: UIViewController {
...
// Contents of ViewController 2
}
Step 2: Create a weak var delegate that's the type of Protocol declared above
protocol ViewController2Protocol {
func didTapRow(squareType: String, material: String)
}
class ViewController2: UIViewController {
weak var delegate: ViewController2Protocol? // 2. Add this
...
// Contents of ViewController 2
}
Step 3: Call your delegate method and pass the required parameters in didSelectRow before dismissing
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Assuming you have: var material: String, and var building: String
delegate?.didTapRow(material: material, building: building) // 3. Not sure what parameters you need to send to ViewController1. Just guessing.
dismiss (animated: true, completion: nil)
}
Step 4: Conform to, and implement the protocol in ViewController 1
class ViewController1: UIViewController, ViewController2Protocol { // Conform to protocol
func didTapRow(squareType: String, material: String) { // Implement it
// Call the method you want to get triggered by tapping here
farmCreator(squareType: squareType, building: building) // Change this method to accept the parameters as needed
}
}
Step 5. Make sure you set ViewController1 as the delegate wherever you're transitioning to ViewController2 in your ViewController1 class. Again not sure what your navigation code looks like so, add this there. For example
// Given ViewController 2
let vc2 = ViewController2()
vc2.delegate = self
I can give more detail in step 5 if I know how your transitioning:
Is your segue from a UI element in your storyboard?
Is your segue from ViewController1 to ViewController2 in your storyboard?
Good day. I'm creating my first own app and have ran into an issue. I have a AR scene with clickable stuff which when you touch them a segue triggers into a ViewController and sets that view controllers labels and textview depending on what was touch on screen.
Explanations: 1. CaseViewController is the target view controller. 2. the "artNews" and "politicalNews" are string arrays in which I've written 3 strings, they are defined and are never nil.
Question: I get a crash due to the segueInputText being nil. Why does it become nil and how do I correct it?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVC = segue.destination as! CaseViewController
destinationVC.segueInputText = sender as? [String]
print("\(String(describing: sender))")
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touchLocation = touches.first?.location(in: sceneView),
let hitNode = sceneView?.hitTest(touchLocation, options: nil).first?.node,
let nodeName = hitNode.name
else { return }
if nodeName == imageNameArray[0] {
performSegue(withIdentifier: "CaseViewController", sender: artNews)
} else {
print("Found no node connected to \(nodeName)")
return
}
if nodeName == imageNameArray[1] {
performSegue(withIdentifier: "CaseViewController", sender: politicalNews)
} else {
print("Found no node connected to \(nodeName)")
return
}
the CaseViewController has UILabels and UITextViews connected and this:
var segueInputText : [String]? {
didSet {
setupText()
}
}
func setupText() {
// Why are these values nil?
testLabel.text = segueInputText![0]
ingressLabel.text = segueInputText![1]
breadLabel.text = segueInputText![2]
testLabel.reloadInputViews()
ingressLabel.reloadInputViews()
breadLabel.reloadInputViews() //breadLabel is a UITextView
}
Thank you for reading my question!
Kind regards.
Remove didSet block as when you set the array inside prepare , observe triggers and the lbl is still nil
OR
func setupText() {
if testLabel == nil { // if one is nil then all of them too
return
}
}
Don't use the didSet observer in this case. It will never work.
In setupText() IBOutlets are accessed which are not connected yet at the moment prepare(for is called.
Remove the observer
var segueInputText : [String]?
and call setupText in viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setupText()
}
At the very moment when you do this:
destinationVC.segueInputText = sender as? [String]
the destination view controller has not yet loaded hence none of the outlets are connected so accessing any of them will crash your app as they are still nil.
You will have to assign any of the values you’d like to pass to the destination controller to a property and assign this property’s value to the the corresponding outlet in viewDidLoad. This way you make sure all outlets have connected.
For the same reason don’t use a property observer to assign the property’s value to any of the labels as this would happen, again, before the view controller had a chance to load…
I'm trying to make my first iOS app - board games tracker. I am using textView delegate to choose and then display players - textViewShouldBeginEditing. When I click it, it sometimes performes the segue twice and I don't know why.
In the second controller, when I come back, I use navigation controller delegate method.
Important parts of my viewController with textView:
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
if textView == playersNameView {
segueKey = "all"
} else if textView == winnersNameView {
segueKey = "winners"
} else if textView == loosersNameView {
segueKey = "loosers"
}
if textView == pointsView {
performSegue(withIdentifier: "addPoints", sender: self)
} else if textView == playersNameView || textView == loosersNameView || textView == winnersNameView {
performSegue(withIdentifier: "choosePlayers", sender: self)
}
return false
}
//MARK: - Managing segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.identifier {
case "chooseGame"?:
let controller = segue.destination as! ChooseGameViewController
controller.gameStore = gameStore
controller.selectedGame = selectedGame
case "choosePlayers"?:
let controller = segue.destination as! ChoosePlayersViewController
controller.key = segueKey
setAvailablePlayers()
switch segueKey {
case "all"?:
controller.availablePlayers = availablePlayers
controller.selectedPlayers = selectedPlayers
if let game = selectedGame {
controller.maxPlayers = game.maxNoOfPlayers
}
case "winners"?:
controller.availablePlayers = availablePlayers
controller.selectedPlayers = winners
if let game = selectedGame {
controller.maxPlayers = game.maxNoOfPlayers - loosers.count
}
case "loosers"?:
controller.availablePlayers = availablePlayers
controller.selectedPlayers = loosers
if let game = selectedGame {
controller.maxPlayers = game.maxNoOfPlayers - winners.count
}
default:
preconditionFailure("Wrong segue key")
}
default:
preconditionFailure("Wrong segue identifier")
}
}
and my second view controller - ChoosePlayersViewController
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if let controller = viewController as? AddMatchViewController {
switch key {
case "all"?:
controller.selectedPlayers = selectedPlayers
controller.deselectedPlayers = deselectedPlayers
controller.setPlayersPoints()
case "winners"?:
controller.winners = selectedPlayers
case "loosers"?:
controller.loosers = selectedPlayers
default:
preconditionFailure("Wrong key!")
}
controller.viewWillAppear(true)
}
}
EDIT
I have a workaround for now - using Date i check if the time between segues is greater than 1 second. If it is greater, then it performs segue, if not - it doesn't. Works for now, but I hope for more elegant answer.
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)
}
I have a Table View with basic label of Table View Cell. I labelled the cell as "January", "February", "March", etc. When user tap on "January", an image with file name "jan.jpeg" will be showed using the following Swift code:
override func viewDidLoad() {
super.viewDidLoad()
let image = UIImage(named: "jan.jpeg")!
imageView = UIImageView(image: image)
imageView.frame = CGRect(origin: CGPointMake(0.0, 0.0), size:image.size)
}
My question is, is it possible when user tap on "February", "feb.jpeg" will be showed, whereas "mar.jpeg will be showed if user tap on "Mar"? How to implement this?
When a cell is selected, the tableView:didSelectRowAtIndexPath: delegate method is called. If you have a predefined list of cells, you can test which cell is being tapped and load the correct image. For example:
have a variable var imgName: String outside of all functions in the viewcontroller with your tableView.
Also put this function in the viewcontroller with your tableView:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row == 0 {
imgName = "jan.jpeg"
}
else if indexPath.row == 1 {
imgName = "feb.jpeg"
}
else if indexPath.row == 2 {
imgName = "mar.jpeg"
}
else {
// Handle else
}
self.performSegueWithIdentifier("showImageSegue", sender: self)
}
You will need to go to interface builder and name the segue between your viewcontrollers to "showImageSegue".
Additionally, implement the prepareForSegue: function:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
let destinationVC = segue.destinationViewController as ImageViewController // Replace ImageViewController with whatever the name of your destination viewcontroller is.
destinationVC.imageName = imgName
}
Finally, inside the ImageViewController class, add this:
var imageName: String
override func viewDidLoad() {
let image = UIImage(named: imageName)
imageView = UIImageView(image: image)
imageView.frame = CGRect(origin: CGPointMake(0,0, 0,0), size: image.size)
}