SharePlay Tutorial 2024 — Share custom data between iOS/visionOS and MacOS
In this tutorial you will extend a small game to learn how you can use SharePlay for your apps.
- Introduction
- Preconditions - Project Setup
- UI Setup + Gameplay - Group Activity API
- Problems & Solutions
- Decoding Error
- Capability Error - Conclusion
- Updates
Introduction
Recently, iOS 15.1 and the macOS 12.1 Beta were released, which both come with the functionality of SharePlay — a new way to share experiences of your app right inside FaceTime. In general you can use the Group Activities API to easily share video and audio, but also custom data with each other. In this tutorial we will especially focus on the last part and create a small simple tug of war game to showcase the functionality of SharePlay in a fun way. Even though the overall usage of the Group Activities API is well set up and easy to use, it can still happen to get stuck at some points, therefore, we go through the setup step-by-step in this tutorial and sum up possible problems and solutions at the end of this post. Lastly I will also always try to add new interesting updates about SharePlay to the end of this post.
Preconditions
For testing the SharePlay functionality you need two physical devices because FaceTime is needed. Since this a cross-platform tutorial, I recommend the following setup, but in general it should also work if you use one of the following devices twice.
- Mac/Macbook with macOS 12.1+ installed
- iPhone/iPad with iOS 15.1+ installed
Additionally you should also download an initial project which already covers the UI and game logic part for you as we won’t go into detail about that:
Project Setup
This tutorial will be kept as simple as possible, to make it easily understandable — therefore, a generic naming of “SharePlay” will be used for the project, models, etc. — feel free to use a different naming and to extend/adapt the project at any time as you would like to. For copy-paste reasons I recommend to use the same naming as a first step. :)
Let’s start by selecting the project on the left side in the navigator where you should see two targets — SharePlayTutorial (iOS) and SharePlayTutorial (macOS). Add now for both targets the Group Activities capability by selecting each target, clicking on +Capability on the top left corner, searching for Group Activities and selecting it.
UI Setup + Gameplay
Since this tutorial is about the SharePlay functionality rather than any kind of UI or Gameplay implementation, we will not go into detail about this. It basically just adds one button for each player — one on top of the screen and the other one on the bottom of the screen — a visual representation of the players and the rope, a button for starting the SharePlay session and a button to restart the game.
The code contains some magic numbers and could also definitely be improved, but as mentioned above, it’s more about the SharePlay part and it’s doing its job.
Group Activity API
Let’s get to the interesting part — the Group Activity API. First, we have to import the GroupActivity framework to create an activity which conforms to the GroupActivity protocol. The activity defines specific parts of our shared experience like the title which gets presented when a person starts the activity and what kind of shared experience we would like to use. Besides listenTogether and watchTogether, which are used for sharing audio or video, we will use the remaining type generic since we want to share custom data.
Then we have to start the activity which is usually done by some kind of user interaction. Therefore, we will create a function in the viewmodel to activate the activity and replace the placeholder comment in the action of the SharePlay button with calling this new function.
Next, we need a way to check when a new participant joins. The system will always add a new element to the activity’s session sequence, as soon as someone joins, therefore, we will iterate over the sequence with the await keyword to receive each value asynchronously as it becomes available. To avoid blocking code we will do the iteration in a task in the GameView.
Then we have to define the configureGroupSession function and will complete several steps at once in there. First, we create a GroupSessionMessenger from the given session, which is basically responsible for receiving and sending all data. This messenger has to be stored in a variable since later we still need it to send the data. Then we define what should happen when a message, in our case a SharePlayModel, is received. Here we can then just replace our viewmodel’s model with the received model and the UI will update accordingly. It’s important to also define the SharePlayViewModel as MainActor since we do not want to update the UI from a background task. Lastly, we will join the session to begin the delivery of synchronized data to the current device.
When we try to run our app now, we see the error No exact matches in call to instance method ‘messages’ appears. This occurs since our model does not conform to the Codable protocol yet. In general we can send any data which conforms to the Codable protocol, but we should still keep it as small as possible because of performance and data usage reasons, since it’s the data which gets sent between all the participants. Therefore, we just send the players’ y-offset in our model. Lastly, we will define a helper method to send data via the messenger.
Now we can send the model whenever we changed it.
Now we can run the app on both devices and call each other via FaceTime. Then we can start SharePlay via the associated button whereby we can join on the other device via an appearing popup. For some reasons you get already linked to the App Store on the Mac where the app won’t get found and you won’t join the SharePlay session. Therefore, start SharePlay on the Mac and join on the iPhone/iPad. Then you can already start to smash the buttons to win the game!
Problems & Solutions
Decoding Error
In a first attempt of creating a tutorial, where I created first an iOS app and added a Mac target afterwads, I got the following error as soon I tried to send some data:
SharePlayTutorialMac[33577:704067] [Default] messageStream:108 Explanation: Decoding message from data Error: Swift.DecodingError.valueNotFound(Any, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: “message”, intValue: nil), CodingKeys(stringValue: “t”, intValue: nil)], debugDescription: “Decoder for value of Swift.Array<SharePlayTutorialMac.SharePlayModel>.self not found.”, underlyingError: nil))
It was quite strange since nowhere an object with a value of message or t inside was used as mentioned in the error. It seems like the type was defined in some kind of absolute way as the model’s type was defined as SharePlayTutorialMac.SharePlayModel in the error instead of just SharePlayModel. The type was somehow connected to the target’s name SharePlayTutorialMac.
Solution
My first attempt for solving this issue was to create a custom package, move the type/model there and importing the package where the model is used. This will even work when you build the app locally, but as soon as you upload the app to App Store Connect for example and download it via TestFlight, you will get the same error as above, just with the package name instead of the project name like SharePlayModelPackage.SharePlayModel.
After comparing the project.pbxproj file of a newly created multiplatform app (where it was working out of the box) and a newly created iOS app where the Mac target was added afterwards, I found the issue in the incorrect Product Name value. So it was not because of the target’s name, but instead of the product name referenced as $(TARGET_NAME), which in turn led to a product name of SharePlayTutorialiOS for the iOS target and SharePlayTutorialMac for the Mac target.
So to fix this error you have to use the same product name for both targets! For multiplatform apps this is set by default.
Capability Error
In case you forgot to add the Group Activities capability to your target you will get the following error.
SharePlayTutorial[11806:1823770] [Client] Error requesting initial state: Error Domain=NSCocoaErrorDomain Code=4099 “The connection to service named com.apple.group-activities.conversationmanagerhost was invalidated: failed at lookup with error 159 — Sandbox restriction.” UserInfo={NSDebugDescription=The connection to service named com.apple.group-activities.conversationmanagerhost was invalidated: failed at lookup with error 159 — Sandbox restriction.}
Solution
This can simply be fixed by adding the capability to your specific target.
Conclusion
As you can see it’s quite simple to setup SharePlay with a custom shared experience for your app — it’s a great new way to let users enjoy your app together.
The final project can be downloaded here: Direct or on Github. Also if you are interested in seeing it in a real app you can try it in my game Ploppy Pairs.
PS: Since this my very first Medium post, there is definitely room for improvement, also in the project itself, so feel free to comment any kind of suggestions, critics or improvements — thanks! Other than that, I hope this post could help you to setup SharePlay for your app — happy coding! :)
Updates
WWDC 2022
Start SharePlay from your app
With iOS 15.4 it’s possible to start SharePlay directly from within your app by using the new GroupActivitySharingController, which only needs your activity as an argument. When using UIKit we can present it as any other ViewController.
let controller = GroupActivitySharingController(SharePlayActivity())
present(controller, animated: true)
With SwiftUI we need a small wrapper via the UIViewControllerRepresentable protocol to get a View for it.
import GroupActivities
import SwiftUI
import UIKit
struct ActivitySharingViewController: UIViewControllerRepresentable {
let activity: GroupActivity
func makeUIViewController(context: Context) -> GroupActivitySharingController {
return try! GroupActivitySharingController(activity)
}
func updateUIViewController(_ uiViewController: GroupActivitySharingController, context: Context) { }
}
We can then use a GroupStateObserver to see, if a FaceTime call is currently active, by checking it’s isEligibleForGroupSession value. So now, whenever a FaceTime call is active, we directly start the SharePlay session and otherwise we present the ActivitySharingViewController.
import GroupActivities
import SwiftUI
struct HUDView: View {
@ObservedObject var viewModel: SharePlayViewModel
@StateObject var groupStateObserver = GroupStateObserver()
@State var isActivitySharingSheetPresented = false
var body: some View {
HStack {
// ...
Button(action: {
if groupStateObserver.isEligibleForGroupSession {
viewModel.startSharing()
} else {
isActivitySharingSheetPresented = true
}
}) {
// ...
}
}
.sheet(isPresented: $isActivitySharingSheetPresented) {
ActivitySharingViewController(activity: SharePlayActivity())
}
}
}
Unreliable Messages
With iOS 16 you can now define the reliability of your messages when initializing your GroupSessionMessenger.
GroupSessionMessenger(session: session, deliveryMode: .unreliable)
It’s up to you to decide which of your messages should be delivered reliably or not.
More information about those updates can be found in the video “What’s new in SharePlay — WWDC22”
WWDC 2023
visionOS SharePlay configurations
In visionOS there is a new variable in the GroupSession called .configuration
.
for await session in SharePlayActivity.sessions() {
#if os(visionOS)
if let systemCoordinator = await session.systemCoordinator {
var configuration = SystemCoordinator.Configuration()
configuration.supportsGroupImmersiveSpace = true
configuration.spatialTemplatePreference = .sideBySide // .none / .conversational / .sideBySide.contentExtent(10)
systemCoordinator.configuration = configuration
}
#endif
// ...
}
There we can set two configurations:
.supportsGroupImmersiveSpace
defines if a Spatial Persona is shown in an immersive space, which is off by default!.spatialTemplatePreference
defines how the Spatial Personas are aligned to the apps content. There are three different options to choose from:
- .none
, the default template, chooses the best arrangement possible. For windows it places the Spatial Personas side by side to the apps content.
For volumes it places the Spatial Personas around the content, therefore it works especially well with 3D content as everyone see’s the objects from a different angle.
App - Window A
A B C D B App - Volume D
C
- .sideBySide
places the Spatial Personas aligned next to each other, works well with watching media together.
App
A B C D
- .conversational
also groups the Spatial Personas around a center point where the content is placed alongside the other participants instead of the center. Works well when participants converse with each other and look at your content or to run your app in the background like a music player for example.
App
A C
B
Lastly, you can also set distance in points between the app’s content and the participants like .sideBySide.contentExtent(10)
.
To keep the feeling of being in the same room, even in the Immersive Space, you can make sure to open and close the Immersive Space for all participants by listening to the change of the immersion style:
for await session in SharePlayActivity.sessions() {
#if os(visionOS)
if let systemCoordinator = await session.systemCoordinator {
// ...
Task.detached { @MainActor in
for await immersionStyle in systemCoordinator.groupImmersionStyle {
if let immersionStyle {
await openImmersiveSpace(id: "ImmersiveSpace")
} else {
await dismissImmersiveSpace()
}
}
}
}
#endif
// ...
}
Tips
If you have audio in your app, I recommend to reduce the volume when FaceTime starts as it does not lower it automatically when someone talks as on iOS. As of today, 08/21/2024, FaceTime also stops the audio and doesn’t restart when the call is declined — feedback submitted FB14867187.
More information can be found here:
- https://developer.apple.com/design/human-interface-guidelines/shareplay
- Design spatial SharePlay experiences: https://developer.apple.com/wwdc23/10075
- Build spatial SharePlay experiences: https://developer.apple.com/wwdc23/10087