how to properly pass data from one viewcontroller's selected cell to previous viewcontroller? - swift

suppose I have two viewcontroller called A and B. In my viewcontroller B I have a tableview in it. When I selected a cell in my tableview, I want to pass that information back to A.
I have a dictionary that is of the following:
myData = [String: DataModel]
where DataModel takes the form of
struct DataModel{
var address = ""
var name = ""
}
I want to send the selected cell's key in B back to A. How should I go about doing that?
thanks for your help

Add this before class BViewController:
protocol ClassBViewControllerDelegate: class {
func didSelectTableViewCell(onRow row: Int)
}
Create a delegate property in BViewController:
weak var delegate: ClassBViewControllerDelegate?
Implement tableView delegate method tableView(_:didSelectRowAt:)
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath:NSIndexPath) {
let row = indexPath.row
delegate?.didSelectTableViewCell(onRow: row)
}
Tell ClassAViewController that its conforms to ClassBViewControllerDelegate as such:
class ClassAViewController: UIViewController, ClassBViewControllerDelegate {
Bind ClassAViewController and ClassBViewController at an appropriate place in ClassAViewController such a, for instance, prepareForSegue:sender:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "SegueIdentifierXYZ" {
if let vc = segue.destination as? ClassBViewController {
vc.delegate = self
}
}
}
Use delegate method didSelectTableViewCell(onRow row: Int) of delegate contract ClassBViewControllerDelegate in ClassAViewController:
func didSelectTableViewCell(onRow row: Int) {
print("Selected table view row is:", row)
}

Using completion block or by using delegates you can achieve this.
Using blocks you can code like this:
In your B VC create one property for completion block.
var cellSelectionCallBackHandler: ((DataModel, Int) -> Void)?
Set the cellSelectionCallBackHandler property from VC A.
VCObjectB.cellSelectionCallBackHandler = { (data, index) in
// Use your data here
}
From VC B call the completion handler on selecting cell like this
cellSelectionCallBackHandler?(yourData, index)
Any doubt plz comment.

A delegate method can be used to achieve it. The place/method where you call to dismiss B call the delegate. You can implement this delegate in A.
You can also have a singleton dataHandler class, where you can set and get required properties and can access it from anywhere within your project.

Related

Usability of a button inside a UICollectionViewCell?

I have a ProductVC.swift (ProductViewController) file and a ProductCell.swift. The ProductVC contains a UICollectinView and ProductCell is a specific UICollectionViewCell.
ProductCell.xib looks like this:
ProductVC contains an array with all the cell data (products) and populates the cells.
My goal: The user should have the possibility to like an product. He can do it by clicking the like button on the top right corner of every cell. Every cell shows a specific product which is specified by a productID.
My Problem: The like button action (IBAction func) is in the ProductCell. ProductCell doesn´t have the cell data. Cell data is stored in ProductVC in an array. So I don´t know how catch the product(productID) the user wants to like.
My Tries: With the code below I can get the indexPath of the cell where the user clicked the like button. But I can´t use this indexPath to get the product data because the data is stored in ProductVC. I could also store the data in ProductCell but it is not a clean way. Is it possible mb to give this indexPath to the ProductVC?
extension UICollectionView {
func indexPathForView(_ view: UIView) -> IndexPath? {
let center = view.center
let viewCenter = self.convert(center, from: view.superview)
let indexPath = self.indexPathForItem(at: viewCenter)
return indexPath
}
}
let superview = self.superview as! UICollectionView
if let indexPath = superview.indexPathForView(button) {
print(indexPath) // indexPath of the cell where the button was pressed
}
SOLVED Solution is a callback closure:
//UICollectionViewCell
var saveProductLike: ((_ index: Int) -> Void)?
#IBAction func likedButtonClicked(_ sender: UIButton) {
print("Liked button clicked!")
let productArrayIndex = calculateProductArrayIndex(for: sender)
saveProductLike?(productArrayIndex!)
}
//UIViewController
cell.saveProductLike = { (index) -> Void in
print(index)
}
There are several approaches to solve this but I'll talk about the most common one which is using delegation.
protocol ProductCellDelegate: AnyObject {
func productCellDidPressLikeButton(_ cell: ProductCell)
}
in ProductCell define a property weak var delegate: ProductCellDelegate? and in the target action of the like button inform your delegate
#objc private likeButtonPressed(_ sender: Any) {
delegate?.productCellDidPressLikeButton(self)
}
In your view controller you could conform to the protocol and implement it like this:
func productCellDidPressLikeButton(_ cell: ProductCell) {
guard let ip = collectionView.indexPath(for: cell) else { return }
// process event, get product via index...
}
Then you need to set the view controller to be the delegate in collectionView(_:willDisplay:forItemAt:) or
collectionView(_:cellForItemAt:): cell.delegate = self.

