List control

Version 1.0 - 3. February 2017

📘

Sourcecode

Please see the UgListControlDemo 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/UgListControlDemo

Introduction

Our list control is very powerful because it's very versatile. You can configure it to do many things.

  • Its can contain list elements of different types
  • Each list element can be composed of many other elements
  • You can hide and show certain elements programmatically
  • You can change a lot of the characteristics by simply binding it to control variables
  • The list control uses native graphics as well as platform specific behaviour, i.e
    • Controls within the list control such as label or button are mapped to its respective native iOS and Android platform
    • Drag and drop & slide or any touch movement use the algorithms of the respective native iOS and Android platform and are native

Displaying an array in a list control

In this example, we want to display the first 100 most popular dog names (from 2020) together with a breed and id.

Add list control to page

  • We use the contraints box to make sure it uses all the space available

Add controls to list template

We add the controls we want to see in each list element

  • Each list control contains a list of list elements
  • Each list element contains the same visual controls to display the data
  • The list element is designed using the the list controls's list element template
  • In this example, we added 3 test labels into a GridView

Define Data Model

  • We use the Dog class to display a list of dog names and breeds
  • We inherit from EObject
import ScadeKit

class Dog : EObject {

  // please make sure you annotate each variable with the type
  // for SCADE to more identify and leverage the type information
  // currently, you must inherit from EObject
  var id: String
  var name: String
  var ageInYears: Int
  var breed: String

  init(id: String, name: String, breed: String, ageInYears: Int) {
    self.id = id
    self.name = name
    self.breed = breed
    self.ageInYears = ageInYears
  }
}

Bind data to controls

Bind the list elements visual control to the data

  • In order to bind the view to the model, we use the SCDWidgetsElementProvider class
  • Its set using .elementProvider
  • You set an instance of SCDWidgetsElementProvider and add the logic that binds the data to the list element's visual controls
  • The template parameter is of type SCDWidgetsGridView. Use the getWidgetByName method to retrieve a reference to the visual control and set the data from the Dog class instance:
class MainPageAdapter: SCDLatticePageAdapter {

  var dogs: [Dog] = []

  // page adapter initialization
  override func load(_ path: String) {

    super.load(path)

    self.list1.elementProvider = SCDWidgetsElementProvider { (dog: Dog, template) in

      (template.getWidgetByName("dogName") as! SCDWidgetsLabel).text = dog.name

      (template.getWidgetByName("dogBreed") as! SCDWidgetsLabel).text = dog.breed

      (template.getWidgetByName("dogId") as! SCDWidgetsLabel).text = dog.id
                                                         
    }
  }
}

Set the data source of the control

  • Define the source of data by setting the items property
  • Its a collection of data objects
self.list1.items = Dogs().dogs

List Control lists data successfully

Add handler to process row click / item selected event

To add logic whenever a row is clicked,

  1. get a handle on the list control and cast the object to SCDWidgetsList
  2. add the event handler to the .onItemSelected
  3. The event handler is called SCDWidgetsItemSelectedEventHandler
// listen to itemSelected events
let listControl = self.page!.getWidgetByName("list1") as! SCDWidgetsList
listControl.onItemSelected.append(SCDWidgetsItemSelectedEventHandler{ ev in self.rowClicked(event: ev!)})
  1. The event it fires is SCDWidgetsItemEvent
  2. The item is contained in a field called item
  3. Just cast it to the target structure
func rowClicked(event:SCDWidgetsItemEvent) {
	if let dog = event.item as? Dog {
		print("Hello \(dog.name)")
 }
}

Entire class looks like this

import ScadeKit

class MainPageAdapter: SCDLatticePageAdapter {

	@objc dynamic var dogs : [Dog] = []
	
