Content Widget

Content Widget is a feature in our Software Development Kit that allows you to embed an easily customizable view with various types of content in your application.

Two view layouts are available:

  • Horizontal Slider - a single row view that slides horizontally within the screen.
  • Grid View - can be displayed as full- or half-screen grid layout within your app.

Both views offer a number of configuration options that allow you to style the view consistently in the app.

Additionally, the Content Widget automatically tracks events such as views or clicks within the view.

Note: Currently, the widget can only be used for displaying AI recommendations. Stay tuned for future development!

Prerequisites


  • Obtain a Client token from User Authentication.
  • Create an AI Recommendation Campaign as described here.
  • Create a Document as described here.
    Such a document should contain the following content:

    {
    	"name": "Similar Products",
    	"recommendations": "{% recommendations_json campaignId=COhsCCOdu8Cg %} {% endrecommendations_json %}"
    }
    
Note: Take note of the Document’s slug for later use.
Tip: It’s a good practice to name slugs based on area of the app that you want to place the content in, for example product-details, menu.

Basic Implementation


Configure the ContentWidgetOptions and ContentWidgetAppearance settings first.

Class Description
SNRContentWidgetOptions SNRContentWidgetOptions contains options for business logic, such as the slug, product identifier, and so on. Read more.
SNRContentWidgetAppearance Contains the UI configuration. Read more.

The example below is the most basic implementation.

let options = ContentWidgetOptions()
options.slug = "similar"

let gridLayout = ContentWidgetGridLayout()
let itemLayout = ContentWidgetBasicProductItemLayout()
let appearance = ContentWidgetAppearance(widgetLayout: gridLayout, itemLayout: itemLayout)

let widget = ContentWidget(options: options, appearance: appearance)

let widgetView = widget.getView()
widgetView.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)

view.addSubview(widgetView)

Widget Options


The SNRContentWidgetOptions class is responsible for defining the business logic options of the widget, for example:

  • slug of the document
  • product identifier

The table explains the parameters that can be configured in SNRContentWidgetOptions.

Parameter Type Default Description
slug String nil Slug responsible for generating data
attributes [AnyHashable: Any] [AnyHashable: Any]() Custom attributes for generating data

Example

let widgetOptions = ContentWidgetOptions()
widgetOptions.slug = "similar"
widgetOptions.attributes[SNRContentWidgetOptionsAttributeKeyProductID] = "12345"

Appearance Configuration


The SNRContentWidgetAppearance class is responsible for defining the appearance of the widget.

The class consists of parameters that define the widget’s appearance, however two of them are the most important:

  • Main layout class: defines the way of distributing elements in the widget. Currently, two layouts are provided: SNRContentWidgetHorizontalSliderLayout and SNRContentWidgetGridLayout.
  • Item layout class: defines appearance and parameters for the item in the widget. Currently, there is only one layout provided: SNRContentWidgetBasicItemLayout.

The table explains the parameters that can be configured in SNRContentWidgetAppearance.

Parameter Type Default Description
layout SNRContentWidgetLayout - Class that inherits from SNRContentWidgetLayout contains the UI details of widgetLayout
itemLayout SNRContentWidgetItemLayout - Class that inherits from SNRContentWidgetItemLayout contains the UI details of a single item in a widget

Layouts

Horizontal Slider


This layout is intended to present recommendations in a fixed-hight horizontal scrollable slider.

Example widget configuration with horizontal slider:

Content Widget - Horizontal Slider

Parameters

The table explains the parameters of SNRContentWidgetHorizontalLayout.

Property Type Default Description
backgroundColor UIColor UIColor.clearColor Background color of a widget
insets UIEdgeInsets (8.0, 8.0, 8.0, 8.0) Inner widget margins in pt
itemSize CGSize (150.0, 200.0) Size of a single item in pt
itemSpacing CGFloat 16.0 Horizontal spacing between items in pt
numberOfItems Int - A read-only property. It returns the number of items after a widget is loaded

Example

let horizontalSliderLayout = ContentWidgetHorizontalSliderLayout()
horizontalSliderLayout.insets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
horizontalSliderLayout.itemSize = CGSize(width: 150, height: 350)
horizontalSliderLayout.itemSpacing = 8.0

Grid View


This layout presents recommendations in a vertical scrollable grid, with elements organized into columns and rows. You can create a full- or half-screen widget.

Example widget configuration with grid layout:

Content Widget - Grid View

Parameters

The table explains the parameters of the grid layout.

Property Type Default Description
backgroundColor UIColor UIColor.clearColor Background color of a widget
insets UIEdgeInsets (8.0, 8.0, 8.0, 8.0) Inner widget margins in pt
itemSize CGSize (150.0, 200.0) Size of a single item in pt
itemHorizontalSpacing CGFloat 16.0 Horizontal spacing between items in pt
itemVerticalSpacing CGFloat 16.0 Vertical spacing between items in pt
numberOfItems Int - A read-only property. It returns the number of items after the widget is loaded

