How to avoid a trail of delegate calls - ios

I have a UIViewController which has a UICollectionView, inside that I have a ListSectionController class that controls a UICollectionViewCell, and inside that cell I have a UIView subclass.
When a button is pressed I need to call a method from the UIViewController. Currently I have a trail of delegate methods as it works it's way back to the view controller like this:
class MyView {
// delegate is the cell that the view is contained in
#IBAction func buttonPress() {
delegate?.myDelegateMethod()
}
}
extension MyCell : MyViewDelegate {
// The delegate is the section controller
func myDelegateMethod() {
delegate?.myDelegateMethod()
}
}
... etc
This seem like a lot of code duplication and a bit of a waste of space. How can I improve that?

When a button is pressed I need to call a method from the UIViewController
One way: give the button a nil-targeted action and implement the action method in the UIViewController. The message will arrive automatically.
For example, we give the button a nil-targeted action:
class Dummy {
#objc func buttonPressed(_:Any) {}
}
button.addTarget(nil,
action: #selector(Dummy.buttonPressed),
for: .touchUpInside)
And in the view controller we have:
#objc func buttonPressed(_ sender: Any) {
This will work, because the view controller is located up the responder chain from the button. This is exactly what nil-targeted actions are for.
Another way is to use NotificationCenter and Notification. I think that’s perfectly appropriate in this situation as well.

You can reach straight up the responder chain to any parent view or view controller in a generic, type safe way:
extension UIResponder {
func firstParent<T: UIResponder>(ofType type: T.Type ) -> T? {
return next as? T ?? next.flatMap { $0.firstParent(ofType: type) }
}
}
guard let ListSectionController = firstParent(ofType: ListSectionController) else {
return // we aren't in a ListSectionController
}
//Call ListSectionController methods here

I have encountered this condition so many time hence I would like to state obvious first:
"If your cell (and views inside it) are responsible of performing one and only one action (may be that is form button inside it)", you can use didSelectItemAtIndexPath". By designing your view with UIImageView. This approach have some UX issue such as, highlight and all but those can be handled with delegate as well.
If that is not the case and you cell is performing more than one action Mat's answer give best approaches.

Related

Assign delegate to a class for another class from inside third class

I have worked with delegate pattern for passing data in the past but that was one-to-one sort of interaction like say I need to pass data back from ViewController B to ViewController A and I set the delegate property defined in B from inside A. Usually we need this kind of delegation.
But I have certain condition where I need to set the delegate property from inside the third, not a ViewController, but a class
Here's how it is laid out -
protocol DataPassingDelegate {
func reloadData()
}
class ButtonView: UIButton {
// Some function that decide which ViewController is to be displayed
func destinationVCDecider() {
// parentController fetched the ViewController in which the button is laid out
let destinationVCObject = self.parentController.storyboard?.instantiateViewController(withIdentifier: Constants.STORYBOARD_IDENTIFIER.JOB_DETAILS_VIEW_CONTROLLER) as! JobDetailsViewController
// Setup for passing data via delegate
let jobsVCObject = JobsViewController()
destinationVCObject.delegate = jobsVCObject
// Displaying the Details of the job
parentController.navigationController?.pushViewController(destinationVCObject, animated: true)
}
}
class JobsViewController: UIViewController,DataPassingDelegate {
func reloadData() {
// Reload the jobs from the server
}
}
class JobDetailsViewController: UIViewController {
weak var delegate: DataPassingDelegate?
func navigateBack() {
delegate?.reloadData()
}
}
navigateBack() inside JobDetailsViewController will be called when certain event has been triggered
Now, when the navigateBack() is called, the delegate property turns out to be nil
Earlier I used to assign self in cases where there was one-to-one interaction but here there are a few classes between them that I don't want to pass them all around
Your approach here is correct. You need to debug it. Create your JobsViewController's instance like this-
let vc = UIStoryboard(name: "Name", bundle: nil).instantiateViewController(identifier: "ViewID") as JobsViewController.
You can debug whether delegate instance is being passed or not by putting a breakpoint in ViewDidLoad method of JobDetailsViewController.
Another approach you can follow is to use NotificationCenter

