https://simth999wrld.tistory.com/77

 

우리말 퀴즈 앱)3.Alamofier, 네이버 검색 API 사용

https://simth999wrld.tistory.com/76 우리말 퀴즈 앱)2.퀴즈뷰 구성중... https://simth999wrld.tistory.com/75 우리말 퀴즈 앱)1.우리말샘 api https://simth999wrld.tistory.com/74 1차시. OT 간단하게 스터디 일정과 인사후 개인

simth999wrld.tistory.com

 

이제 조금은 기능들을 거의 다 만든것같습니다 ㅎㅎ..

 

이번엔 위젯을 간단하게 만들며 이전에 저장해 두었던 네이버 API키를 코드상에서 지웠습니다!

 


 

API-KEY 숨기기.

import Foundation

struct NaverStorage {
    let naverClientID: String = "Your naverClientID"
    let naverClientSecret: String = "Your naverClientSecret"
}

위처럼 키를 그대로 넣게되면 보안상의 문제가 많아 보입니다.. 별거 아닌 api이긴 하지만 그래도 조금은 신경써주는 것이 좋을것 같네요 ㅎㅎ..

 

Product -> Edit Scheme -> Run 부분의 Arguments에 Enviroment Variables + 를 클릭하여 apikey를 저장해줍니다.

 

이렇게 된다면 환경변수를 사용해서 API Key를 저장하였습니다.

 

- NaverNetwork.swift -

let headers: HTTPHeaders = [
    "X-Naver-Client-Id": ProcessInfo.processInfo.environment["naverClientID"] ?? "",
    "X-Naver-Client-Secret": ProcessInfo.processInfo.environment["naverClientSecret"] ?? ""
]

 

네이버 api를 연동하는 부분의 코드인 헤더 부분에 ProcessInfo.processInfo.environment["API-KEY"] 를 사용해 값을 가져오도록 합니다.

 

이렇게 한다면 코드상에 api키를 저장하지 않고 사용 할 수 있습니다.

 


위젯

다음은 간단한 위젯을 만들어보았습니다.

 

간단한 위젯이니 위젯의 사이즈는 2개로 하였습니다 많은 정보를 알려주는 앱은 아니니까요 

systemSmall과 systemMedium의 크기만 제작하였습니다.

 

위젯을 제작하기전 기본 지식을 알고 가야겠죠..? 하하


위젯 생성은 Xcode -> File -> Target -> Widget Extension을 클릭후 이름을 입력하고 finish를 하면 됩니다.

(finish를 하기전 include Configuration Intent의 체크박스가 있습니다. 이 체크박스는 활성화 하게 된다면 사용자가 위젯 편집이 가능하게 합니다. 따라서 체크 하였습니다 ^_^)

 

위에서 입력한 이름의 위젯 파일로 가게된다면 어질어질한 코드들이 생성이 됩니다..(저도 처음이라 어질어질,,)

여러개의 struct들이 존재합니다!

 

  • Provider :TimelineEntry
  • SimpleEntry : TimelineEntry
  • (위젯이름)EntryView : View
  • (위젯이름) : Widget

이렇게 4개로 이루어져있습니다.

 

Provider

이 구조체는 위젯을 업데이트 할 시기를 WidgetKit에 알리는 역할을 한다고 합니다.

이 구조체엔 여러 메소드가 있습니다. ( placeholder, getSnapshot, getTimeline )

 

func placeholder

데이터를 불러오기 전 getSnapshot에 보여줄 placeholder라고 합니다.

 

func getSnapshot

getSnapshot은 위젯 갤러리에서 위젯을 고를때 보이는 샘플 데이터를 보여줄때 해당 메소드를 호출합니다.

 

func getTimeline

홈 화면에 있는 위젯을 업데이트하는 시간을 구현하는 부분 입니다.

let timeline = Timeline(entries: entries, policy: .atEnd)

 

이부분의 policy 부분에 들어가는 값은

.atEnd - 마지막 date가 끝난 후 타임라인 reloading