Example

let gridLayout = ContentWidgetGridLayout()
gridLayout.insets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
gridLayout.itemSize = CGSize(width: 150, height: 350)
gridLayout.horizontalItemSpacing = 8.0
gridLayout.verticalItemSpacing = 8.0

Basic product item layout


This is the basic layout for items. It contains: the image, the title, and the price from the uploaded data.

Parameters

The table below contains all parameters you can configure in the basic item layout.

Property Type Default Description
backgroundColor UIColor UIColor.whiteColor Background color of an item
cornerRadius CGFloat 0.0 Radius of the item corners
borderWidth CGFloat 0.0 Width of the item’s border
borderColor CGFloat nil Color of the item’s border
shadowColor UIColor nil Color of the item’s shadow
imageWidthRatio CGFloat 1.0 Image width. A ratio of 1.0 means that the image width equals to 100% of the entire height of the item
imageHeightRatio CGFloat 0.35 Image height. A ratio of 0.3 means that image height equals to 35% of the entire height of the item
imageBackground UIColor UIColor.clearColor Background color of the image
titleInsets UIEdgeInsets (8.0, 8.0, 8.0, 8.0) Inner margins of the title label
titleFont UIFont UIFont.systemFont(ofSize: 16.0) Font of the item title label
titleFontColor UIColor UIColor.blackColor Color of the title label
titleAlignment NSTextAlignment NSTextAlignment.center Alignment of the title label
priceInsets UIEdgeInsets (8.0, 8.0, 8.0, 8.0) Inner margins of the price label
priceFont UIFont UIFont.systemFont(ofSize: 14.0) Font of the price label
priceFontColor UIColor UIColor.blackColor Color of the price label
priceAlignment NSTextAlignment NSTextAlignment.center Alignment of the price label
isSalePriceVisible Bool true Flag determining whether to show the sale price label or not
salePriceOrientation UILayoutConstraintAxis UILayoutConstraintAxis.Horizontal Orientation of the sale price label
salePriceMargin CGFloat 4.0 Margin between the price label and the sale price label
salePriceFont UIFont UIFont.systemFont(ofSize: 12.0) Font of the sale price label
salePriceFontColor UIColor UIColor.redColor Color of the sale price label
actionButton SNRContentWidgetImageButtonCustomAction nil Optional button for your own custom action
actionButtonPosition CGPoint CGPoint.Zero Position of the action button

Example

let itemLayout = ContentWidgetBasicProductItemLayout()
itemLayout.imageWidthRatio = 1.0
itemLayout.imageHeightRatio = 0.4
itemLayout.borderWidth = 2.0
itemLayout.borderColor = UIColor.black
itemLayout.shadowColor = UIColor.black
itemLayout.cornerRadius = 12.0

Interaction with the widget

Public Interface


load() - Starts fetching data and creates a view structure of the widget.

isLoaded() - Checks whether the widget is successfully loaded.

getView() - Gets the root view of the whole widget view structure.

Delegation


SNRContentWidgetDelegate is used to inform developers about the state of a widget.

  • snr_widgetIsLoading(widget:isLoading:) - Called when the widget’s loading state changes. It’s an optional method.
  • snr_widgetDidLoad(widget:) - Called after the widget is loaded. It’s a required method.
  • snr_widgetDidNotLoad(widget:error:) - Called when an error occurs while loading. It’s a required method.
  • snr_widgetDidChangeSize(widget:size:) - Called when the widget size changes. It’s an optional method.
  • snr_widgetDidReceiveClickAction(widget:model:) - Called when the user clicks a widget item. It’s a required method.
Note: Check the SNRContentWidgetDelegate section for more details.

Image Button Custom Action


SNRContentWidgetImageButtonCustomAction is used to add image button to your widget (only if the item layout allows). You can add a button with single state or make it selectable.

Parameters

Property Type Default Description
predefinedActionType SNRContentWidgetBaseCustomActionPredefiniedActionType .none It determines which event is sent on click
size CGSize CGSize.Zero Button size
backgroundColor UIColor Background color of the button
tintColor UIColor UIColor.blackColor Fill color of the button’s image, if an asset supports it
image UIImage nil Button image
isSelectable Bool nil Flag determining whether the button is selectable
selectedImage UIImage nil Image of the button when the button is selected
isSelected SNRContentWidgetImageButtonCustomActionIsSelectedBlock nil Block/closure to be executed when the widget needs to determine the state of a button in the cell
onReceiveClickAction SNRContentWidgetImageButtonCustomActionReceiveClickActionBlock nil Block/closure to be executed when the button is clicked

