I am doing some drag and rotation calculations using UIPanGestureRecognizer. The rotation angle is correct, and the drag location is almost correct. The problem is that as you go around the center of the box needs to be adjusted according to the angle and I can't figure out how.
I've included pictures of what a 180 rotation looks like but where the finger is during the rotation. I just don't know how to adjust to make the block stay with your finger appropriately. And heres a video just to clarify because it is strange behavior. http://tinypic.com/r/mhx6a1/5
EDIT: Here is a real world video of what should be happening. The problem being that in the iPad video your finger is moving where in the real world your finger would be cemented in a particular place on the item moving. The math needed is to adjust your touch location along the angle with a difference from the actual center. I just can't figure out the math. http://tinypic.com/r/4vptnk/5
Thanks very much!
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateBegan) {
// set original center so we know where to put it back if we have to.
originalCenter = dragView.center;
} else if (gesture.state == UIGestureRecognizerStateChanged) {
[dragView setCenter:CGPointMake( originalCenter.x + [gesture translationInView:self.view].x , originalCenter.y + [gesture translationInView:self.view].y )];
CGPoint p1 = button.center;
CGPoint p2 = dragView.center;
float adjacent = p2.x-p1.x;
float opposite = p2.y-p1.y;
float angle = atan2f(adjacent, opposite);
[dragView setTransform:CGAffineTransformMakeRotation(angle*-1)];
}
}
I've finally solved this issue and have it working perfectly. Persistence am I right??
Here is the code for the solution with a few comments to explain the changes.
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateBegan) {
// Get the location of the touch in the view we're dragging.
CGPoint location = [gesture locationInView:dragView];
// Now to fix the rotation we set a new anchor point to where our finger touched. Remember AnchorPoints are 0.0 - 1.0 so we need to convert from points to that by dividing
[dragView.layer setAnchorPoint:CGPointMake(location.x/dragView.frame.size.width, location.y/dragView.frame.size.height)];
} else if (gesture.state == UIGestureRecognizerStateChanged) {
// Calculate Our New Angle
CGPoint p1 = button.center;
CGPoint p2 = dragView.center;
float adjacent = p2.x-p1.x;
float opposite = p2.y-p1.y;
float angle = atan2f(adjacent, opposite);
// Get the location of our touch, this time in the context of the superview.
CGPoint location = [gesture locationInView:self.view];
// Set the center to that exact point, We don't need complicated original point translations anymore because we have changed the anchor point.
[dragView setCenter:CGPointMake(location.x, location.y)];
// Rotate our view by the calculated angle around our new anchor point.
[dragView setTransform:CGAffineTransformMakeRotation(angle*-1)];
}
}
Hope my month+ struggle and solution helps someone else in the future. Happy Coding :)
Based on touch events
https://github.com/kirbyt/KTOneFingerRotationGestureRecognizer
Helped me to solve similar problem
Related
I have been trying to implement a UI feature which I've seen in a few apps which use cards to display information. My current view controller looks like this:
and users are able to drag the card along the x axis to the left and right. Dragging to the right side of the screen does nothing to the scale of the card (simply changes position) but if the user swipes it to the left I wanted to slowly decrease the scale of it depending on its y coordinate (e.g. the scale is smallest when card is furthest to the left, getting bigger from that point until the original size is reached). If the card is dragged far enough to the left it will fade out, but if the user does not drag it far enough it increases in scale and moves back into the middle. Code I've tried so far:
- (void)handlePanImage:(UIPanGestureRecognizer *)sender
{
static CGPoint originalCenter;
if (sender.state == UIGestureRecognizerStateBegan)
{
originalCenter = sender.view.center;
sender.view.alpha = 0.8;
[sender.view.superview bringSubviewToFront:sender.view];
}
else if (sender.state == UIGestureRecognizerStateChanged)
{
CGPoint translation = [sender translationInView:self.view];
NSLog(#"%f x %f y",translation.x ,translation.y);
sender.view.center=CGPointMake(originalCenter.x + translation.x, yOfView);
CGAffineTransform transform = sender.view.transform;
i-=0.001;
transform = CGAffineTransformScale(transform, i, i);
//transform = CGAffineTransformRotate(transform, self.rotationAngle);
sender.view.transform = transform;
}
else if (sender.state == UIGestureRecognizerStateEnded || sender.state == UIGestureRecognizerStateCancelled || sender.state == UIGestureRecognizerStateFailed)
{
if(sender.view.center.x>0){
[UIView animateWithDuration:0.2 animations:^{
CGRect rect=[sender.view frame];
rect.origin.x=([self.view frame].size.width/2)-_protoypeView.frame.size.width/2;
rect.size.height=originalHeight;
rect.size.width=originalWidth;
[sender.view setFrame:rect];
i=1.0;
}];
}
[UIView animateWithDuration:0.1 animations:^{
sender.view.alpha = 1.0;
}];
}
}
This seems very buggy and doesn't properly work. I also tried to change scale according to translation:
else if (sender.state == UIGestureRecognizerStateChanged)
{
CGPoint translation = [sender translationInView:self.view];
NSLog(#"%f x %f y",translation.x ,translation.y);
sender.view.center=CGPointMake(originalCenter.x + translation.x, yOfView);
CGAffineTransform transform = sender.view.transform;
transform = CGAffineTransformScale(transform, translation.x/100, translation.x/100);
//transform = CGAffineTransformRotate(transform, self.rotationAngle);
sender.view.transform = transform;
}
but the scale either gets too big or too small. Any help would be greatly appreciated :)
In the last piece of code you tried, you're doing a couple of things wrong. The scale transform will be cumulative because you never set the translation of your pan gesture recognizer back to 0. Also, if you look at the math in your transform, you'll see that a small movement of say -1 point would scale the view to 0.01 times its original size. You obviously don't want that. You need to add that small negative number to 1, so that initial -1 point move would scale to 0.99. The change of setting the panner's translation back to zero necessitates changing the center calculation to use sender.view.center.x rather than originalCenter.X. You also need an if statement to check whether the center is left or right of its starting position so you know whether you should apply the scaling transform. Something like this,
-(void)handlePan:(UIPanGestureRecognizer *) sender {
if (sender.state == UIGestureRecognizerStateBegan) {
originalCenter = sender.view.center;
sender.view.alpha = 0.8;
[sender.view.superview bringSubviewToFront:sender.view];
}else if (sender.state == UIGestureRecognizerStateChanged){
CGPoint translation = [sender translationInView:self.view];
sender.view.center=CGPointMake(sender.view.center.x + translation.x, sender.view.center.y);
if (sender.view.center.x < originalCenter.x) {
CGAffineTransform transform = sender.view.transform;
transform = CGAffineTransformScale(transform, 1+translation.x/100.0, 1+translation.x/100.0);
sender.view.transform = transform;
}
[sender setTranslation:CGPointZero inView:self.view];
}
}
This doesn't take care of animating the view back, or fading out the view, but it should get you most of the way there.
I'm writing a Mac app that contains a collection view. This app is to be run on a large touchscreen display (55" EP series from Planar). Due to hardware limitation, the touchscreen doesn't send scroll events (or even any multitouch events). How can I go about tricking the app into thinking a "mousedown+drag" is the same as a "mousescroll"?
I got it working halfway by subclassing NSCollectionView and implementing my own NSPanGestureRecognizer handler in it. Unfortunately the result is clunky and doesn't have the feeling of a normal OS X scroll (i.e., the velocity effect at the end of a scroll, or scroll bounce at the ends of the content).
#implementation UCTouchScrollCollectionView
...
- (IBAction)showGestureForScrollGestureRecognizer:(NSPanGestureRecognizer *)recognizer
{
CGPoint location = [recognizer locationInView:self];
if (recognizer.state == NSGestureRecognizerStateBegan) {
touchStartPt = location;
startOrigin = [(NSClipView*)[self superview] documentVisibleRect].origin;
} else if (recognizer.state == NSGestureRecognizerStateEnded) {
/* Some notes here about a future feature: the Scroll Bounce
I don't want to have to reinvent the wheel here, but it
appears I already am. Crud.
1. when the touch ends, get the velocity in view
2. Using the velocity and a constant "deceleration" factor, you can determine
a. The time taken to decelerate to 0 velocity
b. the distance travelled in that time
3. If the final scroll point is out of bounds, update it.
4. set up an animation block to scroll the document to that point. Make sure it uses the proper easing to feel "natural".
5. make sure you retain a pointer or something to that animation so that a touch DURING the animation will cancel it (is this even possible?)
*/
[self.scrollDelegate.pointSmoother clearPoints];
refreshDelegateTriggered = NO;
} else if (recognizer.state == NSGestureRecognizerStateChanged) {
CGFloat dx = 0;
CGFloat dy = (startOrigin.y - self.scrollDelegate.scrollScaling * (location.y - touchStartPt.y));
NSPoint scrollPt = NSMakePoint(dx, dy);
[self.scrollDelegate.pointSmoother addPoint:scrollPt];
NSPoint smoothedPoint = [self.scrollDelegate.pointSmoother getSmoothedPoint];
[self scrollPoint:smoothedPoint];
CGFloat end = self.frame.size.height - self.superview.frame.size.height;
CGFloat threshold = self.superview.frame.size.height * kUCPullToRefreshScreenFactor;
if (smoothedPoint.y + threshold >= end &&
!refreshDelegateTriggered) {
NSLog(#"trigger pull to refresh");
refreshDelegateTriggered = YES;
[self.refreshDelegate scrollViewReachedBottom:self];
}
}
}
A note about this implementation: I put together scrollScaling and pointSmoother to try and improve the scroll UX. The touchscreen I'm using is IR-based and gets very jittery (especially when the sun is out).
In case it's relevant: I'm using Xcode 6 beta 6 (6A280e) on Yosemite beta (14A329r), and my build target is 10.10.
Thanks!
I managed to have some success using an NSPanGestureRecognizer and simulating the track-pad scroll wheel events. If you simulate them well you'll get the bounce from the NSScrollView 'for free'.
I don't have public code, but the best resource I found that explained what the NSScrollView expects is in the following unit test simulating a momentum scroll. (See mouseScrollByWithWheelAndMomentumPhases here).
https://github.com/WebKit/webkit/blob/master/LayoutTests/fast/scrolling/latching/scroll-iframe-in-overflow.html
The implementation of mouseScrollByWithWheelAndMomentumPhases gives some tips on how to synthesize the scroll events at a low level. One addition I found I needed was to actually set an incrementing timestamp in the event in order to get the scroll-view to play ball.
https://github.com/WebKit/webkit/blob/master/Tools/WebKitTestRunner/mac/EventSenderProxy.mm
Finally, in order to actually create the decaying velocity, I used a POPDecayAnimation and tweaked the velocity from the NSPanGestureRecognizer to feel similar. Its not perfect but it does stay true to NSScrollView's bounce.
I have a (dead) project on Github that does this with an NSTableView, so hopefully it will work well for an NSCollectionView.
Disclaimer: I wrote this while I was still learning GCD, so watch for retain cycles... I did not vet what I just posted for bugs. feel free to point any out :) I just tested this on Mac OS 10.9 and it does still work (originally written for 10.7 IIRC), not tested on 10.10.
This entire thing is a hack to be sure, it looks like it requires (seems to anyway) asynchronous UI manipulation (I think to prevent infinite recursion). There is probably a cleaner/better way and please share it when you discover it!
I havent touched this in months so I cant recall all the specifics, but the meat of it surely is in the NBBTableView code, which will paste snippets of.
first there is an NSAnimation subclass NBBScrollAnimation that handles the "rubber band" effect:
#implementation NBBScrollAnimation
#synthesize clipView;
#synthesize originPoint;
#synthesize targetPoint;
+ (NBBScrollAnimation*)scrollAnimationWithClipView:(NSClipView *)clipView
{
NBBScrollAnimation *animation = [[NBBScrollAnimation alloc] initWithDuration:0.6 animationCurve:NSAnimationEaseOut];
animation.clipView = clipView;
animation.originPoint = clipView.documentVisibleRect.origin;
animation.targetPoint = animation.originPoint;
return [animation autorelease];
}
- (void)setCurrentProgress:(NSAnimationProgress)progress
{
typedef float (^MyAnimationCurveBlock)(float, float, float);
MyAnimationCurveBlock cubicEaseOut = ^ float (float t, float start, float end) {
t--;
return end*(t * t * t + 1) + start;
};
dispatch_sync(dispatch_get_main_queue(), ^{
NSPoint progressPoint = self.originPoint;
progressPoint.x += cubicEaseOut(progress, 0, self.targetPoint.x - self.originPoint.x);
progressPoint.y += cubicEaseOut(progress, 0, self.targetPoint.y - self.originPoint.y);
NSPoint constraint = [self.clipView constrainScrollPoint:progressPoint];
if (!NSEqualPoints(constraint, progressPoint)) {
// constraining the point and reassigning to target gives us the "rubber band" effect
self.targetPoint = constraint;
}
[self.clipView scrollToPoint:progressPoint];
[self.clipView.enclosingScrollView reflectScrolledClipView:self.clipView];
[self.clipView.enclosingScrollView displayIfNeeded];
});
}
#end
You should be able to use the animation on any control that has an NSClipView by setting it up like this _scrollAnimation = [[NBBScrollAnimation scrollAnimationWithClipView:(NSClipView*)[self superview]] retain];
The trick here is that the superview of an NSTableView is an NSClipView; I dont know about NSCollectionView, but I suspect that any scrollable control uses NSClipView.
Next here is how the NBBTableView subclass makes use of that animation though the mouse events:
- (void)mouseDown:(NSEvent *)theEvent
{
_scrollDelta = 0.0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if (_scrollAnimation && _scrollAnimation.isAnimating) {
[_scrollAnimation stopAnimation];
}
});
}
- (void)mouseUp:(NSEvent *)theEvent
{
if (_scrollDelta) {
[super mouseUp:theEvent];
// reset the scroll animation
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSClipView* cv = (NSClipView*)[self superview];
NSPoint newPoint = NSMakePoint(0.0, ([cv documentVisibleRect].origin.y - _scrollDelta));
NBBScrollAnimation* anim = (NBBScrollAnimation*)_scrollAnimation;
[anim setCurrentProgress:0.0];
anim.targetPoint = newPoint;
[anim startAnimation];
});
} else {
[super mouseDown:theEvent];
}
}
- (void)mouseDragged:(NSEvent *)theEvent
{
NSClipView* clipView=(NSClipView*)[self superview];
NSPoint newPoint = NSMakePoint(0.0, ([clipView documentVisibleRect].origin.y - [theEvent deltaY]));
CGFloat limit = self.frame.size.height;
if (newPoint.y >= limit) {
newPoint.y = limit - 1.0;
} else if (newPoint.y <= limit * -1) {
newPoint.y = (limit * -1) + 1;
}
// do NOT constrain the point here. we want to "rubber band"
[clipView scrollToPoint:newPoint];
[[self enclosingScrollView] reflectScrolledClipView:clipView];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NBBScrollAnimation* anim = (NBBScrollAnimation*)_scrollAnimation;
anim.originPoint = newPoint;
});
// because we have to animate asyncronously, we must save the target value to use later
// instead of setting it in the animation here
_scrollDelta = [theEvent deltaY] * 3.5;
}
- (BOOL)autoscroll:(NSEvent *)theEvent
{
return NO;
}
I think that autoscroll override is essential for good behavior.
The entire code is on my github page, and it contains several other "touch screen" emulation tidbits, if you are interested, such as a simulation for the iOS springboard arrangeable icons (complete with "wiggle" animation using NSButtons.
Hope this helps :)
Edit: It appears that constrainScrollPoint: is deprecated in OS X 10.9. However, It should fairly trivial to reimplement as a category or something. Maybe you can adapt a solution from this SO question.
I have a viewController that should not "autorotate", but manually rotate specific GUI elements. The reason is that I use the front camera for taking a picture and I don't want the UIView that contains my UIImageView to be rotated.
My code looks like this:
-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
[self performSelector:#selector(refreshView) withObject:nil afterDelay:1.0];
return NO; // don't autorotate!
}
and:
- (void) refreshView {
UIDeviceOrientation actualDeviceOrientation = [[UIDevice currentDevice] orientation];
float rotation = 0; // UIDeviceOrientationPortrait
if (actualDeviceOrientation == UIDeviceOrientationPortraitUpsideDown) rotation = 180;
else if (actualDeviceOrientation == UIDeviceOrientationLandscapeLeft) rotation = 90;
else if (actualDeviceOrientation == UIDeviceOrientationLandscapeRight) rotation = 270;
float rotationRadians = rotation * M_PI / 180;
[UIView animateWithDuration:0.4
animations:^(void) {
self.labelPrize.center = self.prizeView.center;
self.prizeView.transform = CGAffineTransformMakeRotation(rotationRadians);
} completion:^(BOOL finished){ }];
}
"labelPrize" is the label with the caption "20 EURO" that is seen on the screenshots below, "prizeView" is it's container. prizeView is the only GUI element that has constraints defined, which look like this:
Just for clarification, here's what "labelPrize" looks like:
And finally, here's what the app produces:
This is not what I want to achieve, I'd like "prizeView"/"labelPrize" to be
always aligned to the horizon
always in the exact center of the screen
Also worth mentioning: I'd like to add labels above (header) and a button below ("okay") my "labelPrize" and rotate/position them as well in refreshView().
Thanks for any help!
There are two big problems here. Let's take them one at a time.
(1)
self.labelPrize.center = self.prizeView.center;
Think about it. labelPrize is a subview of prizeView. So you are mixing apples with oranges as far as coordinate systems go: labelPrize.center is measured with respect to prizeView.bounds, but prizeView.center is measured with respect to self.view.bounds. To keep the center of labelPrize at the center of prizeView, position it at the midpoint of prizeView's bounds. (However, you should not have to move it at all because the transform transforms the bounds.)
(2)
Rotation view transforms and auto layout are deadly enemies, as I explain here. That is why rotating the transform of prizeView seems to shift its position as well. My answer there gives you several possible workarounds.
I have an icon within my sprite kit game that I intend to use as an animated projectile for when one character shoots another one. I'm trying to orient this projectile to point at the target.
I'm rotating it by a base angle of pi/4.0 to point it straight to the right. then I want the arrow to rotate from this position and point at target.
The code below almost works, but to my eye, it always looks as if the projectile is off from the correct angle. If the angle is correct, the arrow will point in the direction of movement when the arrow is given an action to move to the target x,y coordinate.
How can I correctly calculate the angle to fire projectile from one (x,y) point to another (x,y) point?
EDIT: The code below works now, just needed to do zeroCheckedY/zeroCheckedX.
//correct for pointing in the bottom right corner
float baseRotation = M_PI/4.0;
float zeroCheckedY = destination.y - origin.y;
float zeroCheckedX = destination.x - origin.x;
SKAction* rotate = nil;
if(zeroCheckedX != 0)
{
//not sure if I'm calculating this angle correctly.
float angle = atanf(zeroCheckedY/zeroCheckedX);
rotate = [SKAction rotateByAngle:baseRotation + angle duration:0];
}else
{
rotate = [SKAction rotateByAngle:baseRotation duration:0];
}
I had a similar problem: cannon must "track" a target.
In CannonNode I defined the following method:
- (void)catchTargetAtPoint:(CGPoint)target {
CGPoint cannonPointOnScene = [self.scene convertPoint:self.position fromNode:self.parent];
float angle = [CannonNode pointPairToBearingDegreesWithStartingPoint:cannonPointInScene endingPoint:target];
if (self.zRotation < 0) {
self.zRotation = self.zRotation + M_PI * 2;
}
if ((angle < self.maxAngle) && (angle> self.minAngle)) {
[self runAction:[SKAction rotateToAngle:angle duration:1.0f]];
}
}
+ (float)pointPairToBearingDegreesWithStartingPoint:(CGPoint)startingPoint endingPoint:(CGPoint) endingPoint {
CGPoint originPoint = CGPointMake(endingPoint.x - startingPoint.x, endingPoint.y - startingPoint.y); // get origin point to origin by subtracting end from start
float bearingRadians = atan2f(originPoint.y, originPoint.x); // get bearing in radians
return bearingRadians;
}
catchTargetAtPoint must be called within update method of the scene.
I am in process of developing a small game where a space-ship travels through a layer (doh!), in some situations the spaceship comes close to an enemy, and the whole layer is zoomed in on the space-ship with the zoom level being dependent on the distance between the ship and the enemy. All of this works fine.
The main question, however, is how do I keep the zoom being centered on the space-ship?
Currently I control the zooming in the GameLayer object through the update method, here is the code:
-(void) prepareLayerZoomBetweenSpaceship{
CGPoint mainSpaceShipPosition = [mainSpaceShip position];
CGPoint enemySpaceShipPosition = [enemySpaceShip position];
float distance = powf(mainSpaceShipPosition.x - enemySpaceShipPosition.x, 2) + powf(mainSpaceShipPosition.y - enemySpaceShipPosition.y,2);
distance = sqrtf(distance);
/*
Distance > 250 --> no zoom
Distance < 100 --> maximum zoom
*/
float myZoomLevel = 0.5f;
if(distance < 100){ //maximum zoom in
myZoomLevel = 1.0f;
}else if(distance > 250){
myZoomLevel = 0.5f;
}else{
myZoomLevel = 1.0f - (distance-100)*0.0033f;
}
[self zoomTo:myZoomLevel];
}
-(void) zoomTo:(float)zoom {
if(zoom > 1){
zoom = 1;
}
// Set the scale.
if(self.scale != zoom){
self.scale = zoom;
}
}
Basically my question is: How do I zoom the layer and center it exactly between the two ships? I guess this is like a pinch zoom with two fingers!
Below is some code that should get it working for you. Basically you want to:
Update your ship positions within the parentNode's coordinate system
Figure out which axis these new positions will cause the screen will be bound by.
Scale and re-position the parentNode
I added some sparse comments, but let me know if you have any more questions/issues. It might be easiest to dump this in a test project first...
ivars to put in your CCLayer:
CCNode *parentNode;
CCSprite *shipA;
CCSprite *shipB;
CGPoint destA, deltaA;
CGPoint destB, deltaB;
CGPoint halfScreenSize;
CGPoint fullScreenSize;
init stuff to put in your CCLayer:
CGSize size = [[CCDirector sharedDirector] winSize];
fullScreenSize = CGPointMake(size.width, size.height);
halfScreenSize = ccpMult(fullScreenSize, .5f);
parentNode = [CCNode node];
[self addChild:parentNode];
shipA = [CCSprite spriteWithFile:#"Icon-Small.png"]; //or whatever sprite
[parentNode addChild:shipA];
shipB = [CCSprite spriteWithFile:#"Icon-Small.png"];
[parentNode addChild:shipB];
//schedules update for every frame... might not run great.
//[self schedule:#selector(updateShips:)];
//schedules update for 25 times a second
[self schedule:#selector(updateShips:) interval:0.04f];
Zoom / Center / Ship update method:
-(void)updateShips:(ccTime)timeDelta {
//SHIP POSITION UPDATE STUFF GOES HERE
...
//1st: calc aspect ratio formed by ship positions to determine bounding axis
float shipDeltaX = fabs(shipA.position.x - shipB.position.x);
float shipDeltaY = fabs(shipA.position.y - shipB.position.y);
float newAspect = shipDeltaX / shipDeltaY;
//Then: scale based off of bounding axis
//if bound by x-axis OR deltaY is negligible
if (newAspect > (fullScreenSize.x / fullScreenSize.y) || shipDeltaY < 1.0) {
parentNode.scale = fullScreenSize.x / (shipDeltaX + shipA.contentSize.width);
}
else { //else: bound by y-axis or deltaX is negligible
parentNode.scale = fullScreenSize.y / (shipDeltaY + shipA.contentSize.height);
}
//calculate new midpoint between ships AND apply new scale to it
CGPoint scaledMidpoint = ccpMult(ccpMidpoint(shipA.position, shipB.position), parentNode.scale);
//update parent node position (move it into view of screen) to scaledMidpoint
parentNode.position = ccpSub(halfScreenSize, scaledMidpoint);
}
Also, I'm not sure how well it'll perform with a bunch of stuff going on -- but thats a separate problem!
Why don't you move the entire view, & position it so the ship is in the centre of the screen? I haven't tried it with your example, but it should be straight forward. Maybe something like this -
CGFloat x = (enemySpaceShipPosition.x - mainSpaceShipPosition.x) / 2.0 - screenCentreX;
CGFloat y = (enemySpaceShipPosition.y - mainSpaceShipPosition.y) / 2.0 - screenCentreY;
CGPoint midPointForContentOffset = CGPointMake(-x, -y);
[self setContentOffset:midPointForContentOffset];
...where you've already set up screenCentreX & Y. I haven't used UISCrollView for quite a while (been working on something in Unity so I'm forgetting all by Obj-C), & I can't remember how the contentOffset is affected by zoom level. Try it & see! (I'm assuming you're using a UIScrollView, maybe you could try that too if you're not)