Getting started: Chat with Swift (Callback Approach)

This guide will help you get started with Ably Chat in a new iOS Swift application built with SwiftUI, using a callback-based approach for handling real-time events.

It will take you through the following steps:

  • Creating a client and establishing a realtime connection to Ably
  • Creating a room and subscribing to its messages
  • Sending messages to the room and editing messages
  • Retrieving historical messages to provide context for new joiners
  • Displaying online status of clients in the room
  • Subscribing to and sending reactions
  • Showing typing indicators when users are composing messages
  • Displaying occupancy information for the room
  • Deleting messages from the room
  • Disconnecting and resource cleanup

Prerequisites

Ably

  1. Sign up for an Ably account.

  2. Create a new app and get your API key. You can use the root API key that is provided by default to get started.

  3. Install the Ably CLI:

npm install -g @ably/cli
  1. Run the following to log in to your Ably account and set the default app and API key:
ably login

ably apps switch
ably auth keys switch

Create a new project

Create a new iOS project with SwiftUI. For detailed instructions, refer to the Apple Developer documentation.

  1. Create a new iOS project in Xcode.

  2. Select App as the template

  3. Name the project ChatExample and set the bundle identifier to com.example.chatexample

  4. Set the minimum iOS deployment target to iOS 15.0 or higher

  5. Select SwiftUI as the interface and Swift as the language

  6. Add the Chat dependency to your project using Swift Package Manager:

    • In Xcode, go to File > Add Package Dependencies
    • Enter the repository URL: https://github.com/ably/ably-chat-swift
    • Select the latest version and add it to your target

Step 1: Setting up Ably

Replace the contents of your ContentView.swift file with the following code to set up the Ably client, this is using a demo API key, if you wish to use this application with the CLI, change the API key with your own key:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

import Ably
import AblyChat
import SwiftUI

struct ContentView: View {
    // Can be replaced with your own room name
    private let roomName = "my-first-room"

    @State private var chatClient: ChatClient

    init() {
        let realtimeOptions = ARTClientOptions()
        realtimeOptions.key = "demokey:*****" // In production, use token authentication
        realtimeOptions.clientId = "my-first-client"
        let realtime = ARTRealtime(options: realtimeOptions)

        let chatClient = DefaultChatClient(realtime: realtime, clientOptions: ChatClientOptions())
        self._chatClient = State(initialValue: chatClient)
    }

    var body: some View {
        VStack {
            Text("Hello Chat App")
                .font(.headline)
                .padding()
        }
    }
}

#Preview {
    ContentView()
}
API key:
DEMO ONLY

Step 2: Connect to Ably

Clients establish a connection with Ably when they instantiate an SDK. This enables them to send and receive messages in realtime across channels.

In your ContentView.swift file, add a connection status display using callbacks:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

struct ContentView: View {
    private let roomName = "my-first-room"

    @State private var chatClient: ChatClient
    @State private var connectionStatus = ""

    init() {
        let realtimeOptions = ARTClientOptions()
        realtimeOptions.key = "demokey:*****"
        realtimeOptions.clientId = "my-first-client"
        let realtime = ARTRealtime(options: realtimeOptions)

        let chatClient = DefaultChatClient(realtime: realtime, clientOptions: ChatClientOptions())
        self._chatClient = State(initialValue: chatClient)
    }

    var body: some View {
        VStack {
            Text("Connection Status: \(connectionStatus)")
                .font(.caption)
                .padding()

            Text("Hello Chat App")
                .font(.headline)
                .padding()
        }
        .task {
            monitorConnectionStatus()
        }
    }

    private func monitorConnectionStatus() {
        chatClient.connection.onStatusChange { status in
            connectionStatus = "\(status.current)"
        }
    }
}
API key:
DEMO ONLY

Run your application and you should see the connection status displayed.

Step 3: Create a room

