i can't seem to get this to work, i'm trying to implant a long press gesture on uicollectionview
found this code here in SO, now i just need to convert it to swift, so i can use it probely
obj-c
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
return;
}
CGPoint p = [gestureRecognizer locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:p];
if (indexPath == nil){
NSLog(#"couldn't find index path");
} else {
// get the cell at indexPath (the one you long pressed)
UICollectionViewCell* cell =
[self.collectionView cellForItemAtIndexPath:indexPath];
// do stuff with the cell
}
}
my approach in swift
func handleLongPress(gestureRecognizer: UILongPressGestureRecognizer){
if (gestureRecognizer.state != UIGestureRecognizerState.Ended){
return
}
let p: CGPoint = gestureRecognizer.locationInView(self.collectionView)
let indexPath: NSIndexPath = self.collectionView.indexPathForItemAtPoint(p)!
if (indexPath == nil) { // Error here 'NSIndexPath' is not convertible to 'UIGestureRecognizerState'
println("couldn't find index path")
} else {
let cell: UICollectionViewCell = self.collectionView.cellForItemAtIndexPath(indexPath)
}
}
appreciate any help, it fails at if (indexPath == nil) with the error 'NSIndexPath' is not convertible to 'UIGestureRecognizerState'
Try to use optional binding to find out whether an optional contains a value. indexPathForItemAtPoint returns an optional value.
if let indexPath = self.collectionView.indexPathForItemAtPoint(p)
{
let cell: UICollectionViewCell = self.collectionView.cellForItemAtIndexPath(indexPath)
}
else
{
println("couldn't find index path")
}
Read about optionals in Apple's swift book.
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/TheBasics.html#//apple_ref/doc/uid/TP40014097-CH5-ID309
You can also use optional chaining and map over the optional indexpath.
let cell = collectionView.indexPathForItemAtPoint(p).map {
self.collectionView.cellForItemAtIndexPath($0)
}
if let cell = cell {
// Do something with non-optional cell
} else {
println("No cell for point")
}
The cell variable will be a UICollectionViewCell? (optional) but that is the return type of the cellForItemAtIndexPath call anyway.
If you can continue with or return the optional cell you can skip the whole if let section especially if you don't need to print the error (or do if cell == nil { println("No cell for point") }
See this blogpost for some more ways to handle optionals: How I Handle Optionals In Swift
Related
I have took below website as reference, and tried to convert this to Swift code. Refer to “collectioViewLayout” part, I have difficulties to make things in order.
Appreciated that.UICollectionVIew circlesLayout
The original code is as below:-
- (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
if ([[self.collectionView.delegate class] conformsToProtocol:#protocol(UICollectionViewDelegateFlowLayout)]) {
return [(id)self.collectionView.delegate collectionView:self.collectionView layout:self sizeForItemAtIndexPath:indexPath];
}
return CGSizeMake(0, 0);}
My revised swift code as follows:-
func sizeForItem(at indexPath: IndexPath?) -> CGSize {
if type(of: collectionView.delegate) is UICollectionViewDelegateFlowLayout {
if let indexPath = indexPath {
return collectionView!.delegate?.collectionView!(collectionView!, layout: self, sizeForItemAt: indexPath) ?? CGSize.zero
}
return CGSize.zero
}
return CGSize(width: 0, height: 0)
}
However, some errors appeared. (see below)
Part 1) Cast from 'UICollectionViewDelegate?.Type' to unrelated type 'UICollectionViewDelegateFlowLayout' always fails
Part 2) Incorrect argument labels in call (have ':layout:sizeForItemAt:', expected':targetIndexPathForMoveFromItemAt:toProposedIndexPath:')
Is there anyone who can guide me how to fix those problem into correct swift code please?
The Objective-C code checks the type of collectionView.delegate, but in Swift you have to do a cast to tell the compiler that you are sure that it is indeed of type UICollectionViewDelegateFlowLayout{
func sizeForItem(at indexPath: IndexPath?) -> CGSize {
// "as?" does the cast, evaluating the expression to nil if it fails.
if let delegate = collectionView!.delegate as? UICollectionViewDelegateFlowLayout,
// you can combine these two checks into one if statement. Just separate them with ","
let indexPath = indexPath {
return delegate.collectionView?(collectionView, layout: self, sizeForItemAt: indexPath) ?? .zero
}
return .zero
}
I'm trying to covert Speakrbox code in Objective-C.
I have already converted most of the code but I have a little problem with this one:
private func updateCallDurationForVisibleCells() {
/*
Modify all the visible cells directly, since -[UITableView reloadData] resets a lot
of things on the table view like selection & editing states
*/
let visibleCells = tableView.visibleCells as! [CallSummaryTableViewCell]
guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows else { return }
for index in 0..<visibleCells.count {
let cell = visibleCells[index]
let indexPath = indexPathsForVisibleRows[index]
guard let call = call(at: indexPath) else { return }
cell.durationLabel?.text = durationLabelText(forCall: call)
}
}
I tried to convert it, and here is what I have:
-(void) updateCallDurationForVisibleCells :(id)sender {
/*
Modify all the visible cells directly, since -[UITableView reloadData] resets a lot
of things on the table view like selection & editing states
*/
_visibleCells = _tableview.visibleCells;
if(_indexPathsForVisibleRows == _tableview.indexPathsForVisibleRows) { return ; }
int index;
for (index=0; index<_visibleCells.count; index ++)
{
UICollectionViewCell *cell = _visibleCells[index];
NSIndexPath *indexPath = _indexPathsForVisibleRows[index];
UITableViewCell* call;
if((call = [self.tableView cellForRowAtIndexPath:indexPath]))
{ return ; }
}
}
Could any one please help me to convert this Swift code into Objective-C? The code doesn't compile for the reason that I didn't know how to convert this line of code:
cell.durationLabel?.text = durationLabelText(forCall: call)
Also, I don't know if I did it in the right way, especially the conversion of guard let.
you will find here Call and durationLabelText functions that I have used in pdateCallDurationForVisibleCells function:
private func call(at indexPath: IndexPath) -> SpeakerboxCall? {
return callManager?.calls[indexPath.row]
}//swift
private func durationLabelText(forCall call: SpeakerboxCall) -> String? {
return call.hasConnected ? callDurationFormatter.format(timeInterval: call.duration) : nil
}//Swift
Here's a fixed up version of your Objective-C code. Please note that when you see var or let in Swift, you are creating a local variable. Do not create a bunch a needless instance variables or properties in the Objective-C code when you should just use local variables.
- (void)updateCallDurationForVisibleCells {
/*
Modify all the visible cells directly, since -[UITableView reloadData] resets a lot
of things on the table view like selection & editing states
*/
NSArray *indexPathsForVisibleRows = self.tableView.indexPathsForVisibleRows;
if(!indexPathsForVisibleRows) { return ; }
NSArray *visibleCells = self.tableview.visibleCells;
for (NSInteger index = 0; index < _visibleCells.count; index++) {
CallSummaryTableViewCell *cell = visibleCells[index];
NSIndexPath *indexPath = indexPathsForVisibleRows[index];
SomeDataType *call = [self call:indexPath];
if (!call) { return; }
cell.durationLabel.text = [self durationLabelText:call];
}
}
There is some information missing which means this conversion may not be 100% correct.
You haven't shown the Objective-C signature of the call: method so I can't be sure how to call it properly and I don't know what data type to use for the call variable. Replace SomeDataType with the proper type.
You haven't shown the Objective-C signature of the durationLabelText: method so I can't be 100% sure about the proper way to call it.
If you post those details in your question I can be sure this answer is updated properly.
Edit: Thanks to users like #rmaddy, newbies like me get to learn how to write answers.Adding the final full code part.
Skip to the final part of the answer to get the working code.
In case you want to know where you went wrong, read the full answer.
There are multiple things that are possibly not right here, let me address what I know:
The guard statement is used for graceful exit. I'm no Java expert, but consider this to be like Exceptions in JAVA. The Obj-C code for your first guard would be:
if (nil == _tableview.indexPathsForVisibleRows){
return
}
This also means that there's no necessity for the _indexPathsForVisibleRows variable.
I'd recommend this article to understand guard better:
https://ericcerney.com/swift-guard-statement/
With respect to your second guard, please pay attention to detail.
call(at: indexPath) is not the same as cellForRowAtIndexPath.
call(at: indexPath) is a class function that returns a datatype, say CallType. cellForRowAtIndexPath is a UITableView function, that returns a cell.
The cell in the below line is more likely to be of type CallSummaryTableViewCell
UICollectionViewCell *cell = _visibleCells[index];
Regarding durationLabelText(), it seems to be a class function in the swift file whose declaration would be something like:
//Assume the type of "call" is CallType
private func durationLabelText(forCall call : CallType) -> String{
//your code here
}
The Objective-C equivalent would be:
-(NSString*) durationLabelTextForCall: (id<CallType>) call{
//your code here
}
The Objective-C call would be:
cell.durationLabel.text = [self durationLabelTextForCall: call]
So, the final code would be like this:
-(NSString*) durationLabelTextForCall: (id<CallType>) call{
//your code here
}
-(id<CallType>) callAtIndexPath:(NSIndexPath) indexPath{
//your code here
}
- (void)updateCallDurationForVisibleCells {
/*
Modify all the visible cells directly, since -[UITableView reloadData] resets a lot
of things on the table view like selection & editing states
*/
if (nil == _tableview.indexPathsForVisibleRows){
return
}
NSArray *visibleCells = self.tableview.visibleCells;
for (index=0; index<visibleCells.count; index ++)
{
CallSummaryTableViewCell *cell = visibleCells[index];
NSIndexPath *indexPath = _tableview.indexPathsForVisibleRows[index];
id<CallType> call = [self callAtIndexPath:indexPath];
if (nil == call){
return
}
cell.durationLabel.text = [self durationLabelTextForCall: call]
}
}
Can someone tell me if I have translated the first 2 lines correctly to Swift and if the first part is correctly? Also, could anyone help me figure out the rest. I can't figure out how to translate the if statement at the bottom..
[C addTarget:self action:#selector(outsideOfKey: forEvent:) forControlEvents:UIControlEventTouchDragOutside|UIControlEventTouchDragInside];
[C addTarget:self action:#selector(keyGetsLeft: forEvent:) forControlEvents:UIControlEventTouchUpOutside | UIControlEventTouchUpInside];
-(void) outsideOfKey:(id)sender forEvent:(UIEvent *)event
{
for(UITouch *t in [event allTouches])
{
CGPoint touchPoint = [t locationInView:window];
if(CGRectContainsPoint(C.frame, touchPoint))
{
C.highlighted = YES;
}
else{
C.highlighted = NO;
}
Translated to swift
C.addTarget(self, action:Selector("outsideOfKey:forEvent:"), forControlEvents:.TouchDragOutside)
C.addTarget(self, action:Selector("outsideOfKey:forEvent:"), forControlEvents:.TouchDragInside)
C.addTarget(self, action:Selector("keyGetsLeft:forEvent:"), forControlEvents:.TouchUpOutside)
C.addTarget(self, action:Selector("keyGetsLeft:forEvent:"), forControlEvents:.TouchUpInside)
func outsideOfKey (sender: AnyObject, forEvent: UIEvent) {
let touch = event.allTouches() as? UITouch
for touch
{
var touchPoint : CGPoint = touch.locationInView(window)
if(CGRectContainsPoint(C.frame, touchPoint))
{
C.highlighted = YES;
}
else{
C.highlighted = NO;
}
}
Try something like:
C.addTarget(self, action:Selector("outsideOfKey:forEvent:"), forControlEvents:.TouchDragOutside | .TouchDragInside)
C.addTarget(self, action:Selector("keyGetsLeft:forEvent:"), forControlEvents:.TouchUpOutside | .TouchUpInside)
func outsideOfKey(sender: AnyObject, forEvent event: UIEvent) {
if let touches = event.allTouches()?.allObjects as? [UITouch] {
for touch in touches {
var touchPoint : CGPoint = touch.locationInView(window)
if CGRectContainsPoint(C.frame, touchPoint) == true {
C.highlighted = true;
} else {
C.highlighted = false;
}
}
}
}
Points:
You can still use the "|" operator on enums in swift (most of the time)
You don't put parentheses around the conditional clause in swift
YES and NO are not valid in swift, you much use true and false
Swift does not support sets so it is easiest to get the touches as an Array so you can iterate using a simple swift for...in loop
The "if let ..." pattern is called optional binding, if the right hand side is not nil "touches" will be set to the value and the code in the following braces will be executed, otherwise it will skip the block of code
The way "?" is used after ".allTouches()" is called optional chaining, if .allTouches() returns nil, the whole expression will return nil
Hope this is of some use to you, let me know if you have any more swift queries!
I have a method of adding secondary nearby annotations (ann2) when I tap on another annotation (ann1). But when I deselect and re-select the exact same annotation (ann1) the ann2 re-creates it self and is getting added again. Is there a way to check if the annotation already exists on the map and if yes then do nothing otherwise add the new annotation. I have already checked this: Restrict Duplicate Annotation on MapView but it did not help me.. Any advice is appreciated. This is what I have so far:
fixedLocationsPin *pin = [[fixedLocationsPin alloc] init];
pin.title = [NSString stringWithFormat:#"%#",nearestPlace];
pin.subtitle = pinSubtitle;
pin.coordinate = CLLocationCoordinate2DMake(newObject.lat, newObject.lon);
for (fixedLocationsPin *pins in mapView.annotations) {
if (MKMapRectContainsPoint(mapView.visibleMapRect, MKMapPointForCoordinate (pins.coordinate))) {
NSLog(#"already in map");
}else{
[mapView addAnnotation:pin];
}
In this case I get the log already on map but I also get the drop animation of the annotation adding to the map. Any ideas?
Thank you in advance..
Your for loop isn't checking if the annotation is on the screen, it is checking if the coordinates of the pin are currently within the visible area. Even if it was checking if the pin object was already in the mapView.annotations it would never be true, because you've only just created pin a few lines earlier, it can't possibly be the same object as on in the mapView.annotations. It might though have the same coordinates and title, and that's what you need to check:
bool found = false;
for (fixedLocationsPin *existingPin in mapView.annotations)
{
if (([existingPin.title isEqualToString:pin.title] &&
(existingPin.coordinate.latitude == pin.coordinate.latitude)
(existingPin.coordinate.longitude == pin.coordinate.longitude))
{
NSLog(#"already in map");
found = true;
break;
}
}
if (!found)
{
[mapView addAnnotation:pin];
}
Annotations array exist in map object so you just have to check
if ( yourmap.annotations.count==0)
{
NSLog(#"no annotations");
}
NSNumber *latCord = [row valueForKey:#"latitude"];
NSNumber *longCord = [row valueForKey:#"longitude"];
NSString *title = [row valueForKey:#"name"];
CLLocationCoordinate2D coord;
coord.latitude = latCord.doubleValue;
coord.longitude = longCord.doubleValue;
MapAnnotation *annotation = [[MapAnnotation alloc]initWithCoordinate:coord withTitle:title];
if([mkMapView.annotations containsObject:annotation]==YES){
//add codes here if the map contains the annotation.
}else {
//add codes here if the annotation does not exist in the map.
}
if (sampleMapView.annotations.count > 0) {
sampleMapView.removeAnnotation(detailMapView.annotations.last!)
}
Following my comment on Craig's answer, I think the solution could look like something like this :
import MapKit
extension MKMapView {
func containsAnnotation(annotation: MKAnnotation) -> Bool {
if let existingAnnotations = self.annotations as? [MKAnnotation] {
for existingAnnotation in existingAnnotations {
if existingAnnotation.title == annotation.title
&& existingAnnotation.coordinate.latitude == annotation.coordinate.latitude
&& existingAnnotation.coordinate.longitude == annotation.coordinate.longitude {
return true
}
}
}
return false
}
}
This code allows you to check if a mapView contains a given annotation. Use this in a "for" loop on all your annotations:
for annotation in annotations {
if mapView.containsAnnotation(annotation) {
// do nothing
} else {
mapView.addAnnotation(annotation)
}
PS: this works well if you need to add new annotations to a mapView. But if you need also to remove entries, you may have to do the opposite: check that each existing annotation exists in the new array of annotations ; if not, remove it.
Or you could remove everything and add everything again (but then you will have the change animated ...)
I have a UICollectionView which I am trying to insert items into it dynamically/with animation. So I have some function that downloads images asynchronously and would like to insert the items in batches.
Once I have my data, I would like to do the following:
[self.collectionView performBatchUpdates:^{
for (UIImage *image in images) {
[self.collectionView insertItemsAtIndexPaths:****]
}
} completion:nil];
Now in place of the ***, I should be passing an array of NSIndexPaths, which should point to the location of the new items to be inserted. I am very confused since after providing the location, how do I provide the actual image that should be displayed at that position?
Thank you
UPDATE:
resultsSize contains the size of the data source array, self.results, before new data is added from the data at newImages.
[self.collectionView performBatchUpdates:^{
int resultsSize = [self.results count];
[self.results addObjectsFromArray:newImages];
NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];
for (int i = resultsSize; i < resultsSize + newImages.count; i++)
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
[self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
} completion:nil];
See Inserting, Deleting, and Moving Sections and Items from the "Collection View Programming Guide for iOS":
To insert, delete, or move a single section or item, you must follow
these steps:
Update the data in your data source object.
Call the appropriate method of the collection view to insert or delete the section or item.
It is critical that you update your data source before notifying the
collection view of any changes. The collection view methods assume
that your data source contains the currently correct data. If it does
not, the collection view might receive the wrong set of items from
your data source or ask for items that are not there and crash your
app.
So in your case, you must add an image to the collection view data source first and then call insertItemsAtIndexPaths. The collection view will then ask the data source delegate function to provide the view for the inserted item.
I just implemented that with Swift. So I would like to share my implementation.
First initialise an array of NSBlockOperations:
var blockOperations: [NSBlockOperation] = []
In controller will change, re-init the array:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
blockOperations.removeAll(keepCapacity: false)
}
In the did change object method:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
if type == NSFetchedResultsChangeType.Insert {
println("Insert Object: \(newIndexPath)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.insertItemsAtIndexPaths([newIndexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.Update {
println("Update Object: \(indexPath)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.reloadItemsAtIndexPaths([indexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.Move {
println("Move Object: \(indexPath)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.moveItemAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
}
})
)
}
else if type == NSFetchedResultsChangeType.Delete {
println("Delete Object: \(indexPath)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.deleteItemsAtIndexPaths([indexPath!])
}
})
)
}
}
In the did change section method:
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
if type == NSFetchedResultsChangeType.Insert {
println("Insert Section: \(sectionIndex)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.insertSections(NSIndexSet(index: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.Update {
println("Update Section: \(sectionIndex)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.reloadSections(NSIndexSet(index: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.Delete {
println("Delete Section: \(sectionIndex)")
blockOperations.append(
NSBlockOperation(block: { [weak self] in
if let this = self {
this.collectionView!.deleteSections(NSIndexSet(index: sectionIndex))
}
})
)
}
}
And finally, in the did controller did change content method:
func controllerDidChangeContent(controller: NSFetchedResultsController) {
collectionView!.performBatchUpdates({ () -> Void in
for operation: NSBlockOperation in self.blockOperations {
operation.start()
}
}, completion: { (finished) -> Void in
self.blockOperations.removeAll(keepCapacity: false)
})
}
I personally added some code in the deinit method as well, in order to cancel the operations when the ViewController is about to get deallocated:
deinit {
// Cancel all block operations when VC deallocates
for operation: NSBlockOperation in blockOperations {
operation.cancel()
}
blockOperations.removeAll(keepCapacity: false)
}
I was facing the similar issue while deleting the item from index and this is what i think we need to do while using performBatchUpdates: method.
1# first call deleteItemAtIndexPath to delete the item from collection view.
2# Delete the element from array.
3# Update collection view by reloading data.
[self.collectionView performBatchUpdates:^{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:sender.tag inSection:0];
[self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
[self.addNewDocumentArray removeObjectAtIndex:sender.tag];
} completion:^(BOOL finished) {
[self.collectionView reloadData];
}];
This help me to remove all the crash and assertion failures.