Choppy animation while pushing UITableViewController with static cells

I am having a UITableViewController with static cells. When I am performing a push segue, the animation is somewhat choppy. I have figured out which line of code is giving the problem. In viewWillAppear(_:) method of the UITableViewController, I am setting self.tableview.isHidden = true. If I remove this line of code then it works fine. However, I need this line as I am making a network call and I want to show the tableview only after the response is received. Any solutions to this issue would be appreciated.
You should set the TableView's Hidden property from the Storyboard. You can find the checkbox for this under "View > Drawing" in Attributes inspector. You can find the screenshot for this here.
That being said, you should find a better approach to indicate that API calls are being made. I would use a protocol that your viewcontrollers could conform to.
protocol ActivityIndicating {
func showLoading()
func hideLoading()
}
And in your ViewController class, you would have something like this
class ViewController: UIViewController, ActivityIndicating {
//protocol methods
func showLoading() {
//implement logic to hide tableview, show indicator, etc.
}
func hideLoading() {
//implement logic to show tableview, hide indicator, etc.
}
func someFunctionThatMakesAPIcalls() {
showLoading()
//makeAPICall and call hideLoading() once the api succeeds or fails
}
}

Value of type [UITableViewCell] has no member 'performSegueWithIdentifier'

If a label is tapped in a table view cell, I would like to perform a Segue to take you to another page.
I get the following error Value of type [UITableViewCell] has no member 'performSegueWithIdentifier'
class IdeaCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
let tapUser = UITapGestureRecognizer(target: self, action: #selector(IdeaCell.goToUserPage(_:)))
tapUser.numberOfTapsRequired = 1
usernameLbl.addGestureRecognizer(tapUser)
}
func goToUserPage(sender: UITapGestureRecognizer) {
self.performSegueWithIdentifier("goToTableFromOriginal", sender: nil)
}
}
Rather than trying to add a tap gesture recognizer to your cells, it would be simpler and cleaner if you implemented the table(_:didSelectRowAt:) method (Swift 3 version of the method signature) in the view controller that manages the table view. (Possibly a table view controller?)
Failing that, you'll need to create a protocol called something like TableViewCellDelegate that includes a didSelect(cell:UITableViewCell) method (or something along those lines). You'd then add a delegate property to your custom table view cells and set that property to the view controller in the cellForRow(at:) method.
Then in the cell, you'd invoke your didSelect(cell:UITableViewCell) on the cell's delegate.
In your view controller you'd use indexPath(for cell: UITableViewCell) to figure out which cell was tapped and use that to decide what action to take on the tapped cell (invoking a segue, in your case.)

How to send values to a parent view controller in Swift

