Trouble with initializing a SharePlay and using GroupSessionJournal.

I am having trouble with initializing the SharePlay. It works but we have to leave the game (click the close button) and rejoin it, sometimes several times, for it to establish the connection.

I am also having trouble sharing images over SharePlay with GroupSessionJournal. I am not able to get it to transfer any amount of data or even get recognition on the other participants in the SharePlay that an image is being sent. We have look at all the information we can find online and are not able to establish a connection. I am not sure if I am missing a step, or if I am incorrectly sending the data through the GroupSessionJournal.

Here are the steps I took take to replicate the issue I have:

  1. FaceTime another person with the app.
  2. Open the app and click the SharePlay button to SharePlay it with the other person.
  3. Establish the SharePlay and by making sure that the board states are syncronized across participants. If its not click the close button and click open app again to rejoin the SharePlay. (This is one of the bugs that I would like to fix. This is just a work around we developed to establish the SharePlay. We would like it so that when you click SharePLay and they join the session it works.)
  4. Once the SharePlay has been established, change the image by clicking change 1 image.
  5. Select a jpg image.
  6. The image that represents 1 should be not set. If you dont see the image click on any of the X in the squares and it will change to the image.
  7. The image should appear on the other participant in the SharePlay. (This does not happen and is what we have not been able to figure out how to get working.)

Here are the classes for the example project I created:

  • 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())
    }
    
  • //
    //  GameModel.swift
    //  mixed_game
    //
    //  Created by Charles Ciampa on 9/3/24.
    //
    
    import Foundation
    
    @Observable
    class GameModel {
        /// Contains the current board state as a 2d array. It starts off as None in every spot. This is what is rendered in all the views to represent the board.
        var board: [Int] = Array(repeating: 0, count: 3)
        
        /// Saves the data for the image for the state representation.
        var imageData: [Int: Data] = [Int: Data]()
        
        /// Sets the image representation of a state.
        /// - Parameters:
        ///   - num: The num to set the image of.
        ///   - data: The data of the image to set it as.
        func setNumberImage(num: Int, data: Data) {
            imageData[num] = data
        }
        
        /// Saves the current set of players, to identify who is who in the game.
        var players: [Player] = [Player(name: "Sam", selection: 1)]
        
        func setBoardPiece(num: Int, index: Int) {
            board[index] = num
        }
        
        func getBoardPiece(index: Int) -> Int {
            return board[index]
        }
        
        func resetBoard() {
            self.board = Array(repeating: 0, count: 3)
        }
    }
    
    struct Player {
        let name: String
        let selection: Int
        static var localName: String = ""
    }
    
  • import GroupActivities
    import SwiftUI
    
    func startSession() async throws {
        let activity = Activity()
        let result = await activity.prepareForActivation()
        if result == .activationPreferred {
            let activationSuccess = try await activity.activate()
            print("Group Activities session activation: ", activationSuccess)
        } else {
          print("Group Activities session activation: ", result)
        }
    }
    
    struct Activity: GroupActivity {
        static let activityIdentifier: String = "com.example.code"
        
        var metadata: GroupActivityMetadata {
            var metadata = GroupActivityMetadata()
            metadata.type = .generic
            metadata.title = "MixedGame"
            metadata.supportsContinuationOnTV = false
            return metadata
        }
    }
    
    struct ArrayMessage: Codable {
        let board: [Int]
    }
    
    struct ImageMetadata: Codable {
        let num: Int
    }
    
    var sessionInfo: ActivitySessionInfo? = nil
    
    
    class ActivitySessionInfo: ObservableObject {
        @Published var session: GroupSession?
        var messenger: GroupSessionMessenger?
        var reliableMessenger: GroupSessionMessenger?
        var journal: GroupSessionJournal?
    }
    
  • //
    //  SharePlay_Test_CodeApp.swift
    //  SharePlay Test Code
    //
    //  Created by Charles Ciampa on 9/25/24.
    //
    
    import SwiftUI
    
    @main
    struct SharePlay_Test_CodeApp: App {
        @State private var gameModel: GameModel = .init()
        var body: some Scene {
            WindowGroup {
                ContentView().environment(gameModel)
            }
        }
    }
    
Trouble with initializing a SharePlay and using GroupSessionJournal.
 
 
Q