Создание игральных карт в SwiftUI с помощью ИИ Grok 3

Мне всегда хотелось создать в 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» и ♥️ по углам.
  • Король пик: отображается как большая «К» в центре черным цветом, с «К» и ♠️ по углам.

Улучшения

  1. Расположение символов: для числовых карт вы можете расположить символы в сетке или узоре (например, два ряда по три для 6 ♣️) вместо вертикального стека. Измените VStack на сетку или пользовательский макет для этого.
  1. Изображения карт с картинками: замените текст заполнителя 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"
        }
    }
}

Объяснение кода

  1. Структура CardView:
  • ZStack накладывает фон, угловые элементы и центральное содержимое карты.
  • Соотношение сторон 2/3 гарантирует, что карта сохраняет стандартные пропорции игральной карты.
  1. Функция symbolPositions:
  • Для ранга 5 она возвращает массив из пяти кортежей (x, y), представляющих позиции как доли ширины и высоты карты:
      • Верхний ряд: (0,25, 0,25) и (0,75, 0,25 ) размещают два символа наверху.
      • Центр: (0,5, 0,5) размещает один символ точно по центру.
      • Нижний ряд: (0,25, 0,75) и (0,75, 0,75) размещают два символа внизу.
    • Для других рангов размещение символов вы можете расширить позже.
    1. Центральное содержимое карты:
    • Для числовых карт (.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()
    }

    Ключевые изменения

    1. Логика вращения:
    • Внутри цикла ForEach, который позиционирует символы масти, я добавил условный поворот:
    rotationEffect(pos.1 > 0.5 ? .degrees(180) : .degrees(0))
    • Здесь pos.1 — это координата y (как доля высоты карты). Если pos.1 > 0.5, символ находится в нижней половине карты, поэтому мы поворачиваем его на 180 градусов. В противном случае он остается в исходном вертикальном положении.
    1. Применение к 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 пик на вашем изображении, где нижние две пики перевернуты.
    1. Никаких изменений для карт с картинками:
    • Карты с картинками (использующие изображения или резервный текст) не затронуты этим изменением, поскольку они не используют логику позиционирования символов.

    Как это соответствует изображению

    На предоставленном вами изображении 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))
                }
            }

    Объяснение каждой компоновки

    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 градусов благодаря существующей логики.
    1. Ранг 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) не повернуты.
    1. Ранг 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) не повернут.
    1. Ранг 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.

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *