overview

hive.art transforms passive waiting time into an active social experience. the game dynamics begin with the lottery of leaders, the 'Queen Bees', who form teams (hives) with the people around them using pairing codes displayed on the Apple TV.

the goal is purely collaborative: the hives receive a surprise theme — such as 'Giraffe' or 'Space' — and each player draws a part of the image on their own iPhone. in this way, multiple people can collaborate on a collective artistic production, which helps to redefine waiting time, strengthen bonds between people in different spaces, and stimulate collective creativity.

technically, the project enables this instant collaboration through a shared modular architecture, represented by a target called HiveArtShared, and synchronization via Firebase Firestore. the use of PencilKit allows the transmission of complex drawing vectors in real time, ensuring that the stroke made on the iPhone appears instantly on other devices.

the system manages concurrent game states to maintain fluidity between the multiple devices of the 'hive', ensuring that the creative experience is continuous and immersive for all participants.

design choices

palette

honey #FF9500
flora #9CD579
bloom #A61825
swarm #2E2E2E

typography

Monley / primary logo
The quick fox jumps over the lazy dog
SF Pro Rounded / interface and text
The quick fox jumps over the lazy dog

some fonts used in this project are proprietary and may not display correctly if they are not installed on your system.

rationale

the design of hive.art was carefully crafted to reflect the game's theme of collaboration and creativity combined with the idea of bees in a hive. the vibrant color palette, with shades of honey yellow, green, red, and black, refers to the nature of bees and the dynamic energy of the collective creative process.

the chosen typography, with rounded and friendly fonts, aims to create a welcoming and accessible atmosphere for players of all ages. the choice of SF Pro Rounded also has a technical reason: considering its native use on Apple platforms and its legibility, SF Pro facilitates navigation between Apple TV and iPhone devices, ensuring a more fluid experience.

hive.art also features a visual construction inspired by hexagons, the natural shape assumed by hives, in addition to graphic details that refer to the universe of bees, such as small illustrations and terms like Queen Bee and hive. these visual and typographic details reinforce the game's identity and provide an immersive experience, connecting players to the central theme while they collaborate on artistic creation.

tech stack

SwiftUI
interface & game logic
tvOS
shared display platform
PencilKit
real-time drawing
Firebase
data synchronization
Combine
reactive state management

code snippets

Hexagon Shape

swift
struct Hexagon: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let center = CGPoint(x: rect.midX, y: rect.midY)
        let radius = min(rect.size.height, rect.size.width) / 2
        let corners = corners(center: center, radius: radius)
        path.move(to: corners[0])
        corners[1...5].forEach() { point in
            path.addLine(to: point)
        }
        path.closeSubpath()
        return path
    }
    
    func corners(center: CGPoint, radius: CGFloat) -> [CGPoint] {
        var points: [CGPoint] = []
        for i in (0...5) {
            let angle = CGFloat.pi / 3 * CGFloat(i)
            let point = CGPoint(
                x: center.x + radius * cos(angle),
                y: center.y + radius * sin(angle)
            )
            points.append(point)
        }
        return points
    }
}

Coordinator View

swift
struct CoordinatorView<C: CoordinatorViewable>: View {
    @ObservedObject var coordinator: C
    var body: some View {
        NavigationStack(path: $coordinator.navigationPath) {
            coordinator.getInitialPage().build(coordinator: coordinator)
                .navigationDestination(for: C.PageType.self) { page in
                    page.build(coordinator: coordinator)
                }
        }
        .sheet(item: $coordinator.sheet) { item in
            item.page.build(coordinator: coordinator)
        }
        .fullScreenCover(item: $coordinator.fullScreenCover) { item in
            item.page.build(coordinator: coordinator)
        }
        .onAppear {
            coordinator.start()
        }
    }
}

Concurrent ViewModel

swift
class DrawingSectionViewModel: ObservableObject {
    private(set) var coordinator: UIDrawingSectionViewCoordinator
    private var drawingService: any DrawingServiceProtocol
    
    init(drawing: UUID, drawingService: any DrawingServiceProtocol = RealtimeDBDrawingService()) {
        coordinator = .init()
        self.drawingService = drawingService
        Task { @MainActor in
            do {
                try await drawingService.subscribeToDrawing(id: drawing)
            } catch {
                print("Failed to subscribe to drawing \(drawing): \(error)")
            }
        }
    }
    
    private func onDrawingDidChange(_ drawing: PKDrawing) {
        Task { @MainActor in
            do {
                try await drawingService.updateDrawingData(newData: drawing)
            } catch {
                print("Error updating drawing: \(error)")
            }
        }
    }
}

Firestore Service

swift
public final class FirestoreDatabaseService<T: Codable>: DatabaseServiceProtocol {
    public typealias Model = T
    private let db = Firestore.firestore()
    private var listeners: [UUID: ListenerRegistration] = [:]
    
    public func addListener(for collection: String, using closure: @escaping ([T], (any Error)?) -> Void) async -> UUID {
        let id = UUID()
        let listener = db.collection(collection).addSnapshotListener({ snapshot, error in
            guard let snapshot else {
                closure([], error)
                return
            }
            let documents = snapshot.documents.compactMap{ doc -> T? in
                try? doc.data(as: T.self)
            }
            closure(documents, error)
        })
        listeners[id] = listener
        return id
    }
    
    public func addDocument(_ document: Model, to collection: String) async throws {
        do {
            try db.collection(collection).addDocument(from: document)
        } catch {
            NSLog("Error fetching documents from collection '(collection)': (error.localizedDescription)")
            throw error
        }
    }
}

Event Queue Processing

swift
final class RealtimeDBEventService: EventServiceProtocol {
    private let db: any DatabaseServiceProtocol<any Event>
    private let collectionPath: String
    func addEventListener(completion: @escaping (any Event) -> Void) async throws -> UUID {
        await db.addListener(for: collectionPath, using: { events, error in
            guard error == nil else { return }
            Task { @MainActor in
                for event in events {
                    completion(event)
                    try await self.db.deleteDocument(event.id.uuidString, from: self.collectionPath)
                }
            }
        })
    }
}

Multiplayer Orchestration

swift
class GameManager {
    func handle(event: any Event) {
        switch event {
            case let e as PlayerJoinRoomEvent:
                handlePlayerJoinRoomEvent(e)
            case let e as PlayerLeaveRoomEvent:
                handlePlayerLeaveRoomEvent(e)
            case let e as PlayerJoinHiveEvent:
                handlePlayerJoinHiveEvent(e)
            case let e as PlayerLeaveHiveEvent:
                handlePlayerLeaveHiveEvent(e)
            case let e as RoomEndStepEvent:
                handleRoomEndStepEvent(e)
            default:
                print("Unhandled event: \(event)")
        }
    }
    func handleRoomEndStepEvent(_ event: RoomEndStepEvent) {
        Task { @MainActor in
            switch event.step {
                case .joining:
                    try await handleThemeReveal()
                case .themeReveal:
                    try await handleLeaderReveal()
                case .leaderReveal:
                    try await roomService.changeStep(to: .hivePairing)
                default:
                    try await roomService.changeStep(to: .end)
            }
        }
    }
}

credits

people