Setup Activity Collection

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.

let 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" 
)
 
let arCloudUIView = ARCloudUIView(service: self.graffityARCloud)

We also have pre-build page to show activity information here is the example:

Activity Image
ActivityCollectionViewController.swift
import UIKit
import GraffityARCloudService
 
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 activityImageUrl = "https://graffity-sdk-public.s3.ap-southeast-1.amazonaws.com/images/coinCollectionGame.png"
    
    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: 10)
    
    init(service: GraffityARCloud) {
        self.graffityService = service
        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 = "Collection Activity"
        totalCoinsLabel.text = "Coins 0/10"
        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)/10"
        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 = UIImage(systemName: "flag.checkered.circle.fill")
            coinImageView.tintColor = .systemYellow
        } else {
            coinImageView.image = UIImage(systemName: "flag.checkered.circle")
            coinImageView.tintColor = .darkGray
        }
    }
}
 
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()
    }
}