UIScrollView does not always animate deceleration when overriding scrollViewWillEndDragging - animation

Here is my override code - it just calculates where to snap to:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
if(targetContentOffset->y < 400) {
targetContentOffset->y = 0;
return;
}
int baseCheck = 400;
while(baseCheck <= 10000) {
if(targetContentOffset->y > baseCheck && targetContentOffset->y < baseCheck + 800) {
targetContentOffset->y = (baseCheck + 340);
return;
}
baseCheck += 800;
}
targetContentOffset->y = 0;
}
When I hold my finger down for more than a second or two to drag the scrollview and then lift my finger, it usually animates into place. However, when I do a quick "flick" it rarely animates - it just snaps to the targetContentOffset. I am trying to emulate default paging behavior (except trying to snap to custom positions).
Any ideas?

I ended up having to animate it manually. In the same function, I set the targetContentOffset to where the user left off (current contentOffset) so that it doesnt trigger it's own animation, and then I set the contentoffset to my calculation. Additionally, I added in a velocity check to trigger'automatic' page changes. It's not perfect, but hopefully it will help other with the same issue.
(I modified the above function for readability since no one needs to see my calculations)
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGPoint newOffset = CGPointMake(targetContentOffset->x, targetContentOffset->y);
*targetContentOffset = CGPointMake([broadsheetCollectionView contentOffset].x, [broadsheetCollectionView contentOffset].y);
if(velocity.y > 1.4) {
newOffset.y += pixelAmountThatWillMakeSurePageChanges;
}
if(velocity.y < -1.4) {
newOffset.y -= pixelAmountThatWillMakeSurePageChanges;
}
// calculate newoffset
newOffset.y = calculatedOffset;
[scrollView setContentOffset:newOffset animated:YES];
}

Related

Changing OSX app window size based on user input that changes views

I'm trying my hand at developing an OSX app. Xcode and Swift are all new to me.
The defaults are all very good at modifying objects when the user changes the window size, but not so good at changing the window size when objects in the view change.
I've seen a couple of examples of recalculating the origin and frame size - I think the math part of it will be straight forward. However, I cannot get a working reference to the NSWindow object. click-drag will not deposit an IBOutlet for the window in any of the .swift files (AppDelegate, ViewController, custom). And typing it in doesn't bind it.
A simplified example of what I am trying to accomplish:
Based on user input, change the content of the display, and adjust the window size to encompass the newly modified display.
On my main storyboard
- Window Controller, segued to
- View Controller, containing a Horizontal Slider and a Container view. The container view is segued to
- Horizontal Split View Controller, segued to
- three repetitive instances of a View Controller.
When the user changes the slider bar, one or more of the three bottom most view controllers will be hidden/unhidden.
The attached pictures show the behavior I am looking for.
Imagine where the text "small group" is, there is a collection of drop down boxes, text boxes, radio buttons, etc.
I am answering my own question here - still unable to bind IBOutlet with NSWindow. Anyone with better solutions, please let me know.
Below is a capture of my Main.storyboard. All of the coding is done in the class junkViewController2 that is associated with the 1st view controller.
Here is the code for junkViewController2. I know, it could be more concise ...
//
// JunkViewController2.swift
// Scratch1
//
import Cocoa
class JunkViewController2: NSViewController {
var junkSplitViewController: JunkSplitViewController!
var myWindow: NSWindow!
var dy: CGFloat!
#IBOutlet weak var mySlider: NSSlider!
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
dy = 0.0
}
#IBAction func mySlider(sender: NSSlider) {
let junkSplitViewController = self.childViewControllers[0]
dy = 0.0
for controller in junkSplitViewController.childViewControllers {
if controller.title == "smallGroup1" {
if mySlider.intValue > 0 {
if controller.view.hidden {
dy = dy + 30.0
}
controller.view.hidden = false
} else {
if !controller.view.hidden {
dy = dy - 30.0
}
controller.view.hidden = true
}
}
if controller.title == "smallGroup2" {
if mySlider.intValue > 1 {
if controller.view.hidden {
dy = dy + 30.0
}
controller.view.hidden = false
} else {
if !controller.view.hidden {
dy = dy - 30.0
}
controller.view.hidden = true
}
}
if controller.title == "smallGroup3" {
if mySlider.intValue > 2 {
if controller.view.hidden {
dy = dy + 30.0
}
controller.view.hidden = false
} else {
if !controller.view.hidden {
dy = dy - 30.0
}
controller.view.hidden = true
}
}
}
resize()
}
func resize() {
var windowFrame = NSApp.mainWindow!.frame
let oldWidth = windowFrame.size.width
let oldHeight = windowFrame.size.height
let old_x = windowFrame.origin.x
let old_y = windowFrame.origin.y
let toAdd = CGFloat(dy)
let newHeight = oldHeight + toAdd
let new_y = old_y - toAdd
windowFrame.size = NSMakeSize(oldWidth, newHeight)
windowFrame.origin = NSMakePoint(old_x, new_y)
NSApp.mainWindow!.setFrame(windowFrame, display: true)
}
}