Block/Closures

  • isSelected - Called when the widget tries to determine button’s state. The only one parameter is model of data for the cell (for example SNRRecommendation). It’s an optional property.
  • onReceiveClickAction - Called when the button was clicked. Parameters are model of data for the cell (for example SNRRecommendation) and current state of button. It’s an optional property.

Sample Implementations

Horizontal Slider


This is an example with ContentWidgetHorizontalSliderLayout. It always has fixed height, so after the widget is loaded, its content height can be calculated.

That is why it is done in the snr_widgetDidLoad method.

The widget content size in a horizontal slider layout can be calculated by the ContentWidgetHorizontalSliderLayout.getSize() method.

class ContentWidgetHorizontalSliderSampleViewController: UIViewController, ContentWidgetDelegate {
	
	var widget: ContentWidget!

    @IBOutlet weak var widgetContainerView: UIView!

	// MARK: - Lifecycle

	override func viewDidLoad() {
		super.viewDidLoad()

		setupWidget()
	}

	// MARK: - Private

	func setupWidget() -> Void {
		let options = ContentWidgetOptions()
		options.slug = "similar"
		options.attributes[SNRContentWidgetOptionsAttributeKeyProductID] = "12345"

		let horizontalSliderLayout = ContentWidgetHorizontalSliderLayout()
		horizontalSliderLayout.insets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
		horizontalSliderLayout.itemSize = CGSize(width: 150, height: 350)
		horizontalSliderLayout.itemSpacing = 8.0

		let itemLayout = ContentWidgetBasicProductItemLayout()
		itemLayout.imageWidthRatio = 1.0
		itemLayout.imageHeightRatio = 0.4
		itemLayout.borderWidth = 2.0
		itemLayout.borderColor = UIColor.black
		itemLayout.shadowColor = UIColor.black
		itemLayout.cornerRadius = 12.0

		let actionButton = ContentWidgetImageButtonCustomAction()
		actionButton.backgroundColor = UIColor.clear
		actionButton.tintColor = UIColor.black
		actionButton.image = UIImage(imageLiteralResourceName: "Shop Flow/icon_favorite_add")
		actionButton.isSelectable = true
		actionButton.selectedImage = UIImage(imageLiteralResourceName: "Shop Flow/icon_favorite_remove")
		actionButton.size = CGSize(width: 40, height: 40)
		actionButton.predefinedActionType = .sendLikeEvent
		actionButton.onReceiveClickAction = {
			model, isSelected in

			if let recommendationModel = model as? Recommendation {
				print("Content Widget did receive click action for action button \(recommendationModel.title)")
			}
		}
		actionButton.isSelected = {
			model in

			return false
		}

		itemLayout.actionButton = actionButton
		itemLayout.actionButtonPosition = CGPoint(x: (150.0 - 40 - 8), y: 8)

		let appearance = ContentWidgetAppearance(widgetLayout: horizontalSliderLayout, itemLayout: itemLayout)

		widget = ContentWidget(options: options, appearance: appearance)
		widget.delegate = self
		widget.load()
	}

	// MARK: - ContentWidgetDelegate

	func snr_widgetIsLoading(widget: ContentWidget, isLoading: Bool) {
		print("Content Widget is loading: \(isLoading)")
	}

	func snr_widgetDidLoad(widget: ContentWidget) {
		print("Content Widget did load")

		let widgetView: UIView = widget.getView()
		let widgetSize: CGSize = (widget.layout as! ContentWidgetHorizontalSliderLayout).getSize()

		widgetContainerView.addSubview(widgetView)

		widgetView.translatesAutoresizingMaskIntoConstraints = false
		widgetView.topAnchor.constraint(equalTo: widgetContainerView.topAnchor).isActive = true
		widgetView.bottomAnchor.constraint(equalTo: widgetContainerView.bottomAnchor).isActive = true
		widgetView.leftAnchor.constraint(equalTo: widgetContainerView.leftAnchor).isActive = true
		widgetView.rightAnchor.constraint(equalTo: widgetContainerView.rightAnchor).isActive = true

		widgetContainerView.heightAnchor.constraint(equalToConstant: widgetSize.height).isActive = true
	}

	func snr_widgetDidNotLoad(widget: ContentWidget, error: Error) {
		print("Content Widget did not load. Error: \(error.localizedDescription)")
	}

	func snr_widgetDidChangeSize(widget: ContentWidget, size: CGSize) {
		print("Content Widget did change size to: \(size)")
	}

	func snr_widgetDidReceiveClickAction(widget: ContentWidget, model: BaseModel) {
		if let recommendationModel = model as? Recommendation {
			print("Content Widget did receive click action for \(recommendationModel.title)")
		}
	}
}		

Grid View


A basic example with ContentWidgetGridLayout and UITableViewController. Remember that cells are prototyped.

Initially, the height of the tenth row equals zero, because there is no possibility of getting the correct height of the widget. Before the widget is loaded, we don’t know how many items it’s going to contain.

