Handles Sign in with Apple authentication.
protocol AuthenticationServiceProtocol: Sendable {
var currentUser: User? { get async }
var isAuthenticated: Bool { get async }
func signInWithApple() async throws -> User
func signOut() async throws
}
Methods:
signInWithApple(): Initiates Sign in with Apple flowsignOut(): Signs out the current usercheckCredentialState(for:): Validates user credentialsErrors:
AuthenticationError.invalidCredential: Invalid credential from AppleAuthenticationError.userCanceled: User canceled sign inAuthenticationError.authorizationFailed: Authorization failedAuthenticationError.invalidResponse: Invalid response from AppleViewModel for authentication UI.
@MainActor
class AuthenticationViewModel: ObservableObject {
@Published private(set) var currentUser: User?
@Published private(set) var isAuthenticated: Bool
@Published private(set) var isLoading: Bool
@Published private(set) var errorMessage: String?
func signInWithApple() async
func signOut() async
func checkCredentialState() async
}
Represents a user in the Layover app.
struct User: LayoverModel {
let id: UUID
var appleUserID: String?
var username: String
var email: String?
var avatarURL: URL?
var isHost: Bool
var isSubHost: Bool
}
Properties:
id: Unique identifierappleUserID: Apple user identifier from Sign in with Appleusername: Display nameemail: Optional email addressavatarURL: Optional avatar image URLisHost: Whether user is a room hostisSubHost: Whether user is a sub-hostRepresents a room where users participate in activities.
struct Room: LayoverModel {
let id: UUID
var name: String
var hostID: UUID
var subHostIDs: Set<UUID>
var participantIDs: Set<UUID>
var activityType: RoomActivityType
var maxParticipants: Int
var isPrivate: Bool
var createdAt: Date
var metadata: [String: String]
}
Methods:
addParticipant(_:): Add user to roomremoveParticipant(_:): Remove user from roompromoteToSubHost(_:): Promote user to sub-hostdemoteSubHost(_:): Demote sub-host to regular participantisSubHost(userID:): Check if user is sub-hostTypes of activities available in rooms.
enum RoomActivityType: String, Codable, Sendable {
case appleTVPlus = "tv_plus"
case appleMusic = "music"
case chess = "chess"
}
Represents media content (movies, shows, songs).
struct MediaContent: LayoverModel {
let id: UUID
var title: String
var contentID: String
var artworkURL: URL?
var duration: TimeInterval
var mediaType: MediaType
}
MediaType:
enum MediaType: String, Codable, Sendable {
case movie
case tvShow
case song
case album
case playlist
}
Manages SharePlay sessions and coordination.
@MainActor
protocol SharePlayServiceProtocol: LayoverService {
var currentSession: GroupSession<LayoverActivity>? { get }
var isSessionActive: Bool { get }
func startActivity(_ activity: LayoverActivity) async throws
func leaveSession() async
func setupPlaybackCoordinator(player: AVPlayer) async throws
}
Usage:
let service = SharePlayService()
// Start SharePlay
let activity = LayoverActivity(
roomID: roomID,
activityType: .appleTVPlus,
customMetadata: ["roomName": "Movie Night"]
)
try await service.startActivity(activity)
// Setup playback coordination
try await service.setupPlaybackCoordinator(player: avPlayer)
// Leave session
await service.leaveSession()
Manages room operations.
@MainActor
protocol RoomServiceProtocol: LayoverService {
var rooms: [Room] { get }
func createRoom(name: String, hostID: UUID, activityType: RoomActivityType) async throws -> Room
func joinRoom(roomID: UUID, userID: UUID) async throws
func leaveRoom(roomID: UUID, userID: UUID) async throws
func promoteToSubHost(roomID: UUID, userID: UUID) async throws
func demoteSubHost(roomID: UUID, userID: UUID) async throws
func deleteRoom(roomID: UUID) async throws
func fetchRooms() async throws -> [Room]
}
Usage:
let service = RoomService()
// Create room
let room = try await service.createRoom(
name: "Game Night",
hostID: userID,
activityType: .chess
)
// Join room
try await service.joinRoom(roomID: room.id, userID: friendID)
// Promote to sub-host
try await service.promoteToSubHost(roomID: room.id, userID: friendID)
Manages Apple TV+ content playback.
@MainActor
protocol AppleTVServiceProtocol: LayoverService {
var currentContent: MediaContent? { get }
var player: AVPlayer? { get }
func loadContent(_ content: MediaContent) async throws
func play() async
func pause() async
func seek(to time: TimeInterval) async
}
Usage:
let service = AppleTVService()
// Load content
let content = MediaContent(
title: "Sample Movie",
contentID: "movie-123",
duration: 7200,
mediaType: .movie
)
try await service.loadContent(content)
// Control playback
await service.play()
await service.pause()
await service.seek(to: 600) // 10 minutes
Manages Apple Music playback.
@MainActor
protocol AppleMusicServiceProtocol: LayoverService {
var currentContent: MediaContent? { get }
var isAuthorized: Bool { get async }
func requestAuthorization() async throws
func loadContent(_ content: MediaContent) async throws
func play() async
func pause() async
}
Usage:
let service = AppleMusicService()
// Request authorization
if !await service.isAuthorized {
try await service.requestAuthorization()
}
// Load and play music
let song = MediaContent(
title: "Sample Song",
contentID: "song-456",
duration: 240,
mediaType: .song
)
try await service.loadContent(song)
await service.play()
Manages room list and operations.
@MainActor
@Observable
final class RoomListViewModel: LayoverViewModel {
private(set) var rooms: [Room]
private(set) var isLoading: Bool
private(set) var errorMessage: String?
func loadRooms() async
func createRoom(name: String, hostID: UUID, activityType: RoomActivityType) async
func joinRoom(_ room: Room, userID: UUID) async
func leaveRoom(_ room: Room, userID: UUID) async
func deleteRoom(_ room: Room) async
}
Usage in SwiftUI:
struct MyView: View {
@State private var viewModel = RoomListViewModel()
var body: some View {
List(viewModel.rooms) { room in
Text(room.name)
}
.task {
await viewModel.loadRooms()
}
}
}
Manages Apple TV+ viewing experience.
@MainActor
@Observable
final class AppleTVViewModel: LayoverViewModel {
private(set) var currentContent: MediaContent?
private(set) var isPlaying: Bool
private(set) var isLoading: Bool
var player: AVPlayer?
func loadContent(_ content: MediaContent) async
func play() async
func pause() async
func seek(to time: TimeInterval) async
func togglePlayPause() async
}
Manages Apple Music listening experience.
@MainActor
@Observable
final class AppleMusicViewModel: LayoverViewModel {
private(set) var currentContent: MediaContent?
private(set) var isPlaying: Bool
private(set) var isAuthorized: Bool
func requestAuthorization() async
func loadContent(_ content: MediaContent) async
func play() async
func pause() async
func togglePlayPause() async
}
enum SharePlayError: LocalizedError {
case activationDisabled
case cancelled
case noActiveSession
case unknown
}
enum RoomError: LocalizedError {
case roomNotFound
case roomFull
case notAuthorized
}
enum MediaError: LocalizedError {
case invalidURL
case loadFailed
}
enum MusicError: LocalizedError {
case notAuthorized
case authorizationDenied
case loadFailed
}
enum GameError: LocalizedError {
case noActiveGame
case invalidPlayerCount
case playerNotFound
case invalidMove
case notYourTurn
}
Main app navigation view.
struct ContentView: View
Displays a room in a list.
struct RoomRowView: View {
let room: Room
}
Sheet for creating new rooms.
struct CreateRoomView: View {
let currentUser: User
let onCreate: (String, RoomActivityType) async -> Void
}
Apple TV+ viewing interface.
struct AppleTVView: View {
let room: Room
let currentUser: User
}
Apple Music listening interface.
struct AppleMusicView: View {
let room: Room
let currentUser: User
}
Chess game interface.
struct ChessView: View {
let room: Room
let currentUser: User
}
// In production, inject services
let viewModel = RoomListViewModel(
roomService: productionRoomService,
sharePlayService: productionSharePlayService
)
// In tests, inject mocks
let viewModel = RoomListViewModel(
roomService: mockRoomService,
sharePlayService: mockSharePlayService
)
// Always handle errors
do {
try await service.performAction()
} catch let error as RoomError {
// Handle room-specific errors
handleRoomError(error)
} catch {
// Handle general errors
handleGeneralError(error)
}
// Use Task for async operations in sync contexts
Button("Create Room") {
Task {
await viewModel.createRoom(name: name, hostID: hostID, activityType: type)
}
}
// Services and ViewModels are @MainActor
@MainActor
func updateUI() {
// Safe to update UI here
viewModel.loadRooms()
}
Extend with new activity types:
RoomActivityTypeAdd new games:
Replace in-memory storage: