Skip to content

Commit a131892

Browse files
Merge pull request #448 from ably/fix-example-app-mac
Improve example app on macOS and tvOS
2 parents d7cc8ee + 062f167 commit a131892

File tree

3 files changed

+124
-86
lines changed

3 files changed

+124
-86
lines changed

Example/AblyChatExample/ContentView.swift

Lines changed: 97 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ private enum Environment: Equatable {
1010
/// - Parameters:
1111
/// - key: Your Ably API key.
1212
/// - clientId: A string that identifies this client.
13-
case live(key: String, clientId: String)
13+
case live(key: String, clientID: String)
1414

1515
@MainActor
1616
func createChatClient() -> any ChatClientProtocol {
@@ -19,10 +19,10 @@ private enum Environment: Equatable {
1919
return MockChatClient(
2020
clientOptions: ChatClientOptions(),
2121
)
22-
case let .live(key: key, clientId: clientId):
22+
case let .live(key: key, clientID: clientID):
2323
let realtimeOptions = ARTClientOptions()
2424
realtimeOptions.key = key
25-
realtimeOptions.clientId = clientId
25+
realtimeOptions.clientId = clientID
2626
let realtime = ARTRealtime(options: realtimeOptions)
2727

2828
return ChatClient(realtime: realtime, clientOptions: .init())
@@ -45,6 +45,7 @@ struct ContentView: View {
4545
@State private var chatClient = Environment.current.createChatClient()
4646
@State private var currentClientID: String?
4747

48+
@State private var isLoadingHistory = true
4849
@State private var reactions: [Reaction] = []
4950
@State private var newMessage = ""
5051
@State private var typingInfo = ""
@@ -114,81 +115,102 @@ struct ContentView: View {
114115
.font(.footnote)
115116
.frame(height: 12)
116117
.padding(.horizontal, 8)
117-
List(listItems, id: \.id) { item in
118-
switch item {
119-
case let .message(messageItem):
120-
if messageItem.message.action == .messageDelete {
121-
DeletedMessageView(item: messageItem)
122-
.flip()
123-
} else {
124-
MessageView(
125-
currentClientID: currentClientID,
126-
item: messageItem,
127-
isEditing: Binding(get: {
128-
editingItemID == messageItem.message.serial
129-
}, set: { editing in
130-
editingItemID = editing ? messageItem.message.serial : nil
131-
newMessage = editing ? messageItem.message.text : ""
132-
}),
133-
onDeleteMessage: {
134-
deleteMessage(messageItem.message)
135-
},
136-
onAddReaction: { reaction in
137-
addMessageReaction(reaction, messageSerial: messageItem.message.serial)
138-
},
139-
onDeleteReaction: { reaction in
140-
deleteMessageReaction(reaction, messageSerial: messageItem.message.serial)
141-
},
142-
).id(item.id)
143-
.flip()
118+
if isLoadingHistory {
119+
VStack(spacing: 16) {
120+
ProgressView()
121+
Text("Loading messages...")
122+
.foregroundStyle(.secondary)
123+
}
124+
.frame(maxWidth: .infinity, maxHeight: .infinity)
125+
} else {
126+
// Don't show the scroll view until we've loaded history, since the defaultScrollAnchor doesn't behave well when you insert a load of messages (i.e. it doesn't remain anchored at bottom).
127+
ScrollView {
128+
// The ideal here would be to use LazyVStack, but that seems to not interact very well with the defaultScrollAnchor; sometimes (e.g. presence message display) new content arrives in the scroll view and it doesn't scroll to the bottom.
129+
//
130+
// No doubt there are performance implications of not using LazyVStack, but we can deal with that some other time.
131+
VStack(alignment: .leading, spacing: 8) {
132+
ForEach(listItems, id: \.id) { item in
133+
Group {
134+
switch item {
135+
case let .message(messageItem):
136+
if messageItem.message.action == .messageDelete {
137+
DeletedMessageView(item: messageItem)
138+
} else {
139+
MessageView(
140+
currentClientID: currentClientID,
141+
item: messageItem,
142+
isEditing: Binding(get: {
143+
editingItemID == messageItem.message.serial
144+
}, set: { editing in
145+
editingItemID = editing ? messageItem.message.serial : nil
146+
newMessage = editing ? messageItem.message.text : ""
147+
}),
148+
onDeleteMessage: {
149+
deleteMessage(messageItem.message)
150+
},
151+
onAddReaction: { reaction in
152+
addMessageReaction(reaction, messageSerial: messageItem.message.serial)
153+
},
154+
onDeleteReaction: { reaction in
155+
deleteMessageReaction(reaction, messageSerial: messageItem.message.serial)
156+
},
157+
).id(item.id)
158+
}
159+
case let .presence(item):
160+
PresenceMessageView(item: item)
161+
}
162+
}
163+
.padding(.horizontal, 12)
164+
}
144165
}
145-
case let .presence(item):
146-
PresenceMessageView(item: item)
147-
.flip()
148166
}
167+
// Keep the scroll view scrolled to the bottom (unless the user manually scrolls away).
168+
.defaultScrollAnchor(.bottom)
149169
}
150-
.flip()
151-
.listStyle(PlainListStyle())
152-
HStack {
153-
TextField("Type a message...", text: $newMessage)
154-
.onChange(of: newMessage) {
155-
// this ensures that typing events are sent only when the message is actually changed whilst editing
156-
if let index = listItems.firstIndex(where: { $0.id == editingItemID }) {
157-
if case let .message(messageItem) = listItems[index] {
158-
if newMessage != messageItem.message.text {
159-
startTyping()
170+
#if !os(tvOS)
171+
HStack {
172+
TextField("Type a message...", text: $newMessage)
173+
.onChange(of: newMessage) {
174+
// this ensures that typing events are sent only when the message is actually changed whilst editing
175+
if let index = listItems.firstIndex(where: { $0.id == editingItemID }) {
176+
if case let .message(messageItem) = listItems[index] {
177+
if newMessage != messageItem.message.text {
178+
startTyping()
179+
}
160180
}
181+
} else {
182+
startTyping()
161183
}
162-
} else {
163-
startTyping()
164184
}
185+
// Send message when user presses Enter
186+
.onSubmit {
187+
sendButtonAction()
188+
}
189+
.textFieldStyle(.roundedBorder)
190+
Button(action: sendButtonAction) {
191+
#if os(iOS)
192+
Text(sendTitle)
193+
.foregroundColor(.white)
194+
.padding(.vertical, 6)
195+
.padding(.horizontal, 12)
196+
.background(Color.blue)
197+
.cornerRadius(15)
198+
#else
199+
Text(sendTitle)
200+
#endif
165201
}
166-
#if !os(tvOS)
167-
.textFieldStyle(.roundedBorder)
168-
#endif
169-
Button(action: sendButtonAction) {
170-
#if os(iOS)
171-
Text(sendTitle)
172-
.foregroundColor(.white)
173-
.padding(.vertical, 6)
174-
.padding(.horizontal, 12)
175-
.background(Color.blue)
176-
.cornerRadius(15)
177-
#else
178-
Text(sendTitle)
179-
#endif
180-
}
181-
if editingItemID != nil {
182-
Button("", systemImage: "xmark.circle.fill") {
183-
editingItemID = nil
184-
newMessage = ""
202+
if editingItemID != nil {
203+
Button("", systemImage: "xmark.circle.fill") {
204+
editingItemID = nil
205+
newMessage = ""
206+
}
207+
.foregroundStyle(.red.opacity(0.8))
208+
.transition(.scale.combined(with: .opacity))
185209
}
186-
.foregroundStyle(.red.opacity(0.8))
187-
.transition(.scale.combined(with: .opacity))
188210
}
189-
}
190-
.animation(.easeInOut, value: editingItemID)
191-
.padding(.horizontal, 12)
211+
.animation(.easeInOut, value: editingItemID)
212+
.padding(.horizontal, 12)
213+
#endif
192214
HStack {
193215
Text(typingInfo)
194216
.font(.footnote)
@@ -260,14 +282,13 @@ struct ContentView: View {
260282
switch event.type {
261283
case .created:
262284
withAnimation {
263-
listItems.insert(
285+
listItems.append(
264286
.message(
265287
.init(
266288
message: message,
267289
isSender: message.clientID == currentClientID,
268290
),
269291
),
270-
at: 0,
271292
)
272293
}
273294
case .updated, .deleted:
@@ -288,14 +309,15 @@ struct ContentView: View {
288309
}
289310
}
290311
}
312+
291313
let previousMessages = try await subscription.historyBeforeSubscribe(withParams: .init())
314+
defer { isLoadingHistory = false }
292315

316+
// previousMessages are in newest-to-oldest order
293317
for message in previousMessages.items {
294318
switch message.action {
295319
case .messageCreate, .messageUpdate, .messageDelete:
296-
withAnimation {
297-
listItems.append(.message(.init(message: message, isSender: message.clientID == currentClientID)))
298-
}
320+
listItems.insert(.message(.init(message: message, isSender: message.clientID == currentClientID)), at: 0)
299321
}
300322
}
301323
}
@@ -332,13 +354,12 @@ struct ContentView: View {
332354
func subscribeToPresence(room: any Room) {
333355
room.presence.subscribe { event in
334356
withAnimation {
335-
listItems.insert(
357+
listItems.append(
336358
.presence(
337359
.init(
338360
presence: event,
339361
),
340362
),
341-
at: 0,
342363
)
343364
}
344365
}
@@ -539,10 +560,3 @@ extension PresenceEventType {
539560
}
540561
}
541562
}
542-
543-
extension View {
544-
func flip() -> some View {
545-
rotationEffect(.radians(.pi))
546-
.scaleEffect(x: -1, y: 1, anchor: .center)
547-
}
548-
}

Example/AblyChatExample/MessageViews/MenuButtonView.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ struct MenuButtonView: View {
66

77
var body: some View {
88
Menu {
9-
Button(action: onEdit) {
10-
Label("Edit", systemImage: "pencil")
11-
}
9+
#if !os(tvOS)
10+
Button(action: onEdit) {
11+
Label("Edit", systemImage: "pencil")
12+
}
13+
#endif
1214

1315
Button(role: .destructive, action: onDelete) {
1416
Label("Delete", systemImage: "trash")

Example/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Ably Chat Swift SDK Example App
2+
3+
This is a simple app that demonstrates the following features:
4+
5+
- sending and receiving messages
6+
- editing and deleting messages
7+
- reacting to messages
8+
- sending room-level reactions
9+
- loading message history
10+
11+
To run the app, open the parent directory's `AblyChat.xcworkspace` workspace in Xcode and run the `AblyChatExample` target. If you wish to run it on an iOS or tvOS device, you’ll need to set up code signing.
12+
13+
By default, the example app uses a mock implementation of the Chat SDK. To switch to using the real SDK, change the `Environment.current` variable in `ContentView.swift` to `.live` and supply your Ably API key and a `clientID`.
14+
15+
In order to allow the app to use modern SwiftUI features, it supports the following OS versions:
16+
17+
- macOS 14 and above
18+
- iOS 17 and above
19+
- tvOS 17 and above
20+
21+
> [!NOTE]
22+
> On tvOS, the app currently does not allow text input (that is, sending or editing of messages).

0 commit comments

Comments
 (0)