	// page adapter initialization
	override func load(_ path: String) {		
		super.load(path)
		
		self.setupDogs()
		
		// listen to itemSelected events
		let listControl = self.page!.getWidgetByName("list1") as! SCDWidgetsList
		listControl.onItemSelected.append(SCDWidgetsItemSelectedEventHandler{ ev in self.rowClicked(event: ev!)})
				
		// Listen to action buttons
		let listControlRowActionButtonRight = self.page!.getWidgetByName("btnListRowInfo") as! SCDWidgetsButton
		listControlRowActionButtonRight.onClick.append(SCDWidgetsEventHandler{_ in print("Info button Clicked")})
		
		// wire toolbar buttons
		let groupedByButton = self.page!.getWidgetByName("itmGroupedByBreed") as! SCDWidgetsClickable
		groupedByButton.onClick.append(SCDWidgetsEventHandler{_ in self.navigation!.go("groupedByBreed.page")})
		
		
	}
	
	func setupDogs() {
		dogs.append(Dog(id:"d100",name:"Hector",breed:"Boxer",ageInYears:2))
		dogs.append(Dog(id:"d101",name:"Max",breed:"Labrador", ageInYears:3))
		dogs.append(Dog(id:"d102",name:"Bailey",breed:"St.Bernard", ageInYears:3))
	}
	
	func rowClicked(event:SCDWidgetsItemEvent) {
		if let dog = event.item as? Dog {
			print("Hello \(dog.name)")
		}
	}
}

Works nicely:

Add swipe support

Swipe support allows you to display custom areas containing buttons etc when you swipe the list row left and right. We describe adding a button to the right hand side of the listelement:

  1. Press the > arrow to open up the panel on the right
  1. You then can add any control, for instance a button that we will call btnListRowInfo with the title Info. We change background and font color:

All you need to do is to wire up the button. That's pretty easy:

let listControlRowActionButtonRight = self.page!.getWidgetByName("btnListRowInfo") as! SCDWidgetsButton
listControlRowActionButtonRight.onClick.append(SCDWidgetsEventHandler{_ in print("Info button Clicked")})

Add grouping separator to list control

Adding a separator row to indicate the start of a new set of data is a common use case:

We use the power and flexibility of the list control to add a seperator row. We insert two horizontal layout grids to the list element, and then add the labels indicating the group value and the data details in the respective grid:

The way to achieve this is to hide the elements that form the separator line when the dog's data need to be displayed, and hide the Dog data when the group view needs to be displayed. Therefore, we put all group separator related elements in viewGroup and all detail related elements into viewDetails.

In order to avoid having to code this using Swift code, we add two variables isGroup and isDetail that indicate if the item is a group separator or if it is a detail row:

import ScadeKit

class DogView : EObject {
		
	// please make sure you annotate each variable with the type
	// for SCADE to more identify and leverage the type information
	let id : String
	let name : String
	let ageInYears : Int 
	let breed:String 
	
  // use to indicate if its a separator item or detail item
	let isGroup:Bool
	let isDetail:Bool
	
	init(id:String,name:String,breed:String,ageInYears:Int) {
		self.id = id
		self.name = name
		self.breed = breed
		self.ageInYears = ageInYears	
		self.isGroup = false
		self.isDetail = !self.isGroup
	}
	
	init(breed:String) {
		self.id = ""
		self.name = ""
		self.breed = breed
		self.ageInYears = -1	
		self.isGroup = true
		self.isDetail = !self.isGroup
	}
}

To hide any layout grid, we need to set two attributes, Layout.exclude and Layout.visible:

  • we set Layout.exlude to true and
  • we set Layout.visible to false

We do this using our binding editor:

Finally, we only need to supply the data item:

import ScadeKit

class GroupedByBreedPageAdapter: SCDLatticePageAdapter {

	@objc dynamic var dogs : [DogView] = []
	
	// page adapter initialization
	override func load(_ path: String) {		
		super.load(path)
		setupDogs()
		
		let backbutton = self.page!.getWidgetByName("itmDoglist") as! SCDWidgetsClickable
		backbutton.onClick.append(SCDWidgetsEventHandler{_ in self.navigation!.go("main.page")})
	}
	
	func setupDogs() {
		dogs.append(DogView(breed:"Boxer"))
		dogs.append(DogView(id:"d100",name:"Hector",breed:"Boxer",ageInYears:2))
		dogs.append(DogView(breed:"Labrador"))
		dogs.append(DogView(id:"d101",name:"Max",breed:"Labrador", ageInYears:3))
		dogs.append(DogView(id:"d102",name:"Moritz",breed:"Labrador", ageInYears:3))
		dogs.append(DogView(breed:"St.Bernard"))
		dogs.append(DogView(id:"d103",name:"Bailey",breed:"St.Bernard", ageInYears:3))
	}
}

