Segue called twice using Text View Delegate - ios

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.

Related

How to pass array of data between UIViewControllers from #objc function

I have a Child UICollectionViewController where I have an array of images.
When I delete any photo I want to send back that array of updated images to Parent UIViewController.
Also in Child controller I have a programatically view which is called when I click on any image to expand it. When the image is expanded the user can click on a Delete button to delete photos from that array.
My array is updated correctly after delete but I can't manage to send it back to parent for some reasons.
I tried to send it back using Delegates and Protocols.
Here is my code for child controller:
protocol ListImagesDelegate {
func receiveImagesUpdated(data: [String]?)
}
class ListImagesVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
// Properties
var receivedImagesPath: [String]? = []
var fullscreenImageView = UIImageView()
var indexOfSelectedImage = 0
var imagesAfterDelete: [String]? = []
var delegate: ListImagesDefectDelegate?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("imagesAfterDelete: \(imagesAfterDelete ?? [])") // I'm getting the right number of images in this array.
delegate?.receiveImagesUpdated(data: imagesAfterDelete)
}
...
...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Click on photo: \(indexPath.item + 1)")
if let imagePath = receivedImagesPath?[indexPath.item] {
guard let selectedImage = loadImageFromDiskWith(fileName: imagePath) else {return}
setupFullscreenImageView(image: selectedImage)
indexOfSelectedImage = indexPath.item
}
}
private func setupFullscreenImageView(image: UIImage){
fullscreenImageView = UIImageView(image: image)
fullscreenImageView.frame = UIScreen.main.bounds
fullscreenImageView.backgroundColor = .black
fullscreenImageView.contentMode = .scaleAspectFit
fullscreenImageView.isUserInteractionEnabled = true
self.view.addSubview(fullscreenImageView)
self.navigationController?.isNavigationBarHidden = true
self.tabBarController?.tabBar.isHidden = true
let deleteButton = UIButton(frame: CGRect(x: fullscreenImageView.bounds.maxX - 50, y: fullscreenImageView.bounds.maxY - 75, width: 30, height: 40))
deleteButton.autoresizingMask = [.flexibleLeftMargin, .flexibleBottomMargin]
deleteButton.backgroundColor = .black
deleteButton.setImage(UIImage(named: "trashIcon"), for: .normal)
deleteButton.addTarget(self, action: #selector(deleteButtonTapped), for: .touchUpInside)
fullscreenImageView.addSubview(deleteButton)
}
#objc func deleteButtonTapped(button: UIButton!) {
print("Delete button tapped")
receivedImagesPath?.remove(at: indexOfSelectedImage)
imagesAfterDelete = receivedImagesPath
collectionView.reloadData()
self.navigationController?.isNavigationBarHidden = false
self.tabBarController?.tabBar.isHidden = false
fullscreenImageView.removeFromSuperview()
}
Here is the Parent controller:
var updatedImages: [String]? = []
...
...
extension NewAlbumVC: ListImagesDelegate {
func receiveImagesUpdated(data: [String]?) {
print("New array: \(data ?? [])") // This print is never called.
updatedImages = data
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "goToImages" {
let listImagesVC = segue.destination as! ListImagesVC
listImagesVC.delegate = self
}
}
}
I want to specify that my child controller have set a Storyboard ID ("ListImagesID") and also a segue identifier from parent to child ("goToImages"). Can cause this any conflict ?
Thanks if you read this.
It appears that the delegate is nil here
delegate?.receiveImagesUpdated(data: imagesAfterDelete)
For this
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
to trigger you must have
self.performSegue(withIdentifier:"goToImages",sender:nil)
Edit: This
let listImagesDefectVC = storyboard?.instantiateViewController(withIdentifier: "ListImagesDefectID") as! ListImagesDefectVC
listImagesDefectVC.receivedImagesPath = imagesPath
navigationController?.pushViewController(listImagesDefectVC, animated: true)
doesn't trigger prepareForSegue , so add
listImagesDefectV.delegate = self
So finally
Solution 1 :
#objc func tapOnImageView() {
let listImagesDefectVC = storyboard?.instantiateViewController(withIdentifier: "ListImagesDefectID") as! ListImagesDefectVC
listImagesDefectVC.receivedImagesPath = imagesPath
listImagesDefectVC.delegate = self
navigationController?.pushViewController(listImagesDefectVC, animated: true)
}
Solution 2 :
#objc func tapOnImageView() {
self.performSegue(withIdentifier:"goToImages",sender:nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "goToImages" {
let listImagesVC = segue.destination as! ListImagesVC
listImagesVC.receivedImagesPath = imagesPath
listImagesVC.delegate = self
}
}

why my back button is not triggered in the navigation bar stack show segue?

