The user interface in my node based image compositing and editing application, Nodality, consists of a large scrollable and zoomable workspace where the user can create and move nodes. This blog post describes creating a similar interface in Swift using UIScrollView (for Flex developers, UIScrollView is analogous to Scroller but with built in support for pinch to zoom).
For this demonstration, I've created three custom components:
- BackgroundControl extends UIControl and is the container for the background grid and newly created nodes.
- BackgroundGridextends CALayer and simply draws a dark gray grid.
- Node also extends UIControl and forms the blue nodes created by a long hold on the background. It handles its own movement internally.
The bulk of the action happens inside my main ViewController. Inside its overridden viewDidLoad() method, it adds an instance of a UIScrollView to its view and an instance of a BackgroundControl to the scroll view:
I've made my view controller implement UIScrollViewDelegate, so viewDidLoad() also sets itself as the delegate and, finally, I add a gesture recognizer, UILongPressGestureRecognizer, to the scroll view.
let scrollView = UIScrollView(frame: CGRectZero)
let backgroundControl = BackgroundControl(frame: CGRect(x: 0, y: 0, width: 5000, height: 5000))
[...]
view.addSubview(scrollView)
scrollView.addSubview(backgroundControl)
I've made my view controller implement UIScrollViewDelegate, so viewDidLoad() also sets itself as the delegate and, finally, I add a gesture recognizer, UILongPressGestureRecognizer, to the scroll view.
scrollView.delegate = self
let longPress = UILongPressGestureRecognizer(target: self, action: "longHoldHandler:")
scrollView.addGestureRecognizer(longPress)
A lot of the functionality I need comes pretty much "out of the box". There's one method from UIScrollViewDelegate I had to implement is viewForZoomingInScrollView():
func viewForZoomingInScrollView(scrollView: UIScrollView!) -> UIView!
{
returnbackgroundControl
}
...without implementing that, there's no zooming and nothing happens!
So, with a handful of lines we have pinch to zoom and panning on the grid workspace.
The next step is to add a new blue node on a long press gesture. Long hold is a continuous gesture and I want to add the new node when the recogniser's state is Began. Therefore, the first few lines of longHoldHandler() look like this:
Next, I only want to create a new node if the user has long held on the background (i.e. not on another node where I might want to respond to that gesture differently). So, I get the gesture location, and use hitTest to ensure the gesture receiver is of type BackgroundControl:
If the code has got that far, I'm ready to create a new node and add it to the background control:
You'll notice that I've also added two actions to respond to control events. These disable scrolling in the scrollview while there's a touch gesture inside the node:
...which allow me to handle panning inside the Node class without unwanted scrolling., Without these, the node's internal pan gesture handler prevents the gesture from propagating but only about half the time, so this is a sort of belt-and-braces piece of code
The Node class has its own UIPanGestureRecognizer added to it inside its didMoveToSuperView() method:
Again, because panning is a continuous gesture, panHandler() looks at the recogniser's state to either offset its frame based on the gesture location or dispatch a TouchUpInside event when the gesture is finished:
There you have it - a user interface that supports panning, pinch zooming, creation of sub nodes with a long press and the ability to drag-move those sub nodes.
All the source code is available in my GitHub repository. Thanks to this tutorial from raywenderlich.com and this article from rockhoppertech.com.
In Other News....
If you read my recent article regarding my CIFilter chaining application, you may be interested to learn that I've now added animation to to the adding and deletion of filters in the collection view (which you can read about here) and added support for the CIToneCurve filter - pictured above.
So, with a handful of lines we have pinch to zoom and panning on the grid workspace.
The next step is to add a new blue node on a long press gesture. Long hold is a continuous gesture and I want to add the new node when the recogniser's state is Began. Therefore, the first few lines of longHoldHandler() look like this:
func longHoldHandler(recognizer: UILongPressGestureRecognizer)
{
if recognizer.state==UIGestureRecognizerState.Began
{
[...]
Next, I only want to create a new node if the user has long held on the background (i.e. not on another node where I might want to respond to that gesture differently). So, I get the gesture location, and use hitTest to ensure the gesture receiver is of type BackgroundControl:
let gestureLocation = recognizer.locationInView(backgroundControl)
ifbackgroundControl.hitTest(gestureLocation, withEvent: nil) isBackgroundControl
{
[...]
If the code has got that far, I'm ready to create a new node and add it to the background control:
let originX = Int( gestureLocation.x-75 )
let originY = Int( gestureLocation.y-75 )
let node = Node(frame: CGRect(x: originX, y: originY, width: 150, height: 150))
node.addTarget(self, action: "disableScrolling", forControlEvents: .TouchDown)
node.addTarget(self, action: "enableScrolling", forControlEvents: .TouchUpInside)
backgroundControl.addSubview(node)
You'll notice that I've also added two actions to respond to control events. These disable scrolling in the scrollview while there's a touch gesture inside the node:
func disableScrolling()
{
scrollView.scrollEnabled = false
}
func enableScrolling()
{
scrollView.scrollEnabled = true
}
...which allow me to handle panning inside the Node class without unwanted scrolling., Without these, the node's internal pan gesture handler prevents the gesture from propagating but only about half the time, so this is a sort of belt-and-braces piece of code
The Node class has its own UIPanGestureRecognizer added to it inside its didMoveToSuperView() method:
let pan = UIPanGestureRecognizer(target: self, action: "panHandler:");
addGestureRecognizer(pan)
Again, because panning is a continuous gesture, panHandler() looks at the recogniser's state to either offset its frame based on the gesture location or dispatch a TouchUpInside event when the gesture is finished:
func panHandler(recognizer: UIPanGestureRecognizer)
{
if recognizer.state==UIGestureRecognizerState.Changed
{
let gestureLocation = recognizer.locationInView(self)
frame.offset(dx: gestureLocation.x-frame.width/2, dy: gestureLocation.y-frame.height/2)
}
elseif recognizer.state==UIGestureRecognizerState.Ended
{
sendActionsForControlEvents(.TouchUpInside)
}
}
There you have it - a user interface that supports panning, pinch zooming, creation of sub nodes with a long press and the ability to drag-move those sub nodes.
All the source code is available in my GitHub repository. Thanks to this tutorial from raywenderlich.com and this article from rockhoppertech.com.
In Other News....
If you read my recent article regarding my CIFilter chaining application, you may be interested to learn that I've now added animation to to the adding and deletion of filters in the collection view (which you can read about here) and added support for the CIToneCurve filter - pictured above.