Now, we grouped our dogs by breed, with full design control of the grouping row.

Create clicked animation effect on list element

This chapter explains on how to add a fade animation effect that visualizes that a row has been clicked. After clicking a row, the background color with turn to gray and that gray color with slowly fade:

  1. Create a new project and add a list control into the main page. Add a label into the list
  2. Create a reference to the list control and add an event handler when the list row is clicked
// create reference to list
let list1 = self.page!.getWidgetByName("list1") as! SCDWidgetsList
		
// populate with some items so that the list is not empty
list1.items = [1,2,3]
		
// add event handler when list row is clicked
list1.onItemSelected.append(SCDWidgetsItemSelectedEventHandler{event in ... })
  1. Each list consists of a list of SCDWidgetsListElement that represent an individual list item. The element variable in the event contains the reference to the element
// reference the SCDWidgetsListElement that has been clicked
let listElement = event!.element as! SCDWidgetsListElement
  1. Now we change the list element's background color. Once the code is executed, the list element becomes gray
// set background color (yes, this API is wordy, we will improve it)
let backgroundStyleClass = SCDWidgetsBackgroundStyle()
let style = listElement.getStyle(backgroundStyleClass.eClass()) as! SCDWidgetsBackgroundStyle
style.type = SCDWidgetsBackgroundType.color
style.color = self.grayColor // previously defined
  1. Now the most important part. Get background rectangle inside list element:. Each list element is represented as a SVGGroup, which in turns contains a box which holds a rectangle. The rectangle is the correct element to modify to change the background:
// find the graphical area (the rectangle) that represents tha background
let drawingGroup = (listElement.drawing) as! SCDSvgGroup
let box = drawingGroup.drawableChilds[0] as! SCDSvgBox
let rect = box.drawableChilds[0] as! SCDSvgRect
  1. Finally, we add an animation that changes the opacity, as such giving a fading effect. (Alternatively, you could have changed the color itself). The animation guide contains a lot more details on animation:
// create animation to change the opacity. See animation guide for more details
let anim = SCDSvgPropertyAnimation("fillOpacity", values: [1, 0.5, 0])
anim.duration = 0.4
anim.repeatCount = 1
anim.delay = 0.2

Please see the full source code below

import ScadeKit

class MainPageAdapter: SCDLatticePageAdapter {

 let grayColor = SCDGraphicsRGB(red: 211, green: 211, blue: 211)
 
 // page adapter initialization
 override func load(_ path: String) {		
  super.load(path)
  
        // create reference to list
	let list1 = self.page!.getWidgetByName("list1") as! SCDWidgetsList
		
	// populate with some items so that the list is not empty
	list1.items = [1,2]
		
	// add event handler when list row is clicked
	list1.onItemSelected.append(SCDWidgetsItemSelectedEventHandler{event in
			
              // reference the SCDWidgetsListElement that has been clicked
              let listElement = event!.element as! SCDWidgetsListElement

              // set background color (yes, this API is wordy, we will improve it)
              let backgroundStyleClass = SCDWidgetsBackgroundStyle()
              let style = listElement.getStyle(backgroundStyleClass.eClass()) as! SCDWidgetsBackgroundStyle
              style.type = SCDWidgetsBackgroundType.color
              style.color = self.grayColor

              // find the graphical area (the rectangle) that represents tha background
              let drawingGroup = (listElement.drawing) as! SCDSvgGroup
              let box = drawingGroup.drawableChilds[0] as! SCDSvgBox
              let rect = box.drawableChilds[0] as! SCDSvgRect

              // create animation to change the opacity. See animation guide for more details
              let anim = SCDSvgPropertyAnimation("fillOpacity", values: [1, 0.5, 0])
              anim.duration = 0.4
              anim.repeatCount = 1
              anim.delay = 0.2

              // adding the animation starts it
              rect.animations.append(anim)
	
  })
}
}

Result: