I have a collection view, and I am trying to get the index of the cell that I am peeking and poping from.
Issue
Currently I am using indexPathForItemAtPoint: however this always returns 0 no mater where I tap on the screen.
Code
collection view controller:
- (void)viewDidLoad {
[super viewDidLoad];
[self registerForPreviewingWithDelegate:self sourceView:self.collectionView];
}
- (UIViewController *) previewingContext:(id <UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location {
CellEditViewController *CEVC = [self.storyboard instantiateViewControllerWithIdentifier:#"detail"]; //The view I want peek/pop to
NSLog(#"Location: %f,%f", location.x, location.y);
NSLog(#"Index of: %lu", [[self.collectionView indexPathForItemAtPoint:location] row]);
[CEVC setPreferredContentSize:CGSizeMake(0.0, 320.0)];
return CEVC;
}
- (void)previewingContext:(id <UIViewControllerPreviewing>)previewingContext commitViewController:(UIViewController *)viewControllerToCommit {
[self showViewController:viewControllerToCommit sender:self];
}
What I have tried
Creating a new location to identify the index cell.
Moving registerForPreviewingWithDelegate:sourceView: to where I create each cell.
Moving previewingContext:viewControllerForLocation: and previewingContext:commitViewController: to the cell view method, this did not work for other reasons.
I do not think this is an issue with previewing, because when I implemented the same thing with a UITapGestureRecognizer, I got a similar output:
Tap recognizer:
- (void) processDoubleTap:(UITapGestureRecognizer *)sender {
NSLog(#"Got tapped twice");
if (sender.state == UIGestureRecognizerStateEnded) {
CGPoint point = [sender locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point];
NSLog(#"Index was: %lu", [indexPath row]);
if (indexPath) {
NSLog(#"Index was double tapped");
}
}
}
Output:
2017-12-25 10:48:13.990523-0800 quickthings[3052:356150] Got tapped twice
2017-12-25 10:48:13.990843-0800 quickthings[3052:356150] Index was: 0
Source
Github Repository
Screenshot
Here is what does happen, this is exactly what I want. The only other thing I would like to do is when the cell is tapped also be able to get the index of the tapped cell.
Collection View In Story Board
The (blue) UIView is "linked" to the Collection View Controller (the top view controller in the second screenshot below).
The Cause:
The main cause of the problem is how the CollectionViewController is setup in the storyboard. In summary these are the main issues with it:
The CollectionViewController was added as a UIViewController and then the class changed to CollectionViewController but the problem is that is a subclass of a UICollectionViewController. So there is a miss match between what the storyboard thinks it is and what it actually is. You should have added it as a UICollectionViewController to begin with.
The CollectionViewControllers top view has had it's class changed from UIView to UICollectionView which I assume was to match how the UICollectionViewController is setup. So again there is a miss match here meaning you can't see any of the correct properties in the Interface Builder.
There is then an additional UICollectionView added as a sub view of the main CollectionViewController main view. This has its data source and delegate linked into the CollectionViewController class and is the one that is actually being displayed. It's all setup correctly with a prototype cell. However it's not linked into the class as an IBOutlet so you can't reference it.
So why is this causing the problem you are seeing. In the CollectionViewController when you refer to self.collectionView you are referring to the top level one which is not the one that is displaying the cells. That's not the one displaying the cells and in fact has no displayed cells so when you ask for the cell at a particular point it will always return nil and hence you get a row value of zero. If you were able to get access to the UICollectionView actually displaying the cells you could get the correct value.
Other than that there will be other issues that you haven't come across yet.
The Solution:
There are basically two main solutions to this problem...
Remove the CollectionViewController from the storyboard and add it again as a UICollectionViewController. This of course is the ideal way to resolve it as it will all be setup correctly however it will require re-creating the entire controller in the storyboard again and is not absolutely necessary as you can do method 2...
You can modify your storyboard and setup to correctly reflect the setup you have and get it working.
I have tried method 2 and this is what I had to do to get it working correctly:
Change the CollectionViewController from subclassing UICollectionViewController to subclassing UIViewController as it will just become a holder for the UICollectionView. You also need to make it support UICollectionViewDataSource and UICollectionViewDelegate directly as the UICollectionViewController already does this but the UIViewController doesn't. So change this line in CollectionViewController.m:
#interface CollectionViewController : UICollectionViewController
to this:
#interface CollectionViewController : UIViewController <UICollectionViewDelegate, UICollectionViewDataSource>
Next in the storyboard change the top level view of the CollectionViewController back to a UIView (i.e. delete the UICollectionView in the class property). It's now just a holder for the real UICollectionView.
Next link the remaining UICollectionView that is setup in the storyboard to an IBOutlet in CollectionViewController.h called 'collectionView' so that all the references to self.collectionView are now referring to the correct thing.
One extra thing I had to do but I'm not sure why is change the cell identifier for the UITableViewController and UICollectionViewController so that they are not both 'Cell'. I just used 'TableCell' and 'CollectionCell'. If you don't do this they both get confused for some reason.
(I think that is all I did but if not I'm sure you will be able to handle any other issues)
Once all that has been done then the self.collectionView will be referring to the correct thing and you will get the correct index path in the - (UIViewController *) previewingContext:(id <UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location method.
Bonus Round:
Now some extra bonus comments. You appear to also have the same kind of issue with the TableViewController although it doesn't appear to be causing any issues at the moment. You may want to fix it though.
When I first ran the project I could not see anyway to add anything to the table because the text field and 'add' button were missing. This turned out to be because I was using an iPhone 8 simulator not an iPhone X simulator and once I switched everything was ok and I could see the text field and 'add' button. What this means is your interface is not adapting to the different device sizes correctly (at all really) and you should really address that.
Phew that ended up longer than I thought but hopefully will clear up the problem. Also as I had access to the git repository I could clone it and make my changes. I obviously haven't committed them as it's your repository but if you have any trouble understanding what I have done to get it to work I can commit them ether into your main branch or as a new branch.
Related
This question already has answers here:
How to know the UITableview row number
(10 answers)
How to get UITableView from UITableViewCell?
(21 answers)
Closed 8 years ago.
I've used this code for to get an UIButton from inside my UITableViewCell but it didn't work. So I've debugged it like half an hour to know what superview it in is.
UIButton *button = (UIButton *)sender;
UITableViewCell* cell = (UITableViewCell*)button.superview.superview.superview;
UITableView* tableView = (UITableView*) cell.superview.superview;
NSIndexPath* indexPath = [tableView indexPathForCell:cell];
The question for me is now: How can I know in the future in what hierarchy it is for a UITableView or others? Isn't there a list where I can see how much superviews I need to go without testing it 100 times and set BreakPoints to see what class it refers to?
Regards
You may just end up going with the superview hierarchy route, but know this is not a good idea because Apple could change their table view hierarchy any time. There are much better ways to get the index path for a table view cell given a button inside one of the cells.
If all the buttons are in one section of the table view, simply set each button's tag to the cell's index path row. Then you can recreate the index path knowing which row the button is in.
You can always subclass UITableViewCell and implement the action method for the button inside the subclass. The subclass might call a delegate method to whichever object wanted to know about the button tap.
So hopefully this gives you an idea of the alternatives to what you were doing before. As you said, it's time-consuming to figure out the superview hierarchy and it could change at any time.
If you want to check multiple superview hierarchy then check like that below:-
UIView * view=(UIView*)sender;
while ((view= [view superview])) {
if(viewisKindOfClass:[UITableView class]) {
//your code
return;
}
}
I need to get an array of all the subviews in a UIScrollView. Right now I'm using
NSArray *subviews = [myScrollView subviews];
but this seems to only be returning the subviews that are visible at the time the code is run. I need all the subviews in the whole extent of the UIScrollView, even those that are currently hidden (as in off screen). How would I get that?
Essentially, I'm looking for something like the contentSize property of a UIScrollView, except instead of returning just the size of the UIScrollView if it were big enough to display all of it's content, I want it to return the content itself.
EDIT: I think I've figured it out: the scroll view this isn't working for is actually a UITableView - and I think it's deque-ing the cells that are off screen on me, and that's why they aren't showing up. I'm going to do some testing to confirm.
Try with following code its working for me.
for(UIView * subView in myScrollView.subviews ) // here write Name of you ScrollView.
{
// Here You can Get all subViews of your myScrollView.
// But For Check subview is specific UIClass such like label, button, textFiled etc.. write following code (here checking for example UILabel class).
if([subView isKindOfClass:[UILabel class]]) // Check is SubView Class Is UILabel class?
{
// You can write code here for your UILabel;
}
}
tl;dr
It turns out that
NSArray *subviews = [myScrollView subviews];
will indeed return all the subviews in a UIScrollView *myScrollView, even if they are off-screen.
The Details
The problem I was actually having was that the scroll view I was trying to use this on was actually a UITableView, and when a UITableViewCell in a UITableView goes off-screen, it actually gets removed from the UITableView - so by the time I was calling subviews, the cells I was looking for were no longer in the scroll view.
My workaround was to build all of my UITableViewCells in a separate method called by my viewDidLoad, then put all of those cells into an array. Then, instead of using subviews, I just used that array. Of course, doing it this way hurts the performance a little (in cellForRowAtIndexPath you just return the cell from the array, which is slower than the dequeueReusableCellWithIdentifier method that is typically used), but it was the only way I could find to get the behavior I needed.
I'm currently trying both KIF and Subliminal for iOS integration testing. But I still cannot figure out how to simulate scrolling on table view or collection view using both frameworks. Like how to scroll to the bottom of table view or collection view.
EDIT 1:
I made simple single collection view app here https://github.com/nicnocquee/TestSubliminal
Basically I want to test the last cell's label. I cannot do
SLElement *lastLabel = [SLElement elementWithAccessibilityLabel:#"Label of the last cell"];
[lastLabel scrollToVisible];
because the label doesn't exist yet until the collection view is scrolled to the bottom.
EDIT 2:
I marked Aaron's answer as the answer. But Jeffrey's also works :)
You could also simulate the user scrolling through the collection looking for the cell, by dragging the collection view until the cell becomes visible:
while (!SLWaitUntilTrue([UIAElement(lastLabel) isValidAndVisible], 1.0)) {
[[SLWindow mainWindow] dragWithStartOffset:CGPointMake(0.5, 0.75) endOffset:CGPointMake(0.5, 0.25)];
}
Those offsets translate to dragging straight up along the middle of the collection view, from 75% down the view to 25% down the view. -isValidAndVisible lets you check for the cell's visibility without worrying about whether it exists yet (whereas -isVisible would throw an exception if the cell didn't exist). And I wrap -isValidAndVisible in SLWaitUntilTrue so that we let the collection view finish scrolling before dragging again.
In contrast to #AaronGolden's app hook solution, this approach requires you be able to identify a particular cell to scroll to. So I'd frame this approach as "scroll to a cell", whereas the app hook lets you "scroll to a position".
This is probably more invasive but so simple too -
Go to SLUIAElement.m and add the following methods:
- (void)scrollDown {
[self waitUntilTappable:NO thenSendMessage:#"scrollDown()"];
}
- (void)scrollUp {
[self waitUntilTappable:NO thenSendMessage:#"scrollUp()"];
}
You will also have to declare those method signature in the SLUIAElement.h file to make those new methods visible to the test suite.
Then what you can do is add an accessibility identifier to a collection view, call that identifier and scroll on it. EXAMPLE:
SLElement *scrollView = [SLElement elementWithAccessibilityIdentifier:#"scrollView"];
[scrollView scrollDown];
The issue is that the cell you're trying to find in your test case, the one with label "This is cell 19", does not exist until the collection view has already been scrolled. So we need to make the view scroll first and then look for the cell. The easiest way to make the collection view scroll with Subliminal is through an application hook. In (for example) your view controller's viewDidLoad method, you could register the view controller to respond to a particular message from any Subliminal test case, like so:
[[SLTestController sharedTestController] registerTarget:self forAction:#selector(scrollToBottom)];
and the view controller could implement that method as:
- (void)scrollToBottom {
[self.collectionView setContentOffset:CGPointMake(0.0, 1774.0)];
}
That 1774 is just the offset that happens to scroll the collection view in your test app all the way to the bottom. In a real application the app hook would probably be more sophisticated. (And in a real application you would want to make sure to call [[SLTestController sharedTestController] deregisterTarget:self] in your view controller's dealloc method.)
To trigger the scrollToBottom method from a Subliminal test case you can use:
[[SLTestController sharedTestController] sendAction:#selector(scrollToBottom)];
or the convenience macro:
SLAskApp(scrollToBottom);
The shared SLTestController will send the scrollToBottom message to the object that registered to receive it (your view controller).
When the sendAction or SLAskApp macro returns your cell 19 will already be visible, so you don't need to bother with the [lastLabel scrollToVisible] call anymore. Your complete test case could look like this:
- (void)testScrollingCase {
SLElement *label1 = [SLElement elementWithAccessibilityLabel:#"This is cell 0"];
SLAssertTrue([UIAElement(label1) isVisible], #"Cell 0 should be visible at this point");
SLElement *label5 = [SLElement elementWithAccessibilityLabel:#"This is cell 5"];
SLAssertFalse([UIAElement(label5) isValid], #"Cell 5 should not be visible at this point");
// Cause the collection view to scroll to the bottom.
SLAskApp(scrollToBottom);
SLElement *lastLabel = [SLElement elementWithAccessibilityLabel:#"This is cell 19"];
SLAssertTrue([UIAElement(lastLabel) isVisible], #"Last cell should be visible at this point");
}
I'm trying to create a view similar to add/edit contact in iOS and there are a few things that are happening, and I'm not sure how they are implemented. Any help in understanding is greatly appreciated.
For each section in contacts i.e. name, phone number, email, etc are these each their own tableview or are these sections within a larger tableview?
When clicking done when adding or editing a contact, the unused tableview cells disappear. Is this using deleteRowsAtIndexPaths:withRowAnimation: or is there a hide method I haven't found? If it is using that method, then when clicking the edit contact button, how does the view brings back these unused tableview cells?
When clicking on a cell in the tableview cell when editing a contact, you are able to change the text. Is this a textfield within a tableview cell or is it actually modifying the label of the tableview cell?
I am not looking for any specific code, as a fairly new programmer I am just trying to understand the strategies/best way to implement these features.
I tried a lot different ways to implement that. the easiest one: Subclass UITableViewCell and overwrite setFrame:. note that this is easy to achieve for grouped tables, but hard for plain ones. in the datasource's tableView:cellForRowAtIndexPath: create an object of this custom cell for the first section. use another identifier for cells of that section, so that only the correct cells will be re-used.
yes, I assume that. The controller has some sort of definition how many cells has to be show in edit mode and how many are actually used with some sort of information. you can easily create a array of indexPaths that must be deleted.
I would do it in tableView:didSelectRowAtIndexPath: by fetching the cell via tableView:cellForRowAtIndexPath:, hide the label and unhide or add a textfield and make this first responder.
code for 1.
the cell
#interface InsetCell : UITableViewCell
#property(nonatomic)CGFloat inset;
#end
#implementation InsetCell
- (void)setFrame:(CGRect)frame {
CGFloat inset;
if (_inset == 0) {
inset = 70; //default value
} else {
inset = _inset;
}
frame.origin.x += inset;
[super setFrame:frame];
}
-(void)setInset:(CGFloat)inset
{
_inset = inset;
[self setNeedsLayout];
}
#end
a project that uses similar code
I've got a UITableView that I'd like to stick a 44px subview on top of. I tried tableViewHeader, but that scrolls with the rest of the table.
I tried searching and have found many people saying I need to add a UIView superview and then add my header and the UITableView to it. However I can't find an example on exactly how to do this. I tried making a new UIView subclass and laying out the subviews in IB, but I ran into trouble getting the table controller to link w/ the UITable (because I don't know enough about IB).
How can I do this with XIBs? Can someone provide an example?
Thanks for any help you guys can provide.
I finally figured this out right after posting. Figures. :)
Here's what I did, in case others run into the same problem:
Delete the existing UITableViewController and its XIB. They're junk. Get really mad while you do.
Make a new UIViewController subclass with a XIB
Open XIB in IB and add your header stuff and a UITableView to the UIView
In the IB Outlets for UITableView make sure you connect Delegate and DataSource to your File Owner
In the header for your view controller, be sure to add <UITableViewDelegate, UITableViewDataSource> to implement these protocols
Implement all the regular UITableView delegate and data source methods you know and love, but in your UIViewController instead of the way you're used to doing it through UITableViewController
After this things should work.
The problem is, UITableViewController's view property is the same thing as the tableView property. I had the same problem, wanting to put some fixed content above the table. I didn't want to change the base class, as it provides lots of great functionality I didn't want to lose or disrupt.
The fix is actually easy. The trick is to create custom set and get for self.tableView property. Then, in loadView, you replace the view with a fresh UIView and add the tableView to it. Then you're free to add subviews around the tableView. Here's how it's done:
In header:
#interface CustomTableViewController : UITableViewController
{
UITableView *tableView;
}
In .m:
- (UITableView*)tableView
{
return tableView;
}
- (void)setTableView:(UITableView *)newTableView
{
if ( newTableView != tableView )
{
[tableView release];
tableView = [newTableView retain];
}
}
- (void)loadView {
[super loadView];
//save current tableview, then replace view with a regular uiview
self.tableView = (UITableView*)self.view;
UIView *replacementView = [[UIView alloc] initWithFrame:self.tableView.frame];
self.view = replacementView;
[replacementView release];
[self.view addSubview:self.tableView];
//code below adds some custom stuff above the table
UIView *customHeader = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 20)];
customHeader.backgroundColor = [UIColor redColor];
[self.view addSubview:customHeader];
[customHeader release];
self.tableView.frame = CGRectMake(0, customHeader.frame.size.height, self.view.frame.size.width, self.view.frame.size.height - customHeader.frame.size.height);
}
Enjoy!
Replace the TableViewController with a ViewController, inside it add a UIView with fixed height to place the fixed content you need, and below that add a UITableView.
Create an outlet to your TableView
#IBOutlet weak var tableView: UITableView!
You can reuse all the funcs you already have removing the override word for example
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCellWithIdentifier(cellId) as UITableViewCell!
c.textLabel?.text = "A title"
c.detailTextLabel?.text = "A subtitle"
return c
}
See this answer to add the automatic refresh control if you need it Pull to refresh UITableView without UITableViewController
As an option it is possible to embed UITableViewController as part of UI into another UIViewController with 'Container View' element (pick one in Interface Builder from the Object library (where all other views are) ).
This way you can use UITableViewController like ordinary view (in terms of positioning) and compose any layout you need without overwritting existing table view code
EDIT:
to further expand my answer, here are the steps to accomplish the described approach:
Open you storyboar in interface builder
Drag'n'drop a new ViewController element to the storyboard from Object Library to add a new controller.
As a child view, drag'n'drop Container View from Object Library and place it anywhere inside the ViewController
Container View creates another view controller and "embedded" segue as a connection. It's save to delete this viewcontroller and to connect the Container View with the required view controller (as per the questions it's UITableViewController)
To connect Container View with UITableViewController just select the container view and control+drag to the UITableViewController - select "Embed" in the popup menu
Now the controller will display inside the Container View with respect to the container's position and boundaries.
It's possible to get a link to the embeeded view controller - the system will fire "prepareForSegue" method of the host viewcontroller (created on the step 1 above) and the controller is in segue.destinationViewController property - one can customize it as required. Just make sure to set an identifier to the "embedded" segue in interface builder - this is the same process just like for any other segues.
Define a custom UIView in storyboard or xib, have a IBOutlet reference for that UIView in View Controller. In -(void)viewWillAppear:(BOOL)animated method write [self.tableView.superview addSubview:filterHeaderView];, here filterHeaderView is the IBOutlet reference for my header view which I want to add as fixed header in my tableview.