Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit 0e916ed

Browse files
authored
Refactor AppsView to avoid compiler timeouts and fix date formatter warnings
1 parent 9e00794 commit 0e916ed

1 file changed

Lines changed: 142 additions & 131 deletions

File tree

Sources/prostore/views/AppsView.swift

Lines changed: 142 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ fileprivate func appDate(for app: AltApp) -> Date? {
376376
if let vd = app.versionDate {
377377
let formatter = ISO8601DateFormatter()
378378
// attempt strict "yyyy-MM-dd"
379-
if let date = ISO8601DateFormatter().date(from: vd + "T00:00:00Z") {
379+
if let date = formatter.date(from: vd + "T00:00:00Z") {
380380
return date
381381
}
382382
// fallback try DateFormatter
@@ -404,15 +404,15 @@ public struct AppsView: View {
404404
_vm = StateObject(wrappedValue: RepoViewModel(sourceURLs: repoURLs))
405405
}
406406

407-
// Sort the vm.apps according to the selected sort option
407+
// MARK: - Smaller computed helpers to keep `body` tiny
408+
408409
private var sortedApps: [AltApp] {
409410
switch sortOption {
410411
case .nameAZ:
411412
return vm.apps.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
412413
case .nameZA:
413414
return vm.apps.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedDescending }
414415
case .repoAZ:
415-
// sort primarily by repo name, then app name
416416
return vm.apps.sorted {
417417
let aRepo = $0.repositoryName ?? ""
418418
let bRepo = $1.repositoryName ?? ""
@@ -448,7 +448,6 @@ public struct AppsView: View {
448448
}
449449
}
450450

451-
// Filtered apps (search)
452451
private var filteredApps: [AltApp] {
453452
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
454453
guard !query.isEmpty else { return sortedApps }
@@ -463,161 +462,167 @@ public struct AppsView: View {
463462
}
464463
}
465464

466-
// Group apps by repository name (fallback to "Unknown Repository")
467465
private var groupedApps: [String: [AltApp]] {
468466
Dictionary(grouping: filteredApps, by: { $0.repositoryName ?? "Unknown Repository" })
469467
}
470468

471-
// Decide an ordered list of repository keys to display. If sorting by repo, keep repo order alphabetical.
472469
private var orderedRepoKeys: [String] {
473470
let keys = Array(groupedApps.keys)
474471
if sortOption == .repoAZ {
475472
return keys.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
476473
} else {
477-
// keep stable order: repositories ordered by first occurrence in sortedApps
478474
var order: [String] = []
479475
for app in sortedApps {
480476
let key = app.repositoryName ?? "Unknown Repository"
481477
if !order.contains(key) { order.append(key) }
482478
}
483-
// include any keys that didn't appear (edge-case)
484479
for k in keys where !order.contains(k) {
485480
order.append(k)
486481
}
487482
return order
488483
}
489484
}
490485

491-
public var body: some View {
492-
VStack(spacing: 0) {
486+
// MARK: - View pieces
493487

494-
// Search + Sort Bar (hidden during initial load)
495-
if !(vm.isLoading && vm.apps.isEmpty) {
496-
HStack(spacing: 8) {
497-
HStack {
498-
Image(systemName: "magnifyingglass")
499-
.foregroundColor(.secondary)
500-
TextField("Search apps, developer or bundle ID", text: $searchText)
501-
.textInputAutocapitalization(.never)
502-
.disableAutocorrection(true)
503-
.focused($searchFieldFocused)
504-
.submitLabel(.search)
505-
if !searchText.isEmpty {
506-
Button(action: { searchText = "" }) {
507-
Image(systemName: "xmark.circle.fill")
508-
.foregroundColor(.secondary)
509-
}
510-
.buttonStyle(.plain)
488+
@ViewBuilder private var searchAndSortBar: some View {
489+
if !(vm.isLoading && vm.apps.isEmpty) {
490+
HStack(spacing: 8) {
491+
HStack {
492+
Image(systemName: "magnifyingglass")
493+
.foregroundColor(.secondary)
494+
TextField("Search apps, developer or bundle ID", text: $searchText)
495+
.textInputAutocapitalization(.never)
496+
.disableAutocorrection(true)
497+
.focused($searchFieldFocused)
498+
.submitLabel(.search)
499+
if !searchText.isEmpty {
500+
Button(action: { searchText = "" }) {
501+
Image(systemName: "xmark.circle.fill")
502+
.foregroundColor(.secondary)
511503
}
504+
.buttonStyle(.plain)
512505
}
513-
.padding(10)
514-
.background(.regularMaterial)
515-
.cornerRadius(10)
516-
.frame(maxWidth: .infinity)
517-
518-
Picker(selection: $sortOption, label: Label("Sort", systemImage: "arrow.up.arrow.down")) {
519-
ForEach(SortOption.allCases) { option in
520-
Text(option.rawValue).tag(option)
521-
}
506+
}
507+
.padding(10)
508+
.background(.regularMaterial)
509+
.cornerRadius(10)
510+
.frame(maxWidth: .infinity)
511+
512+
Picker(selection: $sortOption, label: Label("Sort", systemImage: "arrow.up.arrow.down")) {
513+
ForEach(SortOption.allCases) { option in
514+
Text(option.rawValue).tag(option)
522515
}
523-
.pickerStyle(MenuPickerStyle())
524-
.padding(.horizontal, 10)
525-
.padding(.vertical, 8)
526-
.background(.regularMaterial)
527-
.cornerRadius(10)
528-
.frame(minWidth: 170)
529516
}
530-
.padding(.horizontal)
531-
.padding(.top, 8)
517+
.pickerStyle(MenuPickerStyle())
518+
.padding(.horizontal, 10)
519+
.padding(.vertical, 8)
520+
.background(.regularMaterial)
521+
.cornerRadius(10)
522+
.frame(minWidth: 170)
532523
}
524+
.padding(.horizontal)
525+
.padding(.top, 8)
526+
}
527+
}
533528

534-
// Content
535-
Group {
536-
if vm.isLoading && vm.apps.isEmpty {
537-
VStack(spacing: 12) {
538-
ProgressView()
539-
Text("Loading apps...")
540-
.font(.subheadline)
541-
.foregroundColor(.secondary)
529+
@ViewBuilder private var loadingView: some View {
530+
VStack(spacing: 12) {
531+
ProgressView()
532+
Text("Loading apps...")
533+
.font(.subheadline)
534+
.foregroundColor(.secondary)
535+
}
536+
.padding()
537+
}
538+
539+
@ViewBuilder private func errorView(_ error: String) -> some View {
540+
VStack(spacing: 12) {
541+
Text("Error")
542+
.font(.headline)
543+
Text(error)
544+
.font(.subheadline)
545+
.foregroundColor(.secondary)
546+
.multilineTextAlignment(.center)
547+
Button("Retry") { vm.refresh() }
548+
.padding(.top, 8)
549+
}
550+
.padding()
551+
}
552+
553+
@ViewBuilder private func repoHeader(_ repoKey: String) -> some View {
554+
HStack(spacing: 8) {
555+
Button(action: {
556+
withAnimation(.spring()) {
557+
if expandedRepos.contains(repoKey) {
558+
expandedRepos.remove(repoKey)
559+
} else {
560+
expandedRepos.insert(repoKey)
542561
}
543-
.padding()
544-
} else if let error = vm.errorMessage, vm.apps.isEmpty {
545-
VStack(spacing: 12) {
546-
Text("Error")
547-
.font(.headline)
548-
Text(error)
562+
}
563+
}) {
564+
HStack(spacing: 8) {
565+
Image(systemName: expandedRepos.contains(repoKey) ? "chevron.down" : "chevron.right")
566+
.font(.system(size: 14, weight: .semibold))
567+
.foregroundColor(.accentColor)
568+
.frame(width: 18, height: 18)
569+
VStack(alignment: .leading, spacing: 1) {
570+
Text(repoKey)
549571
.font(.subheadline)
572+
.fontWeight(.semibold)
573+
Text("\(groupedApps[repoKey]?.count ?? 0) app\( (groupedApps[repoKey]?.count ?? 0) == 1 ? "" : "s")")
574+
.font(.caption)
550575
.foregroundColor(.secondary)
551-
.multilineTextAlignment(.center)
552-
Button("Retry") { vm.refresh() }
553-
.padding(.top, 8)
554576
}
555-
.padding()
577+
Spacer()
578+
}
579+
}
580+
.buttonStyle(.plain)
581+
}
582+
.padding(.vertical, 6)
583+
.contentShape(Rectangle())
584+
}
585+
586+
@ViewBuilder private func repoSection(_ repoKey: String) -> some View {
587+
Section {
588+
if expandedRepos.contains(repoKey) {
589+
ForEach(groupedApps[repoKey] ?? []) { app in
590+
Button { selectedApp = app } label: { AppRowView(app: app) }
591+
.buttonStyle(.plain)
592+
}
593+
} else {
594+
if let first = groupedApps[repoKey]?.first {
595+
Button { selectedApp = first } label: { AppRowView(app: first).opacity(0.85) }
596+
.buttonStyle(.plain)
597+
}
598+
}
599+
} header: {
600+
repoHeader(repoKey)
601+
}
602+
}
603+
604+
@ViewBuilder private var listView: some View {
605+
List {
606+
ForEach(orderedRepoKeys, id: \.self) { repoKey in
607+
repoSection(repoKey)
608+
}
609+
}
610+
.listStyle(PlainListStyle())
611+
.refreshable { vm.refresh() }
612+
}
613+
614+
// MARK: - Body stays small and easy to type-check
615+
public var body: some View {
616+
VStack(spacing: 0) {
617+
searchAndSortBar
618+
619+
Group {
620+
if vm.isLoading && vm.apps.isEmpty {
621+
loadingView
622+
} else if let error = vm.errorMessage, vm.apps.isEmpty {
623+
errorView(error)
556624
} else {
557-
// List of grouped repositories
558-
List {
559-
ForEach(orderedRepoKeys, id: \.self) { repoKey in
560-
// header row
561-
Section {
562-
if expandedRepos.contains(repoKey) {
563-
// show apps for this repo
564-
ForEach(groupedApps[repoKey] ?? []) { app in
565-
Button {
566-
selectedApp = app
567-
} label: {
568-
AppRowView(app: app)
569-
}
570-
.buttonStyle(.plain)
571-
}
572-
} else {
573-
// collapsed: show a compact preview (optional - show first 1 app)
574-
if let first = groupedApps[repoKey]?.first {
575-
Button {
576-
selectedApp = first
577-
} label: {
578-
AppRowView(app: first)
579-
.opacity(0.85)
580-
}
581-
.buttonStyle(.plain)
582-
}
583-
}
584-
} header: {
585-
// Custom header with toggle
586-
HStack(spacing: 8) {
587-
Button(action: {
588-
withAnimation(.spring()) {
589-
if expandedRepos.contains(repoKey) {
590-
expandedRepos.remove(repoKey)
591-
} else {
592-
expandedRepos.insert(repoKey)
593-
}
594-
}
595-
}) {
596-
HStack(spacing: 8) {
597-
Image(systemName: expandedRepos.contains(repoKey) ? "chevron.down" : "chevron.right")
598-
.font(.system(size: 14, weight: .semibold))
599-
.foregroundColor(.accentColor)
600-
.frame(width: 18, height: 18)
601-
VStack(alignment: .leading, spacing: 1) {
602-
Text(repoKey)
603-
.font(.subheadline)
604-
.fontWeight(.semibold)
605-
Text("\(groupedApps[repoKey]?.count ?? 0) app\( (groupedApps[repoKey]?.count ?? 0) == 1 ? "" : "s")")
606-
.font(.caption)
607-
.foregroundColor(.secondary)
608-
}
609-
Spacer()
610-
}
611-
}
612-
.buttonStyle(.plain)
613-
}
614-
.padding(.vertical, 6)
615-
.contentShape(Rectangle())
616-
}
617-
}
618-
}
619-
.listStyle(PlainListStyle())
620-
.refreshable { vm.refresh() }
625+
listView
621626
}
622627
}
623628
.padding(.top, 8)
@@ -626,26 +631,32 @@ public struct AppsView: View {
626631
AppDetailView(app: app)
627632
}
628633
.toolbar {
629-
ToolbarItem(placement: .navigationBarTrailing) {
634+
ToolbarItemGroup(placement: .navigationBarTrailing) {
630635
Button(action: { vm.refresh() }) {
631636
Image(systemName: "arrow.clockwise")
632637
}
633-
.help("Refresh repository")
638+
Button(action: {
639+
withAnimation(.spring()) { expandedRepos = Set(orderedRepoKeys) }
640+
}) {
641+
Image(systemName: "rectangle.expand.vertical")
642+
}
643+
Button(action: {
644+
withAnimation(.spring()) { expandedRepos.removeAll() }
645+
}) {
646+
Image(systemName: "rectangle.compress.vertical")
647+
}
634648
}
635649
}
636-
// When apps change (or on appear), default to all repos expanded so they start maximised
637650
.onAppear {
638651
expandedRepos = Set(orderedRepoKeys)
639652
}
640653
.onChange(of: vm.apps) { _ in
641654
expandedRepos = Set(orderedRepoKeys)
642655
}
643656
.onChange(of: searchText) { _ in
644-
// If search modifies the repo list, ensure expansion state includes newly visible repos
645657
expandedRepos.formUnion(orderedRepoKeys)
646658
}
647659
.onChange(of: sortOption) { _ in
648-
// keep currently expanded repos where possible, but ensure new repos are included
649660
expandedRepos.formUnion(orderedRepoKeys)
650661
}
651662
}

0 commit comments

Comments
 (0)