Creating a Node Based User Interface for iOS Using Swift
Join the DZone community and get the full member experience.
Join For FreeI've long been a fan of node based user interfaces and I've been coding various implementations from my first Flex version back in 2008 up to my recent Adobe AIR based Nodality image compositing app for iPads.
Based on my recent Swift experiments with the UIScrollView and my implementation of the Presentation Model pattern, I've now created a functional proof-of-concept version of a node based calculator. A nodal UI probably isn't the best solution for a simple calculator, but it's a great way to get the user interface up and running before extending it for other purposes.
The video above describes the interaction design. Put simply, to create a node, the user does a long press on the background. Nodes can either be numeric input or an operator (e.g. add or subtract). To create a relationship between two nodes, a long press on the input nodes puts the app into 'relationship creation mode' where the user selects a target.
I have played with a more traditional drag-to-create-relationship interaction, but when the nodes a far apart, it can be quite fiddly. The 'relationship creation mode' technique works pretty well, especially when the UI is zoomed out.
Diving into the code and the main view controller looks pretty simple: it contains a toolbar and an instance of myBackgroundControl that lives inside a UIScrollView. There are also some observers on the presentation model that position node renderers, disable scrolling and change change the background colour in response to specific events.
The BackgroundControl contains an instance of BackgroundGrid, that simply draws a grid, andRelationshipCurvesLayer that draws the Bezier curves between related nodes.
Initially, both BackgroundGrid and RelationshipCurvesLayer were extended CALayers that drew themselves inside an overridden drawInContext() method in response to a needsDisplay(). However, this technique was slow and jerky, caused memory warnings and crashed my iPad. The solution was to extend CAShapeLayer and draw the grids and curves immediately with some public methods by setting the class's path property.
The nodes themselves are rendered using instances of NodeWidget that are added as sub views to BackgroundControl. These handle their own movement with a UIPanGestureRecognizer, and, with the help of aUILongPressGestureRecognizer, inform the presentation model when to enter 'relationship creation mode'.
The Toolbar component contains two buttons to toggle between numeric and operator node types and either a range of buttons to select an operator or a simple numeric keyboard (there's no dedicated purely numeric keyboard for iPads).
So, with a handful of node renderers, a hierarchy of background components and a toolbar, the presentation model pattern has worked beautifully to choreograph the application's logic.
Its key properties are an array of node value objects, NodeVO, which model a node, and a reference to the currently selected node. It also exposes a number of methods, which are invoked directly by the view components, such aschangeSelectedNodeOperator() and changeSelectedNodeValue() both invoked by the toolbar.
When a node's operator or value is changed, the NodesPM invokes a recursive function, nodeUpdated() which updates the value of a node value object, then finds all that node's descendants and updates their values:
static func nodeUpdated(node: NodeVO) { node.updateValue() postNotification(.NodeUpdated, payload: node) for candidateNode in nodes { for inputNode in candidateNode.inputNodes { if inputNode == node && candidateNode.nodeType == NodeTypes.Operator { nodeUpdated(candidateNode) } } } }
The updateValue() method inside the node simply switches between possible operators and updates its value based on its inputs:
func updateValue() { if inputNodes.count >= 2 { let valueOne = inputNodes[0] let valueTwo = inputNodes[1] switch nodeOperator { case .Null: value = 0 case .Add: value = valueOne.value + valueTwo.value case .Subtract: value = valueOne.value - valueTwo.value case .Multiply: value = valueOne.value * valueTwo.value case .Divide: value = valueOne.value / valueTwo.value } } }
Swift's requirement for exhaustive switch statements coupled with its typed enumerations is fantastic. It makes forgetting to add a case impossible simply because the code won't compile without it.
I'm really impressed by the speed of Swift. I've added loads of animations to fade in new controls and animate between colours, there are drop shadows on the node widgets and Bezier curves and blurs on the toolbar, yet the UI is smooth and responsive.
This proof-of-concept, along with the experiments I did recently chaining together CIFilters paves the way for version 2 ofNodality written entirely in Swift. My next step is to look at Core Data to persist state and allow the user to save networks of nodes. The best place to keep an eye on my progress is through my NSFlexMonkey account on Twitter.
All the source code for this application is available in my GitHub repository.
Published at DZone with permission of Simon Gladman, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
Part 3 of My OCP Journey: Practical Tips and Examples
-
A React Frontend With Go/Gin/Gorm Backend in One Project
-
From On-Prem to SaaS
-
Cucumber Selenium Tutorial: A Comprehensive Guide With Examples and Best Practices
Comments