import SwiftUI import RealityKit import RealityKitContent import GroupActivities struct ContentView: View { @Environment(GameModel.self) var gameModel @State var openFiles: Bool = false @State private var session: GroupSession? = nil @StateObject private var groupStateObserver = GroupStateObserver() var body: some View { VStack { HStack { ForEach(0..<3) { index in Image(uiImage: imageForValue(gameModel.board[index])) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 100, height: 100) .onTapGesture { gameModel.board[index] = gameModel.board[index] == 1 ? 0 : 1 sendBoardUpdate(gameModel.board) } } } Button("Close", systemImage: "xmark.circle") { sessionInfo?.session?.leave() exit(0) } Button { if sessionInfo != nil { gameModel.resetBoard() sendBoardUpdate(gameModel.board) } } label: { Label("Reset", systemImage: "arrow.circlepath") } Button { Task { do { try await startSession() } catch { print("SharePlay session failure", error) } } } label: { Label("SharePlay", systemImage: "shareplay") } .disabled(!groupStateObserver.isEligibleForGroupSession) Button("Change 1 Image") { openFiles = true } .fileImporter(isPresented: $openFiles, allowedContentTypes: [.jpeg]) { result in do { let file = try result.get() if file.startAccessingSecurityScopedResource() { gameModel.setNumberImage(num: 1, data: try! Data(contentsOf: file)) file.stopAccessingSecurityScopedResource() Task { print("Setting 1 Image") let result = try await sessionInfo?.journal?.add(gameModel.imageData[1]!, metadata: ImageMetadata(num: 1)) print("Adding 1 Image to journal result: \(result)") } } openFiles = false } catch { print("Failed to read file") } } Text("Hello, world!") } .padding() .task { // Initialize sessionInfo sessionInfo = .init() // Wait for a SharePlay session to start for await newSession in Activity.sessions() { print("New GroupActivities session", newSession) session = newSession sessionInfo?.session = newSession // Configure spatial coordination if let coordinator = await newSession.systemCoordinator { var config = SystemCoordinator.Configuration() config.spatialTemplatePreference = .sideBySide config.supportsGroupImmersiveSpace = true coordinator.configuration = config } // Join the session newSession.join() do { print("Waiting before starting group activity.") while newSession.activeParticipants.isEmpty { try await Task.sleep(for: .seconds(3)) } } catch { print("Couldn't sleep.", error) } // Set up messengers for SharePlay communication sessionInfo?.messenger = GroupSessionMessenger(session: newSession, deliveryMode: .unreliable) sessionInfo?.reliableMessenger = GroupSessionMessenger(session: newSession, deliveryMode: .reliable) sessionInfo?.journal = GroupSessionJournal(session: newSession) // Update player list when participants join Task { for try await updatedPlayerList in newSession.$activeParticipants.values { for participant in updatedPlayerList { try await sessionInfo!.reliableMessenger!.send(ArrayMessage(board: gameModel.board), to: .only(participant)) let potentialNewPlayer = Player(name: String(participant.id.asPlayerName), selection: 0) if !gameModel.players.contains(where: { $0.name == potentialNewPlayer.name }) { gameModel.players.append(potentialNewPlayer) } } } } // Handle board state updates Task { for await (message, _) in sessionInfo!.reliableMessenger!.messages(of: ArrayMessage.self) { gameModel.board = message.board } } // Handle image updates for board states Task { for await files in sessionInfo!.journal!.attachments { print("New attachments") for attachment in files { print("New attachment: \(attachment)") let data = try await attachment.load(Data.self) let metadata = try await attachment.loadMetadata(of: ImageMetadata.self) gameModel.setNumberImage(num: metadata.num, data: data) } } } } } } /// Sends the new Board to all other SharePlay participants. /// - Parameters: /// - board: The new Board. func sendBoardUpdate(_ board: [Int]) { if let sessionInfo = sessionInfo, let session = sessionInfo.session, let messenger = sessionInfo.reliableMessenger { let everyoneElse = session.activeParticipants.subtracting([session.localParticipant]) messenger.send(ArrayMessage(board: board), to: .only(everyoneElse)) { error in if let error = error { print("Message failure:", error) } } } } /// Returns the appropriate UIImage based on the board value. /// - Parameter value: The value from the board array. /// - Returns: The UIImage to display. func imageForValue(_ value: Int) -> UIImage { if value == 1 { if let data = gameModel.imageData[1], let image = UIImage(data: data) { return image } else { return UIImage(systemName: "xmark.circle")! } } else { return UIImage(systemName: "xmark.square")! } } } extension UUID { var asPlayerName: String { String(uuidString.split(separator: "-").last!) } } #Preview(windowStyle: .automatic) { ContentView().environment(GameModel()) }