.after - 다음 date가 지난 후 타임라인 reloading

.never - 즉시/바로 타임라인 reloading

 

 

SimpleEntry

TimelineEntry를 준수하는 구조체이며 위젯을 표시할 Date를 정하고 그 Data에 표시할 데이터를 나타냅니다.

 

 

(위젯이름)EntryView : View

위젯을 나타낼 뷰의 코드입니다.

@Environment(\.widgetFamily) var family: WidgetFamily 를 사용하여 여러 위젯의 크기에 접근할 수 있습니다.

switch self.family {
    case .systemSmall:
    case .systemMedium:
...

 

 

 

(위젯이름) : Widget

위에서 설명된 메소드들이 호출되는 곳입니다. (+ @main)

struct WordQuizWidget: Widget {
    let kind: String = "WordQuizWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            if #available(iOS 17.0, *) {
                WordQuizWidgetEntryView(entry: entry)
                    .containerBackground(.fill.tertiary, for: .widget)
                    
            } else {
                WordQuizWidgetEntryView(entry: entry)
                    .padding()
                    .background()
            }
        }
        .configurationDisplayName("WordQuizDaily Widget")
        .description("문해력을 높여주는 하루 한단어")
        .supportedFamilies([.systemSmall,
                            .systemMedium])
    }
}

저의 코드입니다.

 

body안의 IntentConfiguration, StaticConfiguration  2개로 설정할 수 있습니다.

IntentConfiguration은 사용자가 위젯에서 Edit을 통해 위젯에 보여지는 내용 변경이 가능합니다.

StaticConfiguration은 사용자가 변경 불가능한 정적 데이터를 보여줍니다.

(저는 단어를 보여주는 위젯이라 Static)

 

kind는 위젯의 ID입니다.

 

entry in 밑의 부분은 위젯에 보여질 뷰 입니다.

 

 


 

이제 위젯에 대한 개념을 알아보았고

저의 앱의 오늘의 단어와 뜻을 넘겨받아 위젯에 보여주는 일만 남았습니다 ^_^

 

ObservedObject를 사용해 변수의 내용을 받아오거나 userdefaults에 저장한 값을 가져오려는 하수같은 생각을 하였습니다만..ㅎㅎ
서치를통해 여러 블로그를 공부한 결과 App Extension Programming Guide를 참고하였고,

 

Extension과 App은 같은 컨테이너를 가지고 있지 않았습니다!

따라서 UserDefaults는 공유되지 않았습니다..

 

하지만 의지의 한국인 다른 방법을 제시해주신 착한 분들이 계셨습니다..

위의 Optional shared container를 사용해서 공유할 수 있었습니다.

 

App Group을 설정하여 데이터를 공유하는 방법이었습니다 ^_^

 

 

 

앱그룹의 컨테이너 이름은 group.(your_app's_bundle_id)로 해야 합니다. (하지만 저는 다르게 해버린걸..)

 

앱부분의 UserDefaults를 저장할 HomeViewModel 부분에서 appGroupId를 맞춰줍니다.

extension UserDefaults {
    static var shared: UserDefaults {
        let appGroupId = "group.wordQuizWidget"
        return UserDefaults(suiteName: appGroupId)!
    }
}

 

Extension부분인 위젯의 코드에서

let customUserDefaults = UserDefaults(suiteName: "group.wordQuizWidget")

으로 appGroupId를 맞춰주면 


shared container를 사용한 UserDefaults를 공유하여 사용할 수 있습니다.

 

위젯 코드

//
//  WordQuizWidget.swift
//  WordQuizWidget
//
//  Created by 정종원 on 3/7/24.
//

import WidgetKit
import SwiftUI

struct Provider: TimelineProvider {
    
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date())
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .minute, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
}

struct WordQuizWidgetEntryView : View {
    var entry: Provider.Entry
        
    @Environment(\.widgetFamily) var family: WidgetFamily
    
