[Swift] タロットカードアプリで画像を自動生成してみた

# はじめに
こんにちは！
先日、以下の記事を書きました。

[Swift & SwiftUIでタロットカードアプリを作る](https://qiita.com/yam_dev/items/871b47c1065f570ff17a)
[[Swift] ホラー画像生成アプリ](https://qiita.com/yam_dev/items/7c09f00c4f2a82fdf7a1)

今回はこれらをマージして、タロットカードの画像を自動生成してみました。
簡易版ですが、動きは良さそうなので共有します！

# 要件
- ボタンをタップすると、タロットカードをランダムに選択する
- タロットカードの名前と意味に沿って画像を生成する
- タロットカードの名前、画像、意味を表示する

# 準備
プロジェクトの作成方法は、[Swift & SwiftUIでタロットカードアプリを作る](https://qiita.com/yam_dev/items/871b47c1065f570ff17a) を参照してください。

User Interfaceは`SwiftUI`を選択しました。

# 実装

ContentViewを修正
```swift:ContentView.swift
//
//  ContentView.swift
//  TarotApp
//

import SwiftUI

struct ContentView: View {
    @State private var selectedCard: TarotCard?

    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Text("TarotApp")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .padding()
                    .frame(maxWidth: .infinity) // タイトルを中央寄せにする
                    .background(Color.gray.opacity(0.2))
                    .cornerRadius(10)

                if let card = selectedCard {
                    CardView(card: card)
                        .id(card.id)
                    Button("もう一度引く") {
                        // ランダムにカードを選択
                        selectedCard = tarotCards.randomElement()
                    }
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
                } else {
                    Button("カードを引く") {
                        // ランダムにカードを選択
                        selectedCard = tarotCards.randomElement()
                    }
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

```

CardViewも修正
```swift:CardView.swift
//  CardView.swift
//  TarotApp
//

import SwiftUI

struct CardView: View {
    let card: TarotCard

    // OpenAI APIキーを設定
    private let apiKey = "sk-proj-xxx"

    // 画状態変数を追加
    @State private var outputImage: UIImage?
    @State private var isLoading = false
    @State private var showAlert = false

    var body: some View {
        VStack(spacing: 10) {
            Text(card.name)
                .font(.largeTitle)
                .fontWeight(.bold)
                .padding()
                .background(Color.gray.opacity(0.2))
                .cornerRadius(10)

            Text(card.description)
                .font(.headline)
                .multilineTextAlignment(.center)
                .padding()

            // 画像を表示
            if let outputImage = outputImage {
                Image(uiImage: outputImage)
                    .resizable()
                    .scaledToFit()
                    .cornerRadius(10)
                    .padding()
            } else {
                // 画像がない場合の表示
                ProgressView()
                    .opacity(isLoading ? 1.0 : 0.0)
                    .padding()
            }

            Spacer()

            Text("意味")
                .font(.title2)
                .fontWeight(.bold)
                .padding()

            Text(card.meaning)
                .font(.body)
                .padding()
        }
        .padding()
        .background(Color(UIColor.systemGray6))
        .cornerRadius(20)
        .shadow(radius: 5)
        .onAppear {
            // カード表示時に画像を生成
            isLoading = true
            generateImage(from: card.name, from: card.description)
        }
        // アラート表示
        .alert("エラー", isPresented: $showAlert) {
            Button("OK", role: .cancel) { }
        } message: {
            Text("画像の生成に失敗しました。")
        }
    }
  
    func generateImage(from name: String, from description: String) {
        let url = URL(string: "https://api.openai.com/v1/images/generations")!

        // ホラーテイストを必ず入れるためのプロンプト調整
        let horrorPrompt = "An eerie and unsettling scene with '\(name), \(description)', in the style of a horror and cyber-punk, dark and gruesome, high quality, digital art."

        let parameters: [String: Any] = [
            "model": "dall-e-3", // dall-e-3に指定
            "prompt": horrorPrompt,
            "n": 1,
            "size": "1024x1024" // 画像サイズ
        ]

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try? JSONSerialization.data(withJSONObject: parameters)

        URLSession.shared.dataTask(with: request) { data, response, error in
            // エラー処理
            if let error = error {
                print("エラー: \(error)")
                DispatchQueue.main.async {
                    self.showAlert = true
                    self.isLoading = false // エラー時にローディングを停止
                }
                return
            }

            guard let data = data else {
                print("データがありません")
                DispatchQueue.main.async {
                    self.showAlert = true
                    self.isLoading = false // エラー時にローディングを停止
                }
                return
            }

            do {
                if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
                   let dataArray = json["data"] as? [[String: Any]],
                   let firstImageData = dataArray.first,
                   let imageUrlString = firstImageData["url"] as? String,
                   let imageUrl = URL(string: imageUrlString) {

                    // 画像の取得
                    if let imageData = try? Data(contentsOf: imageUrl), let image = UIImage(data: imageData) {
                        DispatchQueue.main.async {
                            self.outputImage = image
                            self.isLoading = false
                        }
                    } else {
                        print("画像の取得またはデコードエラー")
                        DispatchQueue.main.async {
                            self.showAlert = true
                            self.isLoading = false // エラー時にローディングを停止
                        }
                    }
                } else {
                    print("画像URLの解析エラー")
                    DispatchQueue.main.async {
                        self.showAlert = true
                        self.isLoading = false // エラー時にローディングを停止
                    }
                }

            } catch {
                print("JSON解析エラー: \(error)")
                DispatchQueue.main.async {
                    self.showAlert = true
                    self.isLoading = false // エラー時にローディングを停止
                }
            }
        }.resume()
    }
}
```

TarotCardsはそのまま使います。
```swift:TarotCards.swift
//
//  TarotCards.swift
//  TarotApp
//

import SwiftUI

struct TarotCard: Identifiable {
  let id = UUID()
  let name: String
  let description: String
  let meaning: String
}

let tarotCards: [TarotCard] = [
  TarotCard(name: "The Fool",
            description: "始まり、無邪気さ、新しい旅立ち",
            meaning: "新しい章の始まり、自由な精神、冒険心"),
  TarotCard(name: "The Magician",
            description: "意志力、創造力、潜在能力",
            meaning: "目標達成、能力発揮、潜在能力の開花"),
  TarotCard(name: "The High Priestess",
            description: "直感、神秘、秘密",
            meaning: "内なる知恵、直感力、秘密の解明"),
  TarotCard(name: "The Empress",
            description: "豊穣、愛情、自然",
            meaning: "豊かさ、愛情、創造性"),
  TarotCard(name: "The Emperor",
            description: "権力、安定、秩序",
            meaning: "安定、統率力、権威"),
  TarotCard(name: "The Hierophant",
            description: "伝統、信仰、精神的指導",
            meaning: "伝統、精神的な指導、社会規範"),
  TarotCard(name: "The Lovers",
            description: "愛、調和、選択",
            meaning: "愛情、調和、パートナーシップ"),
  TarotCard(name: "The Chariot",
            description: "意志、勝利、克服",
            meaning: "意志力、目標達成、克服"),
  TarotCard(name: "Strength",
            description: "強さ、勇気、忍耐",
            meaning: "内なる強さ、克服、忍耐"),
  TarotCard(name: "The Hermit",
            description: "内省、孤独、自己探求",
            meaning: "内観、自己成長、探求"),
  TarotCard(name: "Wheel of Fortune",
            description: "運命、変化、サイクル",
            meaning: "運命、変化、サイクル"),
  TarotCard(name: "Justice",
            description: "公正、均衡、法則",
            meaning: "公正、バランス、調和"),
  TarotCard(name: "The Hanged Man",
            description: "犠牲、視点を変える、新しい見方",
            meaning: "新しい視点、見直し、変化"),
  TarotCard(name: "Death",
            description: "終わり、変容、再生",
            meaning: "終わりと始まり、変容、再生"),
  TarotCard(name: "Temperance",
            description: "調和、バランス、忍耐",
            meaning: "バランス、調和、忍耐"),
  TarotCard(name: "The Devil",
            description: "誘惑、執着、物質主義",
            meaning: "誘惑、執着、依存"),
  TarotCard(name: "The Tower",
            description: "破壊、変化、混乱",
            meaning: "破壊、変革、混乱"),
  TarotCard(name: "The Star",
            description: "希望、再生、癒し",
            meaning: "希望、再生、癒し"),
  TarotCard(name: "The Moon",
            description: "幻想、直感、隠された真実",
            meaning: "幻想、直感、隠された真実"),
  TarotCard(name: "The Sun",
            description: "喜び、成功、幸福",
            meaning: "喜び、成功、幸福"),
  TarotCard(name: "Judgement",
            description: "再生、自己評価、新しい始まり",
            meaning: "再生、自己評価、新しい始まり"),
  TarotCard(name: "The World",
            description: "完成、達成、統合",
            meaning: "完成、達成、統合"),
  TarotCard(name: "The Major Arcana",
            description: "運命のサイクル、人生の旅路",
            meaning: "人生の重要な課題、試練"),
  TarotCard(name: "Ace of Wands",
            description: "新しい始まり、情熱、創造性",
            meaning: "新しいプロジェクト、情熱、創造性"),
  TarotCard(name: "Two of Wands",
            description: "計画、展望、決断",
            meaning: "計画、展望、決断"),
  TarotCard(name: "Three of Wands",
            description: "拡大、成長、冒険",
            meaning: "拡大、成長、冒険"),
  TarotCard(name: "Four of Wands",
            description: "安定、祝賀、達成",
            meaning: "安定、祝賀、達成"),
  TarotCard(name: "Five of Wands",
            description: "競争、対立、混乱",
            meaning: "競争、対立、混乱"),
  TarotCard(name: "Six of Wands",
            description: "成功、勝利、栄光",
            meaning: "成功、勝利、栄光"),
  TarotCard(name: "Seven of Wands",
            description: "防衛、抵抗、忍耐",
            meaning: "防衛、抵抗、忍耐"),
  TarotCard(name: "Eight of Wands",
            description: "動き、変化、スピード",
            meaning: "動き、変化、スピード"),
  TarotCard(name: "Nine of Wands",
            description: "忍耐、決意、防衛",
            meaning: "忍耐、決意、防衛"),
  TarotCard(name: "Ten of Wands",
            description: "負担、責任、完成",
            meaning: "負担、責任、完成"),
  TarotCard(name: "Page of Wands",
            description: "新しいアイデア、熱意、探検",
            meaning: "新しいアイデア、熱意、探検"),
  TarotCard(name: "Knight of Wands",
            description: "行動力、情熱、冒険心",
            meaning: "行動力、情熱、冒険心"),
  TarotCard(name: "Queen of Wands",
            description: "独立心、自信、創造性",
            meaning: "独立心、自信、創造性"),
  TarotCard(name: "King of Wands",
            description: "リーダーシップ、成功、野心",
            meaning: "リーダーシップ、成功、野心"),
  TarotCard(name: "Ace of Cups",
            description: "愛情、感情、創造性",
            meaning: "愛情、感情、創造性"),
  TarotCard(name: "Two of Cups",
            description: "調和、愛情、パートナーシップ",
            meaning: "調和、愛情、パートナーシップ"),
  TarotCard(name: "Three of Cups",
            description: "祝賀、友情、喜び",
            meaning: "祝賀、友情、喜び"),
  TarotCard(name: "Four of Cups",
            description: "内省、満足、孤独",
            meaning: "内省、満足、孤独"),
  TarotCard(name: "Five of Cups",
            description: "悲しみ、喪失、失望",
            meaning: "悲しみ、喪失、失望"),
  TarotCard(name: "Six of Cups",
            description: "過去、思い出、ノスタルジア",
            meaning: "過去、思い出、ノスタルジア"),
  TarotCard(name: "Seven of Cups",
            description: "幻想、誘惑、選択",
            meaning: "幻想、誘惑、選択"),
  TarotCard(name: "Eight of Cups",
            description: "離脱、失望、新しい出発",
            meaning: "離脱、失望、新しい出発"),
  TarotCard(name: "Nine of Cups",
            description: "満足、幸福、願いの成就",
            meaning: "満足、幸福、願いの成就"),
  TarotCard(name: "Ten of Cups",
            description: "完璧な幸福、家庭、愛",
            meaning: "完璧な幸福、家庭、愛"),
  TarotCard(name: "Page of Cups",
            description: "新しい愛、感情、創造性",
            meaning: "新しい愛、感情、創造性"),
  TarotCard(name: "Knight of Cups",
            description: "ロマンチック、夢、理想",
            meaning: "ロマンチック、夢、理想"),
  TarotCard(name: "Queen of Cups",
            description: "共感、慈悲、直感",
            meaning: "共感、慈悲、直感"),
  TarotCard(name: "King of Cups",
            description: "感情の支配、感情の安定、調和",
            meaning: "感情の支配、感情の安定、調和"),
  TarotCard(name: "Ace of Swords",
            description: "新しいアイデア、決断、突破",
            meaning: "新しいアイデア、決断、突破"),
  TarotCard(name: "Two of Swords",
            description: "停滞、決断の遅延、葛藤",
            meaning: "停滞、決断の遅延、葛藤"),
  TarotCard(name: "Three of Swords",
            description: "悲しみ、失恋、苦痛",
            meaning: "悲しみ、失恋、苦痛"),
  TarotCard(name: "Four of Swords",
            description: "休息、回復、静寂",
            meaning: "休息、回復、静寂"),
  TarotCard(name: "Five of Swords",
            description: "敗北、裏切り、争い",
            meaning: "敗北、裏切り、争い"),
  TarotCard(name: "Six of Swords",
            description: "移転、旅立ち、癒し",
            meaning: "移転、旅立ち、癒し"),
  TarotCard(name: "Seven of Swords",
            description: "裏切り、詐欺、隠蔽",
            meaning: "裏切り、詐欺、隠蔽"),
  TarotCard(name: "Eight of Swords",
            description: "制限、束縛、囚われ",
            meaning: "制限、束縛、囚われ"),
  TarotCard(name: "Nine of Swords",
            description: "不安、恐怖、悪夢",
            meaning: "不安、恐怖、悪夢"),
  TarotCard(name: "Ten of Swords",
            description: "破壊、敗北、絶望",
            meaning: "破壊、敗北、絶望"),
  TarotCard(name: "Page of Swords",
            description: "知性、メッセージ、情報",
            meaning: "知性、メッセージ、情報"),
  TarotCard(name: "Knight of Swords",
            description: "迅速な行動、攻撃性、決断力",
            meaning: "迅速な行動、攻撃性、決断力"),
  TarotCard(name: "Queen of Swords",
            description: "独立心、知性、冷静",
            meaning: "独立心、知性、冷静"),
  TarotCard(name: "King of Swords",
            description: "権威、支配、正義",
            meaning: "権威、支配、正義"),
  TarotCard(name: "Ace of Pentacles",
            description: "豊かさ、財産、新しい始まり",
            meaning: "豊かさ、財産、新しい始まり"),
  TarotCard(name: "Two of Pentacles",
            description: "バランス、多忙、調整",
            meaning: "バランス、多忙、調整"),
  TarotCard(name: "Three of Pentacles",
            description: "チームワーク、職人技、協力",
            meaning: "チームワーク、職人技、協力"),
  TarotCard(name: "Four of Pentacles",
            description: "安全、安定、所有",
            meaning: "安全、安定、所有"),
  TarotCard(name: "Five of Pentacles",
            description: "貧困、失業、孤独",
            meaning: "貧困、失業、孤独"),
  TarotCard(name: "Six of Pentacles",
            description: "慈善、寛大、共有",
            meaning: "慈善、寛大、共有"),
  TarotCard(name: "Seven of Pentacles",
            description: "努力、辛抱、成果",
            meaning: "努力、辛抱、成果"),
  TarotCard(name: "Eight of Pentacles",
            description: "技能、熟練、努力",
            meaning: "技能、熟練、努力"),
  TarotCard(name: "Nine of Pentacles",
            description: "成功、豊かさ、満足",
            meaning: "成功、豊かさ、満足"),
  TarotCard(name: "Ten of Pentacles",
            description: "遺産、財産、家族",
            meaning: "遺産、財産、家族"),
  TarotCard(name: "Page of Pentacles",
            description: "新しい機会、学習、潜在能力",
            meaning: "新しい機会、学習、潜在能力"),
  TarotCard(name: "Knight of Pentacles",
            description: "勤勉、責任、安定",
            meaning: "勤勉、責任、安定"),
  TarotCard(name: "Queen of Pentacles",
            description: "豊穣、実用性、安定",
            meaning: "豊穣、実用性、安定"),
  TarotCard(name: "King of Pentacles",
            description: "富、成功、物質的な豊かさ",
            meaning: "富、成功、物質的な豊かさ")
]
```

# 動作確認
起動直後の画面。4回引いてみます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407770/54b1d110-4c86-681e-0d26-1e82941b2d81.png" width="300px" />

1回目
Ace of Wands

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407770/63c7588a-43e2-5732-5ffc-dd0c625e3c7e.png" width="300px" />

2回目
Knight of Swords

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407770/7525136a-b159-3e97-84e5-2c91107d925d.png" width="300px" />

3回目
The Emperor

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407770/59752055-4ee1-ca0f-28df-fe7564760de3.png" width="300px" />

4回目
The Fool

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407770/3e9c1053-40e2-163f-b898-f0e81db7aff0.png" width="300px" />

いい感じですね！
あとは画像サイズやデザインを工夫すれば、良さそうです。

# 最後に
今回は、タロットカードと画像生成を組み合わせてみました。
プロンプトによって様々なテイストの画像を生成できるので、みなさんもチャレンジしてみてください😀