UIDragInteraction / UIDropItem get frame of preview - objective-c

Currently I’m trying to get the frame of my drag/drop interaction object preview view. I can get the position of my finger thanks to:
- (CGPoint)locationInView:(UIView *)view;
I’ve searched through everything and can’t find a way to access the preview view object, or a frame reference. Has anyone figured this out?

Figured this one out - I found out there's a method called after the actual drop function that provides a UITargetedDragPreview object. So my logic is to store the drop session and interaction, then call my "update" function when this preview function has been called.
First, I create some variables for future use:
CGRect destinationFrame; // destination frame.
UIDropInteraction *storedInteraction; // stored interaction.
id <UIDropSession> storedSession; // stored session.
- (UITargetedDragPreview *)dropInteraction:(UIDropInteraction *)interaction previewForDroppingItem:(UIDragItem *)item withDefault:(UITargetedDragPreview *)defaultPreview {
if (!item.localObject) { return nil; }
CGPoint center = defaultPreview.target.center;
CGPoint converted = [defaultPreview.target.container convertPoint:center toView:<TARGETVIEW>];
CGPoint origin = CGPointMake(converted.x - (defaultPreview.view.frame.size.width / 2), converted.y - (defaultPreview.view.frame.size.height / 2));
self->destinationFrame = CGRectMake(origin.x, origin.y, defaultPreview.view.frame.size.width, defaultPreview.view.frame.size.height);
if (self->storedSession && self->storedInteraction) {
[self performDropWithInteraction:self->storedInteraction session:self->storedSession];
}
return nil;
}
- (void)dropInteraction:(UIDropInteraction *)interaction performDrop:(id<UIDropSession>)session {
self->storedInteraction = interaction;
self->storedSession = session;
}
- (void)performDropWithInteraction:(UIDropInteraction *)interaction session:(id<UIDropSession>)session {
// this where I will perform my actual drop, I get the frame with:
// self->destinationFrame.
}

Related

How to get bounds of current map view?

How do I automatically get the bounds of the current map view? I've been looking everywhere. The Google Maps API has it: http://www.w3schools.com/googleapi/ref_getbounds.asp
It would be nice to do it for React Native
If you want it then you can have one extension for the MKMapView as below.
extension MKMapView {
func getBounds() -> (southWestPoint: CLLocationCoordinate2D, northEastPoint: CLLocationCoordinate2D) {
let visibleMapRect = self.visibleMapRect
let neMapPoint = MKMapPointMake(MKMapRectGetMaxX(visibleMapRect), visibleMapRect.origin.y)
let swMapPoint = MKMapPointMake(visibleMapRect.origin.x, MKMapRectGetMaxY(visibleMapRect))
return (southWestPoint: MKCoordinateForMapPoint(swMapPoint), northEastPoint:MKCoordinateForMapPoint(neMapPoint))
}
}
Whenever you want the bound just call the getBounds method.

Reactivecocoa two-way binding of UISlider to a value, update UI only when user isn't touching it

I have a UISlider which shows playback progress through a song and allows seeking within the song.
Using RAC it's very easy to obtain a two-way binding between a UISlider's value and a viewModel's value:
RACChannelTo(self, slider.value) = RACChannelTo(viewModel, progress);
However problems arise while the user is interacting with the slider:
with continuous updates, while the user is moving the slider updates are constantly being sent to the VM leading to jittery sound;
without continuous updates, the jittery sound issue is no more, but the VM is constantly updating its progress value leading to the UI jumping to the latest value from under the user's finger.
I've tried the [slider rac_newValueChannelWithNilValue:#0] approach with same results. My current solution works, but is shamefully complex:
// Sadly the rac_signalForControlEvents doesn't send the event that triggered it so two signals are needed
RACSignal *touchDown = [self.slider rac_signalForControlEvents:UIControlEventTouchDown];
RACSignal *touchUp = [self.slider rac_signalForControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside];
RACSignal *isTouched = [[[touchDown map:^id(id value) {
return #(YES);
}] merge:[touchUp map:^id(id value) {
return #(NO);
}]] startWith:#(NO)]; // Needs a startWith because combineLatestWith doesn't send signals until it has at least one value from each component
[[RACObserve(viewModel, progress) combineLatestWith:isTouched] subscribeNext:^(RACTuple *tuple) {
RACTupleUnpack(NSNumber * progress, NSNumber * isTouched) = tuple;
// Update the slider value only if the user isn't touching it
if (!isTouched.boolValue) {
weakSelf.slider.value = progress.floatValue;
}
}];
// Send the value only when user stops interacting
[[touchUp map:^id(UISlider *slider) {
return #(slider.value);
}] subscribeNext:^(id x) {
weakSelf.viewModel.progress = [x floatValue];
}];
Is there a better way of achieving this?
Have you tried using throttle to decrease the number of values sent from the slider?

Position of child of instance variable not updating

Hopefully this is a fairly simple question. I create an instance variable named tutorialBox1 from my main game scene Grid.m. Inside my class TutorialBox.m, I create a sprite named textBox1, and as I tap on the main game scene, I want the position of that sprite, textBox1 to be updated. I've been trying to figure out for hours why the position of the sprite will not update after adding the child once. Here is my attempt, and all the necessary code.
Grid.m
# implementation Grid {
TutorialBox *tutorialBox1;
}
-(void)checkTutorials {
//This method is called upon loading of the scene
tutorialBox1 = (TutorialBox*)[CCBReader load:#"TutorialBox"] //creates instance of variable using Spritebuilder (don't worry if you don't use SB, this part works)
[tutorialBox1 updateBoxDisplay:1] //used to add text box child to textBox1
[self addChild:tutorialBox1];
}
-(void)gridTapped {
//When game scene is tapped, this is called to update position of textBox1 in tutorialBox1
[tutorialBox1 updateBoxDisplay:2]; //updates position of textBox1 in tutorialBox1
}
TutorialBox.m
# implementation Grid {
CCSprite9Slice *textBox1;
}
-(void)didLoadFromCCB {
//Called upon initialization of tutorialBox1
textBox1 = [CCSprite9Slice spriteWithImageNamed:#"RetroSprites/tutorialtextBox1.png"];
}
-(void)updateBoxDisplay:(int)newTutorialLevelNumber {
if (newTutorialLevelNumber == 1) {
textBox1.position = ccp(0,0);
[self addChild:textBox1];
}
else if (newTutorialLevelNumber == 2) {
textBox1.position = ccp(100,100)
}
}
TutorialBox.h
#interface TutorialBox : CCNode
- (void)updateBoxDisplay:(int)newTutorialLevelNumber
#end
I have checked to make sure everything is running properly with print statements, so it isn't an issue with anything else. It is simply being called, but the position in my main game scene, Grid.m is not being updated.
Thanks for the help!
I am not sure about this, but I guess its issue with the threading. Give a try with this code:
-(void)updateBoxDisplay:(int)newTutorialLevelNumber {
dispatch_async(dispatch_get_main_queue(), ^{
if (newTutorialLevelNumber == 1) {
textBox1.position = ccp(0,0);
[self addChild:textBox1];
}
else if (newTutorialLevelNumber == 2) {
textBox1.position = ccp(100,100)
}
});
}

UICollectionView exception in UICollectionViewLayoutAttributes from iOS7

I have done a View in CollectionView with CustomLayout. In iOS6 it worked great but iOS7 it throws an exception like this.
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:
'layout attributes for supplementary item at index path ( {length = 2, path = 0 - 0}) changed from CustomSupplementaryAttributes: 0xd1123a0 index path: (NSIndexPath: 0xd112580 {length = 2, path = 0 - 0}); element kind: (identifier); frame = (0 0; 1135.66 45); zIndex = -1; to CustomSupplementaryAttributes: 0xd583c80 index path: (NSIndexPath: 0xd583c70 {length = 2, path = 0 - 0}); element kind: (identifier); frame = (0 0; 1135.66 45); zIndex = -1; without invalidating the layout'
iOS 10
At iOS 10, a new feature is introduced, it is Cell Prefetching. It will let dynamic position of SupplementaryView crash. In order to run in the old behavior, it needs to disable prefetchingEnabled. It's true by default at iOS 10.
// Obj-C
// This function is available in iOS 10. Disable it for dynamic position of `SupplementaryView `.
if ([self.collectionView respondsToSelector:#selector(setPrefetchingEnabled:)]) {
self.collectionView.prefetchingEnabled = false;
}
// Swift
if #available(iOS 10, *) {
// Thanks #maksa
collectionView.prefetchingEnabled = false
// Swift 3 style
colView.isPrefetchingEnabled = false
}
I hate this problem. I spend 2 days for this problem.
A reference about Cell Pre-fetch #iOS 10.
iOS 9 and before ...
#Away Lin is right.. I solve the same problem by implementing that delegate method.
My Custom UICollectionViewLayout will modify the attributes in layoutAttributesForElementsInRect. The section position is dynamic, not static. So, I obtain warnings about the layout attributes for supplementary item at index path ... changed from ... to .... Before the changes, invalideLayout related methods should be called.
And, after implementing this delegate method to return true, the method invalidateLayoutWithContext: will be called when scrolling the UICollectionViewLayout. By default, it returns false.
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
From Apple Docs
Return Value
true if the collection view requires a layout update or false if the
layout does not need to change.
Discussion The default implementation of this method returns false.
Subclasses can override it and return an appropriate value based on
whether changes in the bounds of the collection view require changes
to the layout of cells and supplementary views.
If the bounds of the collection view change and this method returns
true, the collection view invalidates the layout by calling the
invalidateLayoutWithContext: method.
Availability Available in iOS 6.0 and later.
And more ...
A nice example project on GitHub, for custom UICollectionViewLayout.
You need to invalidate the existing layout before updating, see the end of the error message:
without invalidating the layout'
[collectionViewLayout invalidateLayout];
Apple Documentation for UICollectionViewLayout
I had the same exception: in iOS 7, you need now to override the inherited isEqual: in your UICollectionViewLayoutAttributes subclass as stated in Apple documentation here.
I solved my problem by override the method at the subclase of UICollectionViewFlowLayout:
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBound
return YES
I'm not entirely certain how or why, but this appears to be fixed in iOS 12, supporting both supplementary view resizing and prefetching. The trick for me was to make sure things are happening in the correct order.
Here is a working implementation of a stretchable header view. Notice the implementation of the header resizing happening in layoutAttributesForElements(in rect: CGRect):
class StretchyHeaderLayout: UICollectionViewFlowLayout {
var cache = [UICollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
cache.removeAll()
guard let collectionView = collectionView else { return }
let sections = [Int](0..<collectionView.numberOfSections)
for section in sections {
let items = [Int](0..<collectionView.numberOfItems(inSection: section))
for item in items {
let indexPath = IndexPath(item: item, section: section)
if let attribute = layoutAttributesForItem(at: indexPath) {
cache.append(attribute)
}
}
}
if let header = layoutAttributesForSupplementaryView(ofKind: StretchyCollectionHeaderKind, at: IndexPath(item: 0, section: 0)) {
cache.append(header)
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let visibleAttributes = cache.filter { rect.contains($0.frame) || rect.intersects($0.frame) }
guard let collectionView = collectionView else { return visibleAttributes }
// Find the header and stretch it while scrolling.
guard let header = visibleAttributes.filter({ $0.representedElementKind == StretchyCollectionHeaderKind }).first else { return visibleAttributes }
header.frame.origin.y = collectionView.contentOffset.y
header.frame.size.height = headerHeight.home - collectionView.contentOffset.y
header.frame.size.width = collectionView.frame.size.width
return visibleAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as! UICollectionViewLayoutAttributes
guard collectionView != nil else { return attributes }
attributes.frame.origin.y = headerHeight.home + attributes.frame.origin.y
return attributes
}
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: StretchyCollectionHeaderKind, with: indexPath)
}
override var collectionViewContentSize: CGSize {
get {
guard let collectionView = collectionView else { return .zero }
let numberOfSections = collectionView.numberOfSections
let lastSection = numberOfSections - 1
let numberOfItems = collectionView.numberOfItems(inSection: lastSection)
let lastItem = numberOfItems - 1
guard let lastCell = layoutAttributesForItem(at: IndexPath(item: lastItem, section: lastSection)) else { return .zero }
return CGSize(width: collectionView.frame.width, height: lastCell.frame.maxY + sectionInset.bottom)
}
}
}
P.S.: I'm aware the cache doesn't actually serve any purpose at this point :)
I had this problem too, because I had code that depended on the content size of the collection view. My code was accessing the content size via the collectionView!.contentSize instead of collectionViewContentSize.
The former uses the collectionView property of UICollectionViewLayout, while the latter uses the custom-implemented layout property. In my code, the first time the layout was asked for attributes, contentSize had not been set yet.
Select the CollectionView and Goto Attribute Inspector. Uncheck The Prefetching Enabled CheckBox. This is Fixed my Issue. Screenshot