    var body: some View {
                
        let customUserDefaults = UserDefaults(suiteName: "group.wordQuizWidget")

        
        switch self.family {
            
        case .systemSmall:
            VStack{
                if let storedWord = customUserDefaults?.string(forKey: "TodayWord"),
                   let storedDefinition = customUserDefaults?.string(forKey: "TodayWordDefinition") {
                    
                    Spacer()

                    
                    Text(storedWord)
                        .font(.largeTitle)
                    
                    Spacer()
                    
                    Text(storedDefinition)
                        .font(.footnote)
                    
                    Spacer()
                    
                    
                } else {
                    let _ = print("Nothing Printed")
                }
            }
            
        case .systemMedium:
            VStack{
                if let storedWord = customUserDefaults?.string(forKey: "TodayWord"),
                   let storedDefinition = customUserDefaults?.string(forKey: "TodayWordDefinition") {
                    
                    Spacer()

                    Text(storedWord)
                        .font(.largeTitle)
                    
                    Spacer()
                    
                    Text(storedDefinition)
                        .font(.footnote)

                    
                    Spacer()
                    
                    
                } else {
                    let _ = print("Nothing Printed")
                }
            }
            
        default:
            Text(".default")
            
        }

        }
}

struct WordQuizWidget: Widget {
    let kind: String = "WordQuizWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            if #available(iOS 17.0, *) {
                WordQuizWidgetEntryView(entry: entry)
                    .containerBackground(.fill.tertiary, for: .widget)
                    
            } else {
                WordQuizWidgetEntryView(entry: entry)
                    .padding()
                    .background()
            }
        }
        .configurationDisplayName("WordQuizDaily Widget")
        .description("문해력을 높여주는 하루 한단어")
        .supportedFamilies([.systemSmall,
                            .systemMedium])
    }
}

#Preview(as: .systemSmall) {
    WordQuizWidget()
} timeline: {
    SimpleEntry(date: .now)
}

 

간단하게 systemSmall과 systemMedium 두개를 만들었습니다.

 

 

 

 

 

 

다음에 할것 -> 파이어베이스 연동하여, 앱푸시 기능 -> NotiView에서 시간대 설정 가능하게 하기. 

지금 퀴즈뷰에서 네이버 이미지api로 관련 이미지를 가져오고 있는데 문제가 스포돼버리는 큰 이슈가 있습니다.. 

하지만 우리말샘에서 수어 이미지를 제공 해주더군요! 그래서 그 사진을 띄우면 어떨까... 합니다...
(다른분의 반응을 알고싶다,,ㅎ흐ㅡ긓ㄱ)

 

 

https://gyuios.tistory.com/102

 

iOS) Kakao QRcode Widget 클론코딩 - Widget 데이터 공유 및 뷰 구현(SwiftUI)

내용 카카오톡 QR코드, 프로필 위젯을 만들어보겠다. 위젯과 앱간의 프로필(이름, 이미지) 데이터 공유로 다음과 같은 프로필 위젯 만들기 시작전 위젯은 기능이 제한적이며 interactive 하지도 않

gyuios.tistory.com

https://www.masrinastudio.com/post/securing-api-keys-xcode-guide/

 

Securing Your API Keys in Xcode: Step-by-step Guide | Masrina Studio

Image credit: Photo by Clint Patterson on Unsplash There are a few different ways to securely add an API key to an Xcode project, depending on the level of security you need and the resources you have available. Here are a few methods: Use a Keychain: The

www.masrinastudio.com

https://ios-development.tistory.com/1131

 

[iOS - SwiftUI] 1. 위젯 Widget 사용 방법 개념 (WidgetKit, WidgetFamily)

1. 위젯 Widget 사용 방법 - WidgetKit, WidgetFamily 2. 위젯 Widget 사용 방법 - API 데이터 로드와 위젯UI 업데이트 3. 위젯 Widget 사용 방법 - 위젯 딥링크 구현 방법 (widgetURL) 4. 위젯 Widget 사용 방법 - 위젯 이

ios-development.tistory.com

