
Мне всегда хотелось создать в SwiftUI «родное» View
для игральных карт, не уступающее по качеству игральным картам, которые уже создал Пол Хэгерти на далеком стэнфордском курсе CS193P 2017 Лекция 6 в UIKit. Наиболее сложной частью было правильное расположение на карте символов масти для числовых карт (2 -10). Я даже интегрировала созданную Полом Хэгерти в UIKit игральную карту в SwiftUI, учитывая легкость интеграции UIViews в SwiftUI. Но это все-таки не «родное» SwiftUI View
.
Я обратилась к ИИ: Grok 3 Think, ChatGPT 3.0-mini Reason, Claude 3.7, Gemini 2.0 Flash, DeepSeek, Qwen 2.5 Max — с запросом о создании игральных карт в SwiftUI.
Самый впечатляющий результат я получила от Grok 3 Think, который предложил компактный и очень гибкий способ конструирования числовых карт (2-10) в современном SwiftUI, тот же самый способ предложил и ChatGPT 3.0-mini Reason с небольшими различиями в реализации, а вот остальные представили довольно громоздкие способы конструирования таких карт, которые фактически сводились к индивидуальным комбинациям HStack
и VStack
для отдельных символов масти («♥️», «♦️», «♣️», «♠️») для каждого ранга (2-10).
Я хочу поделиться с вами протоколом взаимодействия с Grok 3 Think, в результате которого и был предложен этот самый оптимальный вариант.
Код находится на Github.
Представленный в этом посте материал может успешно использоваться для обучения программированию на SwiftUI.
Grok 3 при создании Playing Card

Чтобы создать игральную карту в SwiftUI, нам нужно разработать View
, представляющее стандартную игральную карту с мастью (червы hearts
, бубны diamonds, трефы clubs
или пики spades
) и рангом (от 2 до 10, валет Jack
, дама Queen
, король King
или туз Ace
). Ниже приведена полная реализация SwiftUI, которая включает необходимые перечисления enum
для мастей и рангов, Модель как структура struct PlayingCard
и View
для ее отображения.
Шаг 1: Определение масти с помощью перечисления enum Suit
Масть Suit
игральной карты может быть одним из четырех вариантов, каждый из которых имеет соответствующий символ и цвет (красный для червей hearts
и бубнов diamonds
, черный для треф clubs
и пик spades
). Мы будем использовать перечисление enum
с raw values
для символов и вычисляемым свойством color
для цвета.
import SwiftUI
enum Suit: String {
case hearts = "♥️"
case diamonds = "♦️"
case clubs = "♣️"
case spades = "♠️"
var color: Color {
switch self {
case .hearts, .diamonds:
return .red
case .clubs, .spades:
return .black
}
}
}
Шаг 2: Определите рангов с помощью перечисления enum Rank
Ранг Rank
представляет собой значение карты, которое может быть числом number
от 2 до 10 или картинкой (валет Jack
, дама Queen
, король King
, туз Ace
). Мы будем использовать перечисление enum
с raw values
для чисел number
и отдельными вариантами case
для карт с картинками, а также вычисляемое свойство display
для отображения ранга в виде строки String
.
enum Rank {
case number(Int)
case jack
case queen
case king
case ace
var display: String {
switch self {
case .number(let num):
return String(num)
case .jack:
return "J"
case .queen:
return "Q"
case .king:
return "K"
case .ace:
return "A"
}
}
}
Шаг 3: Определите модель игральной карты
Структура struct PlayingCard
объединяет масть suit
и ранг rank
для View одной карты.
struct PlayingCard {
let suit: Suit
let rank: Rank
}
Шаг 4: Создание многоразового RankSuitView
Поскольку ранг rank
и масть suit
отображаются как в верхнем левом, так и в нижнем правом углах карты, мы создадим многоразовый RankSuitView
, чтобы отобразить их вертикально.
struct RankSuitView: View {
let rank: String
let suit: String
var body: some View {
VStack {
Text(rank)
.font(.largeTitle)
Text(suit)
.font(.largeTitle)
}
}
}
Шаг 5: Создание CardView
CardView
— это основное SwiftUI View
, отображающее игральную карту. Оно использует ZStack
для наложения белого прямоугольника с черной границей в качестве фона и VStack
с HStacks
для размещения ранга rank
и масти suit
в углах. Нижний правый угол повернут на 180 градусов, чтобы имитировать внешний вид стандартной игральной карты.
struct CardView: View {
let card: PlayingCard
var body: some View {
ZStack {
// Card background
RoundedRectangle(cornerRadius: 10)
.fill(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black, lineWidth: 2)
)
// Rank and suit in corners
VStack {
HStack {
RankSuitView(rank: card.rank.display,
suit: card.suit.rawValue)
Spacer()
}
Spacer()
HStack {
Spacer()
RankSuitView(rank: card.rank.display,
suit: card.suit.rawValue)
.rotationEffect(.degrees(180))
}
}
.padding()
.foregroundColor(card.suit.color)
}
.aspectRatio(2/3, contentMode: .fit)
}
}
- Фон:
RoundedRectangle
с белой заливкойfill
и черной границейstroke
придает карте ее форму. - Макет:
VStack
размещает содержимое сверху и снизу карты, в то время какHStacks
соSpacers
смещаетRankSuitView
влево (вверх) и вправо (вниз). - Вращение: нижний
RankSuitView
поворачивается на 180 градусов, чтобы отображаться перевернутым, как в традиционных игральных картах. - Цвет:
foregroundColor
устанавливается на основе цвета мастиcard.suit.color
. - Соотношение сторон aspectRatio: соотношение сторон 2:3 гарантирует, что карта сохраняет стандартные пропорции.
Шаг 6: Пример использования
Чтобы увидеть карту в действии, вы можете использовать ее в ContentView
следующим образом:
struct ContentView: View {
var body: some View {
CardView(card: PlayingCard(suit: .hearts, rank: .ace))
}
}
#Preview {
ContentView()
.padding()
}
В этом примере изображен Туз червей с красными буквами «A» и «♥️» в верхнем левом и нижнем правом углах (последняя перевернута).

