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#FF9500flora#9CD579bloom#A61825swarm#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) } } }}