I wonder if anybody can explain the difference between using a unwind segue and using delegation in the example I have below:
I have a FriendsTableViewController populated by an array of friends and another AddFriendTableViewController with a function to add a friend to the FriendsTableViewController.
This is how I send the data from my AddFriendViewController with a unwind segue:
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Check whether the Done button was tapped.
// If it wasn’t, instead of saving the friend, the method returns without doing anything else.
if (sender != self.doneButton) return;
// See whether there’s text in the text field.
if (self.nameTextField.text.length > 0) {
// If there’s text, create a new friend and give it's properties the input from the text fields.
self.friend = [[Friend alloc] initWithName:self.nameTextField.text
dateOfBirth:self.birthdayDatePicker.date
numberOfGifts:0];
}
}
This is how I add the data from the AddFriendTableViewController to the array in FriendsTableViewController with the unwind segue action:
#pragma mark - Actions
- (IBAction)unwindSegue:(UIStoryboardSegue *)segue
{
// Retrieve the source view controller, the controller that is unwinding from.
AddFriendTableViewController *soruce = [segue sourceViewController];
// Retrieve the soruce view controller’s friend object.
Friend *friend = soruce.friend;
// See whether the item exists.
// If it’s nil, either the Cancel button closed the screen or the text field had no text, so you don’t want to save the item.
if (friend != nil) {
// If it does exist, add the item to the friends array.
[self.friends addObject:friend];
// Reload the data in the table.
[self.tableView reloadData];
}
}
Now this works as I want so I hope I'm not breaking any stackoverflow rules or offending anyone, but I just wanted to know what is the difference between the way my example code is used and if the same scenario was made but with custom delegate methods for the AddFriendViewController. If some can explain it would be great!
Using an unwind segue is very similar to having a delegate, with the following advantages:
You don't need to implement any dismiss logic
You don't need to pass references up and down the navigation stack
You don't need to declare a delegate protocol
It's trivial to unwind back a number of stages in your app
The disadvantages are
Dependent on using storyboards (which may hamper reusability)
If there are a lot of them, it can lead to the same messiness as you can get in prepareForSegue with a lot of branching off identifier names
If you decide to present the view controller via some other method (not a segue) then you can't unwind back from it
The code you have looks good. I'd stick with it.
Related
I am rebuilding an application where I need to show a list of items. This list is retrieved using an API or retrieved from Core Data and shown in a UITableViewController. The problem I am having is that there are already seven different lists where there are small differences. Mostly of them are just different items, but also a search bar included in one list and previously stated one list will not load using an API, but from Core Data.
In my Storyboard I have added a UITableViewController with the class ItemsTableViewController which has a designed UITableViewCell. I have added an identifier to this cell so I can reuse it inside this view controller. There is a segue to this view controller from the home screen.
The idea was to create one parent object (ItemsTableViewController) and add multiple child objects (SavedItemsTableViewController, LocalItemsTableViewController, etc.) which will all use the shared logic of the parent with only some small changes (API and some custom things).
What I currently have is working, but without the child objects:
- (void)offlineButtonPressed {
[self performSegueWithIdentifier:#"openItemsTableViewController" sender:#(ItemListOffline)];
//[[self navigationController] pushViewController:[[OfflineItemsTableViewController alloc] init] animated:YES];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:#"openItemsTableViewController"]) {
switch((ItemList)[sender unsignedIntegerValue]) {
case ItemListOffline: { [(ItemsTableViewController *)[segue destinationViewController] retrieveOfflineDocuments]; break; }
case ItemListSearch: {
[(ItemsTableViewController *)[segue destinationViewController] retrieveDocumentsWithQuery:#""];
[(ItemsTableViewController *)[segue destinationViewController] addSearchBar];
break;
}
// Loop through all the list...
default: { NSAssert(NO, #"Unhandled type of document list."); break; }
}
}
}
So the application is calling just a function of the parent object which handles the request. But as you probably can feel, the object will be less maintainable. In comments I pushed the child view controller, but since it doesn't include the storyboard view, I need to recreate the cell from scratch, which doesn't feel good. Plus since the reuse identifier, it is not working since it is unable to dequeue a cell with the identifier set in the tableView:cellForRowAtIndexPath: method.
Now I am wondering what would be the best practice for this? It seems I can't use the save UITableViewController from the storyboard for multiple child classes, can I? But creating seven (and probably a even more in the future) controllers in the storyboard where I need to copy the cells to each controller and just give them different classes doesn't seem the way to do it. And add a method to the parent where the list would be retrieved differently and changing some stuff like adding a search bar is working, but also not the nicest way it seems.
Update: The logic in the ItemsTableViewController is pretty simple. In the delegate and datasource I handle the documents almost the same. The method that does the retrieving per list type is something like:
- (void)retrieveOfflineItems {
[self startLoading];
[[APIManager instance] getOfflineItems:^(NSArray<ItemList *> *list, NSError *error) {
[self setDocuments:list];
[[self tableView] reloadData];
}];
}
But there are more things, like the search has to add a search bar (once the view is loaded). So it needs to call multiple methods when performing the segue.
You can have a single UITableViewController, which includes all of your possible table cells, and as part of the prepareForSegue call, you should set an type identifier for the controller and the data - no matter where the data came from.
Within the UITableViewController class, you can hide / display the features that you need for this data type - such as the search bar, and in the tableView methods, choose which data source you need.
This way, the one UITableViewController class that you need to maintain is a little more complex than one dedicated class, but a lot more maintainable than 7 or more!
Assuming you have set up a variable dataType to identify the type of data you need, you could have something like this for the numberOfRowsInSection, and then similar for the other tableView methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
switch self.dataType {
case dataType.MyFirstDataType:
return myFirstDataTypeArray.count
case dataType.MySecondDataType:
return mySecondDataTypeArray.count
case dataType.TheCoreDataType:
return myCoreDataArray.count
default:
break
}
}
Create a parent UITableViewController with all subviews and cells including searchController. Initialize it and add/remove views and cells based on conditions
Still fairly new to iOS. I've managed to write a basic app to display a list/table of documents. I've included:
cell.accessoryType = UITableViewCellAccessoryDetailButton;
which display a blue i within a circle button which I guess can be used to perform some sort of action.
What I'd like to do now is, upon click, to display a subsequent screen with information about the document, buttons, add, delete functions, date, file size etc...
Is this done via segue or some other method?
Being a novice I am not sure what is/are the next step(s). If I know what steps I must take in order to get to next scree(s) I can search the net for example of how to do any given step.
Thank you for your help
The accessoryButton triggers it's own delegate method, distinct from the row selection, called:
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
Add that method to your delegate, and then do a vc transition. There are several ways to do that transition, most of the common ones are discussed here...
First, welcome to the iOS developing community!
In order to do this, I would use a segue to a detail view that you can design. When you select a row in your table using didSelectRowAtIndexPath, you would set an #property to the object in your array that was selected: self.selectedObject = self.tableviewarray objectAtIndex:indexPath.row; Then in the prepareForSegue method you can get the destination view controller and do destinationViewController.myObject = self.selectedObject; Now the detail view knows what object to display info for!
You'll need to implement the delegate method
-tableView:(UITableView*) didSelectRowAtIndexPath:(NSIndexPath*)
and handle the row tap in that callback accordingly.
An example would be like this:
-tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
YourObject *theModelForRow = [_itemList objectAtIndex:indexPath.row];
YourViewController *someNewViewController = [YourViewController viewControllerWithModel:theModelForRow];
[self.navigationController pushViewController:someNewViewController animated:YES];
}
I have a cameraViewController that is essentially a barcode scanner. I also have 3 view controllers (A, B and C) each with a button that lead to this cameraViewController.
When cameraViewController scans a barcode, it does the following:
if (self.detectionString != nil)
{
[self.delegate cameraViewController:self withCardNumber:self.detectionString];
[self.navigationController popViewControllerAnimated:YES ];
break;
}
It has a delegate and sends the detected string back to the previous/parent view controller.
All three viewControllers have the following methodimplemented:
#pragma mark - CameraViewControllerDelegate
- (void)cameraViewController:(CameraViewController *)cameraViewController withCardNumber:(NSString *)number
{
self.CardNumbertext.text = number ;
}
So both methods work with cameraViewController and viewControllerA. However, when the parentViewController is B or C, the cameraViewController still pops back to the right controller but the delegate function does not run. What am I doing wrong?
It's iffy to have just one instance of cameraViewController and three different view controllers that "fight over it" by each setting the cameraVC's delegate to themselves. I think it's a better use of system resources and better architecture if each of the A, B, C viewcontrollers responds to the button press by instantiating a new instance of CameraViewController and setting that instance's delegate to self. This should fix your issue and improve memory management/leak issues as well.
Post a notification to NSNotificationCenter, it's a much better solution to this problem :)
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/nsnotificationcenter_Class/Reference/Reference.html
If you want keep the current implementation then update the delegate when you are presenting ViewController A,B and C. From the way you described in the question seems like you need to do this in -(void)viewWillAppear or -(void)viewDidAppear. Or use the notification manager as the OP suggested or else use blocks.
I have a very complex situation (well for me) I am trying to resolve but thus far am having trouble with it.
I will outline the structure of the application now and then explain the problem I am having.
The names I am using are made up due to sensitivity of the data I am using.
secondToLastViewController // is a UITableView on the navigation stack
lastViewController // is just a normal UIView that i want to push onto the navigation stack
RequestClass // this class dose requests to my database and passed the data back to correct classes
getInfoClass // class is used for this specific request stores the information correctly and passes it back to secondToLastViewController
So as the user initiates didSelectRowAtIndexPath inside secondToLastViewController I make a request for the data using the RequestClass
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//..
[RequestClass Getinfo:storedInfoPram];
}
now the thread shoots off to my RequestClass, which in turn queries the DB for some data which is then received and this data is passed off to my getInfoClass the reason I have done this is because there are dozens and dozens of different calls in RequestClass all doing different things, this particular request brings back alot of data I have to sort into correct object types so have created this class to do that for me.
anyway inside getInfoClass I sort everything into their correct types etc and pass this data back to secondToLastViewController in a method called recivedData, this is also where I think things are going wrong... as I create a new instance of secondToLastViewController the thing is I dont know how to pass the data back to the same secondToLastViewController that is already on the stack and was where the original request came from.
- (void) recivedData {
// do some stuff then pass data back to secondToLastViewController
SecondToLastViewController *sec = [[SecondToLastViewController alloc] init];
[sec sendGetSeriesArrays:pram1 Pram2:pram2 Pram3:pram3 Pram4:pram4 Pram5:pram5];
}
Now going back into SecondToLastViewController the thread lands in this method
- (void)sendGetSeriesArrays:pram1 Pram2:pram2 Pram3:pram3 Pram4:pram4 Pram5:pram5{
// call detailed view onto the stack
lastViewController *last = [[lastViewController alloc] initWithNibName:#"lastViewController" bundle:nil];
[self.navigationController pushViewController:last animated:YES];
}
after the thread reaches this point nothing happens... all the data is there and ready to be sent but the new view is never pushed to the controller stack.. and I think it is due to me declaring another version of secondToLastViewController when I am inside getInfoClass
what I would like to know firstly is how do I pass the recived data in sendGetSeriesArrays to the final view and secondly how do i even load the lastview onto the navigation stack?
Your observation is correct you are creating the secondToLastViewController instance again inside the getInfoClass. Dont do like that you have to use delegate/protocol approach for passing the data back to the secondToLastViewController.
Do like this
Define a protocol in getInfo class
getInfoClass.h
#protocol GetInfoClassProtocol <NSObject>
//delegate method calling after getting data
// I dont know the argument types give it properly
- (void)sendGetSeriesArrays:pram1 Pram2:pram2 Pram3:pram3 Pram4:pram4 Pram5:pram5;
#end
// declare the delegate property
#property (assign, nonatomic)id<GetInfoClassProtocol>delegate;
getInfoClass.m
- (void) recivedData {
// do some stuff then pass data back to secondToLastViewController
if ([self.delegate respondsToSelector:#selector(sendGetSeriesArrays: param2:)])
{
[self.delegate sendGetSeriesArrays:pram1 Pram2:pram2 Pram3:pram3 Pram4:pram4 Pram5:pram5];
}
}
secondToLastViewController.m
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//..
RequestClass.delegate = self;
[RequestClass Getinfo:storedInfoPram];
}
Your secondToLastViewController should conform to the GetInfoClassProtocol
There are lots of ways you can accomplish this. In your revivedData function, instead of creating a new instance, you could:
1) Maintain a pointer to the navigation controller in getInfoClass, then you can get the last view controller from the view controllers on the navigation stack and use that. This will be the active instance of the view controller. There are ways to recover this from the window object, but those seem fragile and I would not recommend that approach.
2) You can pass a pointer to self from secondToLastViewController to your RequestClass getInfo call, then hold on to that and pass it back. This is probably a pain depending on the amount of code you have already.
3) You can maintain a static instance of the class if you will never have more than one secondToLastViewController. See How do I declare class-level properties in Objective-C?
I am working on an iOS app that uses a very common Core Data based tableview to display items and when one it selected, it shows a more detailed view, much like the Contacts app. The detail view itself is a programmatically generated grouped table with a custom (nib-defined) view for a header that has a picture and a name. Some of the cells in the table are custom cells that have a label name and a textbox value. In "edit" mode, the editable table cells (and the name in the header) have .clearButtonMode set to UITextFieldViewModeAlways to show that they are editable.
I am currently using the same view controller to display the detailed information, edit the information, and add a new record to the original list.
When a new item is being added, the view controller is created modally with a custom init overload that sets a flag in the view controller to indicate that it is adding the record. This allows it to start in edit mode and if edit mode is left, the model view is dropped away. The right menubar button is the usual Edit/Done, and the left one is a cancel button. When an existing item is being edited, the left button (normal back button) is replaced with a cancel button.
I am starting to have second thoughts as to whether or not having one view controller handle three different modes is the way to go. There are few issues that I am not sure how to handle.
1) How do I tell if edit mode is left by hitting "Done"? Is there an action for it? If cancel is hit, the action either dismisses itself (add mode) or restores the previous values leaves edit mode. I suppose I could put a check in my setEditing override to handle it, but it seems that there should be a better way.
2) When edit mode is entered and I set the editable text fields to UITextFieldViewModeAlways, is there a way to animate the appearance of the 'X' buttons so that they fade in with the editing indicators on the regular cells?
Are there easy solutions to these problems or is my 3-in-1 view controller a bad idea? It doesn't seem right to remake the same view for different modes, but having multiple modes for a view controller seems to be a bit of a hassle.
jorj
I like the 3-in-1 approach and use it all the time. There are lots of advantages: one xib, one view controller, one simple protocol between the list and detail view controllers. Yes, there are a few more checks like if (self.editing) ... but I like that better than more view controllers and xibs.
To help with adding I expose a BOOL that the delegate can set.
#property (nonatomic) BOOL adding;
1) The built-in editButtonItem does not allow you to intercept it before setEditing:animated: This is problematic when you are doing data validation after Done is tapped. For that reason I rarely use editButtonItem and use my own Edit, Done, and Cancel buttons with their own action methods. See below.
2) For this I like UITableView's reloadSections:withRowAnimation. It might work in your case.
- (void)edit:(id)sender
{
self.editing = YES;
}
- (void)done:(id)sender
{
// data validation here
if (everythingChecksOut)
{
//save here
} else {
return; //something didn't validate
}
//if control reaches here all is good
//let the delegate know what happened...
if (self.adding) {
[self.delegate didFinishAddingWithData:self.yourData];
} else {
[self.delegate didFinishEditingWithData:self.yourData];
}
self.adding = NO;
self.editing = NO;
}
- (void)cancel:(id)sender
{
[self.view endEditing:YES]; //in theory, forces the view that is editing to resign first responder
//in practise I find it doesn't work with the YES parameter and I have to use my own flag
// roll back any changes here
self.editing = NO;
if (self.adding) //let the delegate know we cancelled the add...
{
[self.delegate didCancelAdd];
}
}
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
//set your nav bar title
[self.tableview.editing = editing]; //you may or may not require this
[self.tableview reloadSections... withRowAnimation:yourChoice];
if (editing)
{
//install your Done and Cancel buttons
} else {
//remove Cancel and put the Edit button back
}
}
Then in viewDidLoad...
- (void)viewDidLoad
{
[super viewDidLoad];
//whatever else you do
if (self.adding)
{
self.editing = YES;
}
}
I haven't fully understood the questions you have raised, but here are some thoughts on structure which are probably more useful in the first instance...
It seems you are doing too much with a single UITableViewController and inevitably you will end up with lots of if statements and confusing code. I'd break it down into two separate UITableViewControllers, one to handle the main view (and any subsequent editing mode you require) and then another to handle the detail view. Either or both of these could then use nibs as you require.
Using two controllers like this will allow you to simply push the second detailViewController onto a navigation stack, rather than presenting it modally which doesn't seem like the obvious thing to do in this instance.
However, if you would prefer it to be presented modally, you could write a protocol for the detailView which sends messages in the event of 'Cancel', 'Edit' or 'Done' buttons being pushed. The first viewController could then implement the protocol and receive these events.
I hope that helps...