Примечания
- Простота: эта реализация фокусируется на базовом текстовом дизайне. Для числовых карт (например, 5 пик) традиционные карты показывают символ масти, повторяющийся в центре, но это опущено для простоты.
- Улучшения: вы можете добавить центральные символы масти, анимацию переворачивания карт или изображения для лицевых карт в качестве будущих улучшений.
- Проверка: случай
Rank.number
принимает любое целое число; в реальном приложении вы можете ограничить его до 2–10.
Этот код предоставляет функциональную и визуально привлекательную игральную карту в SwiftUI, подходящую для использования в карточной игре или аналогичном приложении.
Добавляем символы или изображение в центр карты

Чтобы добавить символы или изображения в игральные карты в приложении SwiftUI, вы можете улучшить CardView
, чтобы отображать символы мастей для числовых карт (от 2 до 10) и заполнитель или изображение для карт с картинками (валет, дама, король, туз). Ниже я объясню, как добиться этого шаг за шагом, и предоставлю полную реализацию.
Шаг 1: Разработайте макет карты
Игральная карта должна иметь:
- Белый фон с черной рамкой.
- Ранг и масть, отображаемые в верхнем левом и нижнем правом углах.
- Центральный контент, показывающий символы масти для числовых карт или заполнитель/изображение для карт с картинками.
Мы будем использовать ZStack
для наложения этих элементов и VStack
для позиционирования центрального контента.
Шаг 2: Добавьте символы масти для числовых карт
Для числовых карт (например, 5 червей) карта должна отображать символ масти (например, ♥️), повторяющийся в соответствии с рангом карты (например, пять червей). Мы будем использовать цикл ForEach
для динамической генерации символов.
Шаг 3: Обработайте карты с картинками
Для карт с картинками (валет, дама, король, туз) мы отобразим простой заполнитель, например букву ранга (например, «K» для короля) крупным шрифтом. При желании вы можете заменить его изображением из своего каталога активов asset catalog
для более изысканного вида.
Реализация
Вот обновленная реализация CardView
в SwiftUI:
import SwiftUI
// Define the PlayingCard model
struct PlayingCard {
let suit: Suit
let rank: Rank
enum Suit: String {
case hearts = "♥️"
case diamonds = "♦️"
case clubs = "♣️"
case spades = "♠️"
var color: Color {
switch self {
case .hearts, .diamonds:
return .red
case .clubs, .spades:
return .black
}
}
}
enum Rank {
case number(Int)
case jack, queen, king, ace
var display: String {
switch self {
case .number(let num): return "\(num)"
case .jack: return "J"
case .queen: return "Q"
case .king: return "K"
case .ace: return "A"
}
}
}
}
// CardView to display the card
struct CardView: View {
let card: PlayingCard
var body: some View {
ZStack {
// Card background
RoundedRectangle(cornerRadius: 10)
.fill(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black, lineWidth: 2)
)
// Central content
if case .number(let num) = card.rank {
// Number cards: display suit symbols
VStack {
ForEach(0..<num, id: \.self) { _ in
Text(card.suit.rawValue)
.font(.largeTitle)
}
}
} else {
// Face cards: display rank as placeholder
Text(card.rank.display)
.font(.system(size: 100))
}
// Rank and suit in corners
VStack {
HStack {
RankSuitView(rank: card.rank.display,
suit: card.suit.rawValue)
Spacer()
}
Spacer()
HStack {
Spacer()
RankSuitView(rank: card.rank.display,
suit: card.suit.rawValue)
.rotationEffect(.degrees(180))
}
}
.padding()
.foregroundColor(card.suit.color)
}
.aspectRatio(2/3, contentMode: .fit)
}
}
// Helper view for rank and suit in corners
struct RankSuitView: View {
let rank: String
let suit: String
var body: some View {
VStack(spacing: 2) {
Text(rank)
.font(.headline)
Text(suit)
.font(.subheadline)
}
}
}
// Example usage
struct ContentView: View {
var body: some View {
VStack {
CardView(card: PlayingCard(suit: .clubs, rank: .ace))
CardView(card: PlayingCard(suit: .hearts, rank: .number(5)))
}
}
}
#Preview {
ContentView()
}
Как это работает
- Фон:
RoundedRectangle
создает белую карту с черной рамкой. - Центр карты:
- Для числовых карт цикл
ForEach
отображает символ масти (например, ♥️), повторяющийсяnumber
раз (например, 5 раз для 5 червей). - Для карт с картинками текст отображает ранг (например, «K» для короля) крупным шрифтом с цветом масти (красный для червей/бубнов, черный для трефов / пик).
- Для числовых карт цикл
- Углы:
RankSuitView
отображает ранг и масть в верхнем левом и нижнем правом углах, а нижняя правая версия повернута на 180 градусов для удобства чтения.
Пример вывода
- 5 червей: отображает пять символов в центре, с «5» и ♥️ по углам.
- Король пик: отображается как большая «К» в центре черным цветом, с «К» и ♠️ по углам.
Улучшения
- Расположение символов: для числовых карт вы можете расположить символы в сетке или узоре (например, два ряда по три для 6 ♣️) вместо вертикального стека. Измените
VStack
на сетку или пользовательский макет для этого.
- Изображения карт с картинками: замените текст заполнителя
Text
наImage View
, например, так:
Image(card.rank.display.lowercased()) // e.g., "king" image from assets
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
Добавьте соответствующие изображения (например, jack.png, queen.png) в свой каталог активов asset catalog.
3. Стиль: отрегулируйте размер шрифта, интервал или цвет символов в соответствии с предпочтениями вашего дизайна.
Эта реализация обеспечивает простой, но эффективный способ добавления символов мастей к числовым картам и заполнителей для карт с картинками, делая ваши игральные карты визуально привлекательными в SwiftUI.

В каталоге активов asset catalog
размещаем изображения:

И для карт с картинками размещаем изображения:
// Central content
if case .number(let num) = card.rank {
// Number cards: display suit symbols
VStack {
ForEach(0..<num, id: \.self) { _ in
Text(card.suit.rawValue)
.font(.largeTitle)
}
}
} else if case .ace = card.rank {
Text(card.suit.rawValue)
.font(.system(size: 70))
.foregroundColor(card.suit.color)
} else {
Image(card.rank.display + card.suit.rawValue)
.resizable()
.scaledToFit()
}

Правильно размещаем символы в центре карты