https://michael-kiley.medium.com/sharing-object-data-between-an-ios-app-and-its-widget-a0a1af499c31

 

Sharing Object Data Between an iOS App and Its Widget

Use Codable and App Groups to get your iOS 14 widget all the data it needs

michael-kiley.medium.com

 

https://simth999wrld.tistory.com/76

 

우리말 퀴즈 앱)2.퀴즈뷰 구성중...

https://simth999wrld.tistory.com/75 우리말 퀴즈 앱)1.우리말샘 api https://simth999wrld.tistory.com/74 1차시. OT 간단하게 스터디 일정과 인사후 개인프로젝트 소개를 하였습니다, ^_^/ 간단하게 아이디어를 정리해

simth999wrld.tistory.com

이번에 구성에 대해 조금더 업그레이드를 해보았는데요,,하하

 

네이버 검색 API를 사용해서 정답 단어의 이미지를 가져오는것 입니다..ㅎ

 

 

 

 

디렉토리 구조를 조금 바꾸었습니다.. (이렇게 하는거 맞나.,.?)

 

조금더 고민한 후에 조금더 보기 편하게 만들어 보겠습니다^_^

 

 

 

 

 

 

 

 

네이버 이미지검색 API

 

일단 네이버 검색 API를 사용하기 위해 네이버 디벨로퍼 회원가입, 키 발급 등을 합니다.

 

https://developers.naver.com/docs/serviceapi/search/image/image.md

 

검색 > 이미지 - Search API

검색 > 이미지 이미지 검색 개요 개요 검색 API와 이미지 검색 개요 검색 API는 네이버 검색 결과를 뉴스, 백과사전, 블로그, 쇼핑, 웹 문서, 전문정보, 지식iN, 책, 카페글 등 분야별로 볼 수 있는 API

developers.naver.com

 

 

 

 

묵념을 검색을 하게된다면 이렇게 최소 10개의 이미지를 가져올 수 있게 됩니다.

 

저는 가장 맨 위의 이미지만을 사용하겠습니다.

 

이 JSON형식에 따라 Swift파일을 만들어 줍니다.

 

 

 

 

 

- NaverImageData.swift -

import Foundation

// MARK: - NaverImageData
struct NaverImageData: Codable {
    let lastBuildDate: String
    let total, start, display: Int
    let items: [NaverItem]
}

// MARK: - NaverItem
struct NaverItem: Codable {
    let title: String
    let link: String //이미지의 URL
    let thumbnail: String
    let sizeheight, sizewidth: String
}

 

이렇게 만들어 주는데 물론 손으로 만드면 좋겠지마는~ 

 

https://app.quicktype.io/

 

Instantly parse JSON in any language | quicktype

 

app.quicktype.io

 

이 사이트에서 여러 타입의 데이터들을 코드로 만들어 줍니다. 편리하죠..?

 

 

클라이언트 아이디와 시크릿 값을 저장하기 위한 파일을 만들어 줍니다.

 

- NaverStorage.swift -

//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"
}

 

이부분은 큰 문제는 아니지만, 조금더 보안을 신경 써준다면 키체인을 사용하면 좋아 보입니다!!!

 

 

문제 2.

 

 

 

 

 

 

 

 

바로 이미지에서 정답을 알수있는 이미지가 나온다는 사실...

관련 이미지를 싹 깃허브에 올려서 가져오는 방법으로 해야 이런 문제가 안날것 같지만..

시간이 많이 걸릴것 같군요 하하..ㅎ 

 

(아이디어 있다면 알려주세요..)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

긴글 읽어주셔서 감사합니다 ^_^🙏

 

 

 

 

 

 

 

 

https://developers.naver.com/docs/serviceapi/search/image/image.md#%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B2%80%EC%83%89-%EA%B2%B0%EA%B3%BC-%EC%A1%B0%ED%9A%8C

 

검색 > 이미지 - Search API

