IMG_0974.-.ROTATE.-.Videobolt.net.mp4
A simple SwiftUI multiplayer demo using GameKit, where two players can increment a counter, and the updated value is synchronized between devices in real-time.
This project demonstrates how to implement real-time multiplayer functionality in a SwiftUI app using GameKit
. It provides a simple counter that two (or more) players can increment, and the counter value is synchronized between both players' devices. The project serves as a starting point for building more complex multiplayer games or apps.
- Real-time multiplayer using GameKit.
- Asynchronous programming with Swift's async/await.
- State management with @Published properties and ObservableObject.
- Clean and maintainable code with MVVM architecture.
- Automatic matchmaking with Game Center.
- Error handling and user feedback.
- Supports iOS 15.0 and above.
- Xcode 13.0 or later.
- Swift 5.9 or later.
- iOS 15.6 or later.
- Two devices or simulators signed into Game Center with different accounts.
- Update the bundle identifier to a unique value associated with your Apple Developer account.
- Go to your project's Signing & Capabilities and set up the necessary capabilities:

- Make sure to create the Project in AppstoreConnect and enable GameKit else it will not work in real devices:

- Open the app on both devices or simulators.
- Initiate Matchmaking
- On each device, tap the Find Match button.
- The Game Center matchmaking UI will appear.
- Connect with Another Player
IMG_0974.-.ROTATE.-.Videobolt.net.mp4
- The matchmaking service will automatically connect the two devices.
- Once connected, the game will start automatically.
- On either device, tap the Increment Counter button.
- The updated counter value will appear on both devices.
- Repeat tapping the button on either device to continue incrementing.
Tap the End Game button to disconnect and return to the main menu.
MultiplayerState
: Enum representing the different states of the multiplayer session.AuthenticationError
: Enum for handling authentication-related errors.
MultiplayerViewModel
: Manages the game logic, state transitions, and communication with GameKitManager.
@MainActor
final class MultiplayerViewModel: ObservableObject {
@Published var state: MultiplayerState = .idle
@Published var connectedPlayers: [GKPlayer] = []
@Published var counter: Int = 0
private var isHost: Bool = false
// Initialization and subscriptions...
func incrementCounter() {
counter += 1
sendCounterUpdate()
}
private func sendCounterUpdate() {
let messageData = [
"action": "updateCounter",
"counterValue": counter
] as [String : Any]
do {
let data = try JSONSerialization.data(withJSONObject: messageData, options: [])
sendData(data)
} catch {
state = .error("Failed to send counter update: \(error.localizedDescription)")
}
}
// Other methods...
}
MultiplayerView
: The main SwiftUI view that updates based on the MultiplayerViewModel's state.
GameKitManager
: Handles all Game Center-related functionality, including authentication, matchmaking, and data transmission.
final class GameKitManager: NSObject, ObservableObject {
static let shared = GameKitManager()
// Authentication Properties
@Published private(set) var isAuthenticated: Bool = GKLocalPlayer.local.isAuthenticated
@Published var authenticationError: AuthenticationError?
@Published var authenticationViewController: UIViewController?
// Multiplayer Properties
@Published var currentMatch: GKMatch?
@Published var matchError: Error?
@Published var receivedData: (data: Data, player: GKPlayer)?
@Published var playerConnectionState: (player: GKPlayer, state: GKPlayerConnectionState)?
func findMatch(minPlayers: Int, maxPlayers: Int, viewController: UIViewController) throws {
guard isAuthenticated else {
throw AuthenticationError.custom(message: "Local player is not authenticated. Please sign in to Game Center.")
}
let request = GKMatchRequest()
request.minPlayers = minPlayers
request.maxPlayers = maxPlayers
let mmvc = GKMatchmakerViewController(matchRequest: request)
mmvc?.matchmakerDelegate = self
if let mmvc {
viewController.present(mmvc, animated: true)
}
}
func sendData(_ data: Data, mode: GKMatch.SendDataMode = .reliable) throws {
guard let match = currentMatch else {
throw NSError(domain: "No active match", code: 0, userInfo: nil)
}
try match.sendData(toAllPlayers: data, with: mode)
}
func disconnectMatch() {
currentMatch?.disconnect()
currentMatch = nil
}
// Other methods...
}
James Thang, find me on LinkedIn
Learn more about SwiftUI, check out my book 📚 on Amazon