Now that you have a connection to Ably, you can create a room. Use rooms to separate and organize clients and messages into different topics, or 'chat rooms'. Rooms are the entry point for Chat, providing access to all of its features, such as messages, presence and reactions.

In your project, open ContentView.swift, and add room status monitoring using callbacks:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

struct ContentView: View {
    private let roomName = "my-first-room"

    @State private var chatClient: ChatClient
    @State private var connectionStatus = ""
    @State private var roomStatus = ""
    @State private var room: Room?

    init() {
        let realtimeOptions = ARTClientOptions()
        realtimeOptions.key = "demokey:*****"
        realtimeOptions.clientId = "my-first-client"
        let realtime = ARTRealtime(options: realtimeOptions)

        let chatClient = DefaultChatClient(realtime: realtime, clientOptions: ChatClientOptions())
        self._chatClient = State(initialValue: chatClient)
    }

    var body: some View {
        VStack {
            Text("Connection Status: \(connectionStatus)")
                .font(.caption)
                .padding(.horizontal)

            Text("Room: \(roomName), Status: \(roomStatus)")
                .font(.caption)
                .padding(.horizontal)

            Text("Hello Chat App")
                .font(.headline)
                .padding()
        }
        .task {
            await setupRoom()
        }
    }

    private func setupRoom() async {
        do {
            let chatRoom = try await chatClient.rooms.get(name: roomName)
            try await chatRoom.attach()
            self.room = chatRoom

            monitorConnectionStatus()
            monitorRoomStatus(room: chatRoom)
        } catch {
            print("Failed to setup room: \(error)")
        }
    }

    private func monitorConnectionStatus() {
        chatClient.connection.onStatusChange { status in
            connectionStatus = "\(status.current)"
        }
    }

    private func monitorRoomStatus(room: Room) {
        room.onStatusChange { status in
            roomStatus = "\(status.current)"
        }
    }
}
API key:
DEMO ONLY

The above code creates a room with the name my-first-room and sets up listeners to monitor both connection and room status. It displays the room name and current status in the UI.

Step 4: Send a message

Messages are how your clients interact with one another.

In your project, open ContentView.swift, and add message functionality using callbacks:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

struct ContentView: View {
    private let roomName = "my-first-room"

    @State private var chatClient: ChatClient
    @State private var connectionStatus = ""
    @State private var roomStatus = ""
    @State private var room: Room?
    @State private var messages: [Message] = []
    @State private var newMessage = ""

    init() {
        let realtimeOptions = ARTClientOptions()
        realtimeOptions.key = "demokey:*****"
        realtimeOptions.clientId = "my-first-client"
        let realtime = ARTRealtime(options: realtimeOptions)

        let chatClient = DefaultChatClient(realtime: realtime, clientOptions: ChatClientOptions())
        self._chatClient = State(initialValue: chatClient)
    }

    var body: some View {
        VStack {
            // Status bar
            VStack {
                Text("Connection: \(connectionStatus)")
                Text("Room: \(roomName), Status: \(roomStatus)")
            }
            .font(.caption)
            .padding(.horizontal)

            // Messages list
            List(messages.reversed(), id: \.id) { message in
                VStack(alignment: .leading, spacing: 4) {
                    HStack {
                        Text("\(message.clientID): \(message.text)")
                            .font(.body)
                        Spacer()
                    }
                    Text(formatTimestamp(message.timestamp))
                        .font(.caption)
                        .foregroundColor(.gray)
                }
                .padding(.vertical, 2)
            }
            .listStyle(.plain)

            // Message input
            HStack {
                TextField("Type a message...", text: $newMessage)
                    .textFieldStyle(RoundedBorderTextFieldStyle())

                Button("Send") {
                    Task {
                        await sendMessage()
                    }
                }
                .disabled(newMessage.isEmpty)
            }
            .padding()
        }
        .task {
            await setupRoom()
        }
    }