Crash when tapping on a WatchKit table row

I can display several table rows but when I tap one of those rows, the app crashes at tableView.setNumberOfRows(:)
I don't have any clue of what is going on. Any advise or tips are welcome. Thanks in advance.
I don't understand why this happens because at the initial setup this code runs without errors. I get this error only when I tap one of the table rows.
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
#IBOutlet var tableView: WKInterfaceTable!
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Configure interface objects here.
loadTableData()
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
private func loadTableData() {
tableView.setNumberOfRows(Categories.NotLocalizedCategories.count, withRowType: "RowController")
for (index, category) in Categories.LocalizedCategories.enumerated() {
if let rowController = tableView.rowController(at: index) as? RowController {
rowController.rowLabel.setText(category)
}
}
}
override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) {
pushController(withName: "DetailInterfaceController", context: Categories.LocalizedCategories[rowIndex])
}
}
Apparently, either tableView or the context Categories.LocalizedCategories[rowIndex] is nil. Use the if let construct or the guard statement before passing the context.
This is happening because probably the DetailInterfaceController class, more precisely in the awake method, is being called super.awake, when doing this, the InterfaceController class will be instantiated again and the awake of this and everything inside is executed. Since the table reference for the storyboard is weak, its instance of the table will be null and then it will crash. One solution to this is to remove super.awake from the controllers you push.

Checking Segue after it initiated

I have two UIViewController: A, B
Lets say there are two segues connecting them: C, D
Once a segue has been activated and I am in view B, can I know which segue got me here? C or D?
I don't know of any built-in mechanism for this, but you could have all your destination view controllers conform to a protocol SourceSegueProtocol that has a var to contain the invoking segue.
Then in the source view controller's prepare(for:sender:) method you could set that variable for destination view controllers that conform to the SourceSegueProtocol.
There is a prepare(for: segue) function that allows you to set a property in the new ViewController.
class OriginViewController : UIViewController {
...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? SegueProtocol {
destination.transitionSegue = segue.identifier
}
}
}
class DestinationViewController : UIViewController, SegueProtocol {
var transitionSegue: String = ""
override func viewDidLoad() {
print("Segue: ", transitionSegue)
}
}
protocol SegueProtocol {
var transitionSegue : String { get set }
}
Edit: As per comment suggestion, it's better to expect a destination that conforms to a protocol rather than one of a specific type.

Swift 3 - Get data back from segue

I have 4 views which contains a segue(it sends data from a json and store it in a variable called email) which i'm passing it like this:
A -> B(email) -> C(email)
... -> B(email) -> D(email)
So what i want to do is send back the information of the segue to:
C-> B
D-> B
Any idea how can i achieve this?
I think you're after an unwind segue.
To quote the docs:
...use the segue object to fetch the view controller being dismissed so that you can retrieve data from it.
Please see Creating an Unwind Segue on the View Controller Programming Guide for iOS.
The common way for passing data back is to use delegation. Delegation in Swift is done using protocols. So, for example, you declare the following protocol (please, use meaningful names for protocol and properties, the following is just an example):
protocol COrDDelegate {
func willReturn(someString: String)
}
Then, you add corresponding delegate properties to C and D view controllers:
class C: UIViewController {
weak var delegate: COrDDelegate?
...
}
(the same goes for D). You'll have to use weak modifier to avoid retain cycles. After that, call delegate's method right before dismissing the view controller:
func dismissMe() {
self.delegate?.willReturn(someString: "Meaningful data")
self.dismiss(animated: true, completion: nil)
}
In B, you implement the protocol:
extension B: COrDDelegate {
func willReturn(someString: String) {
print(someString)
}
}
and assign the delegate property to C or D in prepare(for:sender:) segue preparation method (just like you are probably doing for email):
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let c = segue.destination as? C {
c.email = self.email
c.delegate = self
}
}
Hopefully, I was able to convey the general idea. Good luck!

