feat(ui): Cardecky-Web-Design — Fan-Stack-Tiles + CardSurface

UI-Refactor angelehnt an cards/apps/web. Drei Killer-Patterns
übernommen:

1. CardSurface (Sources/Core/Theme/CardSurface.swift)
   - Drei Sizes md/lg/hero mit identischem Border-Radius 14pt,
     1pt Border, layered Shadows je nach Elevation
   - Aspect-Ratio 5:7 für md/hero, 12:16.8 für lg
   - Optional Color-Accent-Stripe links (6pt, deck.color)

2. DeckStackTile (Sources/Features/Decks/DeckStackTile.swift)
   - Spielkarten-Stack-Visual: 3 gestaffelt-rotierte
     Hintergrund-Layer hinter der CardSurface
   - Layer-Offsets + Tilts deterministisch aus Deck-ID gehasht
     (gleiches Deck = gleiche Asymmetrie)
   - Inhalt: Category-Icon oben rechts, Titel + Description
     zentriert, Counts unten als Pill für dueCount

3. RatingBar mit Good-Emphasis (Features/Study/RatingBar.swift)
   - "Good" als full primary background (hero action)
   - again/hard/easy mit subtle border-tint + opacity-08-Background
   - Keyboard-Shortcut im Button-Label als kbd-Style-Pill

DeckListView komplett umgebaut:
- Horizontale ScrollView mit scrollTransition + viewAligned-Snap
- Zwei Sektionen: "Eigene Decks" und "Abonniert"
- Inbox-Banner als highlight (primary opacity 0.08 mit border)
- Pending-Share-Banner mit warning-Tint
- Section-Headers mit Icon + Title + Count

StudySessionView.cardSurface nutzt jetzt CardSurface(.hero, .raised).

Build 6 → 7. Drei native Targets bauen, 35 Tests grün.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-13 17:28:11 +02:00
parent 0b0872c8c0
commit aa94601409
6 changed files with 396 additions and 162 deletions

View file

@ -0,0 +1,108 @@
import SwiftUI
/// Wiederverwendbare Karten-Hülle in drei Größen entspricht den
/// Web-`CardSurface.svelte`-Varianten. Konsistenter Border-Radius (14pt),
/// gleicher Border-Stil, gleiche Shadow-Behandlung über alle Größen,
/// optional ein linker Color-Accent-Streifen.
///
/// Spec aus `cards/apps/web/src/lib/components/CardSurface.svelte`:
/// - Alle Größen Border-Radius 0.875rem (14pt)
/// - Border 1px hsl(--color-border)
/// - Background hsl(--color-surface)
/// - Aspect-Ratio 5/7 für `.md` und `.hero`, fix für `.lg`
struct CardSurface<Content: View>: View {
enum Size: Sendable {
case md // Deck-Tile in der Liste (max-width 18rem)
case lg // Fan-Detail (12rem x 16.8rem)
case hero // Study-Lernkarte (max-width 24rem)
}
enum Elevation: Sendable {
case flat // Subtle shadow
case standard // Default Karten-Shadow
case raised // Study-Hero
}
let size: Size
let elevation: Elevation
let colorAccentHex: String?
let content: () -> Content
init(
size: Size = .md,
elevation: Elevation = .standard,
colorAccentHex: String? = nil,
@ViewBuilder content: @escaping () -> Content
) {
self.size = size
self.elevation = elevation
self.colorAccentHex = colorAccentHex
self.content = content
}
var body: some View {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(CardsTheme.surface)
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(CardsTheme.border, lineWidth: 1)
)
if let colorAccentHex {
Color.swatchFromHex(colorAccentHex)
.frame(width: 6)
.clipShape(
UnevenRoundedRectangle(
topLeadingRadius: 14,
bottomLeadingRadius: 14,
bottomTrailingRadius: 0,
topTrailingRadius: 0,
style: .continuous
)
)
}
content()
.padding(EdgeInsets(top: 16, leading: 22, bottom: 18, trailing: 16))
}
.frame(maxWidth: maxWidth)
.aspectRatio(aspectRatio, contentMode: .fit)
.shadow(color: shadowColor, radius: shadowRadius, x: 0, y: shadowY)
}
private var maxWidth: CGFloat? {
switch size {
case .md: 288 // 18rem
case .lg: 192 // 12rem
case .hero: 384 // 24rem
}
}
private var aspectRatio: CGFloat? {
switch size {
case .md, .hero: 5.0 / 7.0
case .lg: 12.0 / 16.8
}
}
private var shadowColor: Color {
CardsTheme.foreground.opacity(elevation == .raised ? 0.18 : 0.08)
}
private var shadowRadius: CGFloat {
switch elevation {
case .flat: 3
case .standard: 8
case .raised: 18
}
}
private var shadowY: CGFloat {
switch elevation {
case .flat: 1
case .standard: 4
case .raised: 12
}
}
}

