Programmatic UI development
Using code to imperatively develop cross platform UI in Apple Swift
Goals and vision
Some people like to design UI using a powerful editor, some like to programmatically program UI. Both approaches have its benefits.
Our vision is to design UIs using a powerful, yet easy to use IDE that enables the developer to create complicated, sophisticated enterprise applications easily. SCADE IDE is very powerful for 1.0 version, and we are planing to improve it make and make it even more fun to develop with. We have tons of new features in mind, plus at the same time constantly improvement speed and usability of the existing features.
For those who like programmatic UI design, SCADE should also support this approach nicely. In this chapter, we describe how to use only code to create a cross platform mobile apps with a nice UI. This demo is done using our IDE, but not using the page editor. I will come up with a Sublime tutorial as well, or perhaps someone who reads this likes to contribute a Sublime/Atom version of this tutorial.
Learning goals
The reader shall learn how to create a hierarchy of containers and widgets to develop the app and how to configure the layout so that the containers and widgets render properly. The details for each widget are discussed in the specific user guide and not part of this guide.
Sourcecode
Please see the UgProgrammaticUIDev project for the source code created in this chapter. See Installing examples for information on how to download the examples from https://github.com/scadedoc/UgExamples/tree/master/UgProgrammaticUIDev
Basics
A few reminder for getting started quicker in the Swift editor:
- To search the class hierarchy, type Command + Shift + T. Use case use * and ? as wildcards
- To inspect a class, press COMMAND and hover over the class, press the link
- A widget is always a container, but a container doesn't need to be a widget
- Widgets are controls such as buttons, checkboxes, map controls etc.
Recommendations
An understanding how components, different containers and visual elements interact and work with each other is extremely helpful for writing code. We would highly recommend to get familiar with the page editor before writing code for generating the UI.
Hello World
- Create an empty new SCADE project and open the main.page.swift
- The following code adds a button and prints out text when pressed.
- It is mandatory to set the layoutData property using the SCDLayoutGridData class
- You append the elements you want to add to the children property
override func load(_ path: String) {
super.load(path)
// setup the button
let button = SCDWidgetsButton.create()
button.text = "Press me!"
button.onClick.append( SCDWidgetsEventHandler{ _ in print("Button pressed" )})
button.layoutData = SCDLayoutGridData()
// add button
self.page!.children.append(button)
}
Always set layoutData attribute
The most important thing to remember is to always set the SCDLayoutGridData in the layoutData attribute for each widget. Without it, the code will crash. We will change the API in the near future and make the constructor require an argument of type SCDLayoutGridData going forward.
Make sure row and column are set correctly
Another issue we currently have if you define layoutData where the row and column exceeds the parent container, it will also crash silently. We address this soon.
Button
SCADE supports the regular button, i.e. UIButton in iOS and android.widget.Button on Android. Creating the button is easy:
- Use SCDWidgetsButton.create()
- Set the button text (or a bitmap)
- Set the handler to do something when the button is pressed
let button = SCDWidgetsButton.create()
button.name = "btnPressMe"
button.text = "Press me!"
button.onClick.append( SCDWidgetsEventHandler{ e in self.handleButtonClick(e!) })
The variable e is of type SCDWidgetsEvent and has a variable target, which contains the reference to the object that caused the event.
if let button = e.target as? SCDWidgetsButton {
button.text = "Hello World. I was clicked!"
}
Now lets configure SCDLayoutGridData to position the button in the middle of the screen. The SCDLayoutGridData will be discussed in detail later on, but for now, the code is pretty self explanatory.
override func load(_ path: String) {
super.load(path)
let button = SCDWidgetsButton.create()
button.text = "Press me!"
button.onClick.append( SCDWidgetsEventHandler{ e in self.handleButtonClick(e!) })
configureFontStyle(of:button)
// It is MANDATORY to set the layoutData property. Without it, code crashes
let gridData = SCDLayoutGridData()
gridData.row = 0 // here, we tell the layout manager that this control belongs into row 1 (i.e 0)
gridData.horizontalAlignment = .center
gridData.verticalAlignment = .middle
gridData.isGrabHorizontalSpace = true
gridData.isGrabVerticalSpace = true
// assign layout to button
button.layoutData = gridData
// Configure current page
let layout = self.page!.layout as! SCDLayoutGridLayout
layout.rows = 2 // here we tell the layout manager that the grid has two rows
// add button to page
self.page!.children.append(button)
}
Running the code, you will see the button nicely centered in the middle of the screen.
- the gridData.row = 0 places the button into the first row
- the layout manager renders both rows, but the second row is empty, therefore taking up no space
- the button fully grabs the available space and is displayed as centered with the space available
Understanding layout
Widgets are rendered within a container of a certain type. The following types determine the layout. Containers can be nested within each other (put one container within another).
Please also see Layout Manager for introduction into layout. This table gives an overview of all your layout types:
View | Class | Comment |
---|---|---|
Grid layout | SCDWidgetsGridView | Creates an n x m matrix of cells. The height / width of each row and column depends on the width / height of its largest cell. |
Horizontal layout | SCDWidgetsListView | Arranges the elements of the container from left to right. |
Vertical layout | SCDWidgetsRowView | Arranges the elements of the container from top to botton. |
XY Layout | SCDWidgetsXYView | Exact positioning of widgets using (x,y). This view should be used for special cases, in general using the above three makes more sense and allows support on all devices, with the layout done be by layout manager. |
Steps for configuring layout
In order to correctly configure the layout of a widget/container, you follow these steps
- Mandatory. Set layoutData attribute to tell the layout manager how to render the widget within the parent
- Configure layout attribute to tell the layout manager how to render the children within the widget/container
- Set column and row attribute for each widget so that the layout manager knows where the widgets are placed.
- Mandatory Add the control to its parent using parent.children.append(<widget/container>) for the current control and .children.append(<widget/container>) for alll widgets you want to add to the widget.
1. Set layoutData with SCDLayoutGridData
The SCDLayoutGridData class is used to inform the layout manager about the behavior of the control within its parent container
let gridData = SCDLayoutGridData()
...
button.layoutData = gridData
The layout manager reads the layoutData property and uses it to position the control within the parent container.. Therefore, it always needs to be set. Introspecting the class using Command + Hover + Click, we see the main attributes:
Attribute | Values_ | Description |
---|---|---|
column | Int | The column the widget is located in |
row | Int | The row the widget is located in |
horizontalAlignment | left,center,right | The position of the widget within its bounding box. |
verticalAlignment | top, middle, bottom | The position of the widget within its bounding box |
isGrabHorizontalSpace | true / false | Shall the widget be greedy and occupy as much space as possible horizontally ? |
isGrabVerticalSpace | true / false | Shall the widget be greedy and occupy as much space as possible vertically ? |
widthConstraint | wrap_content / match_parent | Make the bounding box so that it wraps the widget (wrap_content) or maximize so that it makes use of the parent's space (match_parent) |
heightConstraint | wrap_content / match_parent | Make the bounding box so that it wraps the widget (wrap_content) or maximize so that it makes use of the parent's space (match_parent) |
isExclude | true / false | If the layout manager should exclude this widget when rendering the page. Default is false. |
2. Setting layout
The layout attribute determines how the children are rendered within the container.
In most cases, we use the SCDLayoutGridLayout class and all you need to do is to set the numbers of columns and rows used. Per default, rows == columns == 1. In this example, we configure the page to have a 2x1 matrix:
// Configure current (main page)
let layout = self.page!.layout as! SCDLayoutGridLayout
layout.rows = 2 // here we tell the layout manager that the grid has two rows
3. Setting row and column attribute
In order to tell the container where which widget is located, each widget has a row and column attribute as part of the SCDLayoutDataGrid structure that you need to set. The default is (0,0). You need to set this for all views (Grid, Horizontal, Vertical) but the XYView.
View | Attribute to set |
---|---|
Grid view | Set both row and column |
Horizontal view | Set column only |
Vertical view | Set row only |
For instance, we configured the layoutData.row attribute to be 0 in order to place the button into the first row.
...
gridData.row = 0 // this control belongs into row 1 (i.e 0)
...
4. Add controls to parent
self.page!.children.append(button)
Mother of all containers - SCDWidgetsContainer
All these views are inherited from SCDWidgetsContainer. This class inherited a lot of behavior from some other classes.
Attribute | Values___ | Description |
---|---|---|
isVisible | true/false | If the element is shown or not. If set to false, the element is not shown, BUT still part of the rendering logic. So for instance if you have a bitmap and set it to isVisible = false, the bitmap will not render BUT it will still occupy the space in the container. |
isEnabled | true / false | Enable or disable widget |
layoutData | Possible values are SCDLayoutGridData SCDLayoutXYLayoutData | Mandatory. Always needs to be set. Determines the layout of the container as part of a larger container, i.e how this container is positioned within its parent. |
layout | Possible values are * SCDLayoutGridLayout | Determines the layout of the children of the container, i.e. the layout of all widgets within this container. |
size.width | Width of bounding box of widget in pixels | |
size.heigth | Height of bounding box of widget in pixels | |
location.x | X location | |
location.y | Y location | |
contentSize.width | width of actual widget | |
contentSize.height | height of actual widget |
Hello World continued
Text style
To set the style of a text, use the SCDWidgetsFontStyle class as described here:
func configureFontStyle(of control : SCDWidgetsTextWidget ) {
if let fontStyle = control.getStyle(SCDWidgetsFontStyle.eClass) as? SCDWidgetsFontStyle {
fontStyle.fontFamily = "Courier"
fontStyle.color = redColor // see start.swift for def of redColor
// fontStyle.isUnderline = true
// fontStyle.isLineThrough = true
}
}
Dynamically adding a widget, i.e. Bitmap
To make our HelloWorld a little bit more interesting, we add the image dynamically whenever we press the button. Please see here Bitmap control for details on loading a bitmap:
func handleButtonClick(_ e:SCDWidgetsEvent) {
...
let svgIcon = getBitmapControl()
(svgIcon.layoutData as! SCDLayoutGridData).row = 1
self.page!.children.append(svgIcon)
}
The interesting part here is setting the row to 1. Doing this, we tell the bitmap that it belongs into the second row of the view. We then append the bitmap to the current page.
Text entry example
This example consists of a header with label, a text entry grid and a bottom with a button inside:
The header
The header consists of a row view with a label inside. We configured it to use minimal space and have a red background.
The row view has been used for demonstration purposes and as a good practice in case you want to add other elements, but you could use the label only.
func getHeader() -> SCDWidgetsRowView {
let headerView = SCDWidgetsRowView.create()
configureBackground(of:headerView)
// Position in row #1 and use minimal space
let layoutData = configureLayoutData(of:headerView)
layoutData.row = 0
layoutData.isGrabVerticalSpace = false
layoutData.heightConstraint = .wrap_content
// Create the label
let label = SCDWidgetsLabel.create();
label.text = "Hello SCADE"
self.configureFontStyle(of:label)
_ = configureLayoutData(of:label)
// and insert into row view
headerView.children.append(label)
return headerView
}
Textentry form
The form part consists of a 2x3 grid. We changed the margin and set the row to 1, making it appear in row 2 of the main page:
Textbox
The textbox has the usual text attribute. Furthermore, the tabindex is used to set the sequence in which textfields are entered. The widthConstraint is set to match_parent, using the maximum available space of the parent (isGrabHorizontalSpace is set to true by configureLayoutData)
func getTextBox(inCol:Int,inRow:Int) -> SCDWidgetsTextbox {
// Create textbox and main attributes
let textbox = SCDWidgetsTextbox.create()
textbox.text = "TB\(inRow)"
textbox.tabIndex = inRow
textbox.hint = "Last name"
let layoutData = configureLayoutData(of:textbox)
// Set column and row
layoutData.column = inCol
layoutData.row = inRow
layoutData.isGrabVerticalSpace = false
layoutData.heightConstraint = .wrap_content
layoutData.widthConstraint = .match_parent
return textbox
}
The bottom
Nothing new here. Make sure that row is set to 2 to position this row view in the 3rd row of the main page:
func getBottomView() -> SCDWidgetsListView {
let bottomView = SCDWidgetsListView.create()
let layoutData = configureLayoutData(of:bottomView)
layoutData.row = 2
layoutData.widthConstraint = .wrap_content
layoutData.heightConstraint = .wrap_content
layoutData.isGrabVerticalSpace = false
let button = SCDWidgetsButton.create()
button.text = "Press me"
_ = configureLayoutData(of:button)
bottomView.children.append(button)
return bottomView
}
List control example
For full details on the list control, please see List control
Now lets programmatically create a list control. As with the creation through the IDE page editor, you need to follow two steps
- Step 1: Configure the template of the list control, i.e. add all controls that are displayed for each row of the list
- Step 2: Populate the rows with data
In comparison to the IDE, you cannot use data binding here, but have to set the data directly. We might add this capability to the API in later versions.
Step 1 : ListControl & template
The class representing a list control is SCDWidgetsList. We configure it to use the entire space and then use _template?.element?.children to set the container/control describing the row.
func createListControl() -> SCDWidgetsList {
// Two steps
let listControl = SCDWidgetsList.create()
let layoutData = configureLayoutData(of: listControl)
layoutData.widthConstraint = .match_parent
layoutData.heightConstraint = .match_parent
// First step, setup template
listControl._template?.element?.children = [createTemplate()]
// return control
return listControl
}
There is nothing special about the creation of the template. You just arrange the controls you want inside a container. The following code adds two labels into a row view:
func createTemplate() -> SCDWidgetsRowView {
// Create row view and configure it
let row = SCDWidgetsRowView.create()
_ = configureLayoutData(of:row)
// configure inner layout
let layout = row.layout as! SCDLayoutGridLayout
layout.columns = 2
// add two labels to rowView
row.children = [createLabel(inColumn:0),createLabel(inColumn:1)]
return row
}
Step 2: Setting the data
In the second step, you set the data that shows up in the list control.
- Population needs to happen in the show method
- First, populate the .items array. It usually hold all the data that is used for data binding. Defining bindings through code is not supported yet, but for each row in the list control, you need to add an entry into the array.
- Second, you need to set the values for each row. The rows in the list control are displayed using the .elements array (which we set using the setValues method)
override func show(_ view: SCDLatticeView?) {
super.show(view)
// Step 2: populate data in list control. Population needs to be done in SHOW method
let names = [("Peter","Parker"),("Louise","Lane"),("Bruce","Willis")]
// 2.1 make sure items array size is equal to number of rows for the list control
self.listControl!.items = names.map{ return $0.0 }
// 2.2. set data in each row
names.enumerated().forEach{ setValues(in: self.listControl!.elements[$0.0], to:$0.1) }
}
The elements in the list control are of type SCDWidgetsListElement. Use element.children[0] to access the first children in the respective element. This in our case is our SCDWidgetsRowView created in createTemplate(). Then drill down and access the labels to set the first and last name:
func setValues(in element:SCDWidgetsListElement,to:(String,String)) {\
if let rowControl = element.children[0] as? SCDWidgetsRowView {
// set labels
if let label1 = rowControl.children[0] as? SCDWidgetsLabel {
label1.text = to.0
}
if let label2 = rowControl.children[1] as? SCDWidgetsLabel {
label2.text = to.1
}
}
}
Dynamic Button grid example
Here another example of how to configure a grid of buttons. The number of columns and rows can be set during runtime.
The main steps are
- Start out with configuring the main page into a 1x2 grid, where the first cell holds another grid containing the buttons, and the second cell holds a simple container with a button
- Create a grid container. Configure the layout of the container using the SCDLayoutGridLayout class and set the layout using the rows and columns properties.
- We used colorOfButtons data structure to hold the color for the button placed in a specific cell.
import ScadeKit
class DynamicButtonGridPageAdapter: SCDLatticePageAdapter {
var colorOfButton : [([(Int,Int)],SCDGraphicsRGB)] = [([(1,1)],orangeColor)]
// page adapter initialization
override func load(_ path: String) {
super.load(path)
// setup grid and place buttons
self.renderGrid(columns:3,rows:2)
}
func renderGrid(columns:Int,rows:Int) {
// configure the main page to be 1x2 (1 column, two rows)
self.page!.layout = self.createLayout(columns: 1, rows: 2)
// create handler for button pressed
let onButtonPressed = { (buttonName:String) in print("Button pressed:\(buttonName)") }
// create grid container to hold buttons. configure the grid to be columns x rows
let gridContainer = createGridContainer(at:(0,0))
gridContainer.layout = createLayout(columns: columns, rows: rows)
// create horizontal container to hold a button
let bottomContainer = createHorizontalContainer(at:(0,1))
self.setBackgroundColor(of: bottomContainer, to: lightGrayColor)
let button = createButton(label: "PressMe", action: { _ in print("PressMe") })
bottomContainer.children.append(button)
// add containers to page
[gridContainer,bottomContainer].forEach { self.page!.children.append($0) }
// create buttons and assign to grid container
for r in (0..<rows) {
for c in (0..<columns) {
// create button
let b = createButton(label: "Button \(r)(c)", action: onButtonPressed)
// create button layout and assign to button
b.layoutData = createLayoutGridData(column: c, row: r)
// check for color and set it
if let color = self.getColor(forButton: (c, r)) {
self.setBackgroundColor(of: b, to: color)
}
// append button to page
gridContainer.children.append(b)
}
}
}
// all these methods should be moved into a utility class
func createButton(label:String,action:@escaping (_ e:String)->Void) -> SCDWidgetsButton {
let button = SCDWidgetsButton.create()
button.layoutData = createLayoutGridData()
button.name = label
button.text = label
// on click, call callback with name of button
button.onClick.append( SCDWidgetsEventHandler{ (e:SCDWidgetsEvent?) in
let button = e!.target as! SCDWidgetsButton
action(button.name)
})
self.configureFontStyle(of: button, to: whiteColor)
self.setBackgroundColor(of: button, to: redColor)
// Add background color of button
return button
}
func createLayoutGridData(column:Int,row:Int) -> SCDLayoutGridData {
let gridData = createLayoutGridData()
gridData.row = row
gridData.column = column
return gridData
}
func createLayoutGridData() -> SCDLayoutGridData {
let gridData = SCDLayoutGridData()
gridData.horizontalAlignment = .center
gridData.verticalAlignment = .middle
gridData.isGrabHorizontalSpace = true
gridData.isGrabVerticalSpace = true
return gridData
}
func createLayout(columns:Int,rows:Int) -> SCDLayoutGridLayout {
let layout = SCDLayoutGridLayout()
layout.rows = rows
layout.columns = columns
return layout
}
func setBackgroundColor(of control : SCDWidgetsWidget, to color:SCDGraphicsRGB ) {
if let backgroundStyle = control.getStyle(SCDWidgetsBackgroundStyle.eClass) as? SCDWidgetsBackgroundStyle {
backgroundStyle.type = .color
backgroundStyle.color = color
}
}
func createHorizontalContainer(at:(Int,Int)) -> SCDWidgetsContainer {
// create a horizontal container
let container = SCDWidgetsListView.create()
container.layoutData = createLayoutGridData(column:at.0,row:at.1)
return container
}
func createGridContainer(at:(Int,Int)) -> SCDWidgetsGridView {
// create a grid container
let container = SCDWidgetsGridView.create()
container.layoutData = createLayoutGridData(column:at.0,row:at.1)
return container
}
func getColor(forButton:(Int,Int)) -> SCDGraphicsRGB? {
for cellColors in self.colorOfButton {
let (cells,color) = (cellColors.0,cellColors.1)
if(cells.contains{ $0 == forButton}) {
return color
}
}
return nil
}
func configureFontStyle(of control : SCDWidgetsTextWidget,to color:SCDGraphicsRGB ) {
if let fontStyle = control.getStyle(SCDWidgetsFontStyle.eClass) as? SCDWidgetsFontStyle {
fontStyle.color = color
}
}
}
Other
API documentation question
Where the heck do i find a complete API documentation??? Sorry, we are a little late concerning this, and will generate documentation automatically in our next release. For now, please inspect the class hierarchy yourself using the shortcuts mentioned here Shortcuts
Updated over 6 years ago