Skip to content

Commit 86ac339

Browse files
committed
Fixed issue in smilies picker where loading spinner was always present even when no smilies were loading in.
Added category selector to smilies picker.
1 parent 36b9b16 commit 86ac339

2 files changed

Lines changed: 114 additions & 39 deletions

File tree

App/Composition/ShowSmilieKeyboardCommand.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ final class ShowSmilieKeyboardCommand: NSObject {
9191

9292
if let sheet = hostingController.sheetPresentationController {
9393
sheet.detents = [UISheetPresentationController.Detent.medium(), UISheetPresentationController.Detent.large()]
94+
// The medium-detent sheet is too small on iPad; start at full height there
95+
if UIDevice.current.userInterfaceIdiom == .pad {
96+
sheet.selectedDetentIdentifier = .large
97+
}
9498
sheet.prefersGrabberVisible = true
9599
sheet.preferredCornerRadius = 20
96100
sheet.delegate = self

App/Composition/SmiliePicker/SmiliePickerView.swift

Lines changed: 110 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ struct SmiliePickerView: View {
1111
@SwiftUI.Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
1212
@SwiftUI.Environment(\.theme) private var theme: Theme
1313
@SwiftUI.Environment(\.horizontalSizeClass) private var horizontalSizeClass
14-
@State private var visibleSections = 3 // Start by showing only first 3 sections
15-
@State private var hasLoadedAllSections = false
16-
14+
// Uniform pill height (fits two lines of .subheadline), scales with Dynamic Type
15+
@ScaledMetric(relativeTo: .subheadline) private var pillHeight: CGFloat = 50
16+
1717
let onSmilieSelected: (Smilie) -> Void
1818

1919
private var columnCount: Int {
@@ -45,12 +45,6 @@ struct SmiliePickerView: View {
4545
errorView(message: error)
4646
} else {
4747
scrollContent
48-
.onAppear {
49-
// Check if we already have all sections loaded from the start
50-
if visibleSections >= viewModel.allSmilies.count {
51-
hasLoadedAllSections = true
52-
}
53-
}
5448
}
5549
}
5650
}
@@ -124,6 +118,20 @@ struct SmiliePickerView: View {
124118
}
125119

126120
private var scrollContent: some View {
121+
GeometryReader { geometry in
122+
ScrollViewReader { proxy in
123+
VStack(spacing: 0) {
124+
if viewModel.searchText.isEmpty && !viewModel.allSmilies.isEmpty {
125+
categoryChipRow(proxy: proxy, maxPillWidth: geometry.size.width * 0.5)
126+
}
127+
128+
scrollingSections
129+
}
130+
}
131+
}
132+
}
133+
134+
private var scrollingSections: some View {
127135
ScrollView {
128136
VStack(alignment: .leading, spacing: 20) {
129137
if !viewModel.searchText.isEmpty {
@@ -138,6 +146,91 @@ struct SmiliePickerView: View {
138146
.padding(.horizontal)
139147
}
140148
}
149+
150+
private var categoryTitles: [String] {
151+
var titles: [String] = []
152+
if !viewModel.recentlyUsedSmilies.isEmpty {
153+
titles.append(recentlyUsedSectionTitle)
154+
}
155+
titles.append(contentsOf: viewModel.allSmilies.map { $0.title })
156+
return titles
157+
}
158+
159+
/// Namespaces section anchor ids so they can't collide with the pill row's own
160+
/// `ForEach` identities — `ScrollViewProxy.scrollTo` searches every scroll view
161+
/// under the reader, and a bare title would match the (already visible) pill.
162+
private func sectionAnchorID(_ title: String) -> String {
163+
"section-\(title)"
164+
}
165+
166+
private func categoryChipRow(proxy: ScrollViewProxy, maxPillWidth: CGFloat) -> some View {
167+
// Alternate titles between the two rows so adjacent categories stay near each other
168+
let titles = categoryTitles
169+
let topRow = stride(from: 0, to: titles.count, by: 2).map { titles[$0] }
170+
let bottomRow = stride(from: 1, to: titles.count, by: 2).map { titles[$0] }
171+
172+
return ScrollView(.horizontal, showsIndicators: false) {
173+
VStack(alignment: .leading, spacing: 8) {
174+
HStack(spacing: 8) {
175+
ForEach(topRow, id: \.self) { title in
176+
categoryPill(title, proxy: proxy, maxWidth: maxPillWidth)
177+
}
178+
}
179+
if !bottomRow.isEmpty {
180+
HStack(spacing: 8) {
181+
ForEach(bottomRow, id: \.self) { title in
182+
categoryPill(title, proxy: proxy, maxWidth: maxPillWidth)
183+
}
184+
}
185+
}
186+
}
187+
.padding(.horizontal)
188+
}
189+
// Size to the two rows' content height; without this the surrounding layout
190+
// can compress the scroller and clip the bottom row
191+
.fixedSize(horizontal: false, vertical: true)
192+
.padding(.bottom, 8)
193+
}
194+
195+
private func categoryPill(_ title: String, proxy: ScrollViewProxy, maxWidth: CGFloat) -> some View {
196+
Button(action: {
197+
// Deferring past the button's own transaction keeps scrollTo reliable on iOS 15
198+
DispatchQueue.main.async {
199+
withAnimation {
200+
proxy.scrollTo(sectionAnchorID(title), anchor: .top)
201+
}
202+
}
203+
}) {
204+
Text(title)
205+
.font(.subheadline)
206+
.dynamicTypeSize(...DynamicTypeSize.accessibility2)
207+
.multilineTextAlignment(.center)
208+
.lineLimit(2)
209+
.minimumScaleFactor(0.9)
210+
.foregroundColor(theme[color: "sheetTextColor"]!)
211+
.frame(maxWidth: maxWidth)
212+
.padding(.horizontal, 12)
213+
.frame(height: pillHeight)
214+
.background(
215+
Capsule()
216+
.fill(pillBackgroundColor)
217+
.overlay(
218+
Capsule()
219+
.stroke(pillStrokeColor, lineWidth: 1)
220+
)
221+
)
222+
}
223+
}
224+
225+
private var pillBackgroundColor: Color {
226+
let fallback = theme.isDark ? Color.white.opacity(0.15) : Color.black.opacity(0.08)
227+
return theme[color: "listSecondaryTextColor"]?.opacity(theme.isDark ? 0.25 : 0.2) ?? fallback
228+
}
229+
230+
private var pillStrokeColor: Color {
231+
let fallback = theme.isDark ? Color.white.opacity(0.3) : Color.black.opacity(0.2)
232+
return theme[color: "listSecondaryTextColor"]?.opacity(0.5) ?? fallback
233+
}
141234

142235
private var searchResultsSection: some View {
143236
VStack(alignment: .leading, spacing: 10) {
@@ -167,10 +260,12 @@ struct SmiliePickerView: View {
167260
}
168261
}
169262

263+
private let recentlyUsedSectionTitle = "Recently Used"
264+
170265
private var recentlyUsedSection: some View {
171266
VStack(alignment: .leading, spacing: 10) {
172267
HStack {
173-
Text("Recently Used")
268+
Text(recentlyUsedSectionTitle)
174269
.font(.title3)
175270
.fontWeight(.bold)
176271
.dynamicTypeSize(...DynamicTypeSize.accessibility2)
@@ -181,53 +276,29 @@ struct SmiliePickerView: View {
181276

182277
smilieGrid(viewModel.recentlyUsedSmilies)
183278
}
279+
.id(sectionAnchorID(recentlyUsedSectionTitle))
184280
}
185281

186282
private var allSmiliesSection: some View {
187283
VStack(alignment: .leading, spacing: 20) {
188-
let sectionsToShow = Array(viewModel.allSmilies.prefix(visibleSections))
189-
190-
ForEach(Array(sectionsToShow.enumerated()), id: \.element.title) { index, section in
284+
ForEach(Array(viewModel.allSmilies.enumerated()), id: \.element.title) { index, section in
191285
VStack(alignment: .leading, spacing: 10) {
192286
if index > 0 {
193287
Divider()
194288
.background(theme[color: "listSeparatorColor"]!)
195289
.padding(.vertical, 10)
196290
}
197-
291+
198292
Text(section.title)
199293
.font(.title3)
200294
.fontWeight(.bold)
201295
.dynamicTypeSize(...DynamicTypeSize.accessibility2)
202296
.foregroundColor(theme[color: "sheetTextColor"]!)
203297
.padding(.bottom, 5)
204-
298+
205299
smilieGrid(section.smilies)
206300
}
207-
}
208-
209-
// Show loading indicator if there are more sections to load
210-
if !hasLoadedAllSections && visibleSections < viewModel.allSmilies.count && !viewModel.allSmilies.isEmpty {
211-
HStack {
212-
Spacer()
213-
ProgressView()
214-
.progressViewStyle(CircularProgressViewStyle())
215-
.scaleEffect(0.8)
216-
Spacer()
217-
}
218-
.padding(.vertical, 20)
219-
.onAppear {
220-
// Load more sections when this view appears
221-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
222-
withAnimation {
223-
let newVisibleSections = min(visibleSections + 3, viewModel.allSmilies.count)
224-
visibleSections = newVisibleSections
225-
if newVisibleSections >= viewModel.allSmilies.count {
226-
hasLoadedAllSections = true
227-
}
228-
}
229-
}
230-
}
301+
.id(sectionAnchorID(section.title))
231302
}
232303
}
233304
}

0 commit comments

Comments
 (0)