나의 발자취

[UIKit] Alamofire, Azure AI Translate API 이용해서 간단 번역 기능 앱 만들기 (1) 본문

앱 개발/iOS

[UIKit] Alamofire, Azure AI Translate API 이용해서 간단 번역 기능 앱 만들기 (1)

달모드 2024. 11. 5. 12:04

거창한건 아니고 그냥 기능 구현 해보는 정도의 수준으로 단기간 안에 끝나는 것이다.ㅎ

어제 한시간동안 하고 자긴 했는데 너무 졸렸음..

 

뷰 구성

단일 뷰로 해서 UITableView를 사용하여 구성해줄 것이다.

 

Make Sure That

Table View Cell Identifier = 'cell'이든 자기맘대로 설정

 


모델 구현 (Models.swift)

지금 Postman에서 내가 사용할 번역기의 JSON input/output 데이터 구조를 살펴보자.

1. 이 아이를 넣으면,

 

이런 식으로 반환이 될 것이다.

 

 

따라서 우리가 만드는 Swift Model 구조는 이렇게 될것이다.

 

2. 그리고 아웃풋 중 "translations"  안쪽 { } 부분을 보면

똑같이 이렇게 구성하면 된다.

 

 

3. 그리고 위에 방금 다룬 output의 상위 구조를 보면 방금 위에 짠 Translation 구조체 데이터는 "translations"라고 하는 [ ] <꺾은 대괄호에 싸여있는 구조다.

 

따라서 [] 대괄호 안에 방금 위의 구조체 Translation 을 담아준다.

 

import Foundation

// user input
struct Text: Codable {
    let text: String
}

// translated output
struct Translation: Codable {
    let text: String
    let to: String
}

// root output
struct Document: Codable {
    let translations: [Translation]
}

 


API endpoint 및 필수값 변수 선언

params

API 호출 시 필요한 필수값들과 함께 endpoint, apiKey를 넣어줄것이다.

region 필수값


func searchBarSearchButtonClicked

ViewDidLoad() 밑에 익스텐션으로 구성해준다.


body variable?

를 보면, 유저가 body에 담아서 보내는 JSON 객체는 우리가 만든 model.swift에서 "Text 구조체 안의 text 변수"들의 모음으로 되어있으며, 딕셔너리(below right capture on line 1, 9) 안에 담겨져 있는 것을 알 수 있다.

 

 

따라서 let body = [Text(text: text)] 로 body 변수를 정의를 해줄 수 있다.

extension MainTableViewController: UISearchBarDelegate {
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        guard let text = searchBar.text else { return }
        let strURL = "\(endpoint)&to=\(targetLanguage)"
        let body = [Text(text: text)]
    }
}

 

let body = [Text(text: text)] 는 아래 세 줄과 같다.

var body: [Text] = []
let txt = Text(text: text)
body.append(txt)

 

추가로 text를 더 받는 경우에는 위 코드를 써주면 된다.

 


AF를 이용해서 body를 JSON 객체로 보내기

1. url 구조 짜기

일단 앞서 url 구조를 짜줄 것이다. 위에 이어서 코드를 작성한다.

// url structure 짜기
guard let url = URL(string: strURL) else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"

 URL()은 옵셔널이라 guard문으로 옵셔널 언래핑을 해주고,

request에는 헤더 옵션도 넣어서 요청을 해야하므로 let 이 아닌 var로 해준다.

 

2. body JSONify

그다음, body를 인코딩해서 JSONify하는 작업을 해준다.

 

이 때는 앞에 try! 구문을 적어주면 된다.

 

더 간결하게 let json 이라는 새로운 변수 대신 요청할 때의 기본 Instance Property인 .httpBody를 사용해준다.

// json encoding for body
request.httpBody = try! JSONEncoder().encode(body)

 

 

3. header 작업

마지막으로 header를 위한 key-value값을 넣어준다.

// add key value for header
request.addValue(subscriptionKey, forHTTPHeaderField: "Ocp-Apim-Subscription-Key")
request.addValue(region, forHTTPHeaderField: "Ocp-Apim-Subscription-Region")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")

 

 

중요 ⭐️

Alamofire request

Alamofire에서 제공하는 두 개의 생성자를 써서, 총 두 가지 방법을 써볼것이다.

1) AF.responseDecodable()
2) AF.request()

 

 

첫번째 방법: AF.responseDecodable()

 

위에 import Alamofire 선언은 필수. 그래야 인스턴스 속성들이 잘 뜬다.

 

 

.responseDecodable()을 Opt+Return을 쳐서 기본 인자값들을 살펴보자.

 

우선 of: 인자(Decodable & Sendable).Type 인 것을 알 수 있다. 

 


이 두 개의 의미는 무엇인가??

(Decodable & Sendable). 여기에 오는 인자값은 두 개의 프로토콜(Decodable, Sendable)을 따라야 한다는 것이다. (Sendable 프로토콜을 따르지 않아도 오류는 나지는 않는다.)

 

그리고  Type.

Type. 이다. 

그 말은? "타입"이 와야한다는 것이다.

 

 

따라서, 일단 (Decodable & Sendable) 먼저 처리를 해주기 위해, responseDecodable()이었으므로 response로 오는 데이터(구조체)들의 프로토콜에 Sendable을 따르도록 추가해준다.

 

Model.swift로 가서 Sendable 프로토콜 추가

 

 

그리고, 다시 돌아와서 response JSON 데이터 구조를 보자.


{"translations":...} 는 지금 우리가 Model.swift에서 만든 구조체의 document다.

그런데 이들은 지금, line 1을 보면 [ ] 이렇게 Array 안에 담겨있다. (여러 개가 올 수 있다는 뜻) -> [Document]로 받아야한다.

