บทความนี้จะพาคุณเชื่อมต่อแอป iOS กับ AI API แบบ Streaming โดยใช้ภาษา Swift ตั้งแต่ขั้นตอนแรกจนถึงขั้นตอนสุดท้าย อธิบายทุกอย่างแบบละเอียดเหมือนกำลังคุยกับเพื่อน ไม่ต้องกังวลว่าจะไม่เข้าใจเพราะเราจะไม่ใช้คำศัพท์เทคนิคยากๆ

HTTP Streaming คืออะไร ทำไมต้องรู้

ปกติเวลาคุณถาม AI แล้วรอ มันจะคิดนานมากแล้วค่อยตอบทีเดียว แต่ Streaming คือการที่ AI จะส่งคำตอบมาทีละคำเหมือนพิมพ์สด ทำให้ผู้ใช้รู้สึกว่าได้คำตอบเร็วและน่าตื่นเต้นกว่าเดิมมาก

เตรียมตัวก่อนเริ่ม

สิ่งที่คุณต้องมี:

ขั้นตอนที่ 1: สร้างโปรเจกต์ใหม่

เปิด Xcode แล้วไปที่ File > New > Project เลือก iOS > App แล้วกด Next ตั้งชื่อโปรเจกต์ว่า AISteamDemo เลือก Swift เป็นภาษาหลัก กด Create เป็นอันเสร็จ

ขั้นตอนที่ 2: สร้างไฟล์สำหรับเรียกใช้ API

ในโฟลเดอร์โปรเจกต์ ให้สร้างไฟล์ใหม่ชื่อ AIService.swift โดยคลิกขวาที่โฟลเดอร์ > New File > Swift File

ขั้นตอนที่ 3: เขียนโค้ดเรียก API แบบ Streaming

คัดลอกโค้ดด้านล่างไปวางในไฟล์ AIService.swift ของคุณได้เลย:

import Foundation

class AIService {
    // ตั้งค่าตามที่ได้จากการสมัคร HolySheep
    private let apiKey = "YOUR_HOLYSHEEP_API_KEY"
    private let baseURL = "https://api.holysheep.ai/v1"
    
    func sendMessageStreaming(
        prompt: String,
        onChunkReceived: @escaping (String) -> Void,
        onComplete: @escaping () -> Void,
        onError: @escaping (Error) -> Void
    ) {
        // สร้าง URL สำหรับ chat completion
        guard let url = URL(string: "\(baseURL)/chat/completions") else {
            onError(NSError(domain: "InvalidURL", code: 400, userInfo: nil))
            return
        }
        
        // สร้าง request และตั้งค่า header
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("text/event-stream", forHTTPHeaderField: "Accept")
        
        // สร้าง body ตามรูปแบบที่ API ต้องการ
        let body: [String: Any] = [
            "model": "gpt-4.1",
            "messages": [
                ["role": "user", "content": prompt]
            ],
            "stream": true
        ]
        
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: body)
        } catch {
            onError(error)
            return
        }
        
        // เริ่ม stream connection
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                onError(error)
                return
            }
            
            guard let data = data, let text = String(data: data, encoding: .utf8) else {
                onError(NSError(domain: "NoData", code: 500, userInfo: nil))
                return
            }
            
            // แยกวิเคราะห์ข้อมูล Streaming
            let lines = text.components(separatedBy: "\n")
            for line in lines {
                if line.hasPrefix("data: ") {
                    let jsonString = String(line.dropFirst(6))
                    if jsonString == "[DONE]" {
                        onComplete()
                        return
                    }
                    // ดึงข้อความจาก JSON
                    if let content = self.extractContent(from: jsonString) {
                        DispatchQueue.main.async {
                            onChunkReceived(content)
                        }
                    }
                }
            }
            onComplete()
        }
        
        task.resume()
    }
    
    // ฟังก์ชันดึงข้อความจาก JSON format
    private func extractContent(from jsonString: String) -> String? {
        guard let data = jsonString.data(using: .utf8),
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
              let choices = json["choices"] as? [[String: Any]],
              let firstChoice = choices.first,
              let delta = firstChoice["delta"] as? [String: Any],
              let content = delta["content"] as? String else {
            return nil
        }
        return content
    }
}

ขั้นตอนที่ 4: นำไปใช้ในหน้าจอแอป

คัดลอกโค้ดด้านล่างไปวางใน ViewController.swift หรือ SwiftUI View ของคุณ:

import UIKit

class ViewController: UIViewController {
    private let aiService = AIService()
    private var responseLabel = UILabel()
    private var activityIndicator = UIActivityIndicatorView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        view.backgroundColor = .white
        
        // สร้างปุ่มสำหรับทดสอบ
        let button = UIButton(type: .system)
        button.setTitle("ถาม AI", for: .normal)
        button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
        button.frame = CGRect(x: 20, y: 100, width: 200, height: 50)
        button.addTarget(self, action: #selector(askAI), for: .touchUpInside)
        view.addSubview(button)
        
        // สร้าง label แสดงคำตอบ
        responseLabel.frame = CGRect(x: 20, y: 170, width: view.bounds.width - 40, height: 500)
        responseLabel.numberOfLines = 0
        responseLabel.font = UIFont.systemFont(ofSize: 16)
        responseLabel.text = "กดปุ่มเพื่อถาม AI..."
        view.addSubview(responseLabel)
        
        // สร้าง loading indicator
        activityIndicator.frame = CGRect(x: 230, y: 100, width: 50, height: 50)
        activityIndicator.hidesWhenStopped = true
        view.addSubview(activityIndicator)
    }
    
