Недавно я работал над функцией NowPlaying, которая использует API SharePlay в iOS, позволяя пользователям присоединяться к сеансам прослушивания и открывать новую музыку вместе со своими друзьями.
Мне пришлось немало потрудиться, чтобы заставить все работать, и я обнаружил, что документация Apple по настройке сессий SharePlay скудная и немного запутанная. По этой причине я решил написать исчерпывающее руководство по настройке сеанса SharePlay, в котором собраны все мои находки.
Добавление возможностей SharePlay
Первый шаг к созданию опыта SharePlay — это добавление необходимых возможностей в ваше приложение. Для этого нужно открыть проект в Xcode, выбрать файл проекта и затем выбрать цель.
На вкладке «Signing & Capabilities» нажмите кнопку «+», найдите «Group Activities» и добавьте эту возможность.
Создание Group Activity
Опыт SharePlay построен на концепции GroupActivitys
(групповых действиях или активностей), которые являются типами, представляющими опыт приложения, которым можно поделиться.
Создать такой тип просто: определите его (или используйте существующий) и заставьте его соответствовать протоколу GroupActivity
.
import GroupActivities struct DemoAppActivity: GroupActivity { static let activityIdentifier = "dev.polpiella.demoapp.DemoAppActivity" var metadata: GroupActivityMetadata { var metadata = GroupActivityMetadata() metadata.title = "Demo Activity" metadata.subtitle = "Share an experience together using SharePlay" metadata.previewImage = UIImage(named: "preview-image")?.cgImage metadata.type = .generic return metadata } }
Как видно из приведенного примера, протокол GroupActivity
требует определения свойства metadata
, которое используется для предоставления информации об активности системе, и уникального идентификатора activityIdentifier
, используемого для идентификации активности.
Запуск сеанса SharePlay
Теперь, когда вы определили свою GroupActivity
, вы готовы начать сеанс SharePlay. Это можно сделать несколькими способами, в зависимости от состояния вашего приложения:
- Если пользователь уже находится на вызове FaceTime, вы можете начать сеанс SharePlay напрямую.
- Если пользователь не находится в режиме FaceTime, вы можете позволить ему поделиться сеансом со своими друзьями с помощью share sheet, который вы реализуете в ближайшее время.
import Foundation import GroupActivities import Combine enum SharePlayActivationOutcome { case local case sharePlay case needsDialog } @Observable final class GroupActivityManager { private let groupStateObserver = GroupStateObserver() func startSharing() async -> SharePlayActivationOutcome { // 1 if groupStateObserver.isEligibleForGroupSession { // 2 let activity = DemoAppActivity() let result = await activity.prepareForActivation() switch result { case .activationPreferred: // 3 _ = try? await activity.activate() return .sharePlay case .activationDisabled: // 4 return .local default: return .local } } else { // 5 return .needsDialog } } }
Давайте разберем приведенный выше код шаг за шагом:
- Проверьте, находится ли пользователь в разговоре по FaceTime и, следовательно, имеет ли он право на сеанс SharePlay.
- Создайте новый экземпляр
GroupActivity
и вызовите методprepareForActivation
, чтобы проверить, выбрал ли пользователь сеанс SharePlay или при появлении запроса он решил начать локальный сеанс. - Если пользователь решил начать сеанс SharePlay, вызовите метод
activate
, чтобы начать сеанс. - Если пользователь решил не начинать сеанс SharePlay, верните значение
.local
. - Если пользователь не находится на вызове FaceTime, верните
.needsDialog
, чтобы предложить пользователю поделиться активностью со своими друзьями.
Запуск сеанса SharePlay из представления
Если вы хотите начать сеанс SharePlay из представления, вы можете просто вызвать метод startSharing
из GroupActivityManager
и обработать результат соответствующим образом.
Если пользователь имеет право на участие в групповых сеансах и подтверждает, что хочет начать совместный опыт, используя системное оповещение, сеанс SharePlay начнется немедленно.
Если нет, вам нужно будет обработать результат и поделиться активностью с друзьями с помощью GroupActivitySharingController:
import SwiftUI import UIKit struct GroupActivityShareSheet<Activity: GroupActivity>: UIViewControllerRepresentable { let preparationHandler: () async throws -> Activity func makeUIViewController(context: Context) -> UIViewController { GroupActivitySharingController(preparationHandler: preparationHandler) } func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} }
В коде представления вы можете просто вызвать метод startSharing
и представить GroupActivityShareSheet
, если результатом будет .needsDialog
:
import SwiftUI struct ContentView: View { @State private var showDialog = false private let activityManager = GroupActivityManager() var body: some View { VStack { Button(action: { Task { let outcome = await activityManager.startSharing() if outcome == .needsDialog { showDialog = true } } }, label: { Label(title: { Text("Start SharePlay") }, icon: { Image(systemName: "shareplay") }) }) .buttonStyle(.borderedProminent) } .sheet(isPresented: $showDialog, content: { GroupActivityShareSheet { DemoAppActivity() } }) .padding() } }
Управление сеансом SharePlay
До сих пор вы только запускали сеанс SharePlay, но не отслеживали его и не управляли им.
Чтобы получить экземпляр текущей сессии SharePlay, вам нужно использовать статическое свойство sessions()
для вашего типа GroupActivity
.
Поскольку это свойство возвращает AsyncStream, вы можете использовать его для подписки и прослушивания новых сессий и отслеживания текущей активной сессии:
@Observable final class GroupActivityManager { var session: GroupSession<DemoAppActivity>? var messenger: GroupSessionMessenger<DemoAppActivity>? init() { Task.detached { for await session in DemoAppActivity.sessions() { self.session = session self.sessionJoined(session) } } } private func sessionJoined(_ session: GroupSession<DemoAppActivity>) { if session.state != .joined { session.join() } messenger = GroupSessionMessenger(session: session) listenToMessages() } private func listenToMessages() { guard let messenger else { return } Task.detached { for await message in messenger.messages(of: String.self) { print("Received message: \(message.0)") } } } }
Как видно из приведенного выше фрагмента, как только новая сессия становится доступной, вы можете позволить своему пользователю присоединиться к ней, вызвав метод join
, а затем начать прослушивать сообщения, используя только что созданный экземпляр GroupSessionMessenger
. Далее в статье вы также будете использовать его для отправки сообщений другим участникам.
Экземпляры GroupSessionMessenger
имеют свойство messages
. Это свойство представляет собой AsyncStream
, который можно использовать для прослушивания новых сообщений и их соответствующей обработки.
Отправка сообщений
Как только у вас есть активная сессия, вы можете отправлять сообщения другим участникам с помощью метода send
в GroupSessionMessenger
:
@Observable final class GroupActivityManager { func send(_ message: String) async throws { try await messenger?.send(message) } }
Обратите внимание, что вы можете отправить в сообщении любой тип Codable
, но вы должны знать, что существует ограничение на размер полезной нагрузки в сообщениях, которые вы отправляете в сеансе SharePlay. Если вы отправите сообщение, превышающее лимит, вы получите ошибку.
Подсчет участников
Допустим, вы хотите показать количество участников в сеансе SharePlay. Вы можете сделать это, прослушивая свойство $activeParticipants
в GroupSession
:
@Observable final class GroupActivityManager { var participantCount = 0 private var cancellables = Set<AnyCancellable>() private func keepParticipantCount() { guard let session else { return } session .$activeParticipants .sink { self.participantCount = $0.count } .store(in: &cancellables) } private func sessionJoined(_ session: GroupSession<DemoAppActivity>) { if session.state != .joined { session.join() } messenger = GroupSessionMessenger(session: session) listenToMessages() keepParticipantCount() } }
Завершение сеанса SharePlay
С этой конкретной функцией я столкнулся в процессе внедрения и практически не смог найти документации по нему.
Пользователь может завершить сеанс SharePlay двумя способами:
- Пользователь нажимает на кнопку в приложении, чтобы завершить сеанс.
- Пользователь покидает вызов FaceTime или решает завершить сеанс SharePlay из системного меню.
Давайте рассмотрим первый случай и позволим пользователям завершать сеанс из приложения:
@Observable final class GroupActivityManager { func stop() { guard let session else { return } switch session.state { case .invalidated: break default: isSessionActive = false; session.end() } } }
Как видно из приведенного выше фрагмента, завершение сеанса — это просто вызов метода end
для экземпляра GroupSession
, только если сеанс еще не признан недействительным.
Стоит отметить, что для других участников существует метод, позволяющий просто покинуть сессию, не завершая ее, который вы можете использовать в зависимости от вашего случая.
Теперь, когда сессия будет признана недействительной, вам нужно будет обработать изменение состояния и очистить все ресурсы для всех участников, а не только для того, кто завершил сессию. Это позволит справиться с ситуацией, когда пользователь решает завершить вызов FaceTime или сеанс SharePlay из системного меню.
Вы можете реагировать на инвалидацию сессий, прослушивая свойство $state
в GroupSession
:
@Observable final class GroupActivityManager { private func sessionJoined(_ session: GroupSession<DemoAppActivity>) { if session.state != .joined { session.join() } messenger = GroupSessionMessenger(session: session) listenToMessages() monitorSessionState() } private func monitorSessionState() { session?.$state.sink { state in switch state { case .invalidated: // Perform any cleanup here break case .joined: // Handle a re-join to the same session self.session.map(self.sessionJoined) default: break } } .store(in: &cancellables) } }