The widget view is flexible, so it fits the dimensions that you set up. If the height of the widget that you set is smaller than the total height of the generated grid, the content can be scrolled vertically.

The grid’s content height depends on:

  • The widget width that you set up
  • The number of items that have been loaded

That is why the code below reloads the tenth row after the widget is loaded. Earlier, it was impossible to calculate the height correctly.

In addition, the widget row is reloaded when the snr_widgetDidChangeSize method is called. In this case, it’s a required action, because the widget has pinned constraints to superview in a prototyped cell.

The widget’s content size changes with the tableview size, for example when the screen orientation changes, the widget’s height needs to be re-calculated. Otherwise, the cell height may be larger that necessary.

The total widget content size in a grid layout can be calculated by the ContentWidgetGridLayout.getSize(preferredWidth:) method.

class ContentWidgetGridViewSampleViewController: UITableViewController, ContentWidgetDelegate {
    
	var widget: ContentWidget!

    @IBOutlet weak var widgetContainerView: UIView!

	// MARK: - Lifecycle

	override func viewDidLoad() {
		super.viewDidLoad()

		setupWidget()
	}

	func setupWidget() -> Void {
		let options = ContentWidgetOptions()
		options.slug = "similar"
		options.attributes[SNRContentWidgetOptionsAttributeKeyProductID] = "12345"

		let gridLayout = ContentWidgetGridLayout()
		gridLayout.insets = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0)
		gridLayout.itemSize = CGSize(width: 150.0, height: 350.0)
		gridLayout.horizontalItemSpacing = 8.0
		gridLayout.verticalItemSpacing = 8.0

		let itemLayout = ContentWidgetBasicProductItemLayout()
		itemLayout.imageWidthRatio = 1.0
		itemLayout.imageHeightRatio = 0.4
		itemLayout.borderWidth = 2.0
		itemLayout.borderColor = UIColor.black
		itemLayout.shadowColor = UIColor.black
		itemLayout.cornerRadius = 12.0

		actionButton.onReceiveClickAction = {
			model, isSelected in

			if let recommendationModel = model as? Recommendation {
				print("Content Widget did receive click action for action button \(recommendationModel.title)")
			}
		}
		actionButton.isSelected = {
			model in

			return false
		}

		itemLayout.actionButton = actionButton
		itemLayout.actionButtonPosition = CGPoint(x: (150.0 - 40.0 - 8.0), y: 8.0)

		let appearance = ContentWidgetAppearance(widgetLayout: gridLayout, itemLayout: itemLayout)

		widget = ContentWidget(options: options, appearance: appearance)
		widget.delegate = self
		widget.load()
	}

	// MARK: - UITableViewDataSource, UITableViewDelegate

	override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
		return 10
	}

	override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
		if indexPath.row == 10 {
			if widget != nil && widget.isLoaded() {
				return (widget.layout as! ContentWidgetGridLayout).getSize(preferredWidth: tableView.bounds.size.width).height
			} else {
				return 0
			}
		}

		return 100.0
	}

	// MARK: - ContentWidgetDelegate

	func snr_widgetIsLoading(widget: ContentWidget, isLoading: Bool) {
		print("Content Widget is loading: \(isLoading)")
	}

	func snr_widgetDidLoad(widget: ContentWidget) {
		print("Content Widget did load")

		view.addSubview(widgetView)

		widgetView = widget.getView()
		widgetContainerView.addSubview(widgetView)

		widgetView.translatesAutoresizingMaskIntoConstraints = false
		widgetView.topAnchor.constraint(equalTo: widgetContainerView.topAnchor).isActive = true
		widgetView.bottomAnchor.constraint(equalTo: widgetContainerView.bottomAnchor).isActive = true
		widgetView.leftAnchor.constraint(equalTo: widgetContainerView.leftAnchor).isActive = true
		widgetView.rightAnchor.constraint(equalTo: widgetContainerView.rightAnchor).isActive = true

		tableView.reloadData()
	}

	func snr_widgetDidNotLoad(widget: ContentWidget, error: Error) {
		print("Content Widget did not load. Error: \(error.localizedDescription)")
	}

	func snr_widgetDidChangeSize(widget: ContentWidget, size: CGSize) {
		print("Content Widget did change size to: \(size)")

		tableView.reloadData()
	}

	func snr_widgetDidReceiveClickAction(widget: ContentWidget, model: BaseModel) {
		if let recommendationModel = model as? Recommendation {
			print("Content Widget did receive click action for \(recommendationModel.title)")
		}
	}
}

More information


You can find more information under the following links:

😕

We are sorry to hear that

Thank you for helping improve out documentation. If you need help or have any questions, please consider contacting support.

😉

Awesome!

Thank you for helping improve out documentation. If you need help or have any questions, please consider contacting support.