또한, 위에서, 분명히 (Decodable & Sendable).Type 이라고 했다. "타입" 이 와야한다고 했다. -> .self 로 타입형을 만들어준다.

 

 

따라서 아래와 같이 받을 수 있다.

 

 

 

 

처리를 해주기 위해 위에 translations 변수 선언

(좌) Model.swift에 정의된 구조체 / (우) MainTableView.swift에 정의한 변수

case 나누기

 

.success 케이스에서, (let documents)를 보면

document이 response 객체로 반환되므로 document"s"로 선언을 해주어야 한다는 점.  <중요

        AF.request(request).responseDecodable(of: [Document].self) { response in
            switch response.result {
                case .success(let documents):
                self.translations = documents.first?.translations
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
                case .failure(let error):
                    print(error.localizedDescription)
                
            }
            
        }

 


테이블뷰 셀 속성 설정하기

override func numberOfSections(in tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return translations?.count ?? 0
    }

  
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        guard let translation = translations?[indexPath.row] else { return cell }
    
        var config = cell.defaultContentConfiguration()
        config.text = "\(translation.to) : \(translation.text)"
        cell.contentConfiguration = config
        
        return cell
    }

 

 

최종 코드

//
//  MainTableViewController.swift
//  AI Translate
//
//  Created by Lia An on 11/5/24.
//

import UIKit
import Alamofire

class MainTableViewController: UITableViewController {
    let endpoint = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0"
    let subscriptionKey = 구독키
    let region = "eastus"
    let targetLanguage = "en,ja,fr,zh,es"
    
    var translations: [Translation]?

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return translations?.count ?? 0
    }

  
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        guard let translation = translations?[indexPath.row] else { return cell }
    
        var config = cell.defaultContentConfiguration()
        config.text = "\(translation.to) : \(translation.text)"
        cell.contentConfiguration = config
        
        return cell
    }

}


extension MainTableViewController: UISearchBarDelegate {
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        guard let text = searchBar.text else { return }
        let strURL = "\(endpoint)&to=\(targetLanguage)"
        let body = [Text(text: text)]
        // url structure 짜기
        guard let url = URL(string: strURL) else { return }
        var request = URLRequest(url: url)
        // POST request
        request.httpMethod = "POST"
        // json encoding for body
        request.httpBody = try! JSONEncoder().encode(body)
        // add key value for header
        request.addValue(subscriptionKey, forHTTPHeaderField: "Ocp-Apim-Subscription-Key")
        request.addValue(region, forHTTPHeaderField: "Ocp-Apim-Subscription-Region")
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        // Alamofire request
        AF.request(request).responseDecodable(of: [Document].self) { response in // response body 구조
            switch response.result {
                case .success(let documents): // 변수 설정 document's'
                self.translations = documents.first?.translations
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
                case .failure(let error):
                    print(error.localizedDescription)
                
            }
            
        }
    }
}

 

 

시뮬레이션으로 결과를 확인해본다!

 


두번째 방법: AF.request()

header

header부터 달라진다.

헤더를 좀 더 간편하게 선언해준다.

// headers
let headers: HTTPHeaders = [
    "Ocp-Apim-Subscription-Key": subscriptionKey,
    "Ocp-Apim-Subscription-Region": region,
    "Content-Type": "application/json"
]

 

 

그리고 AF.request()의 기본 생성자가 어떤 인자값들을 받는지 살펴본다.

 

alamo라는 변수에 담았다. 훨씬 더 간결하다.

let alamo = AF.request(strURL, method: .post, parameters: body, encoder: JSONParameterEncoder.default, headers: headers)

 

이어서 alamo에 접근해서 responseDecodable()로 아까와 같은 switch문을 그대로 재사용한다.

alamo.responseDecodable(of: [Document].self) { response in
            switch response.result {
            case .success(let documents): // 변수 설정 document's'
                self.translations = documents.first?.translations
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            case .failure(let error):
                print(error.localizedDescription)
                
            }
            
        }

 

 

 

최종 코드

//
//  MainTableViewController.swift
//  AI Translate
//
//  Created by Lia An on 11/5/24.
//

import UIKit
import Alamofire

class MainTableViewController: UITableViewController {
    let endpoint = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0"
    let subscriptionKey = 구독키
    let region = "eastus"
    let targetLanguage = "en,ja,fr,zh,es"
    
    var translations: [Translation]?
    
    override func viewDidLoad() {
        super.viewDidLoad()

    }
    
    // MARK: - Table view data source
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 1
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return translations?.count ?? 0
    }
    
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        guard let translation = translations?[indexPath.row] else { return cell }
        
        var config = cell.defaultContentConfiguration()
        config.text = "\(translation.to) : \(translation.text)"
        cell.contentConfiguration = config
        
        return cell
    }

    
}


extension MainTableViewController: UISearchBarDelegate {
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        guard let text = searchBar.text else { return }
        let strURL = "\(endpoint)&to=\(targetLanguage)"
        let body = [Text(text: text)]
        // headers
        let headers: HTTPHeaders = [
            "Ocp-Apim-Subscription-Key": subscriptionKey,
            "Ocp-Apim-Subscription-Region": region,
            "Content-Type": "application/json"
        ]
        // Alamofire request
        let alamo = AF.request(strURL, method: .post, parameters: body, encoder: JSONParameterEncoder.default, headers: headers)
        
        alamo.responseDecodable(of: [Document].self) { response in
            switch response.result {
            case .success(let documents): // 변수 설정 document's'
                self.translations = documents.first?.translations
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            case .failure(let error):
                print(error.localizedDescription)
                
            }
            
        }
    }
}
Comments