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.

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()
    }
}

``` */}