View file

@ -2,8 +2,9 @@ import ManaCore
import SwiftData import SwiftData
import SwiftUI import SwiftUI
/// β-1 Hauptbildschirm: Liste aller Decks mit Card- und Due-Counts. /// Decks-Hauptbildschirm im Cardecky-Look: horizontale Scroll-Reihen
/// Web-Vorbild: `cards/apps/web/src/routes/decks/+page.svelte`. /// mit Fan-Stack-Karten-Tiles. Web-Vorbild:
/// `cards/apps/web/src/routes/decks/+page.svelte`.
struct DeckListView: View { struct DeckListView: View {
@Environment(AuthClient.self) private var auth @Environment(AuthClient.self) private var auth
@Environment(\.modelContext) private var context @Environment(\.modelContext) private var context
@ -69,25 +70,107 @@ struct DeckListView: View {
if decks.isEmpty { if decks.isEmpty {
emptyState emptyState
} else { } else {
List { ScrollView {
VStack(alignment: .leading, spacing: 24) {
pendingShareSection pendingShareSection
inboxBannerSection inboxBanner
ownDecksSection deckSection(title: "Eigene Decks", icon: "rectangle.stack", decks: ownDecks)
if !subscribedDecks.isEmpty {
deckSection(title: "Abonniert", icon: "globe", decks: subscribedDecks)
} }
.listStyle(.plain) }
.scrollContentBackground(.hidden) .padding(.vertical, 12)
}
}
}
private var ownDecks: [CachedDeck] {
decks.filter { !$0.isFromMarketplace }
}
private var subscribedDecks: [CachedDeck] {
decks.filter { $0.isFromMarketplace }
}
@ViewBuilder
private func deckSection(title: String, icon: String, decks: [CachedDeck]) -> some View {
if !decks.isEmpty {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 6) {
Image(systemName: icon)
.foregroundStyle(CardsTheme.primary)
Text(title)
.font(.title3.weight(.semibold))
.foregroundStyle(CardsTheme.foreground)
Text("\(decks.count)")
.font(.subheadline)
.foregroundStyle(CardsTheme.mutedForeground)
}
.padding(.horizontal, 20)
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 16) {
ForEach(decks) { deck in
NavigationLink(value: deck.id) {
DeckStackTile(deck: deck)
.frame(width: 240)
}
.buttonStyle(.plain)
.scrollTransition(.animated) { content, phase in
content
.scaleEffect(phase.isIdentity ? 1 : 0.92)
.opacity(phase.isIdentity ? 1 : 0.7)
}
}
}
.padding(.horizontal, 20)
.padding(.bottom, 12)
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
}
}
}
@ViewBuilder
private var inboxBanner: some View {
if let inbox = decks.first(where: { $0.isFromMarketplace && $0.dueCount > 0 }) {
HStack(spacing: 12) {
Image(systemName: "tray.full.fill")
.font(.title3)
.foregroundStyle(CardsTheme.primary)
VStack(alignment: .leading, spacing: 2) {
Text("Inbox")
.font(.subheadline.weight(.semibold))
.foregroundStyle(CardsTheme.foreground)
Text("\(inbox.dueCount) fällige Karten aus abonnierten Decks")
.font(.caption)
.foregroundStyle(CardsTheme.mutedForeground)
}
Spacer()
Image(systemName: "chevron.right")
.font(.footnote)
.foregroundStyle(CardsTheme.mutedForeground)
}
.padding(14)
.background(CardsTheme.primary.opacity(0.08), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(CardsTheme.primary.opacity(0.18), lineWidth: 1)
)
.padding(.horizontal, 20)
} }
} }
@ViewBuilder @ViewBuilder
private var pendingShareSection: some View { private var pendingShareSection: some View {
if !pendingShares.isEmpty { if !pendingShares.isEmpty {
Section { VStack(alignment: .leading, spacing: 8) {
ForEach(pendingShares) { share in ForEach(pendingShares) { share in
NavigationLink(value: PendingShareRoute(share: share)) { NavigationLink(value: PendingShareRoute(share: share)) {
HStack(spacing: 12) { HStack(spacing: 12) {
Image(systemName: "square.and.arrow.down") Image(systemName: "square.and.arrow.down")
.foregroundStyle(CardsTheme.primary) .foregroundStyle(CardsTheme.warning)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Aus Teilen-Menü") Text("Aus Teilen-Menü")
.font(.subheadline.weight(.semibold)) .font(.subheadline.weight(.semibold))
@ -98,16 +181,17 @@ struct DeckListView: View {
.lineLimit(2) .lineLimit(2)
} }
Spacer() Spacer()
Image(systemName: "chevron.right")
.font(.footnote)
.foregroundStyle(CardsTheme.mutedForeground)
} }
.padding() .padding(14)
.background(CardsTheme.warning.opacity(0.12), in: RoundedRectangle(cornerRadius: 10)) .background(CardsTheme.warning.opacity(0.12), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
} }
} }
.padding(.horizontal, 20)
} }
} }
@ -131,7 +215,7 @@ struct DeckListView: View {
Label("Noch keine Decks", systemImage: "rectangle.stack") Label("Noch keine Decks", systemImage: "rectangle.stack")
.foregroundStyle(CardsTheme.foreground) .foregroundStyle(CardsTheme.foreground)
} description: { } description: {
Text("Erstelle dein erstes Deck auf cardecky.mana.how oder ziehe nach unten zum Aktualisieren.") Text("Tippe oben auf »+«, um dein erstes Deck zu erstellen, oder browse den Marketplace im Entdecken-Tab.")
.foregroundStyle(CardsTheme.mutedForeground) .foregroundStyle(CardsTheme.mutedForeground)
} }
} }
@ -139,45 +223,6 @@ struct DeckListView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
} }
@ViewBuilder
private var inboxBannerSection: some View {
if let inbox = decks.first(where: { $0.isFromMarketplace && $0.dueCount > 0 }) {
Section {
HStack(spacing: 12) {
Image(systemName: "tray.full.fill")
.foregroundStyle(CardsTheme.primary)
VStack(alignment: .leading, spacing: 2) {
Text("Inbox")
.font(.subheadline.weight(.semibold))
.foregroundStyle(CardsTheme.foreground)
Text("\(inbox.dueCount) fällige Karten aus abonnierten Decks")
.font(.caption)
.foregroundStyle(CardsTheme.mutedForeground)
}
Spacer()
}
.padding()
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 10))
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
}
}
}
private var ownDecksSection: some View {
Section {
ForEach(decks) { deck in
NavigationLink(value: deck.id) {
DeckRow(deck: deck)
}
.buttonStyle(.plain)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
}
}
}
@ToolbarContentBuilder @ToolbarContentBuilder
private var toolbar: some ToolbarContent { private var toolbar: some ToolbarContent {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
@ -205,78 +250,3 @@ struct DeckListView: View {
return "person.crop.circle.badge.exclamationmark" return "person.crop.circle.badge.exclamationmark"
} }
} }
/// Einzelne Deck-Zeile in der Liste.
struct DeckRow: View {
let deck: CachedDeck
var body: some View {
HStack(spacing: 12) {
// Farbiger Streifen aus deck.color (Hex), default forest-primary
RoundedRectangle(cornerRadius: 3)
.fill(deckColor)
.frame(width: 4)
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(deck.name)
.font(.headline)
.foregroundStyle(CardsTheme.foreground)
if deck.isFromMarketplace {
Image(systemName: "globe")
.font(.caption)
.foregroundStyle(CardsTheme.mutedForeground)
}
}
if let category = deck.category {
Text(category.label)
.font(.caption)
.foregroundStyle(CardsTheme.mutedForeground)
}
HStack(spacing: 12) {
Label("\(deck.cardCount)", systemImage: "rectangle.stack")
.font(.caption)
.foregroundStyle(CardsTheme.mutedForeground)
if deck.dueCount > 0 {
Label("\(deck.dueCount) fällig", systemImage: "clock.badge.exclamationmark")
.font(.caption.weight(.semibold))
.foregroundStyle(CardsTheme.primary)
}
}
}
Spacer()
Image(systemName: "chevron.right")
.font(.footnote)
.foregroundStyle(CardsTheme.mutedForeground)
}
.padding(.vertical, 12)
.padding(.horizontal, 12)
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 10))
}
private var deckColor: Color {
guard let hex = deck.color, let rgb = parseHex(hex) else {
return CardsTheme.primary
}
return Color.manaHexLocal(rgb)
}
private func parseHex(_ hex: String) -> UInt32? {
var trimmed = hex.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.hasPrefix("#") { trimmed = String(trimmed.dropFirst()) }
return UInt32(trimmed, radix: 16)
}
}
private extension Color {
/// Lokales Hex-Helper analog zu `ManaTokens.Color.manaHex`. Hier
/// dupliziert, weil DeckRow nicht von ManaTokens abhängen muss.
static func manaHexLocal(_ rgb: UInt32) -> Color {
let r = Double((rgb >> 16) & 0xFF) / 255.0
let g = Double((rgb >> 8) & 0xFF) / 255.0
let b = Double(rgb & 0xFF) / 255.0
return Color(red: r, green: g, blue: b)
}
}