Segue w/ Tab View Controller Keeps Passing Value

I am working on an iOS application that is built around a Tab View Controller. I have created a "Contacts" tab, where a user can find and select a contact from a list. When the user selects the contact, it takes the contact's name and passes it to a different tab. That function is being done like so:
func passName(name: String) {
let navTab = self.tabBarController!.viewControllers![2] as! UINavigationController
let homeTab = navTab.viewControllers[0] as! MainController
homeTab.passedName = name
tabBarController?.selectedIndex = 2
}
Everything works as it should so far (name is loaded into text field). My issue is that the value seems to keep coming back every time I change tabs and then go back to my Home tab. For example, if I select "John" from my contacts, it will take me to the Home Tab and put John's name in a textfield. Let's say I delete the last two letters of the name, so now it is "Jo". If I load a different tab and come back, the name field has been reset to "John". It's as if the value gets re-passed every time I open the Home Tab. Also, every time I load the Home Tab after passing a name, my console prints: "Name Passed: John", so it shows that this is being processed every single time the tab appears. Here is my code for processing the name:
var passedName: String!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
//Checks if name was passed to controller
if let validName = passedName {
print("Name passed: \(validName)")
nameTextField.text = validName
}
}
Am I passing the data incorrectly? I was thinking it might be because I have the above code being called in the viewWillAppear method, but that doesn't make sense, as essentially the data is only being passed one time from the Contacts tab. Thanks!
The problem is that you're not actually passing the value back to the original view. Apple's recommendation for passing information between classes is to use the delegate pattern. This allows the modal view to call the delegate class's function, which changes the name local to the original view because that function is declared in the original view's viewController. You can read more about the pattern in this tutorial, but I've also included a brief example relevant to your use case below.
mainViewController:
class namesTableViewController: UITableViewController, editNameDetailsViewControllerDelegate {
var name : String
#IBAction func editButtonPressed(_ sender: UIBarButtonItem) {
performSegue(withIdentifier: "editPerson", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "editPerson" { //Modal segue
let navController = segue.destination as! UINavigationController
let controller = navController.topViewController as! editNameViewController
controller.delegate = self
if let person = sender as? Person {
print("Sending person to edit")
controller.personToEdit = person
}
} else {
super.prepare(for: segue, sender: sender)
}
}
//Protocol function
func changeName(n: String, controller: UIViewController) {
name = n
dismiss(animated: true, completion: nil)
}
}
editNameViewController:
class editNameViewController: UIViewController {
#IBOutlet weak var personNameTextField: UITextField!
var personToEdit : Person?
weak var delegate : PersonTableViewController?
override func viewDidLoad() {
super.viewDidLoad()
if personToEdit != nil {
personNameTextField.text = personToEdit?.name
}
}
// Button Actions
#IBAction func saveButtonPressed(_ sender: UIBarButtonItem) {
delegate?.personDetailsView(n: personNameTextField.text, controller: self)
}
}
Finally, the protocol class :
protocol editNameDetailsViewControllerDelegate : class {
func personDetailsView(n: String, controller: UIViewController)
}
Hope this helps.
The problem is "passedName" variable doesn't changed its value every time you edit it in your UITextField. Keep in mind that every time you change tabs, the UIViewController will call viewWillAppear and viewDidAppear. So your UITextField will always show passedName value once you select other tab and return.
I suggest that every time you edit the textfield you should update passedName value.
Sorry for my bad english.