    private func setupRoom() async {
        do {
            let chatRoom = try await chatClient.rooms.get(name: roomName)
            try await chatRoom.attach()
            self.room = chatRoom

            setupMessages(room: chatRoom)
            monitorConnectionStatus()
            monitorRoomStatus(room: chatRoom)
        } catch {
            print("Failed to setup room: \(error)")
        }
    }

    private func setupMessages(room: Room) {
        room.messages.subscribe { event in
            withAnimation {
                switch event.type {
                case .created:
                    messages.append(event.message)
                case .updated:
                    if let index = messages.firstIndex(where: { $0.id == event.message.id }) {
                        messages[index] = event.message
                    }
                case .deleted:
                    if let index = messages.firstIndex(where: { $0.id == event.message.id }) {
                        messages[index] = event.message
                    }
                }
            }
        }
    }

    private func sendMessage() async {
        guard !newMessage.isEmpty, let room = room else { return }

        do {
            _ = try await room.messages.send(params: .init(text: newMessage))
            newMessage = ""
        } catch {
            print("Failed to send message: \(error)")
        }
    }

    private func monitorConnectionStatus() {
        chatClient.connection.onStatusChange { status in
            connectionStatus = "\(status.current)"
        }
    }

    private func monitorRoomStatus(room: Room) {
        room.onStatusChange { status in
            roomStatus = "\(status.current)"
        }
    }

    private func formatTimestamp(_ timestamp: Date?) -> String {
        let formatter = DateFormatter()
        formatter.timeStyle = .medium
        guard let timestamp else { return "" }
        return formatter.string(from: timestamp)
    }
}
API key:
DEMO ONLY

The UI should automatically render the new component, and you should be able to send messages to the room.

Type a message in the input box and click the send button. You'll see the message appear in the chat list.

You can also use the Ably CLI to send a message to the room from another environment:

ably rooms messages send my-first-room 'Hello from CLI!'

You'll see the message in your app's chat list. If you have sent a message via CLI, it should appear from a different client ID than the one you sent from the app.

Step 5: Edit a message

If your client makes a typo, or needs to update their original message then they can edit it. To do this, you can extend the functionality to allow updating of messages.

Update your ContentView.swift to add message editing capability:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

struct ContentView: View {
    private let roomName = "my-first-room"

    @State private var chatClient: ChatClient
    @State private var connectionStatus = ""
    @State private var roomStatus = ""
    @State private var room: Room?
    @State private var messages: [Message] = []
    @State private var newMessage = ""
    @State private var editingMessage: Message?

    init() {
        let realtimeOptions = ARTClientOptions()
        realtimeOptions.key = "demokey:*****"
        realtimeOptions.clientId = "my-first-client"
        let realtime = ARTRealtime(options: realtimeOptions)

        let chatClient = DefaultChatClient(realtime: realtime, clientOptions: ChatClientOptions())
        self._chatClient = State(initialValue: chatClient)
    }

    private var sendButtonTitle: String {
        editingMessage != nil ? "Update" : "Send"
    }

    var body: some View {
        VStack {
            // Status bar
            VStack {
                Text("Connection: \(connectionStatus)")
                Text("Room: \(roomName), Status: \(roomStatus)")
            }
            .font(.caption)
            .padding(.horizontal)

            // Messages list
            List(messages.reversed(), id: \.id) { message in
                MessageRowView(
                    message: message,
                    isEditing: editingMessage?.id == message.id,
                    onEdit: {
                        editingMessage = message
                        newMessage = message.text
                    }
                )
                .buttonStyle(.plain)
            }
            .listStyle(.plain)

            // Message input
            HStack {
                TextField("Type a message...", text: $newMessage)
                    .textFieldStyle(RoundedBorderTextFieldStyle())

                Button(sendButtonTitle) {
                    Task {
                        if editingMessage != nil {
                            await updateMessage()
                        } else {
                            await sendMessage()
                        }
                    }
                }
                .disabled(newMessage.isEmpty)

                if editingMessage != nil {
                    Button("Cancel") {
                        editingMessage = nil
                        newMessage = ""
                    }
                    .foregroundColor(.red)
                }
            }
            .padding()
        }
        .task {
            await setupRoom()
        }
    }