    @objc private func askAI() {
        responseLabel.text = "กำลังคิด..."
        activityIndicator.startAnimating()
        
        aiService.sendMessageStreaming(
            prompt: "อธิบายเรื่อง AI แบบเข้าใจง่าย",
            onChunkReceived: { chunk in
                // ต่อข้อความทีละส่วน
                self.responseLabel.text = (self.responseLabel.text ?? "") + chunk
            },
            onComplete: {
                DispatchQueue.main.async {
                    self.activityIndicator.stopAnimating()
                }
            },
            onError: { error in
                DispatchQueue.main.async {
                    self.responseLabel.text = "เกิดข้อผิดพลาด: \(error.localizedDescription)"
                    self.activityIndicator.stopAnimating()
                }
            }
        )
    }
}

ขั้นตอนที่ 5: ทดสอบและรันแอป

กดปุ่ม Run (⌘+R) เพื่อทดสอบแอป ถ้าทุกอย่างถูกต้อง คุณจะเห็นข้อความปรากฏทีละตัวแบบ Streaming สวยงาม

ราคาและค่าใช้จ่าย

หลายคนกังวลเรื่องค่าใช้จ่าย เลือกใช้ HolySheep AI ประหยัดกว่ามาก เพราะอัตราแลกเปลี่ยน ¥1=$1 คิดเป็นเงินไทยก็ถูกมาก:

ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข

กรณีที่ 1: แสดงข้อความ "401 Unauthorized"

ข้อผิดพลาดนี้เกิดจาก API Key ไม่ถูกต้องหรือหมดอายุ ให้ไปที่หน้า API Keys ในบัญชี HolySheep แล้วคัดลอก Key ใหม่มาแทนที่

// ❌ ผิด - Key ไม่ถูกต้อง
private let apiKey = "sk-wrong-key-here"

// ✅ ถูกต้อง - ใส่ Key ที่คัดลอกมาจาก HolySheep
private let apiKey = "YOUR_HOLYSHEEP_API_KEY"

กรณีที่ 2: แสดงข้อความ "No Data" หรือข้อความว่างเปล่า

อาจเกิดจากการตั้งค่า Accept Header ผิดพลาด หรือ Network ไม่ได้เชื่อมต่อ ให้ตรวจสอบว่าแอปมีสิทธิ์เข้าถึง Network หรือไม่ และเปลี่ยน URLSession configuration

// เพิ่ม configuration สำหรับ allow arbitrary loads
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 60
config.timeoutIntervalForResource = 120

// ใช้ configuration นี้แทน default
let task = URLSession(configuration: config).dataTask(with: request) { ... }

กรณีที่ 3: Streaming หยุดกลางคันแล้วไม่ต่อ

ปัญหานี้มักเกิดจากการ parse JSON ผิดพลาด ให้เพิ่มการตรวจสอบและการจัดการ error ที่ดีขึ้น รวมถึงการ reconnection เมื่อ connection หลุด

private func extractContent(from jsonString: String) -> String? {
    // ข้ามบรรทัดว่าง
    if jsonString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
        return nil
    }
    
    guard let data = jsonString.data(using: .utf8) else {
        return nil
    }
    
    do {
        if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
           let choices = json["choices"] as? [[String: Any]],
           let firstChoice = choices.first,
           let delta = firstChoice["delta"] as? [String: Any],
           let content = delta["content"] as? String {
            return content
        }
    } catch {
        print("Parse error: \(error)")
    }
    return nil
}

กรณีที่ 4: แอป crash เมื่อกดปุ่ม

ตรวจสอบว่า Info.plist มีการอนุญาต Network แล้วหรือยัง และดูว่าโค้ดอยู่ใน main thread หรือไม่ เพราะ UI ต้องอัพเดตบน main thread เท่านั้น

// ใช้ DispatchQueue.main.async สำหรับทุก UI update
@objc private func askAI() {
    DispatchQueue.main.async {
        self.responseLabel.text = "กำลังคิด..."
        self.activityIndicator.startAnimating()
    }
    
    aiService.sendMessageStreaming(
        prompt: "อธิบายเรื่อง AI",
        onChunkReceived: { chunk in
            DispatchQueue.main.async {
                self.responseLabel.text = (self.responseLabel.text ?? "") + chunk
            }
        },
        onComplete: {
            DispatchQueue.main.async {
                self.activityIndicator.stopAnimating()
            }
        },
        onError: { error in
            DispatchQueue.main.async {
                self.responseLabel.text = "ผิดพลาด: \(error.localizedDescription)"
                self.activityIndicator.stopAnimating()
            }
        }
    )
}

สรุป

การเชื่อมต่อ Swift iOS กับ AI API แบบ Streaming ไม่ได้ยากอย่างที่คิด แค่รู้พื้นฐา