검색 > 이미지 이미지 검색 개요 개요 검색 API와 이미지 검색 개요 검색 API는 네이버 검색 결과를 뉴스, 백과사전, 블로그, 쇼핑, 웹 문서, 전문정보, 지식iN, 책, 카페글 등 분야별로 볼 수 있는 API

developers.naver.com

https://github.com/Alamofire/Alamofire

 

GitHub - Alamofire/Alamofire: Elegant HTTP Networking in Swift

Elegant HTTP Networking in Swift. Contribute to Alamofire/Alamofire development by creating an account on GitHub.

github.com

https://velog.io/@dbqls200/iOS-%EB%84%A4%EC%9D%B4%EB%B2%84-%EA%B2%80%EC%83%89%EB%8F%84%EC%84%9C-API-%EC%82%AC%EC%9A%A9%EA%B8%B0

 

[iOS] 네이버 검색(도서) API 사용기

네이버 Open API를 사용해서 책 검색 해보기

velog.io

 

https://simth999wrld.tistory.com/75

 

우리말 퀴즈 앱)1.우리말샘 api

https://simth999wrld.tistory.com/74 1차시. OT 간단하게 스터디 일정과 인사후 개인프로젝트 소개를 하였습니다, ^_^/ 간단하게 아이디어를 정리해보았습니다.. 진행과정을 간간히 올려보도록 노력! 프로

simth999wrld.tistory.com

 

네~! 저번엔 간단히 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에 추가합니다. (정렬이 우리말샘순 이여서 첫번째가 가장 적합하였습니다.)

데이터가 없을 경우는 설명을 찾을 수 없습니다 의 내용을 저장합니다.

 

정답 확인 메서드 checkAnswer()

func checkAnswer(selectedWord: String) -> Bool {
    return selectedWord == correctWord
}

 

선택된 단어가 정답과 일치하는지 확인하는 메서드 입니다.

 

 

init()

init() {
    correctWord = hardKoreanWords.hardWords.randomElement() ?? ""
    generateChoices()
}

 

클래스의 인스턴스가 생성될때 호출되는 init입니다.

generateChoices()메서드를 호출하여 선택지를 생성하며 단어의 데이터를 검색하고,저장할 수 있도록 합니다.

 

QuizView.swift

