Setup Activity Collection
To enable activity collection in AR experience, simply add analyticId: "<YOUR_USER_ID>"
when you init service.
With this config, the system can analyticId
to track how users interact with collection activity through an AR experience.
class YourViewController: UIViewController, ActivityCollectionDelegate {
let graffityARCloud = GraffityARCloud(
accessToken: "YOUR_ACCESS_TOKEN",
pointCloudMode: true,
// Open built-in activity collection page
activityCollectionMode: true,
// This have to be unique ID for each user
// So, you can use your way to pass userID from your app to this class
analyticId: "YOUR_USER_ID"
)
func onFinishedActivity(activity: ActivityCollection) {
debugPrint("onFinishedActivity")
}
override func viewDidLoad() {
super.viewDidLoad()
self.graffityARCloud.activityDelegate = self
let arCloudUIView = ARCloudUIView(service: self.graffityARCloud)
self.present(arCloudUIView.view)
}
}
We also have pre-build page to show activity information here is the example:
<Image src="/images/ios-options/activity-image.webp" alt="Activity Image" height={750} width={370} />
{/* ```swift copy filename="ActivityCollectionViewController.swift"
import UIKit
import GraffityARCloudService
let maxCoinNumber = 10
let activityName = "Collection Activity"
let activityImageUrl = "https://graffity-sdk-public.s3.ap-southeast-1.amazonaws.com/images/coinCollectionGame.png"
let coinImage = UIImage(systemName: "flag.checkered.circle")
let coinImageColor = UIColor.darkGray
let collectedCoinImage = UIImage(systemName: "flag.checkered.circle")
let collectedCoinImageColor = UIColor.systemYellow
struct Coin {
var id: String = ""
var isCollected: Bool = false
}
class ActivityCollectionViewController: UIViewController {
// TODO: Config your data here
var graffityService = GraffityARCloud(
accessToken: "YOUR_ACCESS_TOKEN",
pointCloudMode: true,
// This have to be unique ID for each user
// So, you can use your way to pass userID from your app to this class
analyticId: "YOUR_USER_ID"
)
var activityCollection: ActivityCollection?
var arCloudUIView: ARCloudUIView?
let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 10
layout.minimumInteritemSpacing = 10
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.translatesAutoresizingMaskIntoConstraints = false
return cv
}()
let dateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 20)
return label
}()
let totalCoinsLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .center
return label
}()
let redeemButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Redeem", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 16)
button.addTarget(self, action: #selector(redeemButtonTapped), for: .touchUpInside)
button.isEnabled = false
button.backgroundColor = .gray
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 10
button.clipsToBounds = true
button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 20)
return button
}()
let imageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "coinCollectionGame")
imageView.layer.cornerRadius = 24
imageView.clipsToBounds = true
return imageView
}()
let openArButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Open AR", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 18)
button.addTarget(self, action: #selector(openARButtonTapped), for: .touchUpInside)
button.backgroundColor = .systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 24
button.clipsToBounds = true
return button
}()
let backbutton: UIButton = {
let backbutton = UIButton(type: .system)
backbutton.setImage(UIImage(systemName: "chevron.backward"), for: .normal)
backbutton.setTitle(" Back", for: .normal)
backbutton.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 55.0)
backbutton.setTitleColor(backbutton.tintColor, for: .normal) // You can change the TitleColor
backbutton.addTarget(self, action: #selector(backAction), for: .touchUpInside)
return backbutton
}()
var coins = [Coin](repeating: Coin(), count: 0)
init(service: GraffityARCloud) {
self.graffityService = service
coins = [Coin](repeating: Coin(), count: maxCoinNumber)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateCoin()
}
override func viewDidLoad() {
super.viewDidLoad()
// Add subviews
view.addSubview(dateLabel)
view.addSubview(imageView)
view.addSubview(collectionView)
view.addSubview(openArButton)
view.addSubview(backbutton)
let labelAndButtonStack = UIStackView(arrangedSubviews: [totalCoinsLabel, redeemButton])
labelAndButtonStack.axis = .horizontal
labelAndButtonStack.distribution = .equalSpacing
labelAndButtonStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(labelAndButtonStack)
// Setup layout constraints
setupConstraints(labelAndButtonStack: labelAndButtonStack)
// Setup the collection view
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(CoinCollectionViewCell.self, forCellWithReuseIdentifier: "CoinCell")
// Setup data
dateLabel.text = activityName
totalCoinsLabel.text = "Coins 0/" + String(maxCoinNumber)
if let url = URL(string: activityImageUrl) {
imageView.loadActivityImage(from: url)
}
updateCoin()
}
func updateCoin() {
if (graffityService.analyticId == nil) { return }
self.graffityService.sdkService?.getActivityCollection(playerId: graffityService.analyticId!) { activity in
if (activity == nil) { return }
self.activityCollection = activity
for (idx, id) in activity!.arContentIdCollection.enumerated() {
self.collectCoin(at: idx, id: id)
}
self.updateTotalCoinsLabel()
}
}
@objc func redeemButtonTapped() {
if (graffityService.analyticId == nil) { return }
self.graffityService.sdkService?.changeActivityCollectionState(playerId: graffityService.analyticId!, isActivityFinished: true) {_ in }
let alert = UIAlertController(title: "Redeem", message: "Redeem reward!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
self.redeemButton.isEnabled = false
self.redeemButton.backgroundColor = .gray
self.redeemButton.setTitle("Redeemed", for: .normal)
}
@objc func openARButtonTapped() {
self.arCloudUIView = ARCloudUIView(service: self.graffityService)
self.arCloudUIView!.modalPresentationStyle = .fullScreen
let arBackbutton = UIButton(type: .custom)
arBackbutton.setImage(UIImage(systemName: "chevron.backward"), for: .normal)
arBackbutton.setTitle(" Back", for: .normal)
arBackbutton.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 50.0)
arBackbutton.center = CGPoint(x: 40.0, y: 50)
arBackbutton.setTitleColor(backbutton.tintColor, for: .normal) // You can change the TitleColor
arBackbutton.addTarget(self, action: #selector(self.arBackAction), for: .touchUpInside)
self.arCloudUIView!.view.addSubview(arBackbutton)
self.arCloudUIView!.view.bringSubviewToFront(arBackbutton)
self.present(self.arCloudUIView!, animated: true, completion: nil)
}
@objc func backAction() {
dismiss(animated: true, completion: nil)
}
@objc func arBackAction() {
self.arCloudUIView?.dismiss(animated: true, completion: nil)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Ensure the button respects the safe area
let safeAreaTop = view.safeAreaInsets.top
self.backbutton.center = CGPoint(x: 40.0, y: safeAreaTop + 30.0)
}
func setupConstraints(labelAndButtonStack: UIStackView) {
NSLayoutConstraint.activate([
dateLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
dateLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
dateLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
labelAndButtonStack.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 20),
labelAndButtonStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
labelAndButtonStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
redeemButton.widthAnchor.constraint(equalToConstant: 120),
imageView.topAnchor.constraint(equalTo: labelAndButtonStack.bottomAnchor, constant: 20),
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1), // Maintain aspect ratio
collectionView.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 20),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
openArButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
openArButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
openArButton.widthAnchor.constraint(equalToConstant: 200),
openArButton.heightAnchor.constraint(equalToConstant: 50)
])
}
func updateTotalCoinsLabel() {
let collectedCoins = coins.filter { $0.isCollected }.count
totalCoinsLabel.text = "Coins \(collectedCoins)/" + String(maxCoinNumber)
updateRedeemButtonState()
}
func updateRedeemButtonState() {
var allCollected = coins.allSatisfy { $0.isCollected }
if (self.activityCollection != nil) {
allCollected = allCollected && !self.activityCollection!.isActivityFinished
if (self.activityCollection!.isActivityFinished) {
self.redeemButton.setTitle("Redeemed", for: .normal)
}
}
redeemButton.isEnabled = allCollected
redeemButton.backgroundColor = allCollected ? .green : .gray
}
func collectCoin(at index: Int, id: String) {
guard index >= 0 && index < coins.count else { return }
coins[index].isCollected = true
coins[index].id = id
updateTotalCoinsLabel()
collectionView.reloadItems(at: [IndexPath(item: index, section: 0)])
}
}
extension ActivityCollectionViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return coins.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CoinCell", for: indexPath) as! CoinCollectionViewCell
let coin = coins[indexPath.row]
cell.configure(with: coin)
return cell
}
}
extension ActivityCollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let totalSpacing = (10 * 4) + 20 // (minimumInteritemSpacing * (number of spaces)) + (sectionInset left + right)
let itemWidth = (collectionView.bounds.width - CGFloat(totalSpacing)) / 5
return CGSize(width: itemWidth, height: itemWidth)
}
}
class CoinCollectionViewCell: UICollectionViewCell {
let coinImageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(coinImageView)
NSLayoutConstraint.activate([
coinImageView.topAnchor.constraint(equalTo: contentView.topAnchor),
coinImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
coinImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
coinImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(with coin: Coin) {
if (coin.isCollected) {
coinImageView.image = collectedCoinImage
coinImageView.tintColor = collectedCoinImageColor
} else {
coinImageView.image = coinImage
coinImageView.tintColor = coinImageColor
}
}
}
extension UIImageView {
func loadActivityImage(from url: URL) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error downloading image: \(error)")
return
}
guard let data = data, let image = UIImage(data: data) else {
print("Error: No data or invalid data")
return
}
DispatchQueue.main.async {
self.image = image
}
}.resume()
}
}
``` */}