    private func sendMessage() async {
        guard !newMessage.isEmpty, let room = room else { return }

        do {
            _ = try await room.messages.send(params: .init(text: newMessage))
            newMessage = ""
        } catch {
            print("Failed to send message: \(error)")
        }
    }

    private func updateMessage() async {
        guard !newMessage.isEmpty, let editingMessage = editingMessage, let room = room else { return }

        do {
            let updatedMessage = editingMessage.copy(text: newMessage)
            _ = try await room.messages.update(newMessage: updatedMessage, description: nil, metadata: nil)
            self.editingMessage = nil
            newMessage = ""
        } catch {
            print("Failed to update message: \(error)")
        }
    }

    // ... rest of the previous functions remain the same ...
}

struct MessageRowView: View {
    let message: Message
    let isEditing: Bool
    let onEdit: () -> Void

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            HStack {
                VStack(alignment: .leading) {
                    Text("\(message.clientID): \(message.text)")
                        .font(.body)
                        .background(isEditing ? Color.blue.opacity(0.1) : Color.clear)
                    Text(formatTimestamp(message.timestamp))
                        .font(.caption)
                        .foregroundColor(.gray)
                }
                Spacer()
                Button("Edit") {
                    onEdit()
                }
                .font(.caption)
                .foregroundColor(.blue)
            }
        }
        .padding(.vertical, 2)
    }

    private func formatTimestamp(_ timestamp: Date?) -> String {
        let formatter = DateFormatter()
        formatter.timeStyle = .medium
        guard let timestamp else { return "" }
        return formatter.string(from: timestamp)
    }
}
API key:
DEMO ONLY

When you tap the "Edit" button next to a message, you can modify the text and it will send the updated message contents to the room.

Step 6: Message history and continuity

Ably Chat enables you to retrieve previously sent messages in a room. This is useful for providing conversational context when a user first joins a room, or when they subsequently rejoin it later on. The message subscription provides historyBeforeSubscribe() to enable this functionality. This method returns a paginated response, which can be queried further to retrieve the next set of messages.

Update the setupMessages function in your ContentView.swift to retrieve previous messages:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

private func setupMessages(room: Room) {
    let subscription = room.messages.subscribe { event in
        withAnimation {
            switch event.type {
            case .created:
                // Only add if not already in the list
                if !messages.contains(where: { $0.id == event.message.id }) {
                    messages.append(event.message)
                }
            case .updated:
                if let index = messages.firstIndex(where: { $0.id == event.message.id }) {
                    messages[index] = event.message
                }
            case .deleted:
                if let index = messages.firstIndex(where: { $0.id == event.message.id }) {
                    messages[index] = event.message
                }
            }
        }
    }

    // Get previous messages after subscribing
    Task {
        do {
            let previousMessages = try await subscription.historyBeforeSubscribe(.init())
            await MainActor.run {
                messages.append(contentsOf: previousMessages.items)
            }
        } catch {
            print("Failed to get previous messages: \(error)")
        }
    }
}

The above code will retrieve previous messages when the component loads, and set them in the state.

Do the following to test this out:

  1. Use the Ably CLI to simulate sending some messages to the room from another client.
  2. Restart the app, this will cause the message history to load.
  3. You'll see the previous messages appear in the chat list.

Step 7: Display who is present in the room

Display the online status of clients using the presence feature. This enables clients to be aware of one another if they are present in the same room. You can then show clients who else is online, provide a custom status update for each, and notify the room when someone enters it, or leaves it, such as by going offline.

Update your ContentView.swift to add presence functionality using callbacks:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

struct ContentView: View {
    private let roomName = "my-first-room"

    @State private var chatClient: ChatClient
    @State private var connectionStatus = ""
    @State private var roomStatus = ""
    @State private var room: Room?
    @State private var messages: [Message] = []
    @State private var newMessage = ""
    @State private var editingMessage: Message?
    @State private var presenceMembers: [String] = []

    // ... initialization code remains the same ...

    var body: some View {
        VStack {
            // Status bar
            VStack {
                Text("Connection: \(connectionStatus)")
                Text("Room: \(roomName), Status: \(roomStatus)")
                Text("Online: \(presenceMembers.count)")
            }
            .font(.caption)
            .padding(.horizontal)

            // ... rest of the UI remains the same ...
        }
        .task {
            await setupRoom()
        }
    }

    private func setupRoom() async {
        do {
            let chatRoom = try await chatClient.rooms.get(name: roomName)
            try await chatRoom.attach()
            self.room = chatRoom

            // Enter presence with data
            try await chatRoom.presence.enter(data: ["status": "📱 Online"])

            setupMessages(room: chatRoom)
            setupPresence(room: chatRoom)
            monitorConnectionStatus()
            monitorRoomStatus(room: chatRoom)
        } catch {
            print("Failed to setup room: \(error)")
        }
    }

    private func setupPresence(room: Room) {
        // Listen for presence events
        room.presence.subscribe(events: [.enter, .leave, .update]) { event in
            Task {
                await updatePresenceMembers()
            }
        }

        // Update initial presence
        Task {
            await updatePresenceMembers()
        }
    }

    private func updatePresenceMembers() async {
        guard let room = room else { return }

        do {
            let members = try await room.presence.get()
            await MainActor.run {
                presenceMembers = members.compactMap { $0.clientID }
            }
        } catch {
            print("Failed to get presence members: \(error)")
        }
    }

    // ... rest of the functions remain the same ...
}

You'll now see your current client ID in the count of present users.

You can also use the Ably CLI to enter the room from another client by running the following command:

ably rooms presence enter my-first-room --client-id "my-cli"

Step 8: Send a reaction

Clients can send a reaction to a room to show their sentiment for what is happening, such as a point being scored in a sports game. These are short-lived (ephemeral) and are not stored in the room history.

Update your ContentView.swift to add reaction functionality using callbacks:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

struct ContentView: View {
    private let roomName = "my-first-room"

    @State private var chatClient: ChatClient
    @State private var connectionStatus = ""
    @State private var roomStatus = ""
    @State private var room: Room?
    @State private var messages: [Message] = []
    @State private var newMessage = ""
    @State private var editingMessage: Message?
    @State private var presenceMembers: [String] = []
    @State private var recentReactions: [String] = []

    // ... initialization code remains the same ...

    var body: some View {
        VStack {
            // Status bar
            VStack {
                Text("Connection: \(connectionStatus)")
                Text("Room: \(roomName), Status: \(roomStatus)")
                Text("Online: \(presenceMembers.count)")
            }
            .font(.caption)
            .padding(.horizontal)

            // Reactions bar
            HStack {
                Text("Reactions:")
                    .font(.caption)

                ForEach(["👍", "❤️", "💥", "🚀", "👎"], id: \.self) { emoji in
                    Button(emoji) {
                        Task {
                            await sendReaction(name: emoji)
                        }
                    }
                    .font(.title2)
                }

                Spacer()
            }
            .padding(.horizontal)

            // Recent reactions display
            if !recentReactions.isEmpty {
                HStack {
                    Text("Recent:")
                        .font(.caption)
                    ForEach(recentReactions.suffix(5), id: \.self) { reaction in
                        Text(reaction)
                            .font(.title3)
                    }
                    Spacer()
                }
                .padding(.horizontal)
            }

            // Messages list
            List(messages.reversed(), id: \.id) { message in
                MessageRowView(
                    message: message,
                    isEditing: editingMessage?.id == message.id,
                    onEdit: {
                        editingMessage = message
                        newMessage = message.text
                    }
                )
                .buttonStyle(.plain)
            }
            .listStyle(.plain)

            // Message input
            HStack {
                TextField("Type a message...", text: $newMessage)
                    .textFieldStyle(RoundedBorderTextFieldStyle())

                Button(sendButtonTitle) {
                    Task {
                        if editingMessage != nil {
                            await updateMessage()
                        } else {
                            await sendMessage()
                        }
                    }
                }
                .disabled(newMessage.isEmpty)

                if editingMessage != nil {
                    Button("Cancel") {
                        editingMessage = nil
                        newMessage = ""
                    }
                    .foregroundColor(.red)
                }
            }
            .padding()
        }
        .task {
            await setupRoom()
        }
    }

    private func setupRoom() async {
        do {
            let chatRoom = try await chatClient.rooms.get(name: roomName)
            try await chatRoom.attach()
            self.room = chatRoom

            // Enter presence with data
            try await chatRoom.presence.enter(data: ["status": "📱 Online"])

            setupMessages(room: chatRoom)
            setupPresence(room: chatRoom)
            setupReactions(room: chatRoom)
            monitorConnectionStatus()
            monitorRoomStatus(room: chatRoom)
        } catch {
            print("Failed to setup room: \(error)")
        }
    }

    private func setupReactions(room: Room) {
        room.reactions.subscribe { event in
            withAnimation {
                recentReactions.append(event.reaction.name)
                // Keep only last 10 reactions
                if recentReactions.count > 10 {
                    recentReactions.removeFirst()
                }
            }
        }
    }

    private func sendReaction(name: String) async {
        guard let room = room else { return }

        do {
            try await room.reactions.send(params: .init(name: name))
        } catch {
            print("Failed to send reaction: \(error)")
        }
    }

    // ... rest of the functions remain the same ...
}

The above code displays a list of reactions that can be sent to the room. When you tap on a reaction, it will send it to the room and display it in the recent reactions area.

You can also send a reaction to the room via the Ably CLI by running the following command:

ably rooms reactions send my-first-room 👍

Step 9: Show typing indicators

Typing indicators let other users know when someone is actively composing a message. This creates a more responsive and interactive chat experience.

Update your ContentView.swift to add typing indicators using callbacks:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

struct ContentView: View {
    private let roomName = "my-first-room"

    @State private var chatClient: ChatClient
    @State private var connectionStatus = ""
    @State private var roomStatus = ""
    @State private var room: Room?
    @State private var messages: [Message] = []
    @State private var newMessage = ""
    @State private var editingMessage: Message?
    @State private var presenceMembers: [String] = []
    @State private var recentReactions: [String] = []
    @State private var typingUsers: [String] = []

    // ... initialization and other code remains the same ...

    var body: some View {
        VStack {
            // Status bar remains the same...

            // Reactions bar remains the same...

            // Recent reactions display remains the same...

            // Messages list remains the same...

            // Typing indicator
            if !typingUsers.isEmpty {
                HStack {
                    Text("Typing: \(typingUsers.joined(separator: ", "))...")
                        .font(.caption)
                        .foregroundColor(.gray)
                    Spacer()
                }
                .padding(.horizontal)
            }

            // Message input
            HStack {
                TextField("Type a message...", text: $newMessage)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .onChange(of: newMessage) { _, newValue in
                        Task {
                            await handleTyping(text: newValue)
                        }
                    }

                Button(sendButtonTitle) {
                    Task {
                        if editingMessage != nil {
                            await updateMessage()
                        } else {
                            await sendMessage()
                        }
                    }
                }
                .disabled(newMessage.isEmpty)

                if editingMessage != nil {
                    Button("Cancel") {
                        editingMessage = nil
                        newMessage = ""
                    }
                    .foregroundColor(.red)
                }
            }
            .padding()
        }
        .task {
            await setupRoom()
        }
    }