I have 3 VC in here. from the blue one, the user can go either to green or the red one based on some logic.
if the user go from blue to red one, the Back button in the red VC can be used. but if the user go from blue to green, the back button in the green VC can't be used, the user can't go back to the blue VC from greenVC, the problem is only in the greenVC
the video / .gif is in here : http://g.recordit.co/yxSiEd6ji7.gif
the trigger in the blueVC is this code
if needSpeaker {
performSegue(withIdentifier: "goToStep5", sender: nil)
} else {
performSegue(withIdentifier: "goToStep6", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
if segue.identifier == "goToStep5" {
let createEventStep5VC = segue.destination as! CreateEventStep5VC
createEventStep5VC.newEvent = newEvent
} else if segue.identifier == "goToStep6" {
let createEventStep6VC = segue.destination as! CreateEventStep6VC
createEventStep6VC.newEvent = newEvent
}
}
and here is the code in the greenVC :
import UIKit
class CreateEventStep5VC: UIViewController {
#IBOutlet weak var speakerNameLabel: UILabel!
#IBOutlet weak var questionTitleLabel: UILabel!
#IBOutlet weak var speakerNameTextField: UITextField!
var newEvent : [String:Any]!
override func viewDidLoad() {
super.viewDidLoad()
print("step 5")
print(newEvent)
// initial value
speakerNameLabel.text = ""
// to make the cursor directly to the textfield when the user open the VC
speakerNameTextField.becomeFirstResponder()
setQuestionTitle()
}
#IBAction func nextStepButtonDidPressed(_ sender: Any) {
guard let speakerName = speakerNameLabel.text, !speakerName.isEmpty, speakerName != " " else {
showAlert(alertTitle: "Mohon Maaf", alertMessage: "Anda harus mengisi nama pematerinya terlebih dahulu.", actionTitle: "Kembali")
return
}
newEvent["speaker"] = speakerName
performSegue(withIdentifier: "goToStep6", sender: nil)
}
// to remove keyboard / cursor if we touch other area
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(false)
}
}
extension CreateEventStep5VC {
func setQuestionTitle() {
let eventTypes = newEvent["eventType"] as! [EventType]
if eventTypes.contains(.ceramah) {
questionTitleLabel.text = "Siapa Nama Penceramahnya?"
}
}
}
extension CreateEventStep5VC {
// MARK : Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "goToStep6" {
let createEventStep6VC = segue.destination as! CreateEventStep6VC
createEventStep6VC.newEvent = newEvent
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
}
}
extension CreateEventStep5VC : UITextFieldDelegate {
// to make live update to UILabel
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let text = textField.text ?? ""
speakerNameLabel.text = (text as NSString).replacingCharacters(in: range, with: string)
return true
}
}
what went wrong in here?

Segueing from TableView with many sections

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
}
}

Get image click inside UI table view cell

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) {
}

NSInvalidArgumentException reason: 'Storyboard doesn't contain a view controller with identifier 'CenterViewController''

Error: Failed to instantiate the default view controller for UIMainStoryboardFile 'Main' - perhaps the designated entry point is not set?
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Storyboard (<UIStoryboard: 0x7ff949770c30>) doesn't contain a view controller with identifier 'CenterViewController''
I dont' know what's wrong with my Main Storyboard.I was building slide out navigation panel of my own.
CenterViewController.swift
import UIKit
#objc
protocol CenterViewControllerDelegate {
optional func toggleLeftPanel()
optional func collapseSidePanels()
}
class CenterViewController: UIViewController, SideMenuPanelViewControllerDelegate {
//#IBOutlet weak private var testimageView: UIImageView!
#IBOutlet weak private var testLabel:UILabel!
var delegate: CenterViewControllerDelegate?
// MARK: Button actions
#IBAction func MenuTapped(sender: AnyObject) {
delegate?.toggleLeftPanel?()
}
func localSelected(local: LocalMenus) {
//imageView.image = animal.image
testLabel.text = local.title
delegate?.collapseSidePanels?()
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
ContainerViewController.swift
import UIKit
import QuartzCore
enum SideOutState {
case BothCollapsed
case LeftPanelExpanded
case RightPanelExpanded
}
class ContainerViewController: UIViewController, CenterViewControllerDelegate, UIGestureRecognizerDelegate{
var centerNavigationController: UINavigationController!
var centerViewController: CenterViewController!
var currentState: SideOutState = .BothCollapsed {
didSet {
let shouldShowShadow = currentState != .BothCollapsed
showShadowForCenterViewController(shouldShowShadow)
}
}
var leftMenuController: SideMenuPanelViewController?
let centerPanelExpandedOffset: CGFloat = 60
override func viewDidLoad() {
super.viewDidLoad()
centerViewController = UIStoryboard.centerViewController()
centerViewController.delegate = self
// wrap the centerViewController in a navigation controller, so we can push views to it
// and display bar button items in the navigation bar
centerNavigationController = UINavigationController(rootViewController: centerViewController)
view.addSubview(centerNavigationController.view)
addChildViewController(centerNavigationController)
centerNavigationController.didMoveToParentViewController(self)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: "handlePanGesture:")
centerNavigationController.view.addGestureRecognizer(panGestureRecognizer)
}
// MARK: CenterViewController delegate methods
func toggleLeftPanel() {
let notAlreadyExpanded = (currentState != .LeftPanelExpanded)
if notAlreadyExpanded {
addLeftPanelViewController()
}
animateLeftPanel(shouldExpand: notAlreadyExpanded)
}
func collapseSidePanels() {
switch (currentState) {
case .LeftPanelExpanded:
toggleLeftPanel()
default:
break
}
}
func addLeftPanelViewController() {
if (leftMenuController == nil) {
leftMenuController = UIStoryboard.leftMenuController()
leftMenuController!.local = LocalMenus.allLocal()
addChildSidePanelController(leftMenuController!)
}
}
func addChildSidePanelController(sidePanelController:SideMenuPanelViewController) {
sidePanelController.delegate = centerViewController
view.insertSubview(sidePanelController.view, atIndex: 0)
addChildViewController(sidePanelController)
sidePanelController.didMoveToParentViewController(self)
}
func animateLeftPanel(#shouldExpand: Bool) {
if (shouldExpand) {
currentState = .LeftPanelExpanded
animateCenterPanelXPosition(targetPosition: CGRectGetWidth(centerNavigationController.view.frame) - centerPanelExpandedOffset)
} else {
animateCenterPanelXPosition(targetPosition: 0) { finished in
self.currentState = .BothCollapsed
self.leftMenuController!.view.removeFromSuperview()
self.leftMenuController = nil;
}
}
}
func animateCenterPanelXPosition(#targetPosition: CGFloat, completion: ((Bool) -> Void)! = nil) {
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: .CurveEaseInOut, animations: {
self.centerNavigationController.view.frame.origin.x = targetPosition
}, completion: completion)
}
func showShadowForCenterViewController(shouldShowShadow: Bool) {
if (shouldShowShadow) {
centerNavigationController.view.layer.shadowOpacity = 0.8
} else {
centerNavigationController.view.layer.shadowOpacity = 0.0
}
}
// MARK: Gesture recognizer
func handlePanGesture(recognizer: UIPanGestureRecognizer) {
// we can determine whether the user is revealing the left or right
// panel by looking at the velocity of the gesture
let gestureIsDraggingFromLeftToRight = (recognizer.velocityInView(view).x > 0)
switch(recognizer.state) {
case .Began:
if (currentState == .BothCollapsed) {
// If the user starts panning, and neither panel is visible
// then show the correct panel based on the pan direction
if (gestureIsDraggingFromLeftToRight) {
addLeftPanelViewController()
}
showShadowForCenterViewController(true)
}
case .Changed:
// If the user is already panning, translate the center view controller's
// view by the amount that the user has panned
recognizer.view!.center.x = recognizer.view!.center.x + recognizer.translationInView(view).x
recognizer.setTranslation(CGPointZero, inView: view)
case .Ended:
// When the pan ends, check whether the left or right view controller is visible
if (leftMenuController != nil) {
// animate the side panel open or closed based on whether the view has moved more or less than halfway
let hasMovedGreaterThanHalfway = recognizer.view!.center.x > view.bounds.size.width
animateLeftPanel(shouldExpand: hasMovedGreaterThanHalfway)
}
default:
break
}
}
}
private extension UIStoryboard {
class func mainStoryboard() -> UIStoryboard {
return UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
}
class func leftMenuController() -> SideMenuPanelViewController? {
return mainStoryboard().instantiateViewControllerWithIdentifier("SideMenuPanelViewController") as? SideMenuPanelViewController
}
class func centerViewController() -> CenterViewController? {
return mainStoryboard().instantiateViewControllerWithIdentifier("CenterViewController") as? CenterViewController
}
}
Very simple.
In your main storyboard, you have no ViewController with a Storyboard ID "CenterViewController"
Look here:
https://stackoverflow.com/a/11604827/3324388
Short answer from Xcode 8.0 onwards,
Go to the Main.storyboard select the target ViewController, press Command+option+3 to display the attributes
Fill the StoryBoard ID input field
Check the Use Storyboard ID checkbox
I know this could be found on the duplicated referred answer but you need to read, try and error etc.

Resources