View file

@ -0,0 +1,131 @@
import SwiftUI
/// Spiel-Karten-Stack-Visual mit drei gestaffelt-rotierten Hintergrund-
/// Layern hinter einer `CardSurface`. Web-Vorbild:
/// `cards/apps/web/src/lib/components/DeckStack.svelte`.
///
/// Die Layer-Offsets + Tilts sind deterministisch aus der Deck-ID
/// gehasht gleiches Deck zeigt immer gleiche Asymmetrie.
struct DeckStackTile: View {
let deck: CachedDeck
var body: some View {
ZStack {
// Drei Hintergrund-Layer (von hinten nach vorne)
ForEach(Array(layers.enumerated()), id: \.offset) { _, layer in
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(CardsTheme.surface)
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(CardsTheme.border, lineWidth: 1)
)
.opacity(layer.opacity)
.rotationEffect(.degrees(layer.tilt))
.offset(x: layer.dx, y: layer.dy)
.shadow(color: CardsTheme.foreground.opacity(0.05), radius: 2, y: 1)
}
CardSurface(size: .md, elevation: .standard, colorAccentHex: deck.color) {
cardContent
}
}
.aspectRatio(5.0 / 7.0, contentMode: .fit)
.frame(maxWidth: 280)
}
private var cardContent: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .top) {
Spacer()
Image(systemName: deck.category?.systemImageName ?? "rectangle.stack")
.font(.title3)
.foregroundStyle(CardsTheme.mutedForeground.opacity(0.85))
}
Spacer(minLength: 0)
VStack(alignment: .leading, spacing: 6) {
Text(deck.name)
.font(.system(size: 17, weight: .semibold))
.foregroundStyle(CardsTheme.foreground)
.lineLimit(3)
if let description = deck.deckDescription, !description.isEmpty {
Text(description)
.font(.caption)
.foregroundStyle(CardsTheme.mutedForeground)
.lineLimit(2)
}
}
Spacer(minLength: 0)
HStack(spacing: 8) {
Label("\(deck.cardCount)", systemImage: "rectangle.stack")
.font(.caption2)
.foregroundStyle(CardsTheme.mutedForeground)
if deck.dueCount > 0 {
Text("\(deck.dueCount) fällig")
.font(.caption2.weight(.semibold))
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(CardsTheme.primary.opacity(0.15), in: Capsule())
.foregroundStyle(CardsTheme.primary)
}
Spacer()
if deck.isFromMarketplace {
Image(systemName: "globe")
.font(.caption2)
.foregroundStyle(CardsTheme.mutedForeground)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
/// Deterministische Stack-Layer aus Deck-ID gehasht.
private var layers: [StackLayer] {
var hash = UInt64(0)
for byte in deck.id.utf8 {
hash = hash &* 31 &+ UInt64(byte)
}
return (0 ..< 3).map { index in
let seed = hash &+ UInt64(index) &* 17
let tiltRaw = Double((seed >> 8) & 0xFF) / 255.0 - 0.5
let xRaw = Double((seed >> 16) & 0xFF) / 255.0 - 0.5
let yRaw = Double((seed >> 24) & 0xFF) / 255.0 - 0.5
let depth = Double(index + 1)
return StackLayer(
tilt: tiltRaw * 4.0,
dx: xRaw * 6.0,
dy: depth * 3.0 + yRaw * 2.0,
opacity: 0.7 - depth * 0.18
)
}
}
}
private struct StackLayer {
let tilt: Double
let dx: Double
let dy: Double
let opacity: Double
}
private extension DeckCategory {
var systemImageName: String {
switch self {
case .language: "character.book.closed"
case .medicine: "cross.case"
case .science: "atom"
case .math: "function"
case .history: "scroll"
case .law: "scale.3d"
case .technology: "cpu"
case .arts: "paintbrush"
case .music: "music.note"
case .sport: "figure.run"
case .other: "rectangle.stack"
}
}
}

View file

@ -4,8 +4,9 @@ import SwiftUI
import UIKit import UIKit
#endif #endif
/// Vier Rating-Buttons unten am Bildschirm. Tap onRate(rating) /// Vier Rating-Buttons mit emphasis auf "Good" (full-width primary).
/// plus Haptic-Feedback. /// Web-Vorbild: `cards/apps/web/src/routes/study/[deckId]/+page.svelte`
/// `.grade.again/.hard/.good/.easy`-Klassen.
struct RatingBar: View { struct RatingBar: View {
let onRate: (Rating) -> Void let onRate: (Rating) -> Void
@ -16,17 +17,24 @@ struct RatingBar: View {
triggerHaptic(for: rating) triggerHaptic(for: rating)
onRate(rating) onRate(rating)
} label: { } label: {
VStack(spacing: 2) { HStack(spacing: 6) {
Text(rating.label) Text(rating.label)
.font(.subheadline.weight(.semibold)) .font(.subheadline.weight(.semibold))
Text(rating.shortcut) Text(rating.shortcut)
.font(.caption2) .font(.caption2.weight(.semibold))
.foregroundStyle(.secondary) .padding(.horizontal, 5)
.padding(.vertical, 1)
.background(kbdBackground(for: rating), in: RoundedRectangle(cornerRadius: 4))
.foregroundStyle(kbdForeground(for: rating))
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 14) .padding(.vertical, 14)
.background(background(for: rating), in: RoundedRectangle(cornerRadius: 10)) .background(background(for: rating), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
.foregroundStyle(foreground(for: rating)) .foregroundStyle(foreground(for: rating))
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.stroke(borderColor(for: rating), lineWidth: rating == .good ? 0 : 1)
)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@ -34,12 +42,14 @@ struct RatingBar: View {
.padding(.horizontal, 16) .padding(.horizontal, 16)
} }
/// `good` ist die Hero-Action (primary full background) analog
/// zum Web-Default-Klick. Andere bekommen subtle tinted borders.
private func background(for rating: Rating) -> Color { private func background(for rating: Rating) -> Color {
switch rating { switch rating {
case .again: CardsTheme.error.opacity(0.12) case .again: CardsTheme.error.opacity(0.06)
case .hard: CardsTheme.warning.opacity(0.12) case .hard: CardsTheme.warning.opacity(0.06)
case .good: CardsTheme.primary.opacity(0.12) case .good: CardsTheme.primary
case .easy: CardsTheme.success.opacity(0.12) case .easy: CardsTheme.success.opacity(0.06)
} }
} }
@ -47,17 +57,37 @@ struct RatingBar: View {
switch rating { switch rating {
case .again: CardsTheme.error case .again: CardsTheme.error
case .hard: CardsTheme.warning case .hard: CardsTheme.warning
case .good: CardsTheme.primary case .good: CardsTheme.primaryForeground
case .easy: CardsTheme.success case .easy: CardsTheme.success
} }
} }
private func borderColor(for rating: Rating) -> Color {
switch rating {
case .again: CardsTheme.error.opacity(0.4)
case .hard: CardsTheme.warning.opacity(0.4)
case .good: .clear
case .easy: CardsTheme.success.opacity(0.4)
}
}
private func kbdBackground(for rating: Rating) -> Color {
rating == .good
? CardsTheme.primaryForeground.opacity(0.18)
: CardsTheme.muted
}
private func kbdForeground(for rating: Rating) -> Color {
rating == .good
? CardsTheme.primaryForeground.opacity(0.85)
: CardsTheme.mutedForeground
}
private func triggerHaptic(for rating: Rating) { private func triggerHaptic(for rating: Rating) {
#if canImport(UIKit) #if canImport(UIKit)
let generator = UIImpactFeedbackGenerator( let style: UIImpactFeedbackGenerator.FeedbackStyle =
style: rating == .easy ? .heavy : .medium rating == .easy ? .heavy : .medium
) UIImpactFeedbackGenerator(style: style).impactOccurred()
generator.impactOccurred()
#endif #endif
} }
} }

View file

@ -101,19 +101,14 @@ struct StudySessionView: View {
} }
private func cardSurface(due: DueReview, isFlipped: Bool) -> some View { private func cardSurface(due: DueReview, isFlipped: Bool) -> some View {
RoundedRectangle(cornerRadius: 16) CardSurface(size: .hero, elevation: .raised) {
.fill(CardsTheme.surface)
.overlay(
CardRenderer( CardRenderer(
card: due.card, card: due.card,
subIndex: due.review.subIndex, subIndex: due.review.subIndex,
isFlipped: isFlipped isFlipped: isFlipped
) )
) .frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay( }
RoundedRectangle(cornerRadius: 16)
.stroke(CardsTheme.border, lineWidth: 1)
)
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.top, 12) .padding(.top, 12)
} }

