FaceTime ํจ๊ป ๋ณด๊ธฐ ๊ฒฝํ ๊ตฌํ ๊ฐ์ด๋. ์ด ๋ฌธ์๋ฅผ ์ฝ๊ณ SharePlay ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
SharePlay๋ FaceTime ํตํ ์ค ์ฝํ ์ธ ๋ฅผ ํจ๊ป ๋ณด๊ณ ์ํธ์์ฉํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค. GroupActivities ํ๋ ์์ํฌ๋ฅผ ํตํด ์ฑ ์ํ๋ฅผ ์ค์๊ฐ ๋๊ธฐํํฉ๋๋ค.
import GroupActivities- Capabilities: Group Activities ์ถ๊ฐ
- Info.plist:
<key>NSSupportsLiveActivities</key>
<true/>struct WatchTogetherActivity: GroupActivity {
// ์ฝํ
์ธ ์ ๋ณด
let movie: Movie
// ๋ฉํ๋ฐ์ดํฐ
var metadata: GroupActivityMetadata {
var metadata = GroupActivityMetadata()
metadata.title = movie.title
metadata.subtitle = "ํจ๊ป ๋ณด๊ธฐ"
metadata.previewImage = movie.thumbnailImage
metadata.type = .watchTogether
return metadata
}
}
struct Movie: Codable, Hashable {
let id: String
let title: String
let url: URL
var thumbnailImage: CGImage? { nil }
}func startSharePlay(movie: Movie) async {
let activity = WatchTogetherActivity(movie: movie)
switch await activity.prepareForActivation() {
case .activationPreferred:
do {
_ = try await activity.activate()
} catch {
print("ํ์ฑํ ์คํจ: \(error)")
}
case .activationDisabled:
// SharePlay ๋นํ์ฑํ๋จ
print("SharePlay๊ฐ ๋นํ์ฑํ๋์ด ์์ต๋๋ค")
case .cancelled:
// ์ฌ์ฉ์ ์ทจ์
break
@unknown default:
break
}
}@Observable
class SharePlayManager {
var session: GroupSession<WatchTogetherActivity>?
var messenger: GroupSessionMessenger?
var isSharePlayActive = false
func configureSession() async {
for await session in WatchTogetherActivity.sessions() {
self.session = session
self.isSharePlayActive = true
// ๋ฉ์ ์ ์ค์
messenger = GroupSessionMessenger(session: session)
// ์ธ์
์ํ ๊ด์ฐฐ
Task {
for await state in session.$state.values {
if case .invalidated = state {
self.isSharePlayActive = false
self.session = nil
}
}
}
// ์ธ์
์ฐธ๊ฐ
session.join()
}
}
}import SwiftUI
import GroupActivities
import AVKit
// MARK: - Activity ์ ์
struct MovieWatchActivity: GroupActivity {
let movieID: String
let movieTitle: String
var metadata: GroupActivityMetadata {
var metadata = GroupActivityMetadata()
metadata.title = movieTitle
metadata.subtitle = "ํจ๊ป ์ํ ๋ณด๊ธฐ"
metadata.type = .watchTogether
return metadata
}
}
// ๋๊ธฐํํ ๋ฉ์์ง
struct PlaybackState: Codable {
let isPlaying: Bool
let currentTime: TimeInterval
}
// MARK: - SharePlay Manager
@Observable
class MovieSharePlayManager {
var session: GroupSession<MovieWatchActivity>?
var messenger: GroupSessionMessenger?
var isSharePlayActive = false
var participants: Set<Participant> = []
private var tasks = Set<Task<Void, Never>>()
init() {
Task {
await observeSessions()
}
}
private func observeSessions() async {
for await session in MovieWatchActivity.sessions() {
cleanUp()
self.session = session
// ๋ฉ์ ์ ์ค์
let messenger = GroupSessionMessenger(session: session)
self.messenger = messenger
// ์ฐธ๊ฐ์ ๊ด์ฐฐ
let participantTask = Task {
for await participants in session.$activeParticipants.values {
await MainActor.run {
self.participants = participants
}
}
}
tasks.insert(participantTask)
// ์ธ์
์ํ ๊ด์ฐฐ
let stateTask = Task {
for await state in session.$state.values {
await MainActor.run {
switch state {
case .joined:
self.isSharePlayActive = true
case .invalidated:
self.isSharePlayActive = false
self.cleanUp()
default:
break
}
}
}
}
tasks.insert(stateTask)
// ๋ฉ์์ง ์์
let messageTask = Task {
for await (message, _) in messenger.messages(of: PlaybackState.self) {
await handlePlaybackState(message)
}
}
tasks.insert(messageTask)
// ์ธ์
์ฐธ๊ฐ
session.join()
}
}
func startSharePlay(movieID: String, title: String) async {
let activity = MovieWatchActivity(movieID: movieID, movieTitle: title)
switch await activity.prepareForActivation() {
case .activationPreferred:
do {
_ = try await activity.activate()
} catch {
print("SharePlay ํ์ฑํ ์คํจ: \(error)")
}
case .activationDisabled:
print("SharePlay๊ฐ ๋นํ์ฑํ๋จ")
case .cancelled:
break
@unknown default:
break
}
}
func sendPlaybackState(isPlaying: Bool, currentTime: TimeInterval) {
guard let messenger else { return }
let state = PlaybackState(isPlaying: isPlaying, currentTime: currentTime)
Task {
do {
try await messenger.send(state)
} catch {
print("๋ฉ์์ง ์ ์ก ์คํจ: \(error)")
}
}
}
@MainActor
private func handlePlaybackState(_ state: PlaybackState) async {
// ViewModel์์ ์ฌ์ ์ํ ๋๊ธฐํ
NotificationCenter.default.post(
name: .sharePlayStateReceived,
object: state
)
}
func endSession() {
session?.end()
cleanUp()
}
private func cleanUp() {
tasks.forEach { $0.cancel() }
tasks.removeAll()
session = nil
messenger = nil
participants = []
}
}
extension Notification.Name {
static let sharePlayStateReceived = Notification.Name("sharePlayStateReceived")
}
// MARK: - Video Player ViewModel
@Observable
class VideoPlayerViewModel {
let movie: Movie
var isPlaying = false
var currentTime: TimeInterval = 0
var sharePlayManager: MovieSharePlayManager
init(movie: Movie, sharePlayManager: MovieSharePlayManager) {
self.movie = movie
self.sharePlayManager = sharePlayManager
observeSharePlay()
}
private func observeSharePlay() {
NotificationCenter.default.addObserver(
forName: .sharePlayStateReceived,
object: nil,
queue: .main
) { [weak self] notification in
guard let state = notification.object as? PlaybackState else { return }
self?.syncPlayback(state)
}
}
private func syncPlayback(_ state: PlaybackState) {
isPlaying = state.isPlaying
currentTime = state.currentTime
}
func togglePlayPause() {
isPlaying.toggle()
if sharePlayManager.isSharePlayActive {
sharePlayManager.sendPlaybackState(isPlaying: isPlaying, currentTime: currentTime)
}
}
func seek(to time: TimeInterval) {
currentTime = time
if sharePlayManager.isSharePlayActive {
sharePlayManager.sendPlaybackState(isPlaying: isPlaying, currentTime: currentTime)
}
}
}
struct Movie: Identifiable {
let id: String
let title: String
let url: URL
}
// MARK: - Views
struct MoviePlayerView: View {
let movie: Movie
@State private var sharePlayManager = MovieSharePlayManager()
@State private var viewModel: VideoPlayerViewModel?
var body: some View {
VStack {
// ๋น๋์ค ํ๋ ์ด์ด (์ค์ ๋ก๋ AVPlayer ์ฌ์ฉ)
Rectangle()
.fill(.black)
.aspectRatio(16/9, contentMode: .fit)
.overlay {
Image(systemName: viewModel?.isPlaying == true ? "pause.fill" : "play.fill")
.font(.system(size: 50))
.foregroundStyle(.white.opacity(0.8))
}
.onTapGesture {
viewModel?.togglePlayPause()
}
// ์ปจํธ๋กค
HStack(spacing: 20) {
// ์ฌ์/์ผ์์ ์ง
Button {
viewModel?.togglePlayPause()
} label: {
Image(systemName: viewModel?.isPlaying == true ? "pause.circle.fill" : "play.circle.fill")
.font(.system(size: 44))
}
Spacer()
// SharePlay ์ํ
if sharePlayManager.isSharePlayActive {
HStack {
Image(systemName: "shareplay")
Text("\(sharePlayManager.participants.count)๋ช
์์ฒญ ์ค")
}
.font(.caption)
.foregroundStyle(.green)
}
// SharePlay ๋ฒํผ
ShareLink(
item: movie.url,
preview: SharePreview(movie.title)
) {
Image(systemName: "shareplay")
.font(.title2)
}
}
.padding()
// SharePlay ์์ ๋ฒํผ
if !sharePlayManager.isSharePlayActive {
Button {
Task {
await sharePlayManager.startSharePlay(
movieID: movie.id,
title: movie.title
)
}
} label: {
Label("SharePlay ์์", systemImage: "shareplay")
}
.buttonStyle(.borderedProminent)
}
}
.onAppear {
viewModel = VideoPlayerViewModel(movie: movie, sharePlayManager: sharePlayManager)
}
.onDisappear {
sharePlayManager.endSession()
}
}
}// CoordinationManager ์ฌ์ฉ (iOS 15+)
func configureAVPlayerSync() {
guard let session else { return }
let coordinator = AVPlaybackCoordinator()
session.coordinator = coordinator
// AVPlayer์ ์ฐ๊ฒฐ
player.playbackCoordinator.coordinateWithSession(session)
}// ๊ฒ์ ์ํ ๋๊ธฐํ
struct GameState: Codable {
let playerPositions: [String: CGPoint]
let score: [String: Int]
let currentTurn: String
}
// ์ ๋ขฐํ ์ ์๋ ์ ์ก (์์ ๋ณด์ฅ)
try await messenger.send(gameState, to: .all, deliveryMode: .reliable)
// ๋น ๋ฅธ ์ ์ก (์ค์๊ฐ, ์์ ๋ฏธ๋ณด์ฅ)
try await messenger.send(position, to: .all, deliveryMode: .unreliable)// ํน์ ์ฐธ๊ฐ์์๊ฒ๋ง ์ ์ก
if let host = participants.first(where: { $0.isLocal == false }) {
try await messenger.send(message, to: .only(host))
}-
FaceTime ํ์
- SharePlay๋ FaceTime ํตํ ์ค์๋ง ๋์
- ์๋ฎฌ๋ ์ดํฐ์์ ์ ํ์ ํ ์คํธ ๊ฐ๋ฅ
-
๋คํธ์ํฌ ์ง์ฐ
- ์ํ ๋๊ธฐํ์ ์ง์ฐ ๋ฐ์ ๊ฐ๋ฅ
- UI์ ๋ฒํผ๋ง ํ์ ๊ถ์ฅ
-
์ธ์ ์ ๋ฆฌ
- ํ๋ฉด ์ดํ ์
session.leave()๋๋session.end()ํธ์ถ - ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง
- ํ๋ฉด ์ดํ ์
-
์ฐธ๊ฐ์ ์ ํ
- FaceTime ๊ทธ๋ฃน ํตํ ์ต๋ 32๋ช
- ์ฑ๋ณ๋ก ์ ์ ํ ์ ํ ์ค์ ๊ถ์ฅ