VStack{

    Text("QuizView")
        .padding(.top, 50)

    Spacer()

    if quizViewModel.isLoading {
        ProgressView("Loading...")
    } else {
        Image(systemName: "square.and.arrow.up")
        Text(quizViewModel.correctWordDefinition)
    }

    Spacer()

    VStack(){

        HStack {
            ForEach(quizViewModel.choiceWord.prefix(2), id: \.self) { word in
                Button(action: {
                    // 선택된 단어에 대한 동작을 추가하세요
                    isAnswerCorrect = quizViewModel.checkAnswer(selectedWord: word)
                    print("선택된 단어: \(word), 정답?: \(isAnswerCorrect)")
                }) {
                    Text(word)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
                .padding()
            }
        }

        HStack {
            ForEach(quizViewModel.choiceWord.dropFirst(2), id: \.self) { word in
                Button(action: {
                    // 선택된 단어에 대한 동작을 추가하세요
                    isAnswerCorrect = quizViewModel.checkAnswer(selectedWord: word)
                    print("선택된 단어: \(word), 정답?: \(isAnswerCorrect)")
                }) {
                    Text(word)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                }
                .padding()
            }
        }
    }

    Text(isAnswerCorrect ? "정답입니다!" : "오답입니다.")

    Spacer()
}//VStack

 

quizview에서 간단하게 보기 단어를 4개 보여주고 정답유무를 확인하는 텍스트를 추가하였습니다 하하..

아직

Image(systemName: "square.and.arrow.up")

를 완성하지 않았는데요!

이부분은 네이버 검색API를 활요해서 정답 단어의 관련 이미지를 보여줄 예정입니다 ㅎㅎ..

 

 

이 정도 구현하는것도 어렵네요,, 하하,,,

저의 수준을 잘 파악 할수 있는 시간이 되었습니다 ㅎㅎ

조금은 SwiftUI와 친숙해진 시간이 된것 같네요??!!

 

다음은 네이버 검색 API를 활용한 글을 가져오겠습니다...

이상 보고 끝!(화이팅,,!👏)

https://simth999wrld.tistory.com/74

 

1차시. OT

간단하게 스터디 일정과 인사후 개인프로젝트 소개를 하였습니다, ^_^/ 간단하게 아이디어를 정리해보았습니다.. 진행과정을 간간히 올려보도록 노력! 프로젝트 개요 앱 이름: ??? 프로젝트의 동

simth999wrld.tistory.com

 

위를 바탕으로 개발을 시작하였습니다.

 

1.  시장조사

 

제가 구상한 퀴즈 앱을 구상하기 위해 앱스토어를 둘러보았습니다 하하.. (대충 요런것 둘러봤다는 뜻)

 

2. 뷰 구성

둘러보며 간단하게 저의 앱의 뷰를 구성하였는데요,

 

홈뷰

오늘의 단어와 뜻을 간단하게 알려줌.

퀴즈뷰

퀴즈 시작 시 단어의 뜻과 관련 사진을 알려주고 단어를 맞추는 방식.

정답 여부와 관계없이 TTS로 단어를 읽어줍니다. 

알림뷰

오늘의 단어를 알림을 통해 사용자에게 알려주기

알림 시간 설정 기능

알림 ON/OFF 기능

 

이렇게 3개의 뷰로 구성할것입니다.

+오늘의 단어와 뜻 보여주는 위젯 구성하기.

 

3. 우리말샘 API호출 테스트

단어와 뜻을 가져오기 위해 우리말샘의 API를 사용하였습니다.

 

사이트에서 저의 API키를 받고 openAPI 예시를 "미끼" 로 확인하였습니다.

 

 

위와 같이 JSON을 불러와야 하니 DataModel을 구성하였구요

struct WordData: Codable {
    let channel: Channel
}

struct Channel: Codable {
    let item: [Item]
}

struct Item: Codable {
    let word: String
    let sense: [Sense]
}

struct Sense: Codable {
    let definition: String
    let pos: String?
    let link: String
    let type: String
}

 

 

 

우리말샘 사용을 위해 UrlSession 네트워크 통신을 하였습니담~

class WordNetwork: ObservableObject {
    
    @Published var wordData: WordData?
    
    func searchWord(_ searchWord: String) {
        let urlString = "https://opendict.korean.go.kr/api/search?certkey_no=6282&key=\(YourApiKey)&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
                            }
                        } catch {
                            print(error)
                        }
                    }
                }
            }
            task.resume()
        }
    }
}

 

 

네트워크 통신을 통해 단어를 검색하여 단어와 뜻을 가져올 수 있게 되었습니다.

 

 

 

 

간단하게 버튼을 활용해서 "살림"이란 단어를 검색하여
테스트 하였습니다.

 

 

 

 

 

4. 문제발생

4.1) 시뮬레이터 잘려보이는 현상

 

 

 

 

 

 

 

 

앱을 빌드하니, 시뮬레이터에서 화면이 꽉차지 않는 현상이 있더라군요,,

이는 간단히 앱의 General-AppIcons and Launch Screen

의 Launch Screen File을 main 또는 LaunchScreen으로 하면 된다고 합니다 하하...😅😅

 

 

 

 

 

 

4.2) 고급 단어 어떻게 가져올까...?

고급 단어를 어떻게 가져올까...합니다..

이는 아직 해결은 안됐습니다,,,

그래서! 원시적인 방법으로 문제해결 같아 보이게 하였는데요,,

 

 

 

 

 

 

하드코딩을 하여 조금 어려울만한 단어들을 검색해 넣어두었습니다. ^_^

이 문제에 대해 아이디어가 있으면 알려주세요,,ㅜㅠ

 

 

 

 

 

 

5. 다음 할일

api호출을 위한 테스트를 해보았구요. 이제 기능들을 차차 구현해 나갈 계획입니다.

