Building a Cheer Meter Widget
Minimum SDK Version
2.11
This is a guide on building a custom Cheer Meter Widget. For an overview of the Custom Widget UI system see Custom Widget UI.
Considerations for WidgetPopupViewController
If you plan on using your Custom Widget UI with the WidgetPopupViewController see Using Custom Widget UI with the WidgetPopupViewController
The Cheer Meter Model
The Cheer Meter Model is a reflection of a Cheer Meter as it is on the server.
Initialize UI With Cheer Meter Data
The Cheer Meter Model provides data about the Cheer Meter such as the title text and the Cheer Meter options.
The model also provides metadata about the widget such as the Date that it was created or the timeout duration set by the Producer.
class CustomCheerMeter: UIViewController {
let titleLabel: UILabel = UILabel()
var optionButtons: [UIButton] = []
let model: CheerMeterWidgetModel
init(model: CheerMeterWidgetModel) {
self.model = model
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// Initializing UI elements with Model data
titleLabel.text = model.title
model.options.enumerated().forEach { index, option in
let optionButton = UIButton()
optionButton.setTitle("\(option.voteCount)", for: .normal)
do {
let imageData = try Data(contentsOf: option.imageURL)
let image = UIImage(data: imageData)
optionButton.setImage(image, for: .normal)
} catch {
print(error)
}
optionButtons.append(optionButton)
}
}
}
Submitting a vote
Due to the high volume of votes expected on a Cheer Meter the SDK will batch the votes puts a 1 second throttle on the vote request.
To submit a vote take the id of the option and call submitVote(optionID:)
class CustomCheerMeter: UIViewController {
...
override func viewDidLoad() {
...
model.options.enumerated().forEach { index, option in
...
optionButton.tag = index // Will be used to lookup option id later
optionButton.addTarget(self, action: #selector(optionButtonHandler), for: .touchUpInside)
...
}
}
// Get the option id by index and submit vote
@objc private func optionButtonHandler(sender: UIButton) {
guard model.options.count > sender.tag else { return }
model.submitVote(optionID: model.options[sender.tag].id)
}
}
Listening for Updates on the Cheer Meter
The CheerMeterModelDelegate gives you access to updates on the Cheer Meter. This includes an event for when the vote count changes on the server and also an event for when the user's batched vote requests are completed.
class CustomCheerMeter: UIViewController {
...
override func viewDidLoad() {
...
model.delegate = self
}
...
}
extension CustomCheerMeter: CheerMeterWidgetModelDelegate {
func cheerMeterWidgetModel(
_ model: CheerMeterWidgetModel,
voteCountDidChange voteCount: Int,
forOption optionID: String
) {
// This method is not guaranteed to be called on the Main thread
DispatchQueue.main.async {
guard let optionIndex = model.options.firstIndex(where: { $0.id == optionID }) else { return }
guard self.optionButtons.count > optionIndex else { return }
self.optionButtons[optionIndex].setTitle("\(voteCount)", for: .normal)
}
}
func cheerMeterWidgetModel(
_ model: CheerMeterWidgetModel,
voteRequest: CheerMeterWidgetModel.VoteRequest,
didComplete result: Result<CheerMeterWidgetModel.Vote, Error>
) {
// To make your UI more responsive
// You may want to optimistically update your UI when the user votes
// While the throttled network request is being made
// You can use this method to validate your UI in case the request fails
}
}
Full CustomCheerMeter Sample
class CustomCheerMeter: UIViewController {
let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.alignment = .leading
return stackView
}()
let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
return label
}()
var optionButtons: [UIButton] = []
let model: CheerMeterWidgetModel
init(model: CheerMeterWidgetModel) {
self.model = model
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = stackView
}
override func viewDidLoad() {
super.viewDidLoad()
stackView.addArrangedSubview(titleLabel)
// Initializing UI elements with Model data
titleLabel.text = model.title
model.options.enumerated().forEach { index, option in
let optionButton = UIButton()
optionButton.translatesAutoresizingMaskIntoConstraints = false
optionButton.tag = index // Will be used to lookup option id later
optionButton.setTitle("\(option.voteCount)", for: .normal)
do {
let imageData = try Data(contentsOf: option.imageURL)
let image = UIImage(data: imageData)
optionButton.setBackgroundImage(image, for: .normal)
} catch {
print(error)
}
optionButton.addTarget(self, action: #selector(optionButtonHandler), for: .touchUpInside)
optionButtons.append(optionButton)
stackView.addArrangedSubview(optionButton)
}
model.delegate = self
}
@objc private func optionButtonHandler(sender: UIButton) {
guard model.options.count > sender.tag else { return }
model.submitVote(optionID: model.options[sender.tag].id)
}
}
extension CustomCheerMeter: CheerMeterWidgetModelDelegate {
func cheerMeterWidgetModel(
_ model: CheerMeterWidgetModel,
voteCountDidChange voteCount: Int,
forOption optionID: String
) {
// This method is not guaranteed to be called on the Main thread
DispatchQueue.main.async {
guard let optionIndex = model.options.firstIndex(where: { $0.id == optionID }) else { return }
guard self.optionButtons.count > optionIndex else { return }
self.optionButtons[optionIndex].setTitle("\(voteCount)", for: .normal)
}
}
func cheerMeterWidgetModel(
_ model: CheerMeterWidgetModel,
voteRequest: CheerMeterWidgetModel.VoteRequest,
didComplete result: Result<CheerMeterWidgetModel.Vote, Error>
) {
// To make your UI more responsive
// You may want to optimistically update your UI when the user votes
// While the throttled network request is being made
// You can use this method to validate your UI in case the request fails
}
}
Updated about 1 year ago