Skip to content

Commit 0fa7560

Browse files
authored
Refactor ChatFeature views (#527)
* update ChatInputView * update ChatFeatureView * Pass store to SendButton instead of individual properties Follow SwiftUI/TCA best practice: pass store reference to prevent parent re-renders. Parent BasicInputView no longer reads store.isSendButtonDisabled or store.isSending. * extract OverlayView
1 parent e43bdac commit 0fa7560

File tree

2 files changed

+88
-56
lines changed

2 files changed

+88
-56
lines changed

CriticalMapsKit/Sources/ChatFeature/ChatFeatureView.swift

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ public struct ChatView: View {
1818
Color(.backgroundPrimary)
1919
.ignoresSafeArea()
2020
.accessibilityHidden(true)
21-
21+
2222
if store.messages.isEmpty {
23-
emptyState
23+
ChatEmptyStateView()
2424
.accessibilitySortPriority(-1)
2525
} else {
2626
List(store.messages) { chat in
@@ -34,35 +34,45 @@ public struct ChatView: View {
3434
.accessibleAnimation(.spring, value: store.messages)
3535
}
3636
}
37-
38-
chatInput
37+
38+
ChatInputArea(
39+
store: store.scope(
40+
state: \.chatInputState,
41+
action: \.chatInput
42+
)
43+
)
3944
}
4045
.alert(store: store.scope(state: \.$alert, action: \.alert))
4146
.onAppear { store.send(.onAppear) }
4247
.navigationBarTitleDisplayMode(.inline)
4348
.ignoresSafeArea(.container, edges: .bottom)
4449
}
45-
46-
private var chatInput: some View {
50+
}
51+
52+
// MARK: - Subviews
53+
54+
private struct ChatInputArea: View {
55+
let store: StoreOf<ChatInput>
56+
57+
var body: some View {
4758
ZStack(alignment: .top) {
4859
BasicInputView(
49-
store: store.scope(
50-
state: \.chatInputState,
51-
action: \.chatInput
52-
),
60+
store: store,
5361
placeholder: L10n.Chat.placeholder
5462
)
5563
.padding(.horizontal, .grid(3))
5664
.padding(.top, .grid(2))
5765
.padding(.bottom, .grid(7))
58-
66+
5967
Color(.border)
6068
.frame(height: 2)
6169
}
6270
.background(Color.backgroundSecondary)
6371
}
64-
65-
private var emptyState: some View {
72+
}
73+
74+
private struct ChatEmptyStateView: View {
75+
var body: some View {
6676
EmptyStateView(
6777
emptyState: .init(
6878
icon: Asset.chatEmpty.image,
@@ -75,7 +85,7 @@ public struct ChatView: View {
7585
}
7686
}
7787

78-
// MARK: Preview
88+
// MARK: - Preview
7989

8090
#Preview {
8191
ChatView(

CriticalMapsKit/Sources/ChatFeature/ChatInputView.swift

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,38 @@ public struct BasicInputView: View {
2525
)
2626
}
2727

28-
private var messageEditorView: some View {
28+
public var body: some View {
29+
VStack {
30+
HStack(alignment: .bottom) {
31+
MessageEditorView(
32+
store: $store,
33+
placeholder: placeholder,
34+
contentSizeThatFits: $contentSizeThatFits,
35+
messageEditorHeight: messageEditorHeight
36+
)
37+
38+
SendButton(store: store)
39+
}
40+
.padding(.grid(1))
41+
.background(Color.backgroundPrimary)
42+
.clipShape(RoundedRectangle(cornerRadius: 23))
43+
.overlay(
44+
RoundedRectangle(cornerRadius: 23)
45+
.stroke(Color.innerBorder, lineWidth: 1)
46+
)
47+
}
48+
}
49+
}
50+
51+
// MARK: - Subviews
52+
53+
private struct MessageEditorView: View {
54+
@Binding var store: StoreOf<ChatInput>
55+
let placeholder: String
56+
@Binding var contentSizeThatFits: CGSize
57+
let messageEditorHeight: CGFloat
58+
59+
var body: some View {
2960
MultilineTextField(
3061
attributedText: $store.internalAttributedMessage.sending(\.messageChanged),
3162
placeholder: placeholder,
@@ -39,54 +70,45 @@ public struct BasicInputView: View {
3970
}
4071
.frame(height: messageEditorHeight)
4172
}
73+
}
4274

43-
private var sendButton: some View {
44-
Button(action: {
45-
store.send(.onCommit)
46-
}, label: {
47-
Circle().fill(
48-
withAnimation {
49-
store.isSendButtonDisabled ? Color(.border) : .blue
50-
}
51-
)
52-
.accessibleAnimation(.spring(duration: 0.13), value: store.isSendButtonDisabled)
53-
.accessibilityLabel(Text(L10n.Chat.send))
54-
.frame(width: 38, height: 38)
55-
.overlay(
56-
Group {
57-
if store.isSending {
58-
ProgressView().tint(.white)
59-
} else {
60-
Image(systemName: "paperplane.fill")
61-
.resizable()
62-
.foregroundColor(.white)
63-
.offset(x: -1, y: 1)
64-
.padding(.grid(2))
65-
}
66-
}
67-
)
68-
})
69-
.disabled(store.isSendButtonDisabled)
75+
private struct SendButton: View {
76+
let store: StoreOf<ChatInput>
77+
78+
var body: some View {
79+
Button(action: { store.send(.onCommit) }) {
80+
Circle()
81+
.fill(Color.brand500)
82+
.frame(width: 38, height: 38)
83+
.overlay(OverlayView(store: store))
84+
.accessibleAnimation(.spring(duration: 0.13), value: store.isSendButtonDisabled)
85+
.accessibilityLabel(Text(L10n.Chat.send))
86+
}
87+
.opacity(store.isSendButtonDisabled ? 0 : 1)
88+
.animation(.snappy.speed(2.5), value: store.isSendButtonDisabled)
7089
}
90+
}
7191

72-
public var body: some View {
73-
VStack {
74-
HStack(alignment: .bottom) {
75-
messageEditorView
76-
sendButton
77-
}
78-
.padding(.grid(1))
79-
.background(Color.backgroundPrimary)
80-
.clipShape(RoundedRectangle(cornerRadius: 23))
81-
.overlay(
82-
RoundedRectangle(cornerRadius: 23)
83-
.stroke(Color.innerBorder, lineWidth: 1)
84-
)
92+
// MARK: - SubViews
93+
94+
private struct OverlayView: View {
95+
let store: StoreOf<ChatInput>
96+
97+
var body: some View {
98+
if store.isSending {
99+
ProgressView()
100+
.tint(.textPrimaryLight)
101+
} else {
102+
Image(systemName: "paperplane.fill")
103+
.resizable()
104+
.foregroundColor(.textPrimaryLight)
105+
.offset(x: -1, y: 1)
106+
.padding(.grid(2))
85107
}
86108
}
87109
}
88110

89-
// MARK: Implementation Details
111+
// MARK: - Implementation Details
90112

91113
public struct ContentSizeThatFitsKey: PreferenceKey {
92114
public static var defaultValue: CGSize = .zero

0 commit comments

Comments
 (0)