퀴즈뷰 부터 시작 하려합니다.

예상은 단어의 뜻과 관련 이미지를 보여주고, 4개의 보기중 정답인 단어를 맞추는 식으로 할것같은데,,

보기의 오답 단어는 어떻게 할까~ 하다 정답과 비슷한 유의어를 보여줄것 같습니다!

이것또한 오픈api를 사용 하려 합니다.

정답 단어의 관련 이미지는 네이버 이미지검색 api를 활용하면 좋을것 같습니다 하하~~

어려운 단어 어떻게 가져올지 알려줘 젭알

 

 

 

https://opendict.korean.go.kr/main

 

우리말샘 - 함께 만들고 모두 누리는 우리말 사전

 

opendict.korean.go.kr

https://aiopen.etri.re.kr/guide/Homonym

 

AI API/DATA

JSON parsing을 위해 Gson 라이브러리를 사용하여 제공하고 있습니다. Gson 라이브러리에 대한 자세한 설명은 https://github.com/google/gson 에서 확인 하실 수 있습니다. import java.io.DataOutputStream; import java.io.

aiopen.etri.re.kr

https://developers.naver.com/docs/serviceapi/search/image/image.md#%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B2%80%EC%83%89-%EA%B2%B0%EA%B3%BC-%EC%A1%B0%ED%9A%8C

 

검색 > 이미지 - Search API

검색 > 이미지 이미지 검색 개요 개요 검색 API와 이미지 검색 개요 검색 API는 네이버 검색 결과를 뉴스, 백과사전, 블로그, 쇼핑, 영화, 웹 문서, 전문정보, 지식iN, 책, 카페글 등 분야별로 볼 수

developers.naver.com

 

간단하게 스터디 일정과 인사후 개인프로젝트 소개를 하였습니다, ^_^/

간단하게 아이디어를 정리해보았습니다..

진행과정을 간간히 올려보도록 노력!

 

  1. 프로젝트 개요
    • 앱 이름: ???
    • 프로젝트의 동기: 청소년 문해력 문제 있어 (80.9%, 2532명)
    • 목적: 문해력 높이기
    • 대상: 청소년
  2. 기술 스택 및 도구 선택프레임워크: SwiftUI, Alamofire도구: Xcode, Cocoapods..?SwiftUI 및 Kingfisher를 사용하기로 결정한 이유: (킹피셔,알라모파이어,rxswift 국밥3대장이라함…)
  3. 기타: Git, GitHub (깃 업데이트 안하니 이제부터라도 씨앗심기…)
  4. 라이브러리: Kingfisher
  5. 언어: Swift
  6. 프로젝트 구조 설계 MVVM..?
  7. API 통신 구현
    • 통신 클래스와 모듈 구현
  8. Kingfisher 및 SwiftUI를 활용한 이미지 표시 구현
    • 어디서 무료 이미지 끌어오는지..?
  9. 퀴즈 로직 구현
    • 우리말샘 API로 받아온 데이터를 활용하여 퀴즈 로직을 구현합니다.
    • 사용자 입력을 처리하고 정답 여부를 확인하는 로직을 작성합니다.
  10. UI/UX 디자인 및 구현
    • 앱의 사용자 인터페이스 및 사용자 경험 구현
    • 사용자에게 편리한 환경을 제공
  11. 테스트 및 디버깅(안해봄..)
  12. 문서화 및 포트폴리오 작성
  13. 앱출시..?

무료API모음

https://github.com/dl0312/open-apis-korea

알라모파이어를 사용해서 우리말샘api 사용.

https://opendict.korean.go.kr/main

 




이렇게 작성해서 이야기를 나누었구요 ^_^

+ TTS기능과 위젯, 매일 알림을 설정해서 단어의 뜻을 알려주는 기능들을 추가하면 좋을것같습니다..

시작전에 앱스토어에 올라와있는 관련 퀴즈 앱을 구경하면서 뷰를 구상해 보겠습니다 ^_^/

+ Recent posts