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

  1. Create an empty new SCADE project and open the main.page.swift
  2. The following code adds a button and prints out text when pressed.
  3. It is mandatory to set the layoutData property using the SCDLayoutGridData class
  4. 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:

ViewClassComment
Grid layoutSCDWidgetsGridViewCreates 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 layoutSCDWidgetsListViewArranges the elements of the container from left to right.
Vertical layoutSCDWidgetsRowViewArranges the elements of the container from top to botton.
XY LayoutSCDWidgetsXYViewExact 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

  1. Mandatory. Set layoutData attribute to tell the layout manager how to render the widget within the parent
  2. Configure layout attribute to tell the layout manager how to render the children within the widget/container
  3. Set column and row attribute for each widget so that the layout manager knows where the widgets are placed.
  4. 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:

AttributeValues_Description
columnIntThe column the widget is located in
rowIntThe row the widget is located in
horizontalAlignmentleft,center,rightThe position of the widget within its bounding box.
verticalAlignmenttop, middle, bottomThe position of the widget within its bounding box
isGrabHorizontalSpacetrue / falseShall the widget be greedy and occupy as much space as possible horizontally ?
isGrabVerticalSpacetrue / falseShall the widget be greedy and occupy as much space as possible vertically ?
widthConstraintwrap_content / match_parentMake 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)
heightConstraintwrap_content / match_parentMake 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)
isExcludetrue / 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.

ViewAttribute to set
Grid viewSet both row and column
Horizontal viewSet column only
Vertical viewSet 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.

AttributeValues___Description
isVisibletrue/falseIf 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.
isEnabledtrue / falseEnable or disable widget
layoutDataPossible 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.
layoutPossible values are
* SCDLayoutGridLayout
Determines the layout of the children of the container, i.e. the layout of all widgets within this container.
size.widthWidth of bounding box of widget in pixels
size.heigthHeight of bounding box of widget in pixels
location.xX location
location.yY location
contentSize.widthwidth of actual widget
contentSize.heightheight 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

  1. Step 1: Configure the template of the list control, i.e. add all controls that are displayed for each row of the list
  2. 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