์จ๋๋ฐ์ด์ค AI/LLM ๊ตฌํ ๊ฐ์ด๋. ์ด ๋ฌธ์๋ฅผ ์ฝ๊ณ Foundation Models๋ฅผ ํ์ฉํ ์ ์์ต๋๋ค.
Foundation Models๋ iOS 26+์์ ์จ๋๋ฐ์ด์ค AI ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ํ๋ ์์ํฌ์ ๋๋ค. Apple Intelligence๋ฅผ ํ์ฉํด ํ๋ผ์ด๋ฒ์๋ฅผ ๋ณดํธํ๋ฉด์ ํ ์คํธ ์์ฑ, ์์ฝ, ๋๊ตฌ ์ฌ์ฉ ๋ฑ์ ๊ตฌํํฉ๋๋ค.
import FoundationModels// ๊ธฐ๋ณธ ์ธ์
let session = LanguageModelSession()
// ์์คํ
ํ๋กฌํํธ ํฌํจ
let session = LanguageModelSession(
instructions: "๋น์ ์ ์น์ ํ ์๋ฆฌ ๋์ฐ๋ฏธ์
๋๋ค. ํ๊ตญ์ด๋ก ๋ต๋ณํ์ธ์."
)// ๋จ์ ์์ฑ
let response = try await session.respond(to: "ํ์คํ ๋ ์ํผ ์๋ ค์ค")
print(response.content)
// ์คํธ๋ฆฌ๋ฐ
for try await partial in session.streamResponse(to: "ํ์คํ ๋ ์ํผ ์๋ ค์ค") {
print(partial.content, terminator: "")
}@Generable
struct WeatherTool: Tool {
static let name = "weather"
static let description = "๋์์ ํ์ฌ ๋ ์จ๋ฅผ ๊ฐ์ ธ์ต๋๋ค"
struct Arguments: Codable, Sendable {
@Guide(description: "๋์ ์ด๋ฆ (์: ์์ธ, ๋ถ์ฐ)")
let city: String
}
func call(arguments: Arguments) async throws -> String {
// ์ค์ ๋ ์จ API ํธ์ถ ๋๋ ์๋ฎฌ๋ ์ด์
return "\(arguments.city)์ ํ์ฌ ๋ ์จ: ๋ง์, 23ยฐC"
}
}import SwiftUI
import FoundationModels
// MARK: - Message Model
struct ChatMessage: Identifiable {
let id = UUID()
let role: Role
let content: String
let timestamp: Date
enum Role {
case user, assistant
}
}
// MARK: - ViewModel
@Observable
class ChatViewModel {
var messages: [ChatMessage] = []
var inputText = ""
var isLoading = false
private var session: LanguageModelSession?
init() {
setupSession()
}
private func setupSession() {
session = LanguageModelSession(
instructions: """
๋น์ ์ ์น์ ํ๊ณ ๋์์ด ๋๋ AI ์ด์์คํดํธ์
๋๋ค.
๊ฐ๊ฒฐํ๊ณ ์ ํํ๊ฒ ๋ต๋ณํ์ธ์.
ํ๊ตญ์ด๋ก ๋ํํฉ๋๋ค.
"""
)
}
func sendMessage() async {
let text = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { return }
// ์ฌ์ฉ์ ๋ฉ์์ง ์ถ๊ฐ
let userMessage = ChatMessage(role: .user, content: text, timestamp: Date())
messages.append(userMessage)
inputText = ""
isLoading = true
do {
// AI ์๋ต ์์ฑ
let response = try await session?.respond(to: text)
let assistantMessage = ChatMessage(
role: .assistant,
content: response?.content ?? "์๋ต์ ์์ฑํ ์ ์์ต๋๋ค.",
timestamp: Date()
)
messages.append(assistantMessage)
} catch {
let errorMessage = ChatMessage(
role: .assistant,
content: "์ค๋ฅ: \(error.localizedDescription)",
timestamp: Date()
)
messages.append(errorMessage)
}
isLoading = false
}
func clearHistory() {
messages.removeAll()
setupSession() // ์ธ์
์ด๊ธฐํ
}
}
// MARK: - View
struct ChatView: View {
@State private var viewModel = ChatViewModel()
var body: some View {
NavigationStack {
VStack(spacing: 0) {
// ๋ฉ์์ง ๋ชฉ๋ก
ScrollViewReader { proxy in
ScrollView {
LazyVStack(spacing: 12) {
ForEach(viewModel.messages) { message in
MessageBubble(message: message)
}
if viewModel.isLoading {
ProgressView()
.padding()
}
}
.padding()
}
.onChange(of: viewModel.messages.count) {
if let lastMessage = viewModel.messages.last {
proxy.scrollTo(lastMessage.id, anchor: .bottom)
}
}
}
Divider()
// ์
๋ ฅ ํ๋
HStack(spacing: 12) {
TextField("๋ฉ์์ง ์
๋ ฅ...", text: $viewModel.inputText)
.textFieldStyle(.roundedBorder)
Button {
Task {
await viewModel.sendMessage()
}
} label: {
Image(systemName: "arrow.up.circle.fill")
.font(.title)
}
.disabled(viewModel.inputText.isEmpty || viewModel.isLoading)
}
.padding()
}
.navigationTitle("AI ์ฑ๋ด")
.toolbar {
Button("์ด๊ธฐํ") {
viewModel.clearHistory()
}
}
}
}
}
struct MessageBubble: View {
let message: ChatMessage
var body: some View {
HStack {
if message.role == .user { Spacer() }
Text(message.content)
.padding(12)
.background(message.role == .user ? Color.blue : Color.gray.opacity(0.2))
.foregroundStyle(message.role == .user ? .white : .primary)
.clipShape(RoundedRectangle(cornerRadius: 16))
if message.role == .assistant { Spacer() }
}
}
}
#Preview {
ChatView()
}// ์ฌ๋ฌ ๋๊ตฌ ์ ์
@Generable
struct CalculatorTool: Tool {
static let name = "calculator"
static let description = "์ํ ๊ณ์ฐ์ ์ํํฉ๋๋ค"
struct Arguments: Codable, Sendable {
@Guide(description: "๊ณ์ฐ์ (์: 2 + 2, 10 * 5)")
let expression: String
}
func call(arguments: Arguments) async throws -> String {
// ๊ฐ๋จํ ๊ณ์ฐ ๋ก์ง
let expr = NSExpression(format: arguments.expression)
if let result = expr.expressionValue(with: nil, context: nil) as? NSNumber {
return "๊ฒฐ๊ณผ: \(result)"
}
return "๊ณ์ฐํ ์ ์์ต๋๋ค"
}
}
@Generable
struct SearchTool: Tool {
static let name = "search"
static let description = "์ ๋ณด๋ฅผ ๊ฒ์ํฉ๋๋ค"
struct Arguments: Codable, Sendable {
@Guide(description: "๊ฒ์ ํค์๋")
let query: String
}
func call(arguments: Arguments) async throws -> String {
// ์ค์ ๋ก๋ API ํธ์ถ
return "'\(arguments.query)'์ ๋ํ ๊ฒ์ ๊ฒฐ๊ณผ์
๋๋ค..."
}
}
// ๋๊ตฌ์ ํจ๊ป ์ธ์
์์ฑ
let session = LanguageModelSession(
instructions: "๋๊ตฌ๋ฅผ ์ ๊ทน ํ์ฉํด ์ฌ์ฉ์๋ฅผ ๋์์ฃผ์ธ์.",
tools: [WeatherTool(), CalculatorTool(), SearchTool()]
)
// ๋๊ตฌ ํธ์ถ์ด ํ์ํ ์ง๋ฌธ
let response = try await session.respond(to: "์์ธ ๋ ์จ ์ด๋?")
// AI๊ฐ ์๋์ผ๋ก WeatherTool ํธ์ถ@Observable
class StreamingViewModel {
var streamedText = ""
var isStreaming = false
func streamResponse(prompt: String) async {
let session = LanguageModelSession()
isStreaming = true
streamedText = ""
do {
for try await partial in session.streamResponse(to: prompt) {
streamedText += partial.content
}
} catch {
streamedText = "์ค๋ฅ: \(error.localizedDescription)"
}
isStreaming = false
}
}
struct StreamingView: View {
@State var viewModel = StreamingViewModel()
var body: some View {
VStack {
ScrollView {
Text(viewModel.streamedText)
.padding()
}
Button("์์ฑ ์์") {
Task {
await viewModel.streamResponse(prompt: "์ธ๊ณต์ง๋ฅ์ ์ญ์ฌ๋ฅผ ์ค๋ช
ํด์ฃผ์ธ์")
}
}
.disabled(viewModel.isStreaming)
}
}
}- iOS 26+ ์ ์ฉ: ์ด์ ๋ฒ์ ์์๋ ์ฌ์ฉ ๋ถ๊ฐ
- Apple Silicon ํ์: ์จ๋๋ฐ์ด์ค AI๋ Neural Engine ํ์
- ํ๋ผ์ด๋ฒ์: ๋ฐ์ดํฐ๊ฐ ๊ธฐ๊ธฐ๋ฅผ ๋ ๋์ง ์์
- Sendable ์ค์: Tool Arguments๋ Sendable ํ์
- @Generable ๋งคํฌ๋ก: Tool ์ ์ ์ ํ์
if LanguageModelSession.isAvailable {
// Foundation Models ์ฌ์ฉ ๊ฐ๋ฅ
} else {
// ๋์ฒด ๋ก์ง (์: ์๋ฒ API)
}