Чтобы ответить на ваш запрос о размещении Text(card.suit.rawValue)
в центре карты, как на прикрепленном изображении 5 пик, я предоставляю решение, которое воспроизводит описанную компоновку с использованием SwiftUI. На изображении показана 5 пик с пятью символами пик, расположенными в определенном порядке: два вверху, один в центре и два внизу. Вот как вы можете добиться этого в своем CardView
.
Понимание требования
Ваш запрос указывает на размещение пяти Text(card.suit.rawValue)
на карте, а прикрепленное изображение предполагает, что для числовой карты, такой как 5 пик:
- Есть центральный символ масти.
- Дополнительные 4 символа (всего пять для ранга 5) расположены симметрично вокруг него.
Это означает, что для числовых карт количество символов масти соответствует рангу rank
, при этом один всегда находится в центре для нечетных рангов, таких как 5, а остальные располагаются в соответствии с рисунком на изображении.
Решение в SwiftUI
В SwiftUI вы можете использовать ZStack
(уже присутствует в вашем CardView
) для наложения элементов и GeometryReader
для точного позиционирования символов масти на основе размеров карты. Поскольку макет различается в зависимости от ранга rank, мы определим функцию symbolPositions (for rank: Int)
для определения позиций символов, пока сосредоточившись на ранге 5, чтобы соответствовать изображению, и предоставив резерв для других рангов.
Вот полная реализация для вашего CardView
:
struct CardView: View {
let card: PlayingCard
// Function to determine symbol positions based on rank
func symbolPositions(for rank: Int) -> [(CGFloat, CGFloat)] {
switch rank {
case 5:
// Define positions as fractions of width and height
return [
(0.25, 0.25), // Top left: slightly lower
(0.75, 0.25), // Top right: slightly higher
(0.5, 0.5), // Center
(0.25, 0.75), // Bottom left: slightly higher
(0.75, 0.75) // Bottom right: slightly lower
]
default:
// For other ranks, stack symbols vertically as a fallback
return (0..<rank).map { i in
(0.5, CGFloat(i + 1) / CGFloat(rank + 1))
}
}
}
var body: some View {
ZStack {
// Background (assuming you have this already)
RoundedRectangle(cornerRadius: 10)
.fill(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black, lineWidth: 2)
)
// Central content
if case .number(let num) = card.rank {
GeometryReader { geometry in
let positions = symbolPositions(for: num)
ForEach(0..<positions.count, id: \.self) { index in
let pos = positions[index]
Text(card.suit.rawValue)
.font(.largeTitle)
.position(x: geometry.size.width * pos.0,
y: geometry.size.height * pos.1)
}
}
} else if case .ace = card.rank {
Text(card.suit.rawValue)
.font(.system(size: 70))
} else {
Image(card.rank.display + card.suit.rawValue)
.resizable()
.scaledToFit()
}
// Corner elements (simplified example)
VStack {
HStack {
RankSuitView(rank: card.rank.display,
suit: card.suit.rawValue)
Spacer()
}
Spacer()
HStack {
Spacer()
RankSuitView(rank: card.rank.display,
suit: card.suit.rawValue)
.rotationEffect(.degrees(180))
}
}
.padding()
.foregroundColor(card.suit.color)
}
.aspectRatio(2/3, contentMode: .fit)
}
}
struct RankSuitView: View {
let rank: String
let suit: String
var body: some View {
VStack {
Text(rank)
.font(.headline)
.bold()
Text(suit)
.font(.headline)
}
}
}
// Assuming these are your PlayingCard and related types
struct PlayingCard {
let suit: Suit
let rank: Rank
}
enum Suit {
case spades, hearts, diamonds, clubs
var rawValue: String {
switch self {
case .spades: return "♠️"
case .hearts: return "♥️"
case .diamonds: return "♦️"
case .clubs: return "♣️"
}
}
var color: Color {
switch self {
case .spades, .clubs: return .black
case .hearts, .diamonds: return .red
}
}
}
enum Rank {
case number(Int)
case ace, jack, queen, king
var display: String {
switch self {
case .number(let num): return "\(num)"
case .ace: return "A"
case .jack: return "J"
case .queen: return "Q"
case .king: return "K"
}
}
}
Объяснение кода
- Структура
CardView
:
ZStack
накладывает фон, угловые элементы и центральное содержимое карты.- Соотношение сторон 2/3 гарантирует, что карта сохраняет стандартные пропорции игральной карты.
- Функция
symbolPositions
:
- Для ранга 5 она возвращает массив из пяти кортежей (x, y), представляющих позиции как доли ширины и высоты карты:
- Верхний ряд: (0,25, 0,25) и (0,75, 0,25 ) размещают два символа наверху.
- Центр: (0,5, 0,5) размещает один символ точно по центру.
- Нижний ряд: (0,25, 0,75) и (0,75, 0,75) размещают два символа внизу.
- Для других рангов размещение символов вы можете расширить позже.
- Центральное содержимое карты:
- Для числовых карт
(.number(let num))
GeometryReader
предоставляет размеры карты. - Цикл
ForEach
помещаетText(card.suit.rawValue)
в каждую позициюposition
, масштабированную по ширине и высоте карты. .font(.largeTitle)
обеспечивает размер символов масти.- Для карт с картинками размещается изображение из
.asset catalog
.
Как он соответствует изображению
- Центральный символ: Положение (0,5, 0,5) обеспечивает центрирование
Text(card.suit.rawValue),
что напрямую соответствует вашему запросу. - Полный узор для 5 пик: Дополнительные четыре символа расположены симметрично вокруг центра, соответствующими описанию изображения (вверху слева и справа, внизу слева и справа).
- Масштабируемость: использование
GeometryReader
гарантирует, что макет адаптируется к размеру карты, сохраняя пропорции.
Тестирование макета
Чтобы увидеть это в действии, создайте карту, например, такую:
let fiveOfSpades = PlayingCard(suit: .spades, rank: .number(5))
CardView(card: fiveOfSpades)
.frame(width: 200)
Это отобразит карту с пятью символами пик, расположенными как на изображении, с одним в центре и остальными вокруг него.
Расширение на другие ранги
Для полной реализации вы можете расширить symbolPositions(for:),
чтобы определить традиционные шаблоны для рангов со 2 по 10. Например:
- Ранг 2: [(0.5, 0.25), (0.5, 0.75)]
- Ранг 3: [(0.5, 0.25), (0.5, 0.5), (0.5, 0.75)]
- Ранг 4: [(0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)]
Другие ранги вы можете уточнить позже.
Заключение
Это решение помещает Text(card.suit.rawValue)
в центр карты, как и требовалось, а для 5 пик оно воспроизводит полный узор с прикрепленного изображения карты. Использование GeometryReader
и дробных позиций обеспечивает гибкость и точность, делая дизайн вашей карты функциональным и визуально согласованным с прикрепленным файлом.
Получаем:
Нам нужно исправить несколько вещей для числовых карт:
- Символы для числовых карт в нижней части карты должны размещаться “вверх ногами”.
- Размер шрифта символов должен масштабироваться в зависимости от размера карты.
- Угловые символы для любых карт должны также масштабироваться в зависимости от размера карты.
Но все по порядку.
Переворачиваем символы в нижней части карты