View file

@ -55,7 +55,7 @@ targets:
path: Sources/Resources/Info.plist path: Sources/Resources/Info.plist
properties: properties:
CFBundleShortVersionString: "0.1.0" CFBundleShortVersionString: "0.1.0"
CFBundleVersion: "6" CFBundleVersion: "7"
CFBundleDevelopmentRegion: de CFBundleDevelopmentRegion: de
CFBundleDisplayName: Cardecky CFBundleDisplayName: Cardecky
LSApplicationCategoryType: "public.app-category.education" LSApplicationCategoryType: "public.app-category.education"
@ -111,7 +111,7 @@ targets:
properties: properties:
CFBundleDisplayName: Als Karte speichern CFBundleDisplayName: Als Karte speichern
CFBundleShortVersionString: "0.1.0" CFBundleShortVersionString: "0.1.0"
CFBundleVersion: "6" CFBundleVersion: "7"
NSExtension: NSExtension:
NSExtensionPointIdentifier: com.apple.share-services NSExtensionPointIdentifier: com.apple.share-services
NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareViewController NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareViewController
@ -144,7 +144,7 @@ targets:
properties: properties:
CFBundleDisplayName: Cardecky Widget CFBundleDisplayName: Cardecky Widget
CFBundleShortVersionString: "0.1.0" CFBundleShortVersionString: "0.1.0"
CFBundleVersion: "6" CFBundleVersion: "7"
NSExtension: NSExtension:
NSExtensionPointIdentifier: com.apple.widgetkit-extension NSExtensionPointIdentifier: com.apple.widgetkit-extension
entitlements: entitlements: