SPB Git forge

spb/ka-ios

Public
20commits 1branches 0releases
17.2 MBsize
maindefault branch
1 mo agolast push
Swift 100%

v1.0.0 (5) — polish : thème CLAIR par défaut, images en fondu + cache 512 Mo, cartes d accueil avec photos (carrousel aligné), squelettes de chargement, micro-interactions à la pression, animations de listes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 18, 2026) parent 68eaecb

8 changed files +103 −45

modified KA/App/KAApp.swift +7 −1
@@ -6,9 +6,15 @@ import SwiftUI
6 6 @main
7 7 struct KAApp: App {
8 8 @AppStorage("ka.onboarded") private var onboarded = false
9 − @AppStorage("ka.appearance") private var appearance = "auto"
9 + @AppStorage("ka.appearance") private var appearance = "clair"
10 10 @StateObject private var favorites = FavoritesStore.shared
11 11
12 + init() {
13 + // cache réseau généreux : images et JSON fluides, consultation hors ligne
14 + URLCache.shared = URLCache(memoryCapacity: 64 * 1024 * 1024,
15 + diskCapacity: 512 * 1024 * 1024)
16 + }
17 +
12 18 var body: some Scene {
13 19 WindowGroup {
14 20 Group {
modified KA/Design/Theme.swift +65 −10
@@ -129,16 +129,7 @@ struct KAItemRow: View {
129 129 var body: some View {
130 130 HStack(alignment: .top, spacing: 12) {
131 131 if let img = item.imageURL {
132 − AsyncImage(url: img) { phase in
133 − switch phase {
134 − case .success(let image):
135 − image.resizable().aspectRatio(contentMode: .fill)
136 − default:
137 − (universe?.accent ?? .gray).opacity(0.12)
138 − .overlay(Image(systemName: universe?.symbol ?? "photo")
139 − .foregroundStyle(universe?.accent ?? .gray))
140 − }
141 − }
132 + KAImage(url: img, accent: universe?.accent ?? .gray, symbol: universe?.symbol ?? "photo")
142 133 .frame(width: 74, height: 74)
143 134 .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
144 135 .overlay(RoundedRectangle(cornerRadius: 10, style: .continuous)
@@ -183,6 +174,70 @@ struct KAItemRow: View {
183 174 }
184 175 }
185 176
177 +// MARK: - Image réseau fluide (fondu à l'arrivée, cache URLCache partagé)
178 +
179 +struct KAImage: View {
180 + let url: URL?
181 + var accent: Color = .gray
182 + var symbol: String = "photo"
183 +
184 + var body: some View {
185 + AsyncImage(url: url, transaction: Transaction(animation: .easeOut(duration: 0.25))) { phase in
186 + switch phase {
187 + case .success(let image):
188 + image.resizable().aspectRatio(contentMode: .fill)
189 + .transition(.opacity)
190 + case .failure:
191 + accent.opacity(0.1)
192 + .overlay(Image(systemName: symbol).foregroundStyle(accent.opacity(0.6)))
193 + default:
194 + accent.opacity(0.08)
195 + .overlay(Image(systemName: symbol).foregroundStyle(accent.opacity(0.35)))
196 + }
197 + }
198 + }
199 +}
200 +
201 +// MARK: - Pression tactile (échelle + réactivité)
202 +
203 +struct KAPressStyle: ButtonStyle {
204 + func makeBody(configuration: Configuration) -> some View {
205 + configuration.label
206 + .scaleEffect(configuration.isPressed ? 0.97 : 1)
207 + .animation(.spring(response: 0.25, dampingFraction: 0.7), value: configuration.isPressed)
208 + }
209 +}
210 +
211 +// MARK: - Squelette de chargement (liste)
212 +
213 +struct KASkeletonRow: View {
214 + @Environment(\.colorScheme) private var scheme
215 + var body: some View {
216 + HStack(alignment: .top, spacing: 12) {
217 + RoundedRectangle(cornerRadius: 10).fill(.quaternary).frame(width: 74, height: 74)
218 + VStack(alignment: .leading, spacing: 8) {
219 + RoundedRectangle(cornerRadius: 4).fill(.quaternary).frame(height: 14)
220 + RoundedRectangle(cornerRadius: 4).fill(.quaternary).frame(width: 180, height: 11)
221 + RoundedRectangle(cornerRadius: 4).fill(.quaternary).frame(width: 90, height: 11)
222 + }
223 + }
224 + .padding(12).kaCard()
225 + .redacted(reason: .placeholder)
226 + .shimmering()
227 + }
228 +}
229 +
230 +struct Shimmer: ViewModifier {
231 + @State private var on = false
232 + func body(content: Content) -> some View {
233 + content
234 + .opacity(on ? 0.5 : 0.9)
235 + .animation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true), value: on)
236 + .onAppear { on = true }
237 + }
238 +}
239 +extension View { func shimmering() -> some View { modifier(Shimmer()) } }
240 +
186 241 // MARK: - États
187 242
188 243 struct KAEmptyState: View {
modified KA/Features/HomeView.swift +23 −17
@@ -102,32 +102,38 @@ struct HomeView: View {
102 102 HStack(spacing: 12) {
103 103 ForEach(items.prefix(8)) { item in
104 104 NavigationLink(value: item) {
105 − VStack(alignment: .leading, spacing: 6) {
106 − Text(item.title).font(.subheadline.weight(.semibold))
107 − .lineLimit(2, reservesSpace: true)
108 − .multilineTextAlignment(.leading)
109 − if let s = item.subtitle {
110 − Text(s).font(.caption).foregroundStyle(.secondary).lineLimit(1)
111 − }
112 − Spacer(minLength: 0)
113 − HStack {
114 − if let p = item.priceLabel {
115 − Text(p).font(.system(.caption, design: .rounded).weight(.bold))
116 − .foregroundStyle(u.accent)
105 + VStack(alignment: .leading, spacing: 0) {
106 + KAImage(url: item.imageURL, accent: u.accent, symbol: u.symbol)
107 + .frame(width: 216, height: 118)
108 + .clipped()
109 + VStack(alignment: .leading, spacing: 4) {
110 + Text(item.title).font(.subheadline.weight(.semibold))
111 + .lineLimit(2, reservesSpace: true)
112 + .multilineTextAlignment(.leading)
113 + HStack {
114 + if let p = item.priceLabel {
115 + Text(p).font(.system(.caption, design: .rounded).weight(.bold))
116 + .foregroundStyle(u.accent)
117 + .lineLimit(1)
118 + }
119 + Spacer()
120 + if let c = item.city {
121 + Text(c).font(.caption2).foregroundStyle(.tertiary).lineLimit(1)
122 + }
117 123 }
118 − Spacer()
119 − if let c = item.city { Text(c).font(.caption2).foregroundStyle(.tertiary) }
120 124 }
125 + .padding(10)
121 126 }
122 − .padding(12)
123 − .frame(width: 190, height: 110, alignment: .topLeading)
127 + .frame(width: 216, alignment: .topLeading)
124 128 .kaCard(accent: u.accent)
125 129 }
126 − .buttonStyle(.plain)
130 + .buttonStyle(KAPressStyle())
127 131 }
128 132 }
133 + .scrollTargetLayout()
129 134 .padding(.vertical, 4)
130 135 }
136 + .scrollTargetBehavior(.viewAligned)
131 137 }
132 138 }
133 139
modified KA/Features/ProfileView.swift +1 −1
@@ -5,7 +5,7 @@ import SwiftUI
5 5
6 6 struct ProfileView: View {
7 7 @AppStorage("ka.favUniverses") private var favUniversesRaw = ""
8 − @AppStorage("ka.appearance") private var appearance = "auto"
8 + @AppStorage("ka.appearance") private var appearance = "clair"
9 9 @Environment(\.colorScheme) private var scheme
10 10 @StateObject private var kaid = KAIDManager.shared
11 11
modified KA/Features/SearchView.swift +1 −1
@@ -118,7 +118,7 @@ struct SearchView: View {
118 118 }
119 119 ForEach(section.items.prefix(5)) { item in
120 120 NavigationLink(value: item) { KAItemRow(item: item) }
121 − .buttonStyle(.plain)
121 + .buttonStyle(KAPressStyle())
122 122 }
123 123 NavigationLink(value: section.universe.id) {
124 124 Text("Tout voir dans \(section.universe.name) →")
modified KA/Features/UniversesView.swift +5 −14
@@ -22,7 +22,7 @@ struct UniversesView: View {
22 22 NavigationLink(value: u.id) {
23 23 UniverseCard(universe: u, total: totals[u.id])
24 24 }
25 − .buttonStyle(.plain)
25 + .buttonStyle(KAPressStyle())
26 26 }
27 27 }
28 28 .padding(16)
@@ -172,7 +172,7 @@ struct UniverseHomeView: View {
172 172 LazyVStack(spacing: 12) {
173 173 hero
174 174 if loading {
175 − ProgressView().padding(40)
175 + ForEach(0..<6, id: \.self) { _ in KASkeletonRow() }
176 176 } else if let e = errorText, items.isEmpty {
177 177 KAEmptyState(symbol: "wifi.exclamationmark", title: "Impossible de charger",
178 178 message: e + "\nTirez pour réessayer.")
@@ -182,11 +182,12 @@ struct UniverseHomeView: View {
182 182 } else {
183 183 ForEach(items) { item in
184 184 NavigationLink(value: item) { KAItemRow(item: item) }
185 − .buttonStyle(.plain)
185 + .buttonStyle(KAPressStyle())
186 186 }
187 187 }
188 188 }
189 189 .padding(16)
190 + .animation(.snappy(duration: 0.3), value: items)
190 191 }
191 192 .navigationDestination(for: KAItem.self) { ItemDetailView(item: $0) }
192 193 .searchable(text: $query, prompt: "Chercher dans \(universe.name)…")
@@ -226,17 +227,7 @@ struct ItemDetailView: View {
226 227 ScrollView {
227 228 VStack(alignment: .leading, spacing: 16) {
228 229 if let img = item.imageURL {
229 − AsyncImage(url: img) { phase in
230 − switch phase {
231 − case .success(let image):
232 − image.resizable().aspectRatio(contentMode: .fill)
233 − case .failure:
234 − EmptyView()
235 − default:
236 − (universe?.accent ?? .gray).opacity(0.1)
237 − .overlay(ProgressView())
238 − }
239 − }
230 + KAImage(url: img, accent: universe?.accent ?? .gray, symbol: universe?.symbol ?? "photo")
240 231 .frame(maxWidth: .infinity)
241 232 .frame(height: 230)
242 233 .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
added docs/screenshots/05-accueil-photos.png +0 −0

Binary file not shown.

modified project.yml +1 −1
@@ -10,7 +10,7 @@ settings:
10 10 base:
11 11 SWIFT_VERSION: "5.0"
12 12 MARKETING_VERSION: "1.0.0"
13 − CURRENT_PROJECT_VERSION: "4"
13 + CURRENT_PROJECT_VERSION: "5"
14 14 DEVELOPMENT_TEAM: "3YM54G49SN"
15 15 CODE_SIGN_STYLE: Automatic
16 16 GENERATE_INFOPLIST_FILE: true
17 17