Чтобы выполнить ваш запрос на то, чтобы символы игральных карт в нижней части карты были перевернуты, нам нужно изменить CardView
, чтобы вращать определенные символы масти в зависимости от их вертикального положения. В стандартном дизайне игральных карт, например, в предоставленной вами 5 пик, символы ближе к нижней половине карты обычно поворачиваются на 180 градусов, чтобы быть «вверх ногами» относительно верхней половины. Это гарантирует, что карта будет выглядеть правильно при просмотре с любого конца.
Давайте обновим предыдущую реализацию, чтобы добиться этого. Основная идея заключается в проверке положения по оси Y каждого символа: если он находится в нижней половине карты (y > 0,5
), мы применим поворот на 180 градусов к Text View
, отображающему символ масти.
Обновленный код
Вот измененный CardView
с логикой вращения нижних символов:
import SwiftUI
struct CardView: View {
let card: PlayingCard
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black, lineWidth: 2)
)
// Central content
if case .number(let num) = card.rank {
// Number cards: display suit symbols
let positions = symbolPositions(for: num)
ForEach(0..<positions.count, id: \.self) { index in
let pos = positions[index]
Text(card.suit.rawValue)
.rotationEffect(pos.1 > 0.5 ?
.degrees(180) : .degrees(0))
.font(.largeTitle)
.position(x: geometry.size.width * pos.0,
y: geometry.size.height * pos.1)
}
} else if case .ace = card.rank {
Text(card.suit.rawValue)
.font(.system(size: 70))
} else {
Image(card.rank.display + card.suit.rawValue)
.resizable()
.scaledToFit()
}
// Rank and suit in corners
VStack {
HStack {
RankSuitView(rank: card.rank.display,
suit: card.suit.rawValue)
Spacer()
}
Spacer()
HStack {
Spacer()
RankSuitView(rank: card.rank.display,
suit: card.suit.rawValue)
.rotationEffect(.degrees(180))
}
}
.padding()
.foregroundColor(card.suit.color)
}
.aspectRatio(2/3, contentMode: .fit)
}
// Function to determine symbol positions based on rank
func symbolPositions(for rank: Int) -> [(CGFloat, CGFloat)] {
// Define positions as fractions of width and height
switch rank {
case 2:
return [
(0.5, 0.25 ), // Top
(0.5, 0.75 ) // Bottom
]
case 3:
return [
(0.5, 0.25 ), // Top
(0.5, 0.5 ), // Center
(0.5, 0.75 ) // Bottom
]
case 4:
return [
(0.25, 0.25 ), // Top left
(0.75, 0.25 ), // Top right
(0.25, 0.75 ), // Bottom left
(0.75, 0.75 ) // Bottom right
]
case 5:
return [
(0.25, 0.25), // Top left
(0.75, 0.25), // Top right
(0.5, 0.5), // Center
(0.25, 0.75), // Bottom left
(0.75, 0.75) // Bottom right
]
default:
// For other ranks, stack symbols vertically as a fallback
return (0..<rank).map { i in
(0.5, CGFloat(i + 1) / CGFloat(rank + 1))
}
}
}
}
struct RankSuitView: View {
let rank: String
let suit: String
var body: some View {
VStack {
Text(rank)
.font(.headline)
.bold()
Text(suit)
.font(.headline)
}
}
}
// PlayingCard and related types (unchanged from your setup)
struct PlayingCard {
let suit: Suit
let rank: Rank
}
enum Suit: String {
case .spades: return "♠️"
case .hearts: return "♥️"
case .diamonds: return "♦️"
case .clubs: return "♣️"
var color: Color {
switch self {
case .hearts, .diamonds: return .red
case .clubs, .spades: return .black
}
}
}
enum Rank {
case number(Int)
case jack, queen, king, ace
var display: String {
switch self {
case .number(let num): return "\(num)"
case .jack: return "J"
case .queen: return "Q"
case .king: return "K"
case .ace: return "A"
}
}
}
// Example usage
struct ContentView: View {
var body: some View {
CardView(card: PlayingCard(suit: .spades, rank: .number(5)))
.padding()
}
}
#Preview {
ContentView()
}
Ключевые изменения
- Логика вращения:
- Внутри цикла
ForEach
, который позиционирует символы масти, я добавил условный поворот:
rotationEffect(pos.1 > 0.5 ? .degrees(180) : .degrees(0))
- Здесь
pos.1
— это координатаy
(как доля высоты карты). Еслиpos.1 > 0.5,
символ находится в нижней половине карты, поэтому мы поворачиваем его на 180 градусов. В противном случае он остается в исходном вертикальном положении.
- Применение к 5 пик:
- На основе позиций, определенных в
symbolPositions(for: 5)
:
- Вверху слева: (0.25, 0.25) → y = 0.25 < 0.5, поворота нет.
- Вверху справа: (0,75, 0,25) → y = 0,25 < 0,5, без вращения.
- В центре: (0,5, 0,5) → y = 0,5, без вращения (центр обычно не вращается, но это можно изменить при желании).
- Внизу слева: (0,25, 0,75) → y = 0,75 > 0,5, поворот на 180 градусов.
- Внизу справа: (0,75, 0,75) → y = 0,75 > 0,5, поворот на 180 градусов.
- Это соответствует 5 пик на вашем изображении, где нижние две пики перевернуты.
- Никаких изменений для карт с картинками:
- Карты с картинками (использующие изображения или резервный текст) не затронуты этим изменением, поскольку они не используют логику позиционирования символов.
Как это соответствует изображению
На предоставленном вами изображении 5 пик:
- Два символа пик вверху находятся в исходном вертикальном положении.
- Центральный символ пик находится в исходном вертикальном положении.
- Два символа пик внизу перевернуты.
Обновленный код в точности повторяет это:
- Два верхних символа (
y < 0,5)
остаются в исходном вертикальном положении. - Центральный символ
(y = 0,5)
находится в исходном вертикальном положении, что является стандартом для большинства игральных карт. - Два нижних символа
(y > 0,5)
повернуты на 180 градусов и отображаются в перевернутом положении.
Регулировка центрального символа (не обязательно)
Если вы предпочитаете, чтобы центральный символ был повернут на основе другого правила (например, всегда в исходном вертикальном положении или совпадал с нижней половиной), вы можете изменить условие. Например, чтобы центр был явно исходно вертикальным, можно настроить логику:
.rotationEffect(pos.1 > 0.5 && pos.1 != 0.5 ? .degrees(180) : .degrees(0))
Однако текущая реализация (pos.1 > 0,5)
уже соответствует стандартному дизайну на вашем изображении, где центральный символ находится в исходном вертикальном положении.
Заключение
Добавив модификатор .rotationEffect
на основе положения Y каждого символа, мы гарантируем, что символы в нижней части карты будут перевернуты, как это стандартно в дизайне игральных карт. Это изменение повышает визуальную точность вашего CardView
, сохраняя при этом гибкость кода для разных рангов и мастей.
В результате получаем:
Адаптируем размер символов к размеру карты