    private func setupRoom() async {
        do {
            let chatRoom = try await chatClient.rooms.get(name: roomName)
            try await chatRoom.attach()
            self.room = chatRoom

            // Enter presence with data
            try await chatRoom.presence.enter(data: ["status": "📱 Online"])

            setupMessages(room: chatRoom)
            setupPresence(room: chatRoom)
            setupReactions(room: chatRoom)
            setupTyping(room: chatRoom)
            monitorConnectionStatus()
            monitorRoomStatus(room: chatRoom)
        } catch {
            print("Failed to setup room: \(error)")
        }
    }

    private func setupTyping(room: Room) {
        room.typing.subscribe { typing in
            withAnimation {
                typingUsers = typing.currentlyTyping.filter { $0 != chatClient.clientID }
            }
        }
    }

    private func handleTyping(text: String) async {
        guard let room = room else { return }

        do {
            if text.isEmpty {
                try await room.typing.stop()
            } else {
                try await room.typing.keystroke()
            }
        } catch {
            print("Failed to handle typing: \(error)")
        }
    }

    // ... rest of the functions remain the same ...
}

Now when you type in the message field, other users will see a typing indicator. The indicator automatically stops when you stop typing or send a message.

Step 10: Display occupancy information

Occupancy shows how many connections and presence members are currently in the room. This helps users understand the activity level of the room.

Update your ContentView.swift to add occupancy information using callbacks:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

struct ContentView: View {
    private let roomName = "my-first-room"

    @State private var chatClient: ChatClient
    @State private var connectionStatus = ""
    @State private var roomStatus = ""
    @State private var room: Room?
    @State private var messages: [Message] = []
    @State private var newMessage = ""
    @State private var editingMessage: Message?
    @State private var presenceMembers: [String] = []
    @State private var recentReactions: [String] = []
    @State private var typingUsers: [String] = []
    @State private var occupancyInfo = "Connections: 0"

    // ... initialization code remains the same ...

    var body: some View {
        VStack {
            // Status bar
            VStack {
                Text("Connection: \(connectionStatus)")
                Text("Room: \(roomName), Status: \(roomStatus)")
                Text("Online: \(presenceMembers.count)")
                Text(occupancyInfo)
            }
            .font(.caption)
            .padding(.horizontal)

            // ... rest of the UI remains the same ...
        }
        .task {
            await setupRoom()
        }
    }

    private func setupRoom() async {
        do {
            let chatRoom = try await chatClient.rooms.get(name: roomName, options: .init(occupancy: .init(enableEvents: true)))
            try await chatRoom.attach()
            self.room = chatRoom

            // Enter presence with data
            try await chatRoom.presence.enter(data: ["status": "📱 Online"])

            setupMessages(room: chatRoom)
            setupPresence(room: chatRoom)
            setupReactions(room: chatRoom)
            setupTyping(room: chatRoom)
            await setupOccupancy(room: chatRoom)
            monitorConnectionStatus()
            monitorRoomStatus(room: chatRoom)
        } catch {
            print("Failed to setup room: \(error)")
        }
    }

    private func setupOccupancy(room: Room) async {
        do {
            // Get initial occupancy
            let currentOccupancy = try await room.occupancy.get()
            await MainActor.run {
                occupancyInfo = "Connections: \(currentOccupancy.connections)"
            }

            // Listen for occupancy changes using callback
            room.occupancy.subscribe { event in
                withAnimation {
                    occupancyInfo = "Connections: \(event.occupancy.connections)"
                }
            }
        } catch {
            print("Failed to setup occupancy: \(error)")
        }
    }

    // ... rest of the functions remain the same ...
}

The occupancy information will now show both the number of presence members and total connections in the room.

Step 11: Delete messages

Users may want to delete messages they've sent, for example if they contain errors or inappropriate content.

Update your MessageRowView and add delete functionality:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

struct MessageRowView: View {
    let message: Message
    let isEditing: Bool
    let onEdit: () -> Void
    let onDelete: () -> Void

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            if message.action == .delete {
                HStack {
                    Text("Message deleted")
                        .font(.body)
                        .italic()
                        .foregroundColor(.gray)
                    Spacer()
                }
            } else {
                HStack {
                    VStack(alignment: .leading) {
                        Text("\(message.clientID): \(message.text)")
                            .font(.body)
                            .background(isEditing ? Color.blue.opacity(0.1) : Color.clear)
                        Text(formatTimestamp(message.timestamp))
                            .font(.caption)
                            .foregroundColor(.gray)
                    }
                    Spacer()

                    HStack {
                        Button("Edit") {
                            onEdit()
                        }
                        .font(.caption)
                        .foregroundColor(.blue)
                        .frame(minHeight: 30)

                        Button("Delete") {
                            onDelete()
                        }
                        .font(.caption)
                        .foregroundColor(.red)
                        .frame(minHeight: 30)
                    }
                }
            }
        }
        .padding(.vertical, 2)
    }

    private func formatTimestamp(_ timestamp: Date?) -> String {
        let formatter = DateFormatter()
        formatter.timeStyle = .medium
        guard let timestamp else { return "" }
        return formatter.string(from: timestamp)
    }
}

Update your main ContentView to handle message deletion:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

// In the messages list section, update the MessageRowView call:
List(messages.reversed(), id: \.id) { message in
    MessageRowView(
        message: message,
        isEditing: editingMessage?.id == message.id,
        onEdit: {
            editingMessage = message
            newMessage = message.text
        },
        onDelete: {
            Task {
                await deleteMessage(message)
            }
        }
    )
    .buttonStyle(.plain)
}
.listStyle(.plain)

// Add this function to ContentView:
private func deleteMessage(_ message: Message) async {
    guard let room = room else { return }

    do {
        _ = try await room.messages.delete(message: message, params: .init())
    } catch {
        print("Failed to delete message: \(error)")
    }
}

Users can now delete their messages by tapping the "Delete" button. Deleted messages will appear as "Message deleted" in the chat.

Step 12: Disconnection and clean up resources

To gracefully close connections and clean up resources, you should handle the app lifecycle appropriately. In SwiftUI, you can use scene phase modifiers to detect when the app enters the background.

Update your ContentView.swift to handle app lifecycle:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

import Ably
import AblyChat
import SwiftUI

struct ContentView: View {
    // ... all state variables remain the same ...

    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        VStack {
            // ... all UI code remains the same ...
        }
        .task {
            await setupRoom()
        }
        .onChange(of: scenePhase) { _, newPhase in
            Task {
                await handleScenePhaseChange(newPhase)
            }
        }
    }

    private func handleScenePhaseChange(_ phase: ScenePhase) async {
        switch phase {
        case .background:
            // Disconnect when app goes to background
            chatClient.realtime.connection.close()
        case .active:
            // Reconnect when app becomes active
            chatClient.realtime.connection.connect()
        case .inactive:
            // Handle inactive state if needed
            break
        @unknown default:
            break
        }
    }

    // ... all other functions remain the same ...
}

You might also want to add a proper cleanup when the view disappears:

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

var body: some View {
    VStack {
        // ... UI code ...
    }
    .task {
        await setupRoom()
    }
    .onChange(of: scenePhase) { _, newPhase in
        Task {
            await handleScenePhaseChange(newPhase)
        }
    }
    .onDisappear {
        // Clean up when view disappears
        Task {
            if let room = room {
                try? await room.presence.leave()
                try? await room.detach()
            }
        }
    }
}

Next steps

Continue exploring Ably Chat with Swift:

Read more about the concepts covered in this guide:

Explore the Ably CLI further, or check out the Chat Swift API references for additional functionality.

Select...