Claude Code Plugins

Community-maintained marketplace

Feedback

iMessage Extension Development

@nategarelik/prop-pigeon
0
0

Messages framework patterns, constraints, and best practices

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name iMessage Extension Development
description Messages framework patterns, constraints, and best practices
when_to_use Working with MessagesViewController, MSMessage, or message passing
version 1.0.0
languages swift

iMessage Extension Guidelines

Critical Constraints

Memory Limit (ABSOLUTE)

HARD LIMIT: 120MB

If exceeded, extension crashes. No exceptions.

  • Monitor with Instruments constantly
  • Optimize assets aggressively
  • Use texture atlases
  • Release resources immediately after games
  • Profile every game individually

Extension Lifecycle

  • No background execution - Suspended when not visible
  • 30 seconds per message - Handle or timeout
  • Handle backgrounding - Save state, release resources
  • State persistence - Use NSUserDefaults or Core Data
  • Quick resume - User expects instant return

Message Passing Constraints

  • Max message size: No official limit, but keep <100KB
  • Network unreliable - Assume poor conditions
  • Delivery not guaranteed - Implement retry logic
  • Messages can be out of order - Use sequence numbers
  • Offline support - Queue messages

Message Structure

Encoding Game State

struct GameMessage: Codable {
    let gameID: UUID
    let messageType: MessageType
    let gameState: Data // Encoded game-specific state
    let timestamp: Date
    let sequenceNumber: Int
    let stateHash: String // For integrity checking
}

enum MessageType: String, Codable {
    case invitation
    case move
    case gameEnd
    case spectatorBet
}

Sending Messages

func sendGameState(_ state: GameState, to conversation: MSConversation) async {
    let gameMessage = GameMessage(
        gameID: currentGameID,
        messageType: .move,
        gameState: try! JSONEncoder().encode(state),
        timestamp: Date(),
        sequenceNumber: nextSequence(),
        stateHash: computeHash(state)
    )
    
    let messageData = try! JSONEncoder().encode(gameMessage)
    
    let layout = MSMessageTemplateLayout()
    layout.caption = "Your turn!"
    layout.image = generateGamePreview(state)
    
    let message = MSMessage()
    message.url = URL(string: "proppigeon://game/\(gameMessage.gameID)")!
    message.layout = layout
    
    // Important: Set messageData
    message.url = message.url?.appending(
        queryItems: [URLQueryItem(name: "data", value: messageData.base64EncodedString())]
    )
    
    conversation.insert(message) { error in
        if let error = error {
            // Implement retry logic
            await retryMessage(message, after: 5.0)
        }
    }
}

Receiving Messages

override func didReceive(_ message: MSMessage, conversation: MSConversation) {
    guard let url = message.url,
          let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let dataItem = components.queryItems?.first(where: { $0.name == "data" }),
          let dataString = dataItem.value,
          let data = Data(base64Encoded: dataString) else {
        return
    }
    
    do {
        let gameMessage = try JSONDecoder().decode(GameMessage.self, from: data)
        
        // Validate integrity
        let state = try JSONDecoder().decode(GameState.self, from: gameMessage.gameState)
        guard gameMessage.stateHash == computeHash(state) else {
            throw GameError.stateCorrupted
        }
        
        // Handle message
        await handleGameMessage(gameMessage, conversation: conversation)
        
    } catch {
        showError("Failed to decode message: \(error)")
    }
}

Turn-Based Game Patterns

State Synchronization

// Games are inherently asynchronous
// Current player has authority over their move
// State updates propagate via messages

class TurnBasedGame {
    var state: GameState
    let localPlayer: PlayerID
    
    var isMyTurn: Bool {
        state.currentPlayer == localPlayer
    }
    
    func makeMove(_ move: GameMove) async throws {
        guard isMyTurn else {
            throw GameError.notPlayerTurn
        }
        
        // Apply locally
        try applyMove(move)
        
        // Send to opponent
        await sendState()
    }
    
    func handleOpponentMove(_ newState: GameState) {
        // Validate state transition
        guard isValidTransition(from: state, to: newState) else {
            // Conflict resolution
            await requestStateResync()
            return
        }
        
        state = newState
        updateUI()
    }
}

Handling Disconnections

struct GameState: Codable {
    var lastUpdateTime: Date
    var turnTimeLimit: TimeInterval = 120 // 2 minutes
    
    func hasTimedOut() -> Bool {
        Date().timeIntervalSince(lastUpdateTime) > turnTimeLimit
    }
}

// In ViewModel
func checkForTimeout() async {
    if gameState.hasTimedOut() {
        // Award win to non-timeout player
        await handleTimeout()
    }
}

Group Chat Detection

extension MSConversation {
    var isGroupChat: Bool {
        // Group chats have multiple recipients
        return recipients.count > 1
    }
    
    var playerIDs: [UUID] {
        // In 1-on-1: [local, remote]
        // In group: [local, ...spectators]
        return recipients.compactMap { UUID(uuidString: $0) }
    }
}

// Enable spectator betting in group chats
func handleConversation(_ conversation: MSConversation) {
    if conversation.isGroupChat {
        enableSpectatorMode()
    } else {
        enablePlayerMode()
    }
}

Performance Optimization

Asset Loading

// Lazy load assets
class AssetManager {
    private var loadedGames: Set<GameType> = []
    
    func loadAssets(for gameType: GameType) async {
        guard !loadedGames.contains(gameType) else { return }
        
        // Load textures, sounds, etc.
        await loadTextures(for: gameType)
        
        loadedGames.insert(gameType)
    }
    
    func releaseAssets(for gameType: GameType) {
        // Free memory when game ends
        unloadTextures(for: gameType)
        loadedGames.remove(gameType)
    }
}

State Compression

// Keep message size small
func compressGameState(_ state: GameState) -> Data {
    let encoder = JSONEncoder()
    encoder.outputFormatting = [] // No pretty printing
    
    let data = try! encoder.encode(state)
    
    // Optional: Use compression
    return try! (data as NSData).compressed(using: .lzfse) as Data
}

Testing iMessage Extensions

Mocking Conversations

class MockMSConversation: MSConversation {
    var insertedMessages: [MSMessage] = []
    
    override func insert(_ message: MSMessage, completionHandler: ((Error?) -> Void)? = nil) {
        insertedMessages.append(message)
        completionHandler?(nil)
    }
}

// Usage in tests
func testSendingMove() async {
    let mockConversation = MockMSConversation()
    let viewModel = GameViewModel(conversation: mockConversation)
    
    await viewModel.makeMove(.hit)
    
    XCTAssertEqual(mockConversation.insertedMessages.count, 1)
}

When This Skill Activates

  • Working with MessagesViewController
  • Encoding/decoding MSMessage
  • Handling conversation events
  • Testing iMessage integration
  • Debugging message passing issues

References

  • Apple Messages Framework Documentation
  • iMessage App Programming Guide
  • MSMessage API Reference