Large Seamless Scrolling Background in SpriteKit?

Okay, so i'm fairly new to SpriteKit, but so far it's been a lot of fun working with it. I'm looking for a way to have a large scrolling background loop endlessly. I've divided my background into small chunks to let SK call as it needs them, using this code:
for (int nodeCount = 0; nodeCount < 50; nodeCount ++) {
NSString *backgroundImageName = [NSString stringWithFormat:#"Sky_%02d.gif", nodeCount +1];
SKSpriteNode *node = [SKSpriteNode spriteNodeWithImageNamed:backgroundImageName];
node.xScale = 0.5;
node.yScale = 0.5;
node.anchorPoint = CGPointMake(0.0f, 0.5f);
node.position = CGPointMake(nodeCount * node.size.width, self.frame.size.height/2);
node.name = #"skyBG";
and I'm able to move this whole block just fine in update, but I can't get it to loop seamlessly. or at all, for that matter. I've tried the method suggested elsewhere on here that takes a copy of the background and takes it to the other end to give the appearance of a seamless loop, and no dice here. I've also tried adding this to the end of the above code, which I had hoped might work:
if (nodeCount == 49)
{
nodeCount = 0;
}
but that just cause the simulator to hang, so I assume it's inadvertently creating a loop or something. Any assistance at all would be great! I feel like there's a simple fix, but I'm just not getting there... thanks a bunch, folks!
UPDATE
here's the code I'm currently using to get the background to scroll, which works just fine:
-(void)update:(CFTimeInterval)currentTime {
_backgroundNode.position = CGPointMake(_backgroundNode.position.x - 1, _backgroundNode.position.y);
}
So to sum up: I have a moving background, but getting it to loop seamlessly has been challenging because of the size of the background. If there's anything at all I can do to help clarify, let me know! Thanks a bunch
This is just an example of the logic you can employ to achieve a scrolling background. It is by no means a final or ideal implementation, but rather a conceptual example.
Here is a simple scroller class :
// Scroller.h
#import <SpriteKit/SpriteKit.h>
#interface Scroller : SKNode
{
// instance variables
NSMutableArray *tiles; // array for your tiles
CGSize tileSize; // size of your screen tiles
float scrollSpeed; // speed in pixels per second
}
// methods
-(void)addTiles:(NSMutableArray *)newTiles;
-(void)initScrollerWithTileSize:(CGSize)scrollerTileSize scrollerSpeed:(float)scrollerSpeed;
-(void)update:(float)elapsedTime;
#end
Implementation :
// Scroller.m
#import "Scroller.h"
#implementation Scroller
-(instancetype)init
{
if (self = [super init])
{
tiles = [NSMutableArray array];
}
return self;
}
-(void)addTiles:(NSMutableArray *)newTiles
{
[tiles addObjectsFromArray:newTiles];
}
-(void)initScrollerWithTileSize:(CGSize)scrollerTileSize scrollerSpeed:(float)scrollerSpeed
{
// set properties
scrollSpeed = scrollerSpeed;
tileSize = scrollerTileSize;
// set initial locations of tile and set their size
for (int index = 0; index < tiles.count; index++)
{
SKSpriteNode *tile = tiles[index];
//set anchorPoint to bottom left
tile.anchorPoint = CGPointMake(0, 0);
// set tilesize
// this implementation requires uniform size
[tile setSize:tileSize];
//calcuate and set initial position of tile
float startX = index * tileSize.width;
tile.position = CGPointMake(startX, 0);
//add child to the display list
[self addChild:tile];
}
}
-(void)update:(float)elapsedTime
{
//calculate speed for this frame
float curSpeed = scrollSpeed * elapsedTime;
// iterate through your screen tiles
for (int index = 0; index < tiles.count; index++)
{
SKSpriteNode *tile = tiles[index];
// set new x location for tile
tile.position = CGPointMake(tile.position.x - curSpeed, 0);
// if new location is off the screen, move it to the far right
if (tile.position.x < -tileSize.width)
{
// calculate the new x position based on number of tiles an tile width
float newX = (tiles.count -1) * tileSize.width;
// set it's new position
tile.position = CGPointMake(newX, 0);
}
}
}
In your scene , init your scroller :
-(void)initScroller
{
// create an array of all your textures
NSMutableArray *tiles = [NSMutableArray array];
[tiles addObject:[SKSpriteNode spriteNodeWithTexture:skyTexture]];
[tiles addObject:[SKSpriteNode spriteNodeWithTexture:skyTexture]];
[tiles addObject:[SKSpriteNode spriteNodeWithTexture:skyTexture]];
[tiles addObject:[SKSpriteNode spriteNodeWithTexture:skyTexture]];
// create scroller instance (ivar or property of scene)
scroller = [[Scroller alloc]init];
// add the tiles to the scroller
[scroller addTiles:tiles];
// init the scroller with desired tile size and scroller speed
[scroller initScrollerWithTileSize:CGSizeMake(1024, 768) scrollerSpeed:500];
// add scroller to the scene
[self addChild:scroller];
}
In your scene update method :
-(void)update:(CFTimeInterval)currentTime
{
float elapsedTime = .0166; // set to .033 if you are locking to 30fps
// call the scroller update method
[scroller update:elapsedTime];
}
Again, this implementation is very barebones and is a conceptual example.
I am currently working on a library for a tile scroller, is using a solution similar to the one provided by prototypical, but using a Tableview datasource pattern to ask for the nodes. Take a look at it:
RPTileScroller
I take some effort to make it the most efficient possible, but mind I am not a game developer. I tried it with my iPhone 5 with random colors tiles of 10x10 pixels, and is running on a solid 60 fps.

How does one manually tell NSTextContainer to recalculate the lineFragmentRects?

In my application, I have been adding support for NSTextView allowing subviews to work naturally with the text editor. (For example, pressing "enter" drops every subview lower than the insertion point, and text wraps around subviews.)
I have successfully been able to get word wrap around subviews by overridinglineFragmentRectForProposedRect(proposedRect: NSRect,
sweepDirection: NSLineSweepDirection,
movementDirection: NSLineMovementDirection,
remainingRect: NSRectPointer) -> NSRect.
However, when a subview is dragged, or resized, the breaks in the text need to be recalculated to get the desired effect. The text needs to update itself to wrap around the subview as it is dragged or resized. However, I noticed that typing on a line updates the lineFragmentRect for that line. Is there a way to immediately mark the text as "dirty" after the subview is dragged? Here is the code for the text container subclass:
import Cocoa
class CASTextContainer: NSTextContainer {
var mainView: CASTextView = CASTextView()
var textSubviews: Array<AnyObject> {
get {
return mainView.subviews
}
}
override var simpleRectangularTextContainer: Bool {
get {
return false
}
}
func rectForActiveRange() -> NSRect {
let range = layoutManager.glyphRangeForCharacterRange(textView.selectedRange, actualCharacterRange:nil)
var rect = layoutManager.boundingRectForGlyphRange(range, inTextContainer: self)
rect = NSOffsetRect(rect, textView.textContainerOrigin.x, textView.textContainerOrigin.y)
return rect;
}
override func lineFragmentRectForProposedRect(proposedRect: NSRect,
sweepDirection: NSLineSweepDirection,
movementDirection: NSLineMovementDirection,
remainingRect: NSRectPointer) -> NSRect
{
remainingRect.initialize(NSZeroRect)
var frames: Array<NSRect> = []
if textSubviews.isEmpty {
let fullRect = NSMakeRect(0, 0, containerSize.width, containerSize.height)
return NSIntersectionRect(fullRect, proposedRect)
}
for view in textSubviews {
frames.append(view.frame)
}
// Sort the array so that the frames are ordered by increasing origin x coordinate.
let fullRect = NSMakeRect(0, 0, containerSize.width, containerSize.height)
let region = NSIntersectionRect(fullRect, proposedRect)
let sortedFrames: Array<NSRect> = sorted(frames, { (first: NSRect, second: NSRect) -> Bool in
return first.origin.x < second.origin.x
})
// Get the first rectangle height that overlaps with the text region's height.
var textDrawingRegion = NSZeroRect
var subviewFrame = NSZeroRect // Will be needed later to set remainingRect pointer.
var precedingRegion: NSRect = NSZeroRect // Hold the view frame preceding our insertion point.
for frame in sortedFrames {
if region.origin.y + NSHeight(region) >= frame.origin.y &&
region.origin.y <= frame.origin.y + NSHeight(frame)
{
if region.origin.x <= frame.origin.x {
// Calculate the distance between the preceding subview and the approaching one.
var width: CGFloat = frame.origin.x - precedingRegion.origin.x - NSWidth(precedingRegion)
textDrawingRegion.origin = region.origin
textDrawingRegion.size = NSMakeSize(width, NSHeight(region))
subviewFrame = frame
break
}
else {
precedingRegion = frame
}
}
}
if textDrawingRegion == NSZeroRect {
return region
}
else {
// Set the "break" in the text to the subview's location:
let nextRect = NSMakeRect(subviewFrame.origin.x + NSWidth(subviewFrame),
region.origin.y,
NSWidth(proposedRect),
NSHeight(proposedRect))
remainingRect.initialize(nextRect)
return textDrawingRegion
}
}
}

Drag image on canvas

I'm trying to drag an image around a canvas element. Although I've had dragging working, it's not working as I'd quite like.
Basically, the image is always bigger than the canvas element, but the left side of the image within the canvas is not able to further right than the left side of the canvas, likewise the right side is not allowed to go "more right" than the right side of the canvas. Basically, the image is constrained to not show any blank canvas space.
The problem with the dragging is that whenever I start to drag the image "pops" back to act as if 0,0 is from the mouse location, whereas I actually want to move the image from the current position.
document.onmousemove = function(e) {
if(mouseIsDown) {
var mouseCoords = getMouseCoords(e);
offset_x += ((mouseCoords.x - canvas.offsetLeft) - myNewX);
offset_y += ((mouseCoords.y - canvas.offsetTop) - myNewY);
draw(offset_x, offset_y);
// offset_x = ((mouseCoords.x - canvas.offsetLeft) - myNewX);
// offset_y = ((mouseCoords.y - canvas.offsetTop) - myNewY);
// offset_x = (mouseCoords.x - canvas.offsetLeft) - myNewX;
// offset_y = (mouseCoords.y - canvas.offsetTop) - myNewY;
offset_x = prevX;
offset_y = prevY;
}
/*if(mouseIsDown) {
var mouseCoords = getMouseCoords(e);
var tX = (mouseCoords.x - canvas.offsetLeft);
var tY = (mouseCoords.y - canvas.offsetTop);
var deltaX = tX - prevX;
var deltaY = tY - prevY;
x += deltaX;
y += deltaY;
prevX = x;
prevY = y;
draw(x, y);
}*/
};
Is what I have now, where I kind of get a parallelex effect.
You will have to record the current offset of the image as it is moved and use that (in addition to the offset from the top left of the canvas) with each mousedown to determine your initial offset.
var dragging = false,
imageOffset = {
x: 0,
y: 0
},
mousePosition;
function dragImage(coords) {
imageOffset.x += mousePosition.x - coords.x;
imageOffset.y += mousePosition.y - coords.y;
// constrain coordinates to keep the image visible in the canvas
imageOffset.x = Math.min(0, Math.max(imageOffset.x, canvas.width - imageWidth));
imageOffset.y = Math.min(0, Math.max(imageOffset.y, canvas.height - imageHeight));
mousePosition = coords;
}
function drawImage() {
// draw at the position recorded in imageOffset
// don't forget to clear the canvas before drawing
}
function getMouseCoords(e) {
// return the position of the mouse relative to the top left of the canvas
}
canvas.onmousedown = function(e) {
dragging = true;
mousePosition = getMouseCoords(e);
};
document.onmouseup = function() {
dragging = false;
};
document.onmousemove = function(e) {
if(dragging) dragImage(getMouseCoords(e));
};
You should probably treat this as pseudocode as I haven't tested it in any way... ;-)

Jaggy paths when blitting an offscreen CGLayer to the current context

In my current project I need to draw a complex background as a background for a few UITableView cells. Since the code for drawing this background is pretty long and CPU heavy when executed in the cell's drawRect: method, I decided to render it only once to a CGLayer and then blit it to the cell to enhance the overall performance.
The code I'm using to draw the background to a CGLayer:
+ (CGLayerRef)standardCellBackgroundLayer
{
static CGLayerRef standardCellBackgroundLayer;
if(standardCellBackgroundLayer == NULL)
{
CGContextRef viewContext = UIGraphicsGetCurrentContext();
CGRect rect = CGRectMake(0, 0, [UIScreen mainScreen].applicationFrame.size.width, PLACES_DEFAULT_CELL_HEIGHT);
standardCellBackgroundLayer = CGLayerCreateWithContext(viewContext, rect.size, NULL);
CGContextRef context = CGLayerGetContext(standardCellBackgroundLayer);
// Setup the paths
CGRect rectForShadowPadding = CGRectInset(rect, (PLACES_DEFAULT_CELL_SHADOW_SIZE / 2) + PLACES_DEFAULT_CELL_SIDE_PADDING, (PLACES_DEFAULT_CELL_SHADOW_SIZE / 2));
CGMutablePathRef path = createPathForRoundedRect(rectForShadowPadding, LIST_ITEM_CORNER_RADIUS);
// Save the graphics context state
CGContextSaveGState(context);
// Draw shadow
CGContextSetShadowWithColor(context, CGSizeMake(0, 0), PLACES_DEFAULT_CELL_SHADOW_SIZE, [Skin shadowColor]);
CGContextAddPath(context, path);
CGContextSetFillColorWithColor(context, [Skin whiteColor]);
CGContextFillPath(context);
// Clip for gradient
CGContextAddPath(context, path);
CGContextClip(context);
// Draw gradient on clipped path
CGPoint startPoint = rectForShadowPadding.origin;
CGPoint endPoint = CGPointMake(rectForShadowPadding.origin.x, CGRectGetMaxY(rectForShadowPadding));
CGContextDrawLinearGradient(context, [Skin listGradient], startPoint, endPoint, 0);
// Restore the graphics state and release everything
CGContextRestoreGState(context);
CGPathRelease(path);
}
return standardCellBackgroundLayer;
}
The code to blit the layer to the current context:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawLayerAtPoint(context, CGPointMake(0.0, 0.0), [Skin standardCellBackgroundLayer]);
}
This actually does the trick pretty nice but the only problem I'm having is that the rounded corners (check the static method). Are very jaggy when blitted to the screen. This wasn't the case when the drawing code was at its original position: in the drawRect method.
How do I get back this antialiassing?
For some reason the following methods don't have any impact on the anti-aliassing:
CGContextSetShouldAntialias(context, YES);
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
CGContextSetAllowsAntialiasing(context, YES);
Thanks in advance!
You can simplify this by just using UIGraphicsBeginImageContextWithOptions and setting the scale to 0.0.
Sorry for awakening an old post, but I came across it so someone else may too. More details can be found in the UIGraphicsBeginImageContextWithOptions documentation:
If you specify a value of 0.0, the scale factor is set to the scale
factor of the device’s main screen.
Basically meaning that if it's a retina display it will create a retina context, that way you can specify 0.0 and treat the coordinates as points.
I'm going to answer my own question since I figured it out some time ago.
You should make a retina aware context. The jaggedness only appeared on a retina device.
To counter this behavior, you should create a retina context with this helper method:
// Begin a graphics context for retina or SD
void RetinaAwareUIGraphicsBeginImageContext(CGSize size)
{
static CGFloat scale = -1.0;
if(scale < 0.0)
{
UIScreen *screen = [UIScreen mainScreen];
if([[[UIDevice currentDevice] systemVersion] floatValue] >= 4.0)
{
scale = [screen scale]; // Retina
}
else
{
scale = 0.0; // SD
}
}
if(scale > 0.0)
{
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
}
else
{
UIGraphicsBeginImageContext(size);
}
}
Then, in your drawing method call the method listed above like so:
+ (CGLayerRef)standardCellBackgroundLayer
{
static CGLayerRef standardCellBackgroundLayer;
if(standardCellBackgroundLayer == NULL)
{
RetinaAwareUIGraphicsBeginImageContext(CGSizeMake(320.0, 480.0));
CGRect rect = CGRectMake(0, 0, [UIScreen mainScreen].applicationFrame.size.width, PLACES_DEFAULT_CELL_HEIGHT);
...

Resources