It’s very common for mobile apps to display lists. Liferay Screens
lets you display asset lists and DDL lists in your iOS app by using
Asset List Screenlet
and
DDL List Screenlet,
respectively. Screens also includes list Screenlets for displaying lists of
other Liferay entities like web content articles, images, and more.
The Screenlet reference documentation
lists all the Screenlets included with Liferay Screens. If there’s not a list
Screenlet for the entity you want to display in a list, you must create your own
list Screenlet. A list Screenlet can display any entity from a Liferay instance.
For example, you can create a list Screenlet that displays standard Liferay
entities like User
, or custom entities from custom Liferay apps.
This tutorial uses code from the sample Bookmark List Screenlet to show you how to create your own list Screenlet. This Screenlet displays a list of bookmarks from Liferay’s Bookmarks portlet. You can find this Screenlet’s complete code here in GitHub.
Note that because this tutorial focuses on creating a list Screenlet, it doesn’t explain general Screenlet concepts and components. Before beginning, you should therefore read the following tutorials:
- Creating iOS Screenlets
- Supporting Multiple Themes in Your Screenlet
- Create and Use a Connector with Your Screenlet
- Add a Screenlet Delegate
- Creating and Using Your Screenlet’s Model Class
This tutorial uses the following steps to show you how to create a list Screenlet:
- Creating the Model class
- Creating the Theme
- Creating the Connector
- Creating the Interactor
- Creating the Delegate
- Creating the Screenlet class
First though, you should understand how pagination works with list Screenlets.
Pagination
To ensure that users can scroll smoothly through large lists of items, list Screenlets support fluent pagination. Support for this is built into the list Screenlet framework. You’ll see this as you construct your list Screenlet.
Now you’re ready to begin!
Creating the Model Class
Recall that a model class transforms each [String:AnyObject]
entity Screens
receives into a model object that represents the corresponding Liferay entity.
For instructions on creating your model class, see the tutorial
Creating and Using Your Screenlet’s Model Class.
The example model class in that tutorial is identical to Bookmark List
Screenlet’s.
Next, you’ll create your Screenlet’s Theme.
Creating the Theme
Recall that each Screenlet needs a Theme to serve as its UI. A Theme needs an XIB file to define the UI’s components and layout. Since a list Screenlet displays a list of entities, its XIB file must contain a Table View. Use these steps to create your Theme’s XIB file:
-
In Xcode, create a new XIB file and name it according to these naming conventions. For example, the XIB for Bookmark List Screenlet’s Default Theme is
BookmarkListView_default.xib
. -
In Interface Builder, drag and drop a View from the Object Library to the canvas. Then add a Table View to the View.
-
Resize the Table View to take up the entire View, and set the constraints the Table View needs to maintain this size dynamically. This ensures that the list fills the Screenlet’s UI regardless of the iOS device’s size or orientation.
For example,
Bookmark List Screenlet’s XIB file
uses a UITableView
inside a parent View to show the list of bookmarks.
Now you’ll create your Theme’s View class. Every Theme needs a View class that
controls its behavior. Since the XIB file uses a UITableView
to show a list of
guestbooks, your View class must extend
the BaseListTableView
class.
Liferay Screens provides this class to serve as the base class for list
Screenlets’ View classes. Since BaseListTableView
provides most of the
required functionality, extending it lets you focus on the parts of your View
class that are unique to your Screenlet. You must also configure the XIB file to
use your View class.
Follow these steps to create your Screenlet’s View class and configure the XIB file to use it:
-
Create your Theme’s View class, and name it according to these naming conventions. Since the XIB uses
UITableView
, your View class must extendBaseListTableView
. For example, this is Bookmark List Screenlet’s View class declaration:public class BookmarkListView_default: BaseListTableView {...
-
Now you must override the View class methods that fill the table cells’ contents. There are two methods for this, depending on the cell type:
-
Normal cells: the cells that show the entities. These cells typically use
UILabel
,UIImage
, or another UI component to show the entity. Override thedoFillLoadedCell
method to fill these cells. For example, Bookmark List Screenlet’s View class overridesdoFillLoadedCell
to set each cell’stextLabel
to a bookmark’s name:override public func doFillLoadedCell(row row: Int, cell: UITableViewCell, object: AnyObject) { let bookmark = object as! Bookmark cell.textLabel?.text = bookmark.name }
-
Progress cell: the cell at the bottom of the list that indicates the list is loading the next page of items. Override the
doFillInProgressCell
method to fill this cell. For example, Bookmark List Screenlet’s View class overrides this method to set the cell’stextLabel
to the string"Loading..."
:override public func doFillInProgressCell(row row: Int, cell: UITableViewCell) { cell.textLabel?.text = "Loading..." }
-
-
Return to the Theme’s XIB in Interface Builder, and set the View class as the the parent View’s custom class. For example, if you were doing this for Bookmark List Screenlet you’d select the Table View’s parent View, click the Identity inspector, and enter
BookmarkListView_default
as the custom class. -
With the Theme’s XIB still open in Interface Builder, set the parent View’s
tableView
outlet to the Table View. To do this, select the parent View and click the Connections inspector. In the Outlets section, drag and drop from thetableView
’s circle icon (it turns into a plus icon on mouseover) to the Table View in the XIB. The new outlet then appears in the Connections inspector.
That’s it! Now that your Theme is finished, you can create the Connector.
Creating the Connector
Recall that Connectors make a server call. To support pagination, a List
Screenlet’s Connector class must extend the
PaginationLiferayConnector
class.
The Connector class must also contain any properties it needs to make the server
call, and an initializer that sets them. To support pagination, the initializer
must also contain the following arguments, which you’ll pass to the superclass
initializer:
startRow
: The number representing the page’s first row.endRow
: The number representing the page’s last row.computeRowCount
: Whether to call the Connector’sdoAddRowCountServiceCall
method (you’ll learn about this method shortly).
For example, Bookmark List Screenlet must retrieve bookmarks from a Bookmarks
portlet folder in a specific site. The Screenlet’s Connector class must
therefore have properties for the groupId
(site ID) and folderId
(Bookmarks
folder ID), and an initializer that sets them. The initializer also calls the
superclass initializer with the startRow
, endRow
, and computeRowCount
arguments:
import UIKit
import LiferayScreens
public class BookmarkListPageLiferayConnector: PaginationLiferayConnector {
public let groupId: Int64
public let folderId: Int64
//MARK: Initializer
public init(startRow: Int, endRow: Int, computeRowCount: Bool, groupId: Int64,
folderId: Int64) {
self.groupId = groupId
self.folderId = folderId
super.init(startRow: startRow, endRow: endRow, computeRowCount: computeRowCount)
}
...
Next, if you want to validate any of your Screenlet’s properties, override the
validateData
method as described in
the tutorial on creating Connectors.
Note that Bookmark List Screenlet only needs to validate the folderId
:
override public func validateData() -> ValidationError? {
let error = super.validateData()
if error == nil {
if folderId <= 0 {
return ValidationError("Undefined folderId")
}
}
return error
}
Lastly, you must override the following two methods in the Connector class:
-
doAddPageRowsServiceCall
: calls the Liferay Mobile SDK service method that retrieves a page of entities. ThedoAddPageRowsServiceCall
method’sstartRow
andendRow
arguments specify the page’s first and last entities, respectively. Make the service call as you would in any Screenlet. For example, thedoAddPageRowsServiceCall
method inBookmarkListPageLiferayConnector
calls the service’sgetEntriesWithGroupId
method to retrieve a page of bookmarks from the folder specified byfolderId
:public override func doAddPageRowsServiceCall(session session: LRBatchSession, startRow: Int, endRow: Int, obc: LRJSONObjectWrapper?) { let service = LRBookmarksEntryService_v7(session: session) do { try service.getEntriesWithGroupId(groupId, folderId: folderId, start: Int32(startRow), end: Int32(endRow)) } catch { // ignore error: the service method returns nil because // the request is sent later, in batch } }
Note that you don’t need to do anything in the
catch
statement because the request is sent later, in batch. Thesession
typeLRBatchSession
handles this for you. You’ll receive the request’s results elsewhere, once the request completes. -
doAddRowCountServiceCall
: calls the Liferay Mobile SDK service method that retrieves the total number of entities. This supports pagination. Make the service call as you would in any Screenlet. For example, thedoAddRowCountServiceCall
inBookmarkListPageLiferayConnector
calls the service’sgetEntriesCountWithGroupId
method to retrieve the total number of bookmarks in the folder specified byfolderId
:override public func doAddRowCountServiceCall(session session: LRBatchSession) { let service = LRBookmarksEntryService_v7(session: session) do { try service.getEntriesCountWithGroupId(groupId, folderId: folderId) } catch { // ignore error: the service method returns nil because // the request is sent later, in batch } }
Now that you have your Connector class, you’re ready to create the Interactor.
Creating the Interactor
Recall that Interactors implement your Screenlet’s actions. In list Screenlets,
loading entities is usually the only action a user can take. The Interactor
class of a list Screenlet that implements fluent pagination must extend the
BaseListPageLoadInteractor
class.
Your Interactor class must also contain any properties the Screenlet needs, and
an initializer that sets them. This initializer also needs arguments for the
following properties, which it passes to the superclass initializer:
screenlet
: ABaseListScreenlet
reference. This ensures the Interactor always has a Screenlet reference.page
: The page number to retrieve.computeRowCount
: Whether to call the Connector’sdoAddRowCountServiceCall
method.
For example, Bookmark List Screenlet’s Interactor class contains the same
groupId
and folderId
properties as the Connector, and an initializer that
sets them. This initializer also passes the screenlet
, page
, and
computeRowCount
arguments to the superclass initializer:
public class BookmarkListPageLoadInteractor : BaseListPageLoadInteractor {
private let groupId: Int64
private let folderId: Int64
init(screenlet: BaseListScreenlet,
page: Int,
computeRowCount: Bool,
groupId: Int64,
folderId: Int64) {
self.groupId = (groupId != 0) ? groupId : LiferayServerContext.groupId
self.folderId = folderId
super.init(screenlet: screenlet, page: page, computeRowCount: computeRowCount)
}
...
The Interactor class must also initiate the server request by instantiating the
Connector, and convert the results into model objects. Override the
createListPageConnector
method to create and return an instance of your
Connector. This method must first get a reference to the Screenlet via the
screenlet
property. When calling the Connector’s initializer, use
screenlet.firstRowForPage
to convert the page number (page
) to the page’s
start and end indices. You must also pass the initializer any other properties
it needs, like groupId
. For example, BookmarkListPageLoadInteractor
’s
createListPageConnector
method does this to create a
BookmarkListPageLiferayConnector
instance:
public override func createListPageConnector() -> PaginationLiferayConnector {
let screenlet = self.screenlet as! BaseListScreenlet
return BookmarkListPageLiferayConnector(
startRow: screenlet.firstRowForPage(self.page),
endRow: screenlet.firstRowForPage(self.page + 1),
computeRowCount: self.computeRowCount,
groupId: groupId,
folderId: folderId)
}
Next, override the convertResult
method to convert each [String:AnyObject]
result into a model object. The Screenlet calls this method once for each entity
retrieved from the server. For example, BookmarkListPageLoadInteractor
’s
convertResult
method converts the [String:AnyObject]
result into a
Bookmark
object:
override public func convertResult(_ serverResult: [String:AnyObject]) -> AnyObject {
return Bookmark(attributes: serverResult)
}
You may also want to support
offline mode
in your Interactor. To do so, the Interactor must override the cacheKey
method
to return a cache key unique to your Screenlet. For example,
BookmarkListPageLoadInteractor
’s cacheKey
method returns a cache key that
includes the Screenlet’s groupId
and folderId
settings:
override public func cacheKey(_ op: PaginationLiferayConnector) -> String {
return "\(groupId)-\(folderId)"
}
Great! Next, you’ll create your Screenlet’s delegate.
Creating the Delegate
Recall that a delegate is required if you want other classes to respond to your Screenlet’s actions. Create your delegate by following the first step in the tutorial on adding a Screenlet delegate. A list Screenlet’s delegate must also define a method for responding to a list item selection. For example, Bookmark List Screenlet’s delegate needs the following methods:
screenlet(_:onBookmarkListResponse:)
: Returns theBookmark
results when the server call succeeds.screenlet(_:onBookmarkListError:)
: Returns theNSError
object when the server call fails.screenlet(_:onBookmarkSelected:)
: Returns theBookmark
when a user selects it in the list.
The BookmarkListScreenletDelegate
protocol defines these methods:
@objc public protocol BookmarkListScreenletDelegate : BaseScreenletDelegate {
optional func screenlet(screenlet: BookmarkListScreenlet,
onBookmarkListResponse bookmarks: [Bookmark])
optional func screenlet(screenlet: BookmarkListScreenlet,
onBookmarkListError error: NSError)
optional func screenlet(screenlet: BookmarkListScreenlet,
onBookmarkSelected bookmark: Bookmark)
}
Nice work! Next, you’ll create the Screenlet class.
Creating the Screenlet Class
Now that your list Screenlet’s other components exist, you can create the
Screenlet class. A list Screenlet’s Screenlet class must extend the
BaseListScreenlet
class
and define the configurable properties the Screenlet needs. You should define
these as IBInspectable
properties. If you want to support offline mode, you
should also add an offlinePolicy
property.
For example,
Bookmark List Screenlet’s Screenlet class
contains the IBInspectable
properties groupId
, folderId
, and
offlinePolicy
:
public class BookmarkListScreenlet: BaseListScreenlet {
@IBInspectable public var groupId: Int64 = 0
@IBInspectable public var folderId: Int64 = 0
@IBInspectable public var offlinePolicy: String? = CacheStrategyType.RemoteFirst.rawValue
...
Next, override the createPageLoadInteractor
method to create and return the
Interactor. If your Screenlet supports offline mode, you should also use
offlinePolicy
to pass a CacheStrategyType
object to the Interactor. For
example, the createPageLoadInteractor
method in BookmarkListScreenlet
creates and returns a BookmarkListPageLoadInteractor
instance. This method
also sets the Interactor’s cacheStrategy
property to a CacheStrategyType
object created with offlinePolicy
:
override public func createPageLoadInteractor(
page page: Int,
computeRowCount: Bool) -> BaseListPageLoadInteractor {
let interactor = BookmarkListPageLoadInteractor(screenlet: self,
page: page,
computeRowCount: computeRowCount,
groupId: self.groupId,
folderId: self.folderId)
interactor.cacheStrategy = CacheStrategyType(rawValue: self.offlinePolicy ?? "") ?? .RemoteFirst
return interactor
}
Now get a reference to your delegate. The BaseScreenlet
class, which
BaseListScreenlet
extends, already defines the delegate
property for the
delegate object. Every list Screenlet therefore has this property, and any app
developer using the Screenlet can access it. To avoid casting this property to
your delegate every time you use it, add a computed property to your Screenlet
class that does so. For example, the following bookmarkListDelegate
property
in BookmarkListScreenlet
casts the delegate
property to
BookmarkListScreenletDelegate
:
public var bookmarkListDelegate: BookmarkListScreenletDelegate? {
return delegate as? BookmarkListScreenletDelegate
}
Next, override the BaseListScreenlet
methods that handle the Screenlet’s
events. Because these events correspond to the events your delegate methods
handle, you’ll call your delegate methods in these BaseListScreenlet
methods:
-
onLoadPageResult
: Called when the Screenlet loads a page successfully. Override this method to call your delegate’sscreenlet(_:onBookmarkListResponse:)
method. For example, here’sBookmarkListScreenlet
’sonLoadPageResult
method:override public func onLoadPageResult(page page: Int, rows: [AnyObject], rowCount: Int) { super.onLoadPageResult(page: page, rows: rows, rowCount: rowCount) bookmarkListDelegate?.screenlet?(screenlet: self, onBookmarkListResponse: rows as! [Bookmark]) }
-
onLoadPageError
: Called when the Screenlet fails to load a page. Override this method to call your delegate’sscreenlet(_:onBookmarkListError:)
method. For example, here’sBookmarkListScreenlet
’sonLoadPageError
method:override public func onLoadPageError(page page: Int, error: NSError) { super.onLoadPageError(page: page, error: error) bookmarkListDelegate?.screenlet?(screenlet: self, onBookmarkListError: error) }
-
onSelectedRow
: Called when an item is selected in the list. Override this method to call your delegate’sscreenlet(_:onBookmarkSelected:)
method. For example, here’sBookmarkListScreenlet
’sonSelectedRow
method:override public func onSelectedRow(_ row: AnyObject) { bookmarkListDelegate?.screenlet?(screenlet: self, onBookmarkSelected: row as! Bookmark) }
Awesome! You’re done! Your list Screenlet, like any other Screenlet, is a ready-to-use component that you can add to your storyboard. You can even package it using the same steps you use to package a Theme, and then contribute it to the Liferay Screens project or distribute it with CocoaPods.
Related Topics
Supporting Multiple Themes in Your Screenlet
Create and Use a Connector with Your Screenlet
Creating and Using Your Screenlet’s Model Class
Using Custom Cells with List Screenlets