Responder Chain in Swift (nil target in UIButton target) - ios

I have got a problem using the responder chain in swift.
When I setup a buttons target using a nil target like:
someButton.addTarget(nil, action:"addButtonTapped:", forControlEvents: .TouchUpInside)
The action will be send up the responder chain until the action is handled in the controller. So far so good :-)
But I want to intercept the action, execute some code and relay it on to the controller. But I cannot find a way to do this in swift. In ObjC this task is easy to do so I guess there should be a way in swift too.
Thanks in advance for any help :-)

One of my co-workers gave me the hint to recreate the selector and send it manually again.
let selector = Selector("someButtonTapped:")
let target: AnyObject? = self.nextResponder()?.targetForAction(selector, withSender: button)
UIApplication.sharedApplication().sendAction(selector, to: target, from: self, forEvent: nil)
This recreates the responder chain and relays the new message to the next responder.
I hope that somebody will find this useful.

I wanted to show a different view controller after dismissing the current one. The
MyContainerViewController container view controller has a function to open a different view controller. Using the responder chain to present a different view controller after dismissing the current one avoids having to keep references or casting the parent view controller. This is especially convenient
when using lots of nested child and container view controllers.
class SomeChildViewController: UIViewController {
#IBAction func closeAndShowSomething(sender: Any?) {}
let showSelector = #selector(MyContainerViewController.showSomething(_:))
let viewController: Any? = next?.target(forAction: showSelector, withSender: nil)
dismiss(animated: true) {
UIApplication.shared.sendAction(showSelector, to: viewController, from: self, for: nil)
}
}
}

Related

Pushing a Duplicate View Controller

I'm trying to build a step by step questionnaire app with a View Controller called QuestionController whose content is dynamically populated via some globals in a constants file. When a user is done answering a question, I want to be able to be able to push another QuestionController for the next question.
#IBAction func goNext(_ sender: UIButton) {
let controller = QuestionController()
navigationController?.pushViewController(controller, animated: true)
}
As you can see, that is exactly as I have done but for whatever reason I'm getting a SIBABRT error. Any ideas why this might be? I don't have any lingering outlets and the only action is the next button callback you see here.
Instead of this
let controller = QuestionController()
load it like this with it's identifer in IB
let controller = storyboard.instantiateViewController(withIdentifier: "identifer") as! QuestionController

IOS swift UIBarButtonItem action

I've a table view with navigation controller embedded in. I've added a UIBarButtonItem (add) button. When I click this button it opens a new view where user enters the data and submits it (which makes a web service call) and returns back to the previous view. This navigation happens as shown below,
func addTapped(_ sender:UIBarButtonItem) {
print("Called Add")
let vc = (storyboard?.instantiateViewController( withIdentifier: "newNote")) as! newNoteVC
self.navigationController?.pushViewController(vc, animated: true)
}
And in new view I do following,
#IBAction func saveButton(_ sender: UIButton) {
if (self.noteDescription.text?.isEmpty)! {
print("Enter missing note description")
return
} else {
let desc = self.noteDescription.text
self.uploadNote(noteText: desc!, noteDate: self.dateInMilliseconds)
self.navigationController?.popViewController(animated: true)
}
}
This way a record gets saved and a view gets popped from the navigation controller stack but only thing I don't how to do is refresh the table view data in the parent view (where I might need to make a new http service call to read all the records).
I hope I'm able to explain the issue? Any help is appreciated.
As mentioned in the comments, making a service call just to update the tableview might be a overkill. However, if this is the business scenario which needs to be implemented, you can do the same in
func viewWillAppear
in the parent view controller. You can make the service call in this method and reload the table view with the data.
You would also need to check the overall navigation of the application as making service calls in viewWillAppear method is not a good approach as these gets called everytime the view is shown. For Ex: If coming back from a navigated view then also the method is called.

Presenting View Controller loses subviews when dismissing presented VC

I'm having some trouble playing around with two viewcontrollers that interact in a straightforward manner:
The homeViewController shows a to-do list, with an addTask button.
The addTask button will launch an additional viewController that acts as a "form" for the user to fill.
However, upon calling
self.dismissViewControllerAnimated(true, completion: nil);
inside the presented view controller I return to my home page, but it's blank white and it seems nothing can be seen except the highest-level view on the storyboard can be seen (i.e. the one that covers the entire screen).
All of my views, scenes, etc. were set up with autolayout in storyboard. I've looked around on Stack Overflow, which lead to me playing around with the auto-resizing subview parameter i.e.:
self.view.autoresizesSubviews = false;
to no avail. I'm either fixing the auto-resizing parameter wrong (in the wrong view of interest, or simply setting it wrong), or having some other problem.
Thanks in advance
edit:
I present the VC as follows:
func initAddNewTaskController(){
let addNewTaskVC = self.storyboard?.instantiateViewControllerWithIdentifier("AddNewTaskViewController") as! AddNewTaskViewController;
self.presentViewController(addNewTaskVC, animated: true, completion: nil);
}
edit2:
While I accept that using delegates or unwinding segue can indeed circumvent the problem I'm encountering (as campbell_souped suggests), I still don't understand what's fundamentally happening when I dismiss my view controller that causes a blank screen.
I understand that calling dismissViewControllerAnimated is passed onto the presenting view controller (in this case my homeViewController). Since I don't need to do any pre or post-dismissal configurations, the use of a delegate is (in my opinion) unnecessary here.
My current thought is that for some reason, when I invoke
dismissViewControllerAnimated(true, completion:nil);
in my addNewTaskViewController, it is actually releasing my homeViewController. I'm hoping someone can enlighten me regarding what it is exactly that I'm not understanding about how view controllers are presented/dismissed.
In a situation like this, I usually take one of two routes. Either set up a delegate on AddNewTaskViewController, or use an unwind segue.
With the delegate approach, set up a protocol:
protocol AddNewTaskViewControllerDelegate {
func didDismissNewTaskViewControllerWithSuccess(success: Bool)
}
Add an optional property that represents the delegate in your AddNewTaskViewController
var delegate: AddNewTaskViewControllerDelegate?
Then invoke the didDismissNewTaskViewControllerWithSuccess whenever you are about to dismiss AddNewTaskViewController:
If the record was added successfully:
self.delegate?.didDismissNewTaskViewControllerWithSuccess(true)
self.dismissViewControllerAnimated(true, completion: nil);
Or if there was a cancelation/ failure:
self.delegate?.didDismissNewTaskViewControllerWithSuccess(false)
self.dismissViewControllerAnimated(true, completion: nil);
Finally, set yourself as the delegate, modifying your previous snippet:
func initAddNewTaskController(){
let addNewTaskVC = self.storyboard?.instantiateViewControllerWithIdentifier("AddNewTaskViewController") as! AddNewTaskViewController;
self.presentViewController(addNewTaskVC, animated: true, completion: nil);
}
to this:
func initAddNewTaskController() {
guard let addNewTaskVC = self.storyboard?.instantiateViewControllerWithIdentifier("AddNewTaskViewController") as AddNewTaskViewController else { return }
addNewTaskVC.delegate = self
self.presentViewController(addNewTaskVC, animated: true, completion: nil);
}
...
}
// MARK: AddNewTaskViewControllerDelegate
extension homeViewController: AddNewTaskViewControllerDelegate {
func didDismissNewTaskViewControllerWithSuccess(success: Bool) {
if success {
self.tableView.reloadData()
}
}
}
[ Where the extension is outside of your homeViewController class ]
With the unwind segue approach, take a look at this Ray Wenderlich example:
http://www.raywenderlich.com/113394/storyboards-tutorial-in-ios-9-part-2
This approach involves Ctrl-dragging from your IBAction to the exit object above the view controller and then picking the correct action name from the popup menu

Pass data between three viewController, all in navigationController, popToRootView

The issue I'm having is this.
I have a navigation controller with 3 viewController. In the 1st controller, I have the user select an image. This image is passed to 2nd and 3rd controller via prepareForSegue.
At the 3rd controller, I have a button that takes the user back to the 1st view controller. I explored 2 ways in doing this:
1) use performSegue, but I don't like this because it just push the 1st controller to my navigation stack. So I have this weird "Back" button at the 1st Viewcontroller now, which is not what I want. I want the app to take user directly to 1st viewcontroller without the back button.
2) I tried Poptorootviewcontroller. This solves the issue of the "back" button. But, when I pop back to the 1st viewcontroller, the user's selected image is still on screen. I want to clear this image when the user goes from the 3rd viewcontroller back to the 1st viewcontroller.
So with approach 2), how do I make sure all memory is refreshed and the image becomes nil in the 1st viewcontroller? Since I'm not using performSegue, 3rd viewcontroller does not have access to the 1st Viewcontroller.
For refresh, you'd have to clear it in viewWillAppear but I find this rather dangerous. Best you can do there is to create a new copy of the view controller everytime and Swift will take care of the rest. I don't know if you are using the storyboard but I would recommend using the class UIStoryboard and the function instiantiateViewControllerWithIdentifier("something") as! YourCustomVC
As long as you stay in the navigation stack, you'll not lose any of the current configurations of previous View Controllers.
As for passing data back to the first controller. You can either just throw it in the global scope which is the easiest way but might be difficult to know when it was updated or if the data is fresh. But you can always just:
var something: String = ""
class someView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
something = "foo"
}
}
Something will be availabe everywhere then.
You could make a protocol and pass the delegate along the 3 view controllers. So when you are starting it you could do:
func someAction() {
let v = SomeViewController()
v.delegate = self
self.navigationController?.pushViewController(v, animated: true)
}
And then with each following view:
func someOtherAction() {
let v = SomeOtherViewController()
v.delegate = self.delegate
self.navigationController?.pushViewController(v, animated: true)
}
Although personally I find it hard to keep track of this.
Lastly you could use the NSNotificationCenter to pass an object along with all the data and catch it in a function on your first controller.
To do this you first register your VC for the action in viewDidLoad() or something:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "someAction:", name: "someNotification", object: nil)
Then when you are done in the 3rd view make some object or a collection of string and send it back as follows:
NSNotificationCenter.defaultCenter().postNotificationName("someNotification", object: CustomObject())
And then lastly you'll catch it in the function "someAction"
func someAction(note: NSNotification) {
if let object = note.object as? CustomObject {
//Do something with it
}
}
Hope this helps!
Use an unwind segue which provides the functionality to unwind from the 3rd to the 1st (root) view controller.
The unwind segue is tied to an action in the root view controller. Within this action, you simply nil the image:
#IBAction func unwindToRootViewController(sender: UIStoryboardSegue)
{
let sourceViewController = sender.sourceViewController
// Pull any data from the view controller which initiated the unwind segue.
// Nil the selected image
myImageView.image = nil
}
As you can see in the action, segues also let you pass data back from the source view controller. This is a much simpler approach than needing to resort to using delegates, notifications, or global variables.
It also helps keep things encapsulated, as the third view controller should never need to know specifics about a parent view controller, or try to nil any image that belongs to another view controller.
In general, you pass details to a controller, which then acts on it itself, instead of trying to manipulate another controller's internals.

Swift: Best way to get value from view

I have a custom UIView (called GridView) that I initialize and then add to a ViewController (DetailViewController). GridView contains several UIButtons and I would like to know in DetailViewController when those buttons are touched. I'm new to Swift and am wondering what is the best pattern to use to get those events?
If you want to do this with notifications, use 1:
func postNotificationName(_ notificationName: String,
object notificationSender: AnyObject?)
in the method that is triggered by your button. Then, in your DetailViewController, add a listener when it is initialized with 2:
func addObserver(_ notificationObserver: AnyObject,
selector notificationSelector: Selector,
name notificationName: String?,
object notificationSender: AnyObject?)
Both functions can be called from NSNotificationCenter.defaultCenter().
Another method would be to add callbacks which you connect once you initialize the GridView in your DetailViewController. A callback is essentially a closure:
var callback : (() -> Void)?
which you can instantiate when needed, e.g.
// In DetailViewController initialization
gridView = GridView()
gridView.callback = { self.doSomething() }
In GridView you can trigger the callback like this:
func onButton()
{
callback?()
}
The callback will only execute, if unwrapping succeeds. Please ensure, that you have read Automatic Reference Counting, because these constructs may lead to strong reference cycles.
What's the difference? You can connect the callback only once (at least with the method I've showed here), but when it triggers, the receiver immediately executes its code. For notifications, you can have multiple receivers but there is some delay in event delivery.
Lets assume your GridView implementation is like as follows:
class GridView : UIView {
// Initializing buttons
let button1:UIButton = UIButton(...)
let button2:UIButton = UIButton(...)
// ...
// Adding buttons to view
self.addSubview(button1)
self.addSubview(button2)
// ...
}
Now, we will add selector methods which will be called when a button is touched. Lets assume implementation of your view controller is like as follows:
class DetailViewController : UIViewController {
let myView:GridView = GridView(...)
myView.button1.addTarget(self, action: "actionForButton1:", forControlEvents: UIControlEvents.TouchUpInside)
myView.button2.addTarget(self, action: "actionForButton2:", forControlEvents: UIControlEvents.TouchUpInside)
// ...
func actionForButton1(sender: UIButton!) {
// Your actions when button 1 is pressed
}
// ... Selectors for other buttons
}
I have to say that my example approach is not a good example for encapsulation principles of Object-Oriented Programming, but I have written like this because you are new to Swift and this code is easy to understand. If you want to prevent duplicate codes such as writing different selectors for each button and if you want to set properties of your view as private to prevent access from "outside" like I just did in DetailViewController, there are much much better solutions. I hope it just helps you!
I think you better create a class called GridView that is inherited from the UIView. Then, you can connect all you UI element with you class as IBOutlet or whatever using tag something like that. Later on, you can ask the instance of GridView in DetailViewController so that you can connect as IBAction.
Encapsulation is one of the principles of OOP.

Resources