I am working on a settings view controller screen for my iOS app written in swift. I am using a navigation controller to run the main settings table view which shows the cell titled, "Input Method." The current method is listed on the right of the cell. They can click the cell to go to the next view controller where they can select the input method that they'd like.
From here, there are two sections. The first is the input method to choose (touchscreen or joystick). The second section is joystick specific on whether or not the person is a lefty or righty. I don't want to have the vc unwind when they choose one box because they may choose one in another section too.
My question: How can I update the text field in the parent controller from the child controller.
Problems I'm having for optional solutions:
let parentVC: UIViewController = (self.navigationController?.parentViewController)!
parentVC.inputMethod.text? = cellSelected // This doesn't work because it cannot find the label inputMethod.
viewDidLoad() will cause a lag and the user sees the old method before it changes.
I cannot find out how to run a segue when someone clicks the back button at the upper left hand side in the navigation controller, since the navigation controller controls the segue.
It is not a good idea to cast the parent view controller, even when you are sure which class represents. I'll do it with a protocol:
In the child controller add a protocol like:
protocol ChildNameDelegate {
func dataChanged(str: String)
}
class ChildClass {
weak var delegate: ChildNameDelegate?
func whereTheChangesAreMade(data: String) {
delegate?.dataChanged(data)
}
}
And in the parent:
class ParentClass: ChildNameDelegate {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
guard let segueId = segue.identifier else { return }
switch segueId {
case "childSegue":
let destVC = segue.destinationViewController as! ChildClass
destVC.delegate = self
break
default:
break
}
}
// Child Delegate
func dataChanged(str: String) {
// Do whatever you need with the data
}
}
You need to cast the parentViewController to whatever custom class it has. For example, if the parent has the class ExampleParentController, you would write:
let parentVC = (self.navigationController?.parentViewController)! as! ExampleParentController
parentVC.inputMethod.text? = cellSelected
I found a solution here: Modifing one variable from another view controller swift
http://www.raywenderlich.com/115300/swift-2-tutorial-part-3-tuples-protocols-delegates-and-table-views
Instead of trying to access the view controller directly (which would be easier if it weren't returning a nil for the view controller) you can use a delegate method to adjust the variables.
The delegate worked like a charm!

Change value / image in view controller by selecting a cell within a collection view within an embedded container

In a detail view controller, I've a 'featureImage' in the top left, and a thin horizontal strip of images below this. The strip of images is an embedded container view managed by a custom CollectionViewController, which shows an array of images. The initial featureImage is the first image in an array of images[0], and the same array is passed to the collection view.
I'd like the featureImage to update to the same image if a cell in the container view is selected / tapped.
I guess I need to call the delegate method didSelectItemAtIndexPath, which will give me the indexPath. Right? But then how do I pass the indexPath, which is already from a delegate, back to the detail view controller.
EDITED - The code shows code overlap and differences between Responder Chain AND delegate approaches. Uncommented in the didSelectItemAtIndex path, the Responder Chain approach works, while the delegate approach does not.
Protocol defined and included at top of DetailViewController (I doesn't seem to matter which file the protocol is in, and is only typed to class to allow the delegate property to be 'weak'). Needed for both approaches.
protocol FeatureImageController: class {
func featureImageSelected(indexPath: NSIndexPath)
}
class DetailViewController: UIViewController, FeatureImageController {
Delegate property declared in the custom UICollectionViewController class. Only needed for delegate approach.
weak var delegate: FeatureImageController?
Delegate property initiated in the DetailViewController. Only needed for delegate approach.
override func viewDidLoad() {
super.viewDidLoad()
let photoCollectionVC = PhotoCollectionVC()
photoCollectionVC.delegate = self as FeatureImageController ... }
The Responder Chain (active) OR the delegate approach (commented out) within the collection view controllers didSelectItemAtIndexPath method.
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath)
{
if let imageSelector = targetForAction("featureImageSelected:", withSender: self) as? FeatureImageController {
imageSelector.featureImageSelected(indexPath)
}
// self.delegate?.featureImageSelected(indexPath)
}
Delegate method in DetailViewController. Needed for both.
func featureImageSelected(indexPath: NSIndexPath) {
record?.featureImage = record?.images[indexPath.row]
self.configureView()
}
The communication of data selection between View Controllers in my experience can best be achieved in two ways- the delegation or responder chain route. Either way the first step would be creating a protocol that your DetailViewController will adhere to. Something like:
protocol FeatureImageController: class {
func featureImageSelected(image: UIImage)
}
Your DetailViewController would then implement this function and use it to change the 'feature image'. How this is communicated then depends on whether you use delegation or the responder chain.
Delegation
If you prefer to use delegation then declare a delegate property on your CollectionViewController like so:
weak var delegate: FeatureImageController?
then in didSelectItemAtIndexPath you would determine the selected image using the provided indexPath and pass it to your delegate:
delegate?.featureImageSelected(selectedImage)
where selectedImage is the image selected from the collection view.
Responder Chain
If you decide to use the responder chain then you need not declare a delegate property. Instead you would ask for the first target that responds to your protocol method. So inside didSelectItemAtIndexPath you would say:
if let imageController = targetForAction("featureImageSelected:", withSender: self) as? FeatureImageController {
imageController.featureImageSelected(selectedImage)
}
Both methods (delegation or responder chain) allow the collection view controller to pass its selection to the detail controller. The delegation route is more common in the Framework but I find as we use containers within containers more often it becomes pretty nasty to properly manage the chain of delegates without an amount of 'coupling' I'm not comfortable with. The responder chain, on the other hand, is already provided by the framework to 'dig' into the hierarchy of controllers to find one willing to handle your action.

Resources