Skip to content

Commit 29091e3

Browse files
Customize channel avatar view (#734)
1 parent 536ca99 commit 29091e3

File tree

14 files changed

+175
-21
lines changed

14 files changed

+175
-21
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33

44
# Upcoming
55

6-
### 🔄 Changed
6+
### ✅ Added
7+
- Add factory method to customize the channel avatar [#734](https://github.com/GetStream/stream-chat-swiftui/pull/734)
78

89
# [4.71.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.71.0)
910
_January 28, 2025_

Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/ChatChannelHeaderViewModifier.swift

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public protocol ChatChannelHeaderViewModifier: ViewModifier {
1111
}
1212

1313
/// The default channel header.
14-
public struct DefaultChatChannelHeader: ToolbarContent {
14+
public struct DefaultChatChannelHeader<Factory: ViewFactory>: ToolbarContent {
1515
@Injected(\.fonts) private var fonts
1616
@Injected(\.utils) private var utils
1717
@Injected(\.colors) private var colors
@@ -34,15 +34,18 @@ public struct DefaultChatChannelHeader: ToolbarContent {
3434
.isEmpty
3535
}
3636

37+
private var factory: Factory
3738
public var channel: ChatChannel
3839
public var headerImage: UIImage
3940
@Binding public var isActive: Bool
4041

4142
public init(
43+
factory: Factory = DefaultViewFactory.shared,
4244
channel: ChatChannel,
4345
headerImage: UIImage,
4446
isActive: Binding<Bool>
4547
) {
48+
self.factory = factory
4649
self.channel = channel
4750
self.headerImage = headerImage
4851
_isActive = isActive
@@ -64,10 +67,13 @@ public struct DefaultChatChannelHeader: ToolbarContent {
6467
resignFirstResponder()
6568
isActive = true
6669
} label: {
67-
ChannelAvatarView(
68-
avatar: headerImage,
69-
showOnlineIndicator: onlineIndicatorShown,
70-
size: CGSize(width: 36, height: 36)
70+
factory.makeChannelAvatarView(
71+
for: channel,
72+
with: .init(
73+
showOnlineIndicator: onlineIndicatorShown,
74+
size: CGSize(width: 36, height: 36),
75+
avatar: headerImage
76+
)
7177
)
7278
.offset(x: 4)
7379
}
@@ -86,19 +92,25 @@ public struct DefaultChatChannelHeader: ToolbarContent {
8692
}
8793

8894
/// The default header modifier.
89-
public struct DefaultChannelHeaderModifier: ChatChannelHeaderViewModifier {
95+
public struct DefaultChannelHeaderModifier<Factory: ViewFactory>: ChatChannelHeaderViewModifier {
9096
@ObservedObject private var channelHeaderLoader = InjectedValues[\.utils].channelHeaderLoader
9197
@State private var isActive: Bool = false
9298

99+
private var factory: Factory
93100
public var channel: ChatChannel
94101

95-
public init(channel: ChatChannel) {
102+
public init(
103+
factory: Factory = DefaultViewFactory.shared,
104+
channel: ChatChannel
105+
) {
106+
self.factory = factory
96107
self.channel = channel
97108
}
98109

99110
public func body(content: Content) -> some View {
100111
content.toolbar {
101112
DefaultChatChannelHeader(
113+
factory: factory,
102114
channel: channel,
103115
headerImage: channelHeaderLoader.image(for: channel),
104116
isActive: $isActive

Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import StreamChat
66
import SwiftUI
77

88
/// View for the channel list item.
9-
public struct ChatChannelListItem: View {
9+
public struct ChatChannelListItem<Factory: ViewFactory>: View {
1010

1111
@Injected(\.fonts) private var fonts
1212
@Injected(\.colors) private var colors
1313
@Injected(\.utils) private var utils
1414
@Injected(\.images) private var images
1515
@Injected(\.chatClient) private var chatClient
1616

17+
var factory: Factory
1718
var channel: ChatChannel
1819
var channelName: String
1920
var injectedChannelInfo: InjectedChannelInfo?
@@ -23,6 +24,7 @@ public struct ChatChannelListItem: View {
2324
var onItemTap: (ChatChannel) -> Void
2425

2526
public init(
27+
factory: Factory = DefaultViewFactory.shared,
2628
channel: ChatChannel,
2729
channelName: String,
2830
injectedChannelInfo: InjectedChannelInfo? = nil,
@@ -31,6 +33,7 @@ public struct ChatChannelListItem: View {
3133
disabled: Bool = false,
3234
onItemTap: @escaping (ChatChannel) -> Void
3335
) {
36+
self.factory = factory
3437
self.channel = channel
3538
self.channelName = channelName
3639
self.injectedChannelInfo = injectedChannelInfo
@@ -45,9 +48,9 @@ public struct ChatChannelListItem: View {
4548
onItemTap(channel)
4649
} label: {
4750
HStack {
48-
ChannelAvatarView(
49-
channel: channel,
50-
showOnlineIndicator: onlineIndicatorShown
51+
factory.makeChannelAvatarView(
52+
for: channel,
53+
with: .init(showOnlineIndicator: onlineIndicatorShown)
5154
)
5255

5356
VStack(alignment: .leading, spacing: 4) {
@@ -127,6 +130,16 @@ public struct ChatChannelListItem: View {
127130
}
128131
}
129132

133+
/// Options for setting up the channel avatar view.
134+
public struct ChannelAvatarViewOptions {
135+
/// Whether the online indicator should be shown.
136+
public var showOnlineIndicator: Bool
137+
/// Size of the avatar.
138+
public var size: CGSize = .defaultAvatarSize
139+
/// Optional avatar image. If not provided, it will be loaded by the channel header loader.
140+
public var avatar: UIImage?
141+
}
142+
130143
/// View for the avatar used in channels (includes online indicator overlay).
131144
public struct ChannelAvatarView: View {
132145
@Injected(\.utils) private var utils
@@ -157,9 +170,10 @@ public struct ChannelAvatarView: View {
157170
public init(
158171
channel: ChatChannel,
159172
showOnlineIndicator: Bool,
173+
avatar: UIImage? = nil,
160174
size: CGSize = .defaultAvatarSize
161175
) {
162-
avatar = nil
176+
self.avatar = avatar
163177
self.channel = channel
164178
self.showOnlineIndicator = showOnlineIndicator
165179
self.size = size
@@ -193,7 +207,7 @@ public struct ChannelAvatarView: View {
193207
}
194208

195209
private func reloadAvatar() {
196-
guard let channel else { return }
210+
guard let channel, avatar == nil else { return }
197211
channelAvatar = utils.channelHeaderLoader.image(for: channel)
198212
}
199213
}

Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelNavigatableListItem.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import SwiftUI
77

88
/// Chat channel list item that supports navigating to a destination.
99
/// It's generic over the channel destination.
10-
public struct ChatChannelNavigatableListItem<ChannelDestination: View>: View {
10+
public struct ChatChannelNavigatableListItem<Factory: ViewFactory, ChannelDestination: View>: View {
11+
private var factory: Factory
1112
private var channel: ChatChannel
1213
private var channelName: String
1314
private var avatar: UIImage
@@ -18,6 +19,7 @@ public struct ChatChannelNavigatableListItem<ChannelDestination: View>: View {
1819
private var onItemTap: (ChatChannel) -> Void
1920

2021
public init(
22+
factory: Factory = DefaultViewFactory.shared,
2123
channel: ChatChannel,
2224
channelName: String,
2325
avatar: UIImage,
@@ -27,6 +29,7 @@ public struct ChatChannelNavigatableListItem<ChannelDestination: View>: View {
2729
channelDestination: @escaping (ChannelSelectionInfo) -> ChannelDestination,
2830
onItemTap: @escaping (ChatChannel) -> Void
2931
) {
32+
self.factory = factory
3033
self.channel = channel
3134
self.channelName = channelName
3235
self.channelDestination = channelDestination
@@ -40,6 +43,7 @@ public struct ChatChannelNavigatableListItem<ChannelDestination: View>: View {
4043
public var body: some View {
4144
ZStack {
4245
ChatChannelListItem(
46+
factory: factory,
4347
channel: channel,
4448
channelName: channelName,
4549
injectedChannelInfo: injectedChannelInfo,

Sources/StreamChatSwiftUI/ChatChannelList/SearchResultsView.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,11 @@ struct SearchResultView<Factory: ViewFactory>: View {
118118
}
119119

120120
/// The search result item user interface.
121-
struct SearchResultItem<ChannelDestination: View>: View {
121+
struct SearchResultItem<Factory: ViewFactory, ChannelDestination: View>: View {
122122

123123
@Injected(\.utils) private var utils
124124

125+
var factory: Factory
125126
var searchResult: ChannelSelectionInfo
126127
var onlineIndicatorShown: Bool
127128
var channelName: String
@@ -134,9 +135,9 @@ struct SearchResultItem<ChannelDestination: View>: View {
134135
onSearchResultTap(searchResult)
135136
} label: {
136137
HStack {
137-
ChannelAvatarView(
138-
avatar: avatar,
139-
showOnlineIndicator: onlineIndicatorShown
138+
factory.makeChannelAvatarView(
139+
for: searchResult.channel,
140+
with: .init(showOnlineIndicator: onlineIndicatorShown, avatar: avatar)
140141
)
141142

142143
VStack(alignment: .leading, spacing: 4) {

Sources/StreamChatSwiftUI/DefaultViewFactory.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ extension ViewFactory {
7474
leadingSwipeButtonTapped: @escaping (ChatChannel) -> Void
7575
) -> some View {
7676
let listItem = ChatChannelNavigatableListItem(
77+
factory: self,
7778
channel: channel,
7879
channelName: channelName,
7980
avatar: avatar,
@@ -95,6 +96,18 @@ extension ViewFactory {
9596
)
9697
}
9798

99+
public func makeChannelAvatarView(
100+
for channel: ChatChannel,
101+
with options: ChannelAvatarViewOptions
102+
) -> some View {
103+
ChannelAvatarView(
104+
channel: channel,
105+
showOnlineIndicator: options.showOnlineIndicator,
106+
avatar: options.avatar,
107+
size: options.size
108+
)
109+
}
110+
98111
public func makeChannelListBackground(colors: ColorPalette) -> some View {
99112
Color(colors.background)
100113
.edgesIgnoringSafeArea(.bottom)
@@ -189,6 +202,7 @@ extension ViewFactory {
189202
channelDestination: @escaping (ChannelSelectionInfo) -> ChannelDestination
190203
) -> some View {
191204
SearchResultItem(
205+
factory: self,
192206
searchResult: searchResult,
193207
onlineIndicatorShown: onlineIndicatorShown,
194208
channelName: channelName,
@@ -280,7 +294,7 @@ extension ViewFactory {
280294
public func makeChannelHeaderViewModifier(
281295
for channel: ChatChannel
282296
) -> some ChatChannelHeaderViewModifier {
283-
DefaultChannelHeaderModifier(channel: channel)
297+
DefaultChannelHeaderModifier(factory: self, channel: channel)
284298
}
285299

286300
public func makeChannelLoadingView() -> some View {

Sources/StreamChatSwiftUI/ViewFactory.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ public protocol ViewFactory: AnyObject {
5858
trailingSwipeLeftButtonTapped: @escaping (ChatChannel) -> Void,
5959
leadingSwipeButtonTapped: @escaping (ChatChannel) -> Void
6060
) -> ChannelListItemType
61+
62+
associatedtype ChannelAvatarViewType: View
63+
/// Creates the channel avatar view shown in the channel list, search results and the channel header.
64+
/// - Parameters:
65+
/// - channel: the channel where the avatar is displayed.
66+
/// - options: the options used to configure the avatar view.
67+
/// - Returns: view displayed in the channel avatar slot.
68+
func makeChannelAvatarView(
69+
for channel: ChatChannel,
70+
with options: ChannelAvatarViewOptions
71+
) -> ChannelAvatarViewType
6172

6273
associatedtype ChannelListBackground: View
6374
/// Creates the background for the channel list.

StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelHeader_Tests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,27 @@ class ChatChannelHeader_Tests: StreamChatTestCase {
2626
// Then
2727
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
2828
}
29+
30+
func test_chatChannelHeaderModifier_channelAvatarUpdated() {
31+
// Given
32+
let channel = ChatChannel.mockDMChannel(name: "Test channel")
33+
34+
// When
35+
let view = NavigationView {
36+
Text("Test")
37+
.applyDefaultSize()
38+
.modifier(
39+
DefaultChannelHeaderModifier(
40+
factory: ChannelAvatarViewFactory(),
41+
channel: channel
42+
)
43+
)
44+
}
45+
.applyDefaultSize()
46+
47+
// Then
48+
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
49+
}
2950

3051
func test_chatChannelHeader_snapshot() {
3152
// Given
Loading

StreamChatSwiftUITests/Tests/ChatChannelList/ChatChannelListView_Tests.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ class ChatChannelListView_Tests: StreamChatTestCase {
7171
// Then
7272
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
7373
}
74+
75+
func test_channelListView_channelAvatarUpdated() {
76+
// Given
77+
let controller = makeChannelListController()
78+
79+
// When
80+
let view = ChatChannelListView(
81+
viewFactory: ChannelAvatarViewFactory(),
82+
channelListController: controller
83+
)
84+
.applyDefaultSize()
85+
86+
// Then
87+
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
88+
}
7489

7590
private func makeChannelListController() -> ChatChannelListController_Mock {
7691
let channelListController = ChatChannelListController_Mock.mock(client: chatClient)
@@ -87,3 +102,17 @@ class ChatChannelListView_Tests: StreamChatTestCase {
87102
return channels
88103
}
89104
}
105+
106+
class ChannelAvatarViewFactory: ViewFactory {
107+
108+
@Injected(\.chatClient) var chatClient
109+
110+
func makeChannelAvatarView(
111+
for channel: ChatChannel,
112+
with options: ChannelAvatarViewOptions
113+
) -> some View {
114+
Circle()
115+
.fill(.red)
116+
.frame(width: options.size.width, height: options.size.height)
117+
}
118+
}

0 commit comments

Comments
 (0)