@Published 로 선언한 변수에서 Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
//TODO: - gitignore에 NaverStorage.swift 추가하기..
import Foundation
struct NaverStorage {
let naverClientID: String = "Your naverClientID"
let naverClientSecret: String = "Your naverClientSecret"
}
앗 그전에 까먹은 것이 있네요..
Alamofire를 사용해 HTTP통신을 하여 네이버 API를 사용해야하니 Alamofire또한 Cocopod를 통해 설치해 줍니다.
(킹피셔로 이미지를 보여줄것이니 같이 다운 받아요 ㅎㅎ)
pod 'Alamofire'
pod 'Kingfisher', '~> 7.0'
pod install을 하였다면,, 이제 HTTP통신을 해야 하겠죠?
HTTP 통신에 사용되는 메소드를 편하게 사용하기 위해 enum에 저장하여 사용합니다.
- HTTPMethod.swift -
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case head = "HEAD"
case delete = "DELETE"
case patch = "PATCH"
case trace = "TRACE"
case options = "OPTIONS"
case connect = "CONNECT"
}
Alamofire를 사용해 네트워크 통신 부분을 작성 해보겠습니다.
- NaverNetwork.swift -
import Foundation
import Alamofire
protocol NaverNetworkDelegate: AnyObject {
func imageDataUpdated(_ imageData: NaverImageData?)
}
class NaverNetwork: ObservableObject {
static let shared = NaverNetwork()
private init() {}
@Published var imageData: NaverImageData?
weak var delegate: NaverNetworkDelegate?
func requestSearchImage(query: String, completion: @escaping () -> Void) {
let baseURL = "https://openapi.naver.com/v1/search/image"
let headers: HTTPHeaders = [
"X-Naver-Client-Id": NaverStorage().naverClientID,
"X-Naver-Client-Secret": NaverStorage().naverClientSecret,
]
let parameters: Parameters = [
"query": query,
"display": 50
]
AF.request(baseURL,
method: .get,
parameters: parameters,
encoding: URLEncoding.default,
headers: headers)
.validate(statusCode: 200...500)
.responseDecodable(of: NaverImageData.self) { response in
switch response.result {
case .success(let data):
guard let statusCode = response.response?.statusCode else { return }
if statusCode == 200 {
DispatchQueue.main.async {
self.imageData = data
self.delegate?.imageDataUpdated(data)
completion()
}
}
print("\(#file) > \(#function) :: SUCCESS")
case .failure(let error):
print("\(#file) > \(#function) :: FAILURE : \(error)")
}
}
}
}
(NaverNetworkDelegate이 뒤에 이미지를 QuizView에서 보여져야 하지만 이미지가 보이지 않았어서 추가하였습니다.. 이부분은 추가로 글쓸 예정!)
QuizView에서 보여질 이미지를 위해 QuizViewModel에서 메소드들을 작성해 줍니다.
//MARK: - KoreanWordSearchAPI
// 단어 데이터 처리 메서드
func handleWordData(word: String, wordData: WordData?) {
if let wordData = wordData {
wordDataDictionary[word] = wordData
// 정답 단어의 설명을 가져오기
if word == correctWord {
fetchCorrectWordDefinition()
fetchImageForWord(correctWord)
}
} else {
print("\(word) 단어 데이터를 가져오지 못함 ")
isLoading = false // 데이터를 가져오지 못한 경우 isLoading을 false로 설정하여 프로그레스 뷰를 숨김
}
isLoading = false
}
// 정답 단어의 설명 가져오기
func fetchCorrectWordDefinition() {
isLoading = true
guard let wordData = wordDataDictionary[correctWord] else {
correctWordDefinition = "설명을 가져올 수 없습니다."
isLoading = false // 작업 완료
return
}
if let firstSense = wordData.channel.item.first?.sense.first {
correctWordDefinition = firstSense.definition
print(correctWordDefinition)
} else {
correctWordDefinition = "설명을 찾을 수 없습니다."
}
isLoading = false // 작업 완료
}
//MARK: - NaverSearchAPI
func fetchImageForWord(_ word: String) {
isLoading = true
naverNetwork.requestSearchImage(query: word){ [weak self] in
// 이미지 데이터 로드 완료 시에만 isLoading을 false로 설정
self?.isLoading = false
}
}
//MARK: - NaverNetworkDelegate
func imageDataUpdated(_ imageData: NaverImageData?) {
DispatchQueue.main.async {
self.imageData = imageData
}
}
}
Kingfisher
킹피셔를 사용하여 이미지를 띄워 줍니다.
//MARK: - 문제의 이미지
VStack{
if quizViewModel.isLoading {
ProgressView("Loading...")
.frame(width: 200, height: 200)
} else {
if let imageData = quizViewModel.imageData {
if let firstImage = imageData.items.first {
KFImage(URL(string: firstImage.link))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
}
}
}
}
.padding()
디자인은 잼병이라,, 다른앱을 참고하며 바꿔보도록 하겠습니다.
이렇게 이미지를 가져올수 있게 되었습니다!!
문제 발생..
문제라 함은 2개가 발생 하였는데요...
문제 1.
//TODO: - gitignore에 NaverStorage.swift 추가하기..
import Foundation
struct NaverStorage {
let naverClientID: String = "Your naverClientID"
let naverClientSecret: String = "Your naverClientSecret"
}
이부분은 큰 문제는 아니지만, 조금더 보안을 신경 써준다면 키체인을 사용하면 좋아 보입니다!!!
네~! 저번엔 간단히 api를 호출해서 단어의 뜻과 설명등을 가져오는 코드를 작성했습니다,,
현재까지 얼마나 했나,,, 보고아닌 보고를 하기위해 작성하였습니다 ^_^/
아직 구상을 하는 중입니다만,,
아마 만들어가면서 계속 구상을 할것같아요 하하
간단히 4개의 보기를 주고, 문제의 뜻에 대한 단어를 찾는
간단한 뷰를 만들어보았습니다.
나름대로 view와 viewModel을 만들어 보았지만,, view의 코드가 깨끗하지 않은점,,
양해 부탁드립니다 ㅎ
저번 글에서 URLSession을 통해 api통신을 하는 코드중 수정을 조금 하였습니다.
func searchWord(_ searchWord: String, completion: @escaping (WordData?) -> Void) {
let urlString = "https://opendict.korean.go.kr/api/search?certkey_no=6282&key=\(myApiKey)&target_type=search&req_type=json&part=word&q=\(searchWord)&sort=dict&start=1&num=10"
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
if let safeData = data {
do {
let decodedData = try JSONDecoder().decode(WordData.self, from: safeData)
DispatchQueue.main.async {
self.wordData = decodedData
completion(decodedData) // 완료 핸들러 호출
}
} catch {
print(error)
completion(nil)
}
}
} else {
print(error)
completion(nil)
}
}
task.resume()
}
}
차이가 조금 보이시나요? ㅎㅎ
저도 아직 감은 잡히지 않았지많,, 완료 핸들러를 추가하였습니다.
흠 추가한 이유는! 4개의 보기의 단어를 검색해서 가져온 데이터를 저장해 주기위해서 입니다,,
추가한 코드를 설명해 드리겠습니다.
QuizViewModel.swift
위에서 보여진 앱은 QuizView.swift 입니다.
이 뷰에 보여지는 데이터 추가를 위해 QuizViewModel을 추가하였는데요!
프로퍼티
let hardKoreanWords = HardKoreanWords()
let wordNetwork = WordNetwork()
var choiceWords = [String]()
var correctWord: String = ""
var correctWordDefinition: String = ""
var wordDataDictionary = [String: WordData]()
@Published var isLoading = false
어려운 단어를 하드코딩해서 넣어둔 hardKoreanWords
우리샘 api를 사용하여 네트워크 통신을 하는 wordNetwork
정답인 단어를 넣어두는 choiceWord
정답인 단어의 뜻을 넣어두는 correctWordDefinition
보기의 단어들을 넣어두는 딕셔너리인wordDataDictionary 등이 있습니다! (isLoading은 프로그래스를 추가해서 검색하는 시간동안 보여주려 하는 함수이지만,, 아직 작동은 안하는..)
보기생성 generateChoices()
func generateChoices() {
choiceWord.append(correctWord)
while choiceWord.count < 4 {
let randomWord = hardKoreanWords.hardWords.randomElement()!
if !choiceWord.contains(randomWord) {
choiceWord.append(randomWord)
}
}
//보기 섞기
choiceWord.shuffle()
//단어 검색 및 데이터 저장
for word in choiceWord {
wordNetwork.searchWord(word) { wordData in
self.handleWordData(word: word, wordData: wordData)
}
}
print(choiceWord)
}
보기를 생성하는 메소드 입니다.
for word in choiceWord를 위해서 앞서 말한 (URLSession을 통해 api통신을 하는 코드중 수정)부분 입니다.
완료 핸들러를 넘겨 받아서 handleWordData를 통해
wordDataDictionary에 보기 단어를 저장합니다.
단어 데이터 처리 handleWordData()
func handleWordData(word: String, wordData: WordData?) {
if let wordData = wordData {
wordDataDictionary[word] = wordData
// 정답 단어의 설명을 가져오기
if word == correctWord {
fetchCorrectWordDefinition()
}
} else {
print("\(word) 단어 데이터를 가져오지 못함 ")
}
}
URLSession을 통해 api통신을 하는 코드를 완료하게 된다면
handleWordData를 호출하여 wordDataDictionary에 저장을 합니다.
정답 단어의 설명 가져오기fetchCorrectWordDefinition()
func fetchCorrectWordDefinition() {
isLoading = true
// choiceWords 배열에서 랜덤하게 정답 선택
correctWord = choiceWord.randomElement() ?? ""
guard let wordData = wordDataDictionary[correctWord] else {
correctWordDefinition = "설명을 가져올 수 없습니다."
isLoading = false
return
}
if let firstSense = wordData.channel.item.first?.sense.first {
correctWordDefinition = firstSense.definition
print(correctWordDefinition)
} else {
correctWordDefinition = "설명을 찾을 수 없습니다."
}
isLoading = false
}
4개의 단어중 무작위로 정답을 선택합니다.
guard let은 프로그래스뷰를 설정하기 위해 추가를 하였지만,,,ㅎ 아직 성공은 못했습니다.
데이터가 있는 경우 첫번째의 뜻을 correctWordDefinition에 추가합니다. (정렬이 우리말샘순 이여서 첫번째가 가장 적합하였습니다.)