Skip to content

Commit 00fa1dc

Browse files
authored
Update Readme (#2)
1 parent 8524f82 commit 00fa1dc

File tree

1 file changed

+178
-64
lines changed

1 file changed

+178
-64
lines changed

README.md

Lines changed: 178 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
# swiftui-messaging-ui
22

3-
A SwiftUI component for building messaging interfaces with support for loading older messages and automatic scroll management.
3+
A high-performance SwiftUI list component built on UICollectionView, designed for messaging interfaces and infinite scrolling lists.
44

55
## Features
66

7-
- **Message List**: Generic, scrollable message list component
8-
- **Automatic Scroll Management**: Handles scroll position when loading older messages
9-
- **Auto Scroll to Bottom**: Optional automatic scrolling to bottom for new messages
10-
- **Loading State Management**: Internal loading state management with async/await support
7+
- **TiledView**: High-performance list view using UICollectionView with custom layout
8+
- **ListDataSource**: Change-tracking data source with efficient diff detection
9+
- **CellState**: Type-safe per-cell state management
10+
- **Scroll Position Control**: Programmatic scrolling with SwiftUI-like API
11+
- **Prepend Support**: Maintains scroll position when loading older content
12+
- **Self-Sizing Cells**: Automatic cell height calculation
1113

1214
## Requirements
1315

@@ -41,100 +43,212 @@ Or add it through Xcode:
4143
import MessagingUI
4244
import SwiftUI
4345

44-
struct Message: Identifiable {
45-
let id: UUID
46-
let text: String
46+
struct Message: Identifiable, Equatable {
47+
let id: Int
48+
var text: String
4749
}
4850

4951
struct ChatView: View {
50-
@State private var messages: [Message] = []
52+
@State private var dataSource = ListDataSource<Message>()
53+
@State private var scrollPosition = TiledScrollPosition()
5154

5255
var body: some View {
53-
MessageList(
54-
messages: messages,
55-
onLoadOlderMessages: {
56-
// Load older messages asynchronously
57-
let olderMessages = await loadOlderMessages()
58-
messages.insert(contentsOf: olderMessages, at: 0)
56+
TiledView(
57+
dataSource: dataSource,
58+
scrollPosition: $scrollPosition,
59+
cellBuilder: { message, _ in
60+
MessageBubble(message: message)
5961
}
60-
) { message in
61-
Text(message.text)
62-
.padding()
63-
.background(Color.blue.opacity(0.1))
64-
.cornerRadius(8)
62+
)
63+
.onAppear {
64+
dataSource.setItems(initialMessages)
6565
}
6666
}
67+
}
68+
```
6769

68-
func loadOlderMessages() async -> [Message] {
69-
// Your loading logic here
70-
[]
70+
### Loading Older Messages (Prepend)
71+
72+
```swift
73+
TiledView(
74+
dataSource: dataSource,
75+
scrollPosition: $scrollPosition,
76+
onPrepend: {
77+
// Called when user scrolls near the top
78+
let olderMessages = await loadOlderMessages()
79+
dataSource.prepend(olderMessages)
80+
},
81+
cellBuilder: { message, _ in
82+
MessageBubble(message: message)
7183
}
84+
)
85+
```
86+
87+
### Programmatic Scrolling
88+
89+
```swift
90+
@State private var scrollPosition = TiledScrollPosition()
91+
92+
// Scroll to bottom
93+
Button("Scroll to Bottom") {
94+
scrollPosition.scrollTo(edge: .bottom)
95+
}
96+
97+
// Scroll to top
98+
Button("Scroll to Top") {
99+
scrollPosition.scrollTo(edge: .top, animated: false)
72100
}
73101
```
74102

75-
### With Auto Scroll to Bottom
103+
### Using CellState
104+
105+
CellState allows you to manage per-cell UI state (like expansion, selection) separately from your data model.
76106

77107
```swift
78-
MessageList(
79-
messages: messages,
80-
autoScrollToBottom: $autoScrollToBottom,
81-
onLoadOlderMessages: {
82-
await loadOlderMessages()
108+
// 1. Define a state key
109+
enum IsExpandedKey: CustomStateKey {
110+
typealias Value = Bool
111+
static var defaultValue: Bool { false }
112+
}
113+
114+
// 2. Add convenience accessor
115+
extension CellState {
116+
var isExpanded: Bool {
117+
get { self[IsExpandedKey.self] }
118+
set { self[IsExpandedKey.self] = newValue }
83119
}
84-
) { message in
85-
MessageView(message: message)
86120
}
121+
122+
// 3. Use in cell builder
123+
TiledView(
124+
dataSource: dataSource,
125+
scrollPosition: $scrollPosition,
126+
cellBuilder: { message, state in
127+
MessageBubble(
128+
message: message,
129+
isExpanded: state.isExpanded
130+
)
131+
}
132+
)
87133
```
88134

89-
## API
135+
### ListDataSource Operations
136+
137+
```swift
138+
var dataSource = ListDataSource<Message>()
90139

91-
### MessageList
140+
// Initial load
141+
dataSource.setItems(messages)
92142

93-
A generic message list component that displays messages using a custom view builder.
143+
// Add to beginning (older messages)
144+
dataSource.prepend(olderMessages)
145+
146+
// Add to end (new messages)
147+
dataSource.append(newMessages)
148+
149+
// Insert at specific position
150+
dataSource.insert(messages, at: 5)
151+
152+
// Update existing items
153+
dataSource.update([updatedMessage])
154+
155+
// Remove items
156+
dataSource.remove(id: messageId)
157+
dataSource.remove(ids: [id1, id2, id3])
158+
159+
// Auto-detect changes from new array
160+
dataSource.applyDiff(from: newMessagesArray)
161+
```
94162

95-
#### Initializers
163+
## API Reference
164+
165+
### TiledView
166+
167+
A SwiftUI view that wraps UICollectionView for high-performance list rendering.
96168

97169
```swift
98-
// Simple message list without older message loading
99-
init(
100-
messages: [Message],
101-
@ViewBuilder content: @escaping (Message) -> Content
102-
)
170+
public struct TiledView<Item: Identifiable & Equatable, Cell: View>: UIViewRepresentable {
171+
public init(
172+
dataSource: ListDataSource<Item>,
173+
scrollPosition: Binding<TiledScrollPosition>,
174+
cellStates: [Item.ID: CellState]? = nil,
175+
onPrepend: (@MainActor () async throws -> Void)? = nil,
176+
@ViewBuilder cellBuilder: @escaping (Item, CellState) -> Cell
177+
)
178+
}
179+
```
103180

104-
// Message list with older message loading support
105-
init(
106-
messages: [Message],
107-
autoScrollToBottom: Binding<Bool>? = nil,
108-
onLoadOlderMessages: @escaping @MainActor () async -> Void,
109-
@ViewBuilder content: @escaping (Message) -> Content
110-
)
181+
### ListDataSource
182+
183+
A change-tracking data source that enables efficient list updates.
184+
185+
```swift
186+
public struct ListDataSource<Item: Identifiable & Equatable> {
187+
public var items: Deque<Item> { get }
188+
public var changeCounter: Int { get }
189+
190+
public mutating func setItems(_ items: [Item])
191+
public mutating func prepend(_ items: [Item])
192+
public mutating func append(_ items: [Item])
193+
public mutating func insert(_ items: [Item], at index: Int)
194+
public mutating func update(_ items: [Item])
195+
public mutating func remove(id: Item.ID)
196+
public mutating func remove(ids: [Item.ID])
197+
public mutating func applyDiff(from newItems: [Item])
198+
}
111199
```
112200

113-
#### Parameters
201+
### TiledScrollPosition
114202

115-
- `messages`: Array of messages to display. Must conform to `Identifiable`.
116-
- `autoScrollToBottom`: Optional binding that controls automatic scrolling to bottom when new messages are added.
117-
- `onLoadOlderMessages`: Async closure called when user scrolls up to trigger loading older messages.
118-
- `content`: A view builder that creates the view for each message.
203+
A struct for programmatic scroll control, similar to SwiftUI's `ScrollPosition`.
119204

120-
## How It Works
205+
```swift
206+
public struct TiledScrollPosition {
207+
public enum Edge {
208+
case top
209+
case bottom
210+
}
121211

122-
### Scroll Position Management
212+
public mutating func scrollTo(edge: Edge, animated: Bool = true)
213+
}
214+
```
215+
216+
### CellState
217+
218+
Type-safe per-cell state storage.
219+
220+
```swift
221+
public protocol CustomStateKey {
222+
associatedtype Value
223+
static var defaultValue: Value { get }
224+
}
225+
226+
public struct CellState {
227+
public static var empty: CellState { get }
228+
public subscript<T: CustomStateKey>(key: T.Type) -> T.Value { get set }
229+
}
230+
```
231+
232+
## How It Works
123233

124-
The component intelligently manages scroll position based on the current state:
234+
### Virtual Content Layout
125235

126-
1. **Loading Older Messages** (highest priority): When loading older messages, the scroll position is preserved by adjusting the content offset.
127-
2. **Auto Scroll to Bottom**: When enabled, automatically scrolls to the bottom when new messages are added.
128-
3. **Normal Operation**: No scroll adjustment, maintaining the user's current scroll position.
236+
TiledView uses a custom UICollectionViewLayout with virtual content height. This enables:
237+
- Efficient prepend operations without scroll jumps
238+
- Smooth bidirectional scrolling
239+
- Minimal memory footprint
129240

130-
### Loading State
241+
### Change Tracking
131242

132-
The loading state is managed internally using async/await. When the user scrolls up to the trigger point:
243+
ListDataSource tracks every mutation as a `Change` enum:
244+
- `.setItems` - Complete replacement
245+
- `.prepend([ID])` - Items added to beginning
246+
- `.append([ID])` - Items added to end
247+
- `.insert(at:, ids:)` - Items inserted at index
248+
- `.update([ID])` - Existing items modified
249+
- `.remove([ID])` - Items removed
133250

134-
1. The component sets the internal loading flag
135-
2. Calls your `onLoadOlderMessages` closure
136-
3. Automatically adjusts scroll position to maintain the user's viewing context
137-
4. Clears the loading flag after completion
251+
TiledView applies only new changes since its last update, ensuring optimal performance.
138252

139253
## License
140254

0 commit comments

Comments
 (0)