Section 11/151 menit

11. Integrasi dengan SwiftUI

11. Integrasi dengan SwiftUI

Chat Interface Lengkap

swift
import FoundationModels
import SwiftUI

struct Message: Identifiable {
    let id = UUID()
    let role: Role
    let content: String

    enum Role {
        case user, assistant
    }
}

@Observable
class ChatViewModel {
    var messages: [Message] = []
    var isLoading = false
    var inputText = ""

    private var session: LanguageModelSession?

    init() {
        setupSession()
    }

    private func setupSession() {
        guard case .available = SystemLanguageModel.default.availability else { return }
        session = LanguageModelSession(
            instructions: Instructions("Kamu adalah asisten yang membantu dan ramah.")
        )
    }

    func sendMessage() async {
        guard let session, !inputText.trimmingCharacters(in: .whitespaces).isEmpty else { return }

        let userText = inputText
        inputText = ""
        messages.append(Message(role: .user, content: userText))
        isLoading = true

        do {
            var assistantText = ""
            messages.append(Message(role: .assistant, content: ""))
            let lastIndex = messages.count - 1

            let stream = session.streamResponse(to: userText)
            for try await partial in stream {
                assistantText = partial.content
                messages[lastIndex] = Message(role: .assistant, content: assistantText)
            }
        } catch {
            messages.append(Message(role: .assistant, content: "Terjadi error: \(error.localizedDescription)"))
        }

        isLoading = false
    }
}

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)
                                    .id(message.id)
                            }
                            if viewModel.isLoading && viewModel.messages.last?.role == .assistant {
                                EmptyView()
                            }
                        }
                        .padding()
                    }
                    .onChange(of: viewModel.messages.count) {
                        if let last = viewModel.messages.last {
                            withAnimation {
                                proxy.scrollTo(last.id, anchor: .bottom)
                            }
                        }
                    }
                }

                Divider()
                InputBar(viewModel: viewModel)
            }
            .navigationTitle("AI Assistant")
        }
    }
}

struct MessageBubble: View {
    let message: Message

    var body: some View {
        HStack {
            if message.role == .user { Spacer() }
            Text(message.content)
                .padding(12)
                .background(message.role == .user ? Color.blue : Color(.secondarySystemBackground))
                .foregroundStyle(message.role == .user ? .white : .primary)
                .clipShape(RoundedRectangle(cornerRadius: 16))
            if message.role == .assistant { Spacer() }
        }
    }
}

struct InputBar: View {
    var viewModel: ChatViewModel

    var body: some View {
        HStack(spacing: 12) {
            TextField("Ketik pesan...", text: $viewModel.inputText, axis: .vertical)
                .textFieldStyle(.roundedBorder)
                .lineLimit(1...5)

            Button {
                Task { await viewModel.sendMessage() }
            } label: {
                Image(systemName: "arrow.up.circle.fill")
                    .font(.title2)
            }
            .disabled(viewModel.isLoading || viewModel.inputText.trimmingCharacters(in: .whitespaces).isEmpty)
        }
        .padding()
    }
}