๐ผ๏ธ Image Playground
Apple Intelligence ๊ธฐ๋ฐ AI ์ด๋ฏธ์ง ์์ฑ ํ๋ ์์ํฌ
โจ Image Playground๋?
Image Playground๋ iOS 18์์ ๋์ ๋ Apple Intelligence ๊ธฐ๋ฐ ์ด๋ฏธ์ง ์์ฑ ํ๋ ์์ํฌ์ ๋๋ค. ํ ์คํธ ํ๋กฌํํธ, ๊ฐ๋ , ์คํ์ผ์ ์กฐํฉํ์ฌ ์จ๋๋ฐ์ด์ค AI๋ก ๋ ์ฐฝ์ ์ธ ์ด๋ฏธ์ง๋ฅผ ์์ฑํฉ๋๋ค. ๋ฉ์์ง ์ฑ, ๋ ธํธ ์ฑ, ์์ ์ฑ ๋ฑ์์ ์ฌ์ฉ์๊ฐ ์ฝ๊ฒ ์ด๋ฏธ์ง๋ฅผ ๋ง๋ค๊ณ ๊ณต์ ํ ์ ์๋๋ก ์์คํ UI์ API๋ฅผ ์ ๊ณตํฉ๋๋ค. ํ๋ผ์ด๋ฒ์๋ฅผ ๋ณดํธํ๋ฉฐ ๋๋ฐ์ด์ค์์ ์ง์ ์ฒ๋ฆฌ๋ฉ๋๋ค.
๐จ 1. ๊ธฐ๋ณธ ์ด๋ฏธ์ง ์์ฑ
ImagePlayground ์์คํ UI๋ฅผ ํตํด ์ด๋ฏธ์ง๋ฅผ ์์ฑํฉ๋๋ค.
import SwiftUI // NOTE: Image Playground๋ iOS 18์ ์๋ก์ด ๊ธฐ๋ฅ์ผ๋ก, // ์์ง ๊ณต๊ฐ API๊ฐ ํ์ ๋์ง ์์์ต๋๋ค. // ์๋๋ ์์๋๋ API ํจํด์ ๋๋ค. struct ImagePlaygroundView: View { @State private var generatedImage: Image? @State private var showImageGenerator = false @State private var prompt = "" var body: some View { VStack(spacing: 20) { // ์์ฑ๋ ์ด๋ฏธ์ง ํ์ if let generatedImage { generatedImage .resizable() .scaledToFit() .frame(maxHeight: 300) .cornerRadius(12) .shadow(radius: 5) } else { RoundedRectangle(cornerRadius: 12) .fill(Color.gray.opacity(0.2)) .frame(height: 300) .overlay { VStack { Image(systemName: "photo.on.rectangle.angled") .font(.system(size: 50)) Text("์ด๋ฏธ์ง๋ฅผ ์์ฑํด๋ณด์ธ์") .font(.headline) } .foregroundStyle(.secondary) } } // ํ๋กฌํํธ ์ ๋ ฅ TextField("์ํ๋ ์ด๋ฏธ์ง ์ค๋ช ", text: $prompt) .textFieldStyle(.roundedBorder) .padding(.horizontal) // ์์ฑ ๋ฒํผ Button { showImageGenerator = true } label: { Label("์ด๋ฏธ์ง ์์ฑ", systemImage: "wand.and.stars") .frame(maxWidth: .infinity) .padding() .background(Color.blue) .foregroundStyle(.white) .cornerRadius(12) } .padding(.horizontal) .disabled(prompt.isEmpty) } .padding() // Image Playground ์ํธ ํ์ // .imagePlayground(isPresented: $showImageGenerator, prompt: prompt) { result in // if case .success(let image) = result { // generatedImage = Image(uiImage: image) // } // } } } // ์์๋๋ ์ด๋ฏธ์ง ์์ฑ ๊ฒฐ๊ณผ ํ์ enum ImageGenerationResult { case success(UIImage) case failure(Error) case cancelled }
๐ญ 2. ์คํ์ผ ์ ํ
Animation, Illustration, Sketch ๋ฑ ๋ค์ํ ์คํ์ผ๋ก ์ด๋ฏธ์ง๋ฅผ ์์ฑํฉ๋๋ค.
import SwiftUI // Image Playground ์คํ์ผ ์ต์ enum ImagePlaygroundStyle: String, CaseIterable, Identifiable { case animation = "์ ๋๋ฉ์ด์ " case illustration = "์ผ๋ฌ์คํธ" case sketch = "์ค์ผ์น" var id: String { rawValue } var icon: String { switch self { case .animation: return "film" case .illustration: return "paintbrush" case .sketch: return "pencil.tip" } } var description: String { switch self { case .animation: return "3D ์บ๋ฆญํฐ์ ์๋๊ฐ ์๋ ์ ๋๋ฉ์ด์ ์คํ์ผ" case .illustration: return "์์ธํ๊ณ ๋ค์ฑ๋ก์ด ์ผ๋ฌ์คํธ๋ ์ด์ " case .sketch: return "๋จ์ํ๊ณ ์์ ์ ์ธ ์ค์ผ์น" } } } struct StyleSelectorView: View { @State private var selectedStyle: ImagePlaygroundStyle = .animation @State private var prompt = "" @State private var generatedImage: Image? @State private var isGenerating = false var body: some View { ScrollView { VStack(spacing: 24) { // ์คํ์ผ ์ ํ VStack(alignment: .leading, spacing: 12) { Text("์คํ์ผ ์ ํ") .font(.headline) HStack(spacing: 12) { ForEach(ImagePlaygroundStyle.allCases) { style in Button { selectedStyle = style } label: { VStack(spacing: 8) { Image(systemName: style.icon) .font(.title2) Text(style.rawValue) .font(.caption) } .frame(maxWidth: .infinity) .padding() .background( selectedStyle == style ? Color.blue : Color.gray.opacity(0.2) ) .foregroundStyle( selectedStyle == style ? .white : .primary ) .cornerRadius(12) } } } Text(selectedStyle.description) .font(.caption) .foregroundStyle(.secondary) } .padding(.horizontal) // ํ๋กฌํํธ ์ ๋ ฅ VStack(alignment: .leading, spacing: 8) { Text("์ด๋ฏธ์ง ์ค๋ช ") .font(.headline) TextField("์: ์ฐ์ฃผ๋ฅผ ์ฌํํ๋ ๊ณ ์์ด", text: $prompt, axis: .vertical) .textFieldStyle(.roundedBorder) .lineLimit(3...5) } .padding(.horizontal) // ์์ฑ ๋ฒํผ Button { generateImage() } label: { HStack { if isGenerating { ProgressView() .tint(.white) Text("์์ฑ ์ค...") } else { Image(systemName: "wand.and.stars") Text("์ด๋ฏธ์ง ์์ฑ") } } .frame(maxWidth: .infinity) .padding() .background(Color.blue) .foregroundStyle(.white) .cornerRadius(12) } .padding(.horizontal) .disabled(prompt.isEmpty || isGenerating) // ์์ฑ๋ ์ด๋ฏธ์ง if let generatedImage { VStack(alignment: .leading, spacing: 8) { Text("์์ฑ๋ ์ด๋ฏธ์ง") .font(.headline) generatedImage .resizable() .scaledToFit() .cornerRadius(12) .shadow(radius: 5) } .padding(.horizontal) } } .padding(.vertical) } } func generateImage() { isGenerating = true // ์ค์ ๊ตฌํ์์๋ Image Playground API ํธ์ถ Task { try? await Task.sleep(for: .seconds(2)) isGenerating = false // generatedImage = ... } } }
๐ฌ 3. ๋ฉ์์ง ์ฑ ํตํฉ
๋ฉ์์ง ์ฑ์์ ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ณ ๊ณต์ ํฉ๋๋ค.
import SwiftUI import Messages // ๋ฉ์์ง ํ์ฅ์์ Image Playground ์ฌ์ฉ struct MessageImageGeneratorView: View { @State private var prompt = "" @State private var generatedImage: UIImage? @State private var showGenerator = false var onImageGenerated: (UIImage) -> Void var body: some View { VStack { TextField("์ด๋ฏธ์ง ์ค๋ช ", text: $prompt) .textFieldStyle(.roundedBorder) .padding() Button("์์ฑํ๊ธฐ") { showGenerator = true } .buttonStyle(.borderedProminent) .disabled(prompt.isEmpty) if let generatedImage { Image(uiImage: generatedImage) .resizable() .scaledToFit() .frame(maxHeight: 200) .cornerRadius(12) Button("๋ฉ์์ง๋ก ์ ์ก") { onImageGenerated(generatedImage) } .buttonStyle(.borderedProminent) } } } } // ๋ฉ์์ง ์คํฐ์ปค๋ก ๋ณํ extension UIImage { func toMessageSticker() -> MSSticker? { // ์์ ํ์ผ๋ก ์ ์ฅ let tempURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("png") guard let data = self.pngData() else { return nil } do { try data.write(to: tempURL) return try MSSticker(contentsOfFileURL: tempURL, localizedDescription: "Generated") } catch { print("โ ์คํฐ์ปค ์์ฑ ์คํจ: \(error)") return nil } } }
๐ 4. ์ปจ์ ๊ธฐ๋ฐ ์์ฑ
์ฌ์ ์ ์๋ ์ปจ์ (ํ ๋ง, ์ฅ์, ์ฌ๋ฌผ)์ ์กฐํฉํ์ฌ ์ด๋ฏธ์ง๋ฅผ ์์ฑํฉ๋๋ค.
import SwiftUI // ์ด๋ฏธ์ง ์์ฑ ์ปจ์ enum ImageConcept: String, CaseIterable { // ์บ๋ฆญํฐ case cat = "๊ณ ์์ด" case dog = "๊ฐ์์ง" case astronaut = "์ฐ์ฃผ์ธ" case robot = "๋ก๋ด" // ์ฅ์ case space = "์ฐ์ฃผ" case beach = "ํด๋ณ" case mountain = "์ฐ" case city = "๋์" // ํ๋ case flying = "๋ ์๊ฐ๋" case dancing = "์ถค์ถ๋" case reading = "์ฑ ์ฝ๋" case playing = "๋๊ณ ์๋" } struct ConceptBasedGeneratorView: View { @State private var selectedCharacter: ImageConcept? @State private var selectedPlace: ImageConcept? @State private var selectedActivity: ImageConcept? @State private var generatedImage: Image? let characters: [ImageConcept] = [.cat, .dog, .astronaut, .robot] let places: [ImageConcept] = [.space, .beach, .mountain, .city] let activities: [ImageConcept] = [.flying, .dancing, .reading, .playing] var combinedPrompt: String { var parts: [String] = [] if let activity = selectedActivity { parts.append(activity.rawValue) } if let character = selectedCharacter { parts.append(character.rawValue) } if let place = selectedPlace { parts.append("in \(place.rawValue)") } return parts.joined(separator: " ") } var body: some View { ScrollView { VStack(spacing: 24) { // ์บ๋ฆญํฐ ์ ํ conceptSection( title: "์บ๋ฆญํฐ", concepts: characters, selected: $selectedCharacter ) // ํ๋ ์ ํ conceptSection( title: "ํ๋", concepts: activities, selected: $selectedActivity ) // ์ฅ์ ์ ํ conceptSection( title: "์ฅ์", concepts: places, selected: $selectedPlace ) // ์กฐํฉ๋ ํ๋กฌํํธ VStack(alignment: .leading, spacing: 8) { Text("์์ฑ๋ ์ด๋ฏธ์ง") .font(.headline) Text(combinedPrompt.isEmpty ? "์์์ ์ ํํด์ฃผ์ธ์" : combinedPrompt) .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color.gray.opacity(0.1)) .cornerRadius(8) } .padding(.horizontal) // ์์ฑ ๋ฒํผ Button { generateImageWithConcepts() } label: { Label("์ด๋ฏธ์ง ์์ฑ", systemImage: "wand.and.stars") .frame(maxWidth: .infinity) .padding() .background(Color.blue) .foregroundStyle(.white) .cornerRadius(12) } .padding(.horizontal) .disabled(combinedPrompt.isEmpty) // ๊ฒฐ๊ณผ if let generatedImage { generatedImage .resizable() .scaledToFit() .cornerRadius(12) .padding(.horizontal) } } .padding(.vertical) } } func conceptSection( title: String, concepts: [ImageConcept], selected: Binding<ImageConcept?> ) -> some View { VStack(alignment: .leading, spacing: 12) { Text(title) .font(.headline) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(concepts, id: \.self) { concept in Button { if selected.wrappedValue == concept { selected.wrappedValue = nil } else { selected.wrappedValue = concept } } label: { Text(concept.rawValue) .padding(.horizontal, 16) .padding(.vertical, 8) .background( selected.wrappedValue == concept ? Color.blue : Color.gray.opacity(0.2) ) .foregroundStyle( selected.wrappedValue == concept ? .white : .primary ) .cornerRadius(20) } } } .padding(.horizontal) } } } func generateImageWithConcepts() { // Image Playground API๋ก ์ด๋ฏธ์ง ์์ฑ print("๐จ ํ๋กฌํํธ: \(combinedPrompt)") } }
โ๏ธ 5. ๊ณ ๊ธ ์ค์
์ด๋ฏธ์ง ํฌ๊ธฐ, ํ์ง, ๋ณํ ๋ฑ ๊ณ ๊ธ ์ต์ ์ ์ค์ ํฉ๋๋ค.
import SwiftUI // ์ด๋ฏธ์ง ์์ฑ ์ค์ struct ImageGenerationSettings { var style: ImagePlaygroundStyle = .animation var aspectRatio: AspectRatio = .square var numberOfVariations: Int = 1 var seed: Int? = nil // ์ผ๊ด๋ ๊ฒฐ๊ณผ๋ฅผ ์ํ ์๋ enum AspectRatio: String, CaseIterable { case square = "1:1" case portrait = "3:4" case landscape = "4:3" } } struct AdvancedGeneratorView: View { @State private var settings = ImageGenerationSettings() @State private var prompt = "" @State private var variations: [Image] = [] var body: some View { Form { Section("ํ๋กฌํํธ") { TextField("์ด๋ฏธ์ง ์ค๋ช ", text: $prompt, axis: .vertical) .lineLimit(3...5) } Section("์คํ์ผ") { Picker("์คํ์ผ", selection: $settings.style) { ForEach(ImagePlaygroundStyle.allCases) { style in Text(style.rawValue).tag(style) } } } Section("๋น์จ") { Picker("์ข ํก๋น", selection: $settings.aspectRatio) { ForEach(ImageGenerationSettings.AspectRatio.allCases, id: \.self) { ratio in Text(ratio.rawValue).tag(ratio) } } .pickerStyle(.segmented) } Section("๋ณํ") { Stepper("๋ณํ ๊ฐ์: \(settings.numberOfVariations)", value: $settings.numberOfVariations, in: 1...4) } Section { Button("์์ฑํ๊ธฐ") { generateVariations() } .frame(maxWidth: .infinity) .disabled(prompt.isEmpty) } if !variations.isEmpty { Section("๊ฒฐ๊ณผ") { ForEach(variations.indices, id: \.self) { index in VStack { variations[index] .resizable() .scaledToFit() .cornerRadius(12) HStack { Button("์ ์ฅ") { saveImage(at: index) } .buttonStyle(.bordered) Button("๊ณต์ ") { shareImage(at: index) } .buttonStyle(.bordered) } } } } } } .navigationTitle("๊ณ ๊ธ ์ค์ ") } func generateVariations() { // Image Playground API๋ก ์ฌ๋ฌ ๋ณํ ์์ฑ print("๐จ \(settings.numberOfVariations)๊ฐ ๋ณํ ์์ฑ") } func saveImage(at index: Int) { // ํฌํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ ์ฅ print("๐พ ์ด๋ฏธ์ง \(index) ์ ์ฅ") } func shareImage(at index: Int) { // ๊ณต์ ์ํธ ํ์ print("๐ค ์ด๋ฏธ์ง \(index) ๊ณต์ ") } }
๐ก HIG ๊ฐ์ด๋๋ผ์ธ
- ํ๋ผ์ด๋ฒ์: ๋ชจ๋ ์ฒ๋ฆฌ๋ ์จ๋๋ฐ์ด์ค์์ ์ํ๋จ
- Apple Intelligence ์๊ตฌ: iOS 18+ ๋ฐ Apple Intelligence ์ง์ ๊ธฐ๊ธฐ ํ์
- ์์คํ UI: ๊ฐ๋ฅํ ๊ฒฝ์ฐ ์์คํ ์ ๊ณต UI ์ฌ์ฉ
- ์ปจํ ์คํธ: ์ฑ ๋งฅ๋ฝ์ ๋ง๋ ํ๋กฌํํธ ์ ๊ณต
- ์ ์๊ถ: ์์ฑ๋ ์ด๋ฏธ์ง๋ ์ฌ์ฉ์ ์์ ์ด์ง๋ง ์์ ์ ์ฌ์ฉ ์ ํ ํ์ธ
๐ฏ ์ค์ ํ์ฉ
- ๋ฉ์์ง ์ฑ: ๋ํ ์ค ์ด๋ชจํฐ์ฝ/์คํฐ์ปค ์์ฑ
- ๋ ธํธ ์ฑ: ๋ ธํธ์ ์ฝํ ์ถ๊ฐ
- ์์ ๋ฏธ๋์ด: ๊ฒ์๋ฌผ์ฉ ์ด๋ฏธ์ง ์์ฑ
- ํฌ๋ฆฌ์์ดํฐ๋ธ ์ฑ: ๋์์ธ ์ด์ ์์ฑ
- ๊ต์ก ์ฑ: ํ์ต ์๋ฃ ์๊ฐํ
๐ ๋ ์์๋ณด๊ธฐ
- Image Playground ๊ณต์ ๋ฌธ์ (์์ )
- WWDC 2024: Apple Intelligence ์ธ์
- HIG: Image Playground (์์ )