How to constrain NSSplitView?

Hi I'm trying to constrain the max and min coordinate of an NSSplitView. I've created a view controller and assigned it as the delegate of an NSSplitView. The delegate methods get called however, the split view does not constrain to the position that I am trying to set it as. Any suggestions as to what I am doing wrong?
- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex
{
NSLog(#"Constrain min");
if (proposedMinimumPosition < 75)
{
proposedMinimumPosition = 75;
}
return proposedMinimumPosition;
}
- (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)dividerIndex
{
NSLog(#"Constrain max");
if (proposedMax > 200)
{
proposedMax = 200;
}
return proposedMax ;
}
Let's assume you want each one of two sections on a vertical splitter to be at least 70.0 high, then what you'd do this:
- (CGFloat) splitView:(NSSplitView *)splitView
constrainMinCoordinate:(CGFloat)proposedMin
ofSubviewAt:(NSInteger)dividerIndex
{
return 70.0;
}
- (CGFloat) splitView:(NSSplitView *)splitView
constrainMaxCoordinate:(CGFloat)proposedMin
ofSubviewAt:(NSInteger)dividerIndex
{
return splitView.frame.size.height - 70.0;
}
The reason for the subtraction is to dynamically account for any resizing (with autolayout, for example) of the overall NSplitView instance. If you're working with a horizontal one, then you'd need to calculate against .width instead of .height. If you have more than 2 subviews, the idea can be extended by looking at dividerIndex and applying values as you see fit.
I solved the problem by doing this.
- (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview
{
return NO;
}