Чтобы настроить размеры шрифтов в Text Views
в игральных картах — в частности, Text(card.suit.rawValue)
с .font(.system(size: 100))
, Text(card.suit.rawValue)
с .font(.largeTitle) и Text(rank)
с .font(.headline)
в RankSuitView
— так, чтобы они зависели от размера игральной карты с помощью GeometryReader
, нам нужно сделать размеры шрифтов динамическими и пропорциональными размерам карты. Вот как этого добиться:
Подход
Мы будем использовать GeometryReader
для доступа к ширине width
и высоте height
карты, а затем вычислим размеры шрифта как доли ширины карты cardWidth
, которая представляет собой min(width, height)
. Это гарантирует, что Text
будет масштабироваться соответствующим образом при изменении размера карты, сохраняя последовательный и визуально привлекательный дизайн.
- Центральные символы масти (карты с числами): замените
.font(.largeTitle)
размером, основанным на ширине картыcardWidth
. - Текст “туза” с символом масти: замените
.font(.system(size: 70))
на больший размер пропорциональный cardWidth. - Угловой ранг и масть: обновите RankSuitView для использования динамических размеров вместо .font(.headline) на основе ширины карты cardWidth, переданной в качестве параметра.
Решение
Ниже приведен полный код SwiftUI, реализующий эти корректировки:
import SwiftUI
struct CardView: View {
let card: PlayingCard
var body: some View {
GeometryReader { geometry in
let cardWidth = min(geometry.size.width, geometry.size.height)
ZStack {
RoundedRectangle(cornerRadius: cardWidth * 0.06)
.fill(Color.white)
.overlay(
RoundedRectangle(cornerRadius: cardWidth * 0.06)
.stroke(Color.black, lineWidth: 2)
)
// Central content
if case .number(let num) = card.rank {
// Number cards: display suit symbols
let positions = symbolPositions(for: num)
ForEach(0..<positions.count, id: \.self) { index in
let pos = positions[index]
Text(card.suit.rawValue)
// Previously .largeTitle
.font(.system(size: cardWidth * 0.2))
.font(.largeTitle)
.rotationEffect(pos.1 > 0.5 ? .degrees(180) :
.degrees(0))
.position(x: geometry.size.width * pos.0,
y: geometry.size.height * pos.1)
}
} else if case .ace = card.rank {
Text(card.suit.rawValue)
// Previously .system(size: 100)
.font(.system(size: cardWidth * 0.3))
} else {
Image(card.rank.display + card.suit.rawValue)
.resizable()
.scaledToFit()
}
// Rank and suit in corners
VStack {
HStack {
RankSuitView(rank: card.rank.display, suit: card.suit.rawValue,
cardWidth: cardWidth)
Spacer()
}
Spacer()
HStack {
Spacer()
RankSuitView(rank: card.rank.display, suit: card.suit.rawValue,
cardWidth: cardWidth)
.rotationEffect(.degrees(180))
}
}
.padding()
.foregroundColor(card.suit.color)
}
}
.aspectRatio(2/3, contentMode: .fit)
}
// Function to determine symbol positions based on rank
func symbolPositions(for rank: Int) -> [(CGFloat, CGFloat)] {
// Define positions as fractions of width and height
switch rank {
case 2:
return [
(0.5, 0.25 ), // Top
(0.5, 0.75 ) // Bottom
]
case 3:
return [
(0.5, 0.25 ), // Top
(0.5, 0.5 ), // Center
(0.5, 0.75 ) // Bottom
]
case 4:
return [
(0.25, 0.25 ), // Top left
(0.75, 0.25 ), // Top right
(0.25, 0.75 ), // Bottom left
(0.75, 0.75 ) // Bottom right
]
case 5:
return [
(0.25, 0.25), // Top left
(0.75, 0.25), // Top right
(0.5, 0.5), // Center
(0.25, 0.75), // Bottom left
(0.75, 0.75) // Bottom right
]
default:
// For other ranks, stack symbols vertically as a fallback
return (0..<rank).map { i in
(0.5, CGFloat(i + 1) / CGFloat(rank + 1))
}
}
}
}
struct RankSuitView: View {
let rank: String
let suit: String
let cardWidth: CGFloat
var body: some View {
VStack {
Text(rank)
.bold()
.font(.system(size: cardWidth * 0.1)) // Previously .headline
Text(suit)
.font(.system(size: cardWidth * 0.08)) // Smaller suit text
}
}
}
//Model
enum Suit: String {
case hearts = "♥️"
case diamonds = "♦️"
case clubs = "♣️"
case spades = "♠️"
var color: Color {
switch self {
case .hearts, .diamonds:
return .red
case .clubs, .spades:
return .black
}
}
}
enum Rank {
case number(Int)
case jack
case queen
case king
case ace
var display: String {
switch self {
case .number(let num):
return String(num)
case .jack:
return "J"
case .queen:
return "Q"
case .king:
return "K"
case .ace:
return "A"
}
}
}
struct PlayingCard {
let suit: Suit
let rank: Rank
}
// Example usage
struct ContentView: View {
var body: some View {
CardView(card: PlayingCard(suit: .clubs, rank: .number(5)))
CardView(card: PlayingCard(suit: .hearts, rank: .ace))
}
}
#Preview {
ContentView()
.padding()
}
Объяснение
1. Настройка GeometryReader
- Весь контент
CardView
упакован вGeometryReader
, предоставляя доступ кgeometry.size.width и geometry.size.height.
- Карта поддерживает соотношение сторон 2:3 (ширина:высота = 2:3) через
.aspectRatio(2/3, contentMode: .fit)
, обеспечивая согласованные пропорции. - Есть вычисляемая переменная
cardWidth
:
let cardWidth = min(geometry.size.width, geometry.size.height)
2. Динамическая настройка шрифта
- Центральные символы масти
(Text(card.suit.rawValue)
с.font(.largeTitle))
: - — Заменено на
.font(.system(size: cardWidth * 0.2))
. - — Использует 20% ширины карты, масштабируя символы пропорционально.
- Текст “туза” с символом масти
Text(card.suit.rawValue)
с.font(.system(size: 100))):
- — Заменено на .
font(.system(size: cardWidth * 0.3)).
- — Использует 30% ширины карты, делая ее больше для заметности.
- — Заменено на .
- Текст угла карты зависит от ранга
Text(rank)
с .font(.headline)
вRankSuitView
:- — Обновлён
RankSuitView
для принятияcardWidth
в качестве параметра. - — Установлен шрифт
.font(.system(size: cardWidth * 0.1))
для рангаrank
(10% ширины) и.font(.system(size: cardWidth * 0.08))
для мастиsuit
(8% ширины), заменив.headline
.
- — Обновлён
3. Масштабируемость
- По мере изменения размера карты (например, с помощью
.frame(width:))
размеры шрифтов автоматически изменяются: - Это обеспечивает читаемость и баланс для разных размеров карт.
4. Дополнительные примечания
- Функция
symbolPositions
размещает центральные символы как доли размеров карты, масштабируя их естественным образом с геометрией.
Результат
Размеры шрифтов всех указанных Text Views
теперь зависят от размера игральной карты через GeometryReader
, заменяя статические значения на динамические, пропорциональные. Это создает адаптивный дизайн, который легко адаптируется к различным размерам карты.
Вот результаты:
Добавляем расположение символов для ранга 6, 7, 8, 9 10

Чтобы расширить функцию symbolPositions(for:)
в вашей SwiftUI реализации игральных карт, нам нужно определить позиции символов мастей для рангов 7, 8, 9 и 10, следуя традиционным шаблонам, которые можно увидеть в стандартных игральных картах. Эти шаблоны разработаны так, чтобы быть симметричными и сбалансированными, с некоторыми символами в исходном вертикальном положении и другими в перевернутом положении (согласно вашему предыдущему запросу, символы в нижней части повернуты на 180 градусов). Давайте разберем типичные макеты для этих рангов, а затем реализуем их.
Понимание традиционных шаблонов символов
Дизайн игральных карт следуют определенным шаблонам для каждого ранга, чтобы расположить символы мастей (например, пики ♠️, червы ♥️) визуально привлекательным и узнаваемым образом. Вот как обычно располагаются символы для рангов с 7 по 10:
- Ранг 7: семь символов, расположенных так, что три сверху, один в центре и три снизу (зеркально по вертикали).
- Ранг 8: Восемь символов, расположенных так, что три сверху, два посередине (по одному с каждой стороны) и три снизу (зеркально).
- Ранг 9: Девять символов, расположенных так, что три сверху, три посередине (один по центру) и три снизу (зеркально).
- Ранг 10: Десять символов, расположенных так, что три сверху, четыре посередине (по два с каждой стороны) и три снизу (зеркально).
И добавим:
- Ранг 6: шесть символов, расположенных так, что два сверху, два в центре и два снизу (зеркально по вертикали).
Поскольку нижние символы должны быть перевернуты, мы положимся на существующую логику в вашем CardView
(т. е., rotationEffect(pos.1 > 0.5 ? .degrees(180) : .degrees(0)))
для обработки вращения. Наша задача — определить координаты (x, y)
для каждого символа, где:
x — горизонтальное положение (доля ширины карты, от 0 до 1).
y — вертикальное положение (доля высоты карты, от 0 до 1).
Обновленная функция symbolPositions(for:)
Давайте расширим функцию symbolPositions(for:)
, включив в нее ранги 6. 7, 8, 9 и 10, сохранив существующую логику для рангов 2 — 5 и резерв по умолчанию для других рангов.
// Function to determine symbol positions based on rank
func symbolPositions(for rank: Int) -> [(CGFloat, CGFloat)] {
// Define positions as fractions of width and height
switch rank {
case 2:
return [
(0.5, 0.25 ), // Top
(0.5, 0.75 ) // Bottom
]
case 3:
return [
(0.5, 0.25 ), // Top
(0.5, 0.5 ), // Center
(0.5, 0.75 ) // Bottom
]
case 4:
return [
(0.25, 0.25 ), // Top left
(0.75, 0.25 ), // Top right
(0.25, 0.75 ), // Bottom left
(0.75, 0.75 ) // Bottom right
]
case 5:
return [
(0.25, 0.25 ), // Top left
(0.75, 0.25 ), // Top right
(0.5, 0.5), // Center
(0.25, 0.75 ), // Bottom left
(0.75, 0.75 ) // Bottom right
]
case 6:
return [
(0.25, 0.25 ), // Top left
(0.75, 0.25 ), // Top right
(0.25, 0.5 ), // Center left
(0.75, 0.5 ), // Center right
(0.25, 0.75 ), // Bottom left
(0.75, 0.75 ) // Bottom right
]
case 7:
return [
// Top row: three symbols
(0.25, 0.15 ), // Top left: slightly lower
(0.5 , 0.3 ), // Top center
(0.75, 0.15 ), // Top right: slightly higher
// Middle: two symbols
(0.25, 0.5), // Center left
(0.75, 0.5), // Center right
// Bottom row: two symbols (mirrored)
(0.25, 0.85 ), // Bottom left
(0.75, 0.85 ) // Bottom right
]
case 8:
return [
// Top row: three symbols
(0.25, 0.15 ), // Top left: slightly lower
(0.5, 0.3), // Top center
(0.75, 0.15 ), // Top right:
// Middle: two symbols (one on each side)
(0.25, 0.5), // Middle left
(0.75, 0.5), // Middle right
// Bottom row: three symbols (mirrored)
(0.25, 0.85 ), // Bottom left
(0.5, 0.7), // Bottom center
(0.75, 0.85 ) // Bottom right
]
case 9:
return [
// Top row: three symbols
(0.25, 0.15 ), // Top left
(0.5, 0.5), // Top center
(0.75, 0.15 ), // Top right
// Middle row: four symbols (two on each side)
(0.25, 0.38 ), // Middle left upper
(0.25, 0.62 ), // Middle left lower
(0.75, 0.38 ), // Middle right upper
(0.75, 0.62 ), // Middle right lower
// Bottom row: three symbols (mirrored)
(0.25, 0.85 ), // Bottom left
(0.75, 0.85 ) // Bottom right
]
case 10:
return [
// Top row: three symbols
(0.25, 0.15 ), // Top left
(0.5, 0.30), // Top center
(0.75, 0.15 ), // Top right
// Middle row: four symbols (two on each side)
(0.25, 0.4 ), // Middle left upper
(0.25, 0.6 ), // Middle left lower
(0.75, 0.4 ), // Middle right upper
(0.75, 0.6 ), // Middle right lower
// Bottom row: three symbols (mirrored)
(0.25, 0.85 ), // Bottom left
(0.5, 0.70 ), // Bottom center
(0.75, 0.85 ) // Bottom right
]
default:
// For other ranks, stack symbols vertically as a fallback
return (0..<rank).map { i in
(0.5, CGFloat(i + 1) / CGFloat(rank + 1))
}
}
Объяснение каждой компоновки
- Ранг 7 (семь символов):
- Верхний ряд (2 символа): расположен на y = 0,15
- Верхний ряд (1 символа): центрирован (0,5, 0,3) на уровне y = 0,3
- Середина (2 символ): центрированы на y = 0,5
- Нижний ряд (2 символа): зеркально отображен на y = 0,85,
- Вращение: символы на y = 0,85 (нижний ряд) имеют y > 0,5, поэтому они будут повернуты на 180 градусов благодаря существующей логики.
- Ранг 8 (восемь символов):
- Верхний ряд (2 символа): то же, что и ранг 7, на y = 0,15.
- Верхний ряд (1 символа): центрирован (0,5, 0,3) на уровне y = 0,3
- Середина (2 символа): по одному с каждой стороны на y = 0,5 (слева на x = 0,25, справа на x = 0,75).
- Нижний ряд (1 символа): Зеркально отображено при y = 0,7.
- Нижний ряд (2 символа): Зеркально отображено при y = 0,85.
- Вращение: Символы нижнего ряда (y = 0,85 или y = 0,7) перевернуты; средние символы (y = 0,5) не повернуты.
- Ранг 9 (девять символов):
- Верхний ряд (2 символа): То же, что и ранг 7, при y = 0,15.
- Средний ряд (2 символа): При y = 0,38
- Центр (1 символ): При y = 0.5
- Средний ряд (2 символа): При y = 0,62
- Нижний ряд (2 символа): Зеркально отображено при y = 0,85.
- Вращение: Символы нижнего ряда (y = 0,85 или y = 0,62) перевернуты; символ среднего ряда (y = 0,5) не повернут.
- Ранг 10 (десять символов):
- Верхний ряд (2 символа): То же, что и ранг 7, при y = 0,15.
- Верхний ряд (1 символа): центрирован (0,5, 0,3) на уровне y = 0,3
- Средний ряд (4 символа): два с каждой стороны, при y = 0,4 и y = 0,6,
- Нижний ряд (2 символа): зеркально отображен при y = 0,85.
- Нижний ряд (1 символа): центрирован (0,5, 0,7) на уровне y = 0,7
- Вращение: символы при y = 0,6 (средний нижний) и y = 0,85 (нижний ряд) и y = 0,7 (нижний ряд) имеют y > 0,5, поэтому они перевернуты; символы при y = 0,4 (средний верхний) и y = 0,15 (верхний ряд) находятся в исходном вертикальном положении.
Тестирование новых макетов
Вы можете протестировать эти макеты, изменив ContentView
:
// Example usage
struct ContentView: View {
@Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
if verticalSizeClass == .compact {
HStack {
CardView(card: PlayingCard(suit: .clubs, rank: .number(7)))
CardView(card: PlayingCard(suit: .hearts, rank: .number(9)))
CardView(card: PlayingCard(suit: .spades, rank: .number(10)))
}
} else {
VStack {
CardView(card: PlayingCard(suit: .clubs, rank: .number(7)))
CardView(card: PlayingCard(suit: .hearts, rank: .number(9)))
CardView(card: PlayingCard(suit: .hearts, rank: .number(10)))
}
}
}
}
Результаты:
Такая реализация обеспечивает точное и традиционное размещение символов для рангов с 2 по 10, гарантируя, что ваши игральные карты будут выглядеть профессионально и соответствовать стандартному дизайну.
Окончательный код с aspectRatio 5 / 8.5
import SwiftUI
struct PlayingCardView: View {
let card: PlayingCard
var body: some View {
GeometryReader { geometry in
let cardWidth = min(geometry.size.width, geometry.size.height)
ZStack {
// 1) Background (assuming you have this already)
RoundedRectangle(cornerRadius: cardWidth * 0.06)
.fill(Color.white)
.overlay(
RoundedRectangle(cornerRadius: cardWidth * 0.06)
.stroke(Color.black, lineWidth: 2)
)
// 2) Central content
if case .number(let num) = card.rank {
let positions = symbolPositions(for: num)
ForEach(0..<positions.count, id: \.self) { index in
let pos = positions[index]
Text(card.suit.rawValue)
.font(Font.system(size: cardWidth * 0.2))
.rotationEffect(pos.1 > 0.5 ? .degrees(180) :
.degrees(0))
.position(x: geometry.size.width * pos.0,
y: geometry.size.height * pos.1)
}
} else if case .ace = card.rank {
Text(card.suit.rawValue)
.font(Font.system(size: cardWidth * 0.3))
.position(x: geometry.size.width * 0.5,
y: geometry.size.height * 0.5)
} else {
Image(card.rank.display + card.suit.rawValue)
.resizable()
.scaledToFit()
.padding(cardWidth * 0.03)
}
// 3) Rank and suit in corners
VStack {
HStack {
RankSuitView(rank: card.rank.display,
suit: card.suit.rawValue,
cardWidth: cardWidth)
Spacer()
}
Spacer()
HStack {
Spacer()
RankSuitView(rank: card.rank.display,
suit: card.suit.rawValue,
cardWidth: cardWidth)
.rotationEffect(.degrees(180))
}
} // VStack
.padding(cardWidth * 0.02)
} // geometry
} // ZStack
.aspectRatio(5/8.5, contentMode: .fit)
.foregroundColor(card.suit.color)
}
// Function to determine symbol positions based on rank
func symbolPositions(for rank: Int) -> [(CGFloat, CGFloat)] {
// Define positions as fractions of width and height
switch rank {
case 2:
return [
(0.5, 0.25 ), // Top
(0.5, 0.75 ) // Bottom
]
case 3:
return [
(0.5, 0.25 ), // Top
(0.5, 0.5 ), // Center
(0.5, 0.75 ) // Bottom
]
case 4:
return [
(0.25, 0.25 ), // Top left
(0.75, 0.25 ), // Top right
(0.25, 0.75 ), // Bottom left
(0.75, 0.75 ) // Bottom right
]
case 5:
return [
(0.25, 0.25 ), // Top left
(0.75, 0.25 ), // Top right
(0.5, 0.5), // Center
(0.25, 0.75 ), // Bottom left
(0.75, 0.75 ) // Bottom right
]
case 6:
return [
(0.25, 0.25 ), // Top left
(0.75, 0.25 ), // Top right
(0.25, 0.5 ), // Center left
(0.75, 0.5 ), // Center right
(0.25, 0.75 ), // Bottom left
(0.75, 0.75 ) // Bottom right
]
case 7:
return [
// Top row: three symbols
(0.25, 0.15 ), // Top left: slightly lower
(0.5 , 0.3 ), // Top center
(0.75, 0.15 ), // Top right: slightly higher
// Middle: two symbols
(0.25, 0.5), // Center left
(0.75, 0.5), // Center right
// Bottom row: two symbols (mirrored)
(0.25, 0.85 ), // Bottom left
(0.75, 0.85 ) // Bottom right
]
case 8:
return [
// Top row: three symbols
(0.25, 0.15 ), // Top left: slightly lower
(0.5, 0.3), // Top center
(0.75, 0.15 ), // Top right:
// Middle: two symbols (one on each side)
(0.25, 0.5), // Middle left
(0.75, 0.5), // Middle right
// Bottom row: three symbols (mirrored)
(0.25, 0.85 ), // Bottom left
(0.5, 0.7), // Bottom center
(0.75, 0.85 ) // Bottom right
]
case 9:
return [
// Top row: three symbols
(0.25, 0.15 ), // Top left
(0.5, 0.5), // Top center
(0.75, 0.15 ), // Top right
// Middle row: four symbols (two on each side)
(0.25, 0.38 ), // Middle left upper
(0.25, 0.62 ), // Middle left lower
(0.75, 0.38 ), // Middle right upper
(0.75, 0.62 ), // Middle right lower
// Bottom row: three symbols (mirrored)
(0.25, 0.85 ), // Bottom left
(0.75, 0.85 ) // Bottom right
]
case 10:
return [
// Top row: three symbols
(0.25, 0.15 ), // Top left
(0.5, 0.30), // Top center
(0.75, 0.15 ), // Top right
// Middle row: four symbols (two on each side)
(0.25, 0.4 ), // Middle left upper
(0.25, 0.6 ), // Middle left lower
(0.75, 0.4 ), // Middle right upper
(0.75, 0.6 ), // Middle right lower
// Bottom row: three symbols (mirrored)
(0.25, 0.85 ), // Bottom left
(0.5, 0.70 ), // Bottom center
(0.75, 0.85 ) // Bottom right
]
default:
// For other ranks, stack symbols vertically as a fallback
return (0..<rank).map { i in
(0.5, CGFloat(i + 1) / CGFloat(rank + 1))
}
}
}
}
struct RankSuitView: View {
let rank: String
let suit: String
let cardWidth: CGFloat
var body: some View {
VStack {
Text(rank)
.font(.system(size: cardWidth * 0.1)) // Previously .headline
.bold()
Text(suit)
.font(.system(size: cardWidth * 0.08)) // Smaller suit text
}
}
}
// Assuming these are your PlayingCard and related types
struct PlayingCard {
let suit: Suit
let rank: Rank
enum Suit {
case spades, hearts, diamonds, clubs
var rawValue: String {
switch self {
case .spades: return "♠️"
case .hearts: return "♥️"
case .diamonds: return "♦️"
case .clubs: return "♣️"
}
}
var color: Color {
switch self {
case .spades, .clubs: return .black
case .hearts, .diamonds: return .red
}
}
}
enum Rank {
case number(Int)
case ace, jack, queen, king
var display: String {
switch self {
case .number(let num): return "\(num)"
case .ace: return "A"
case .jack: return "J"
case .queen: return "Q"
case .king: return "K"
}
}
}
}
// Example usage
struct ContentView: View {
@Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
if verticalSizeClass == .compact {
HStack {
// CardView(card: PlayingCard(suit: .spades, rank: .ace))
PlayingCardView(card: PlayingCard(suit: .clubs, rank: .king))
PlayingCardView(card: PlayingCard(suit: .hearts, rank: .ace))
PlayingCardView(card: PlayingCard(suit: .clubs, rank: .number(9)))
}
} else {
VStack {
// CardView(card: PlayingCard(suit: .spades, rank: .ace))
PlayingCardView(card: PlayingCard(suit: .clubs, rank: .king))
PlayingCardView(card: PlayingCard(suit: .hearts, rank: .ace))
PlayingCardView(card: PlayingCard(suit: .clubs, rank: .number(9)))
}
}
}
}
#Preview {
ContentView()
.padding()
}
Результаты:
Заключение
- Grok 3 прекрасно справился с задачей создания игральной карты в SwiftUI, предложив для размещения символов масти на числовых картах с рангом 2-10 использовать функцию
symbolPositions (for rank: Int)
, возвращающую массив из rank кортежей (x, y), представляющих позиции символов масти как доли ширины и высоты карты. - Такая реализация обеспечивает точное и традиционное размещение символов для рангов с 2 по 10, гарантируя, что ваши игральные карты будут выглядеть профессионально и соответствовать стандартному дизайну.
- Весь контент
CardView
упакован вGeometryReader
, предоставляя доступ кgeometry.size.width
иgeometry.size.height
и автоматически настраивая размеры шрифтов в зависимости от размера игральной карты. Это создает дизайн, который легко адаптируется к различным размерам карты. - Код находится на Github.