https://simth999wrld.tistory.com/80

 

우리말 퀴즈 앱)6. Local Notification 적용, API KEY 숨기기.

https://simth999wrld.tistory.com/79 우리말 퀴즈 앱)5. urlSession을 async await으로 바꾸기..https://simth999wrld.tistory.com/78 우리말 퀴즈 앱)4. 위젯, Scheme 사용한 API KEY 숨기기https://simth999wrld.tistory.com/77 우리말 퀴

simth999wrld.tistory.com


간단한 기능들은 일단락 된것같아 앱스토어에 앱을 올려보았습니다.. 
의외로 간단해서 조금 당황했습니다. (딱히 심사할게 없어서 그런지..?)

배포에 대한 자세한 내용은 다른분들이 정성스럽게 정리해 주셔서.. 많이 참고했습니다.. (밑의 링크 확인해주셔요~)


 

일단 앱에 대한 정보 등등을 입력하여서 심사를 넣었습니다.

큰 어려움은 없더라구요.

 

아! 개인정보 처리 방침과, 지원 URL에 대한 노션 페이지를 연결해주었습니다.
회원 관리는 하지 않아 이부분도 큰 어려움은 없었스니다 하하..

 

앱 스크린샷 또한 해외 사이트를 이용해 간단하게 제작 했습니다.

 

반려..

 

 

 

 

 

 

 

대충 메일의 내용은

당신의 앱이 iPad 기기 화면 크기나 해상도에 최적화 되어있지 않습니다.

 

라네요 하하. 저는 iPhone용으로 개발을 했지만

리뷰어들은 무조건 아이패드로 앱 실행 한다고 합니다.

 

 

 

 

 

 

 

그래서 저도 아이패드로 시뮬레이터를 돌려보니 저런 현상이...

이게뭐람...

 

다시 뷰를 고치고 재심사를 하였습니다.

 

심사가 아주 빠르더군요,,

다른분들은 회사의 앱인지 몇십일 정도씩 걸리시던데

 

저는 작아서 빠른건가요? 하하~

 

 

 

 

해결해야할 문제

일단 배포는 했지만! (많이 한것도 없는데 뿌듯함 ㅋㅋ)

혼자 사용을 해보다 크디큰 이슈가 많습니다...

 

1. 가장 큰 기능인 "오늘의 단어"가 하루에 한번 바뀌지 않습니다... 전혀 바뀌지 않음..

2. 푸시 알림 시간을 설정후 앱 껐다키면 시간이 현재시간으로 바뀌는 문제가 있더군요

예를 들어 8:30 분에 알림을 설정 해두었다 앱을 껐다 다시 키면, 알림 시간이 현제 시간으로 바뀌어있습니다. (알림은 8:30에 울림)

 

 

느낀점 이랄까..?

SwiftUI를 배워보면서 다른분들이 항상 이야기 하는 "앱을 만들면서 공부해라~" 라는 말이 정말 무슨말인지

깨닫게 되었네요. 이 앱을 만들면서 여러 가지 기능에 대해 공부하고 문법에 대해 조금더 친숙해진 느낌입니다.

 

혹자가 말하기를 강의 지옥에 빠진다고들 하잖아요. 제가 그 경우였습니다. 

강의만 여러개 들으며 공부를 하고 있다는 자아도취에 빠지는 경우였네요;;

(물론 지금도 결제하고 안들은 강의가 있ㄷ,,)

그래도 저만의 앱을 만들어 배포까지 하는 경험 이 첫경험이 아주 귀중한거 같습니다.

음 기술적으로 성장했는지는 몰라도 개발자로서 한발자국 다가간 느낌입니다.

 

여러 방면에서도 성장했겠죠..?

지금은 모든 코드를 검색을 해가면서 작성해야하는 수준이지만..

언젠간 눈감고도 코드를 작성하는 그날이 오겠죠? (오겠냐)

 

다음 포스팅은 현재 이앱의 문제점을 개선하는 주제가 될것입니다..

 

 

 

참고

https://developer.apple.com/kr/help/app-store-connect/reference/app-information/

 

앱 정보 - 참고 - App Store Connect - 도움말 - Apple Developer

참고 앱 정보 앱 정보는 앱에 추가된 플랫폼 간에 공유되는 속성을 나타냅니다. 계정에 앱을 추가할 때 해당 속성 중 일부를 설정하고, 추후 필요한 경우 앱 정보를 수정하거나 앱 상태에 따라

developer.apple.com

https://developer.apple.com/kr/app-store/review/guidelines/?source=post_page-----f30e54b87dba--------------------------------

 

앱 심사 지침 - Apple Developer

앱 심사 지침에는 사용자 인터페이스 디자인, 기능, 콘텐츠 및 특정 기술 사용 등을 비롯하여 개발과 관련된 다양한 주제에 대한 지침과 예가 나와 있습니다. 이러한 지침은 앱 승인 절차를 준비

developer.apple.com

https://hammerbrother.tistory.com/entry/%EC%95%B1%EC%8A%A4%ED%86%A0%EC%96%B4-%EC%95%B1-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0-%EC%8A%A4%ED%81%AC%EB%A6%B0%EC%83%B7-%EB%AF%B8%EB%A6%AC%EB%B3%B4%EA%B8%B0-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%82%AC%EC%9D%B4%EC%A6%88-%EC%A1%B0%EC%A0%88%ED%95%98%EA%B8%B0-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A1%B0%EC%A0%88-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%AA%A9%EC%97%85%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EC%86%8C%EA%B0%9C

 

앱스토어 앱 등록하기, 스크린샷 미리보기 이미지 가이드! 이미지 사이즈 조절하기, 이미지 조절

열심히 기획/디자인하고 개발까지 하여 아주아주 소중한 우리의 앱이 탄생하기 직전이다. 앱을 출시하려면 앱스토어에 등록하고, 구글 플레이 스토어에 등록해야 한다. 앱을 등록하는 과정에서

hammerbrother.tistory.com

https://devsrkim.tistory.com/entry/%EC%95%B1-%EC%8A%A4%ED%86%A0%EC%96%B4-%EC%95%B1-%EB%B0%B0%ED%8F%AC-%ED%95%98%EA%B8%B0

 

앱 스토어 앱 배포 하기

1. AppStore Connect 접속 (appstoreconnect.apple.com/) - 계정이 없다면, 계정 생성 필수. 2. 로그인 후, 상단 메뉴 '앱' > (+) 버튼 클릭 3. 신규 클릭 > 기본 정보 입력 > 생성 - 플랫폼 : iOS - 이름 : 앱 이름 - 기본

devsrkim.tistory.com

 

https://simth999wrld.tistory.com/79

 

우리말 퀴즈 앱)5. urlSession을 async await으로 바꾸기..

https://simth999wrld.tistory.com/78 우리말 퀴즈 앱)4. 위젯, Scheme 사용한 API KEY 숨기기https://simth999wrld.tistory.com/77 우리말 퀴즈 앱)3.Alamofier, 네이버 검색 API 사용 https://simth999wrld.tistory.com/76 우리말 퀴즈

simth999wrld.tistory.com

 

흠 이번 포스팅을 하기전에 앱의 디자인과 앱 아이콘을 설정해서 바뀐모습 먼저 보여드리겠습니다 ^_^7

 

 

 

보시는 분들은 이정도로 만족 못하시겠지만..

보시는것처럼 제가 감각이 없나봐요...하하

 

일단 모든 뷰의 NavigationTitle을 추가했습니다.

 

조금이나마 뷰를 채웠으면 하는 마음에 크게 했습니다..

 

 

 

 

 

세번째의 탭이 Notification에서 Setting으로 바뀌었습니다.

조금더 범용적인 단어가 좋을것 같아 바꿨습니다.

 

네, 맞아요 푸시 알림 하나 해서 포스팅 하러 왔습니다...

 

SwiftUI를 조금 배우고 바로 시작한 프로젝트라 부족한점이 많지만

조금씩 바꾸고 있습니다 ㅎㅎ 많관부 

 

 

 

 


API KEY 숨기기

 

일단 이전 포스팅에서 API키를 Product/Scheme/Edit Scheme/Run에 적용을 했는데,

이건 빌드 환경을 분리할때 사용하는것 같아서, 이후에 제가 앱을 완성한후에 테스트 관련 포스팅에 다시 작성을 하겠습니다.

 

API키를 왜 숨기냐.. 

바로 github에 올리게 된다면 다른 사람이 API Key를 스리슬쩍해서 대신 사용할수도 있고, 유료라면 엄청난 가격 폭탄을 맞을수 있겠죠..

저또한 우리말샘API키를 깃에 올라가 다시 발급 받았더랬죠..

 

따라서 .xcconfig를 사용해서 API키들을 관리하겠습니다.

 

 

cmd + N으로 신규 파일을 생성하고 Configuration Settings File을 추가해줍니다.

 

이 파일 안에 사용하는 API키등을 넣고 문자열을 관리할 수 있게 됩니다.

 

 

 

파일 생성 후에

 

 

 

사용하려는 API키의 이름을 적고 API키를 넣으시면 됩니다. (""사용X)

 

 

이렇게 넣어준 후

 

 

 

Project의 Info/Configuration에 Release와 Debug에

추가한 Config를 설정해주시면 됩니다.

그러면 Config.xcconfig파일을 사용할수 있습니다.

 

 

이후 .xcconfig의 변수를 사용 설정을 해줘야겠죠?

Targets/info에 +버튼을 눌러 변수 이름과 value엔 $(변수)로 넣게 된다면 info.plist파일 생성과 함께 같이 적용이 됩니다.

info.plist

자 이러면 사용 준비는 완료가 되었는데요.

네트워킹을 하는 파일에서 API키의 변수를 사용하려면

let naverClientID = Bundle.main.object(forInfoDictionaryKey: "NAVER_CLIENT_ID") as? String

의 형식으로 사용하면 됩니다.

저는 info.plist의 NAVER_CLIENT_ID의 변수에 연결된 api키가 naverClientID에 할당이 됩니다.

 

이렇게 변수를 사용할 수있는데요, 아직 중요한 작업을 하나 안했습니다요~

이렇게 git에 push를 하게 된다면 우리가 만든 .xcconfig파일이 올라가서 무용지물이 됩니다.

따라서 .gitignore에 *.xcconfig 를 넣어 git버전관리에서 제외 시켜줍니다.

 

이렇게 api키를 숨겨보았는데요.

저는 git에 이미 올라간 흔적이 있어,, 재발급 받았습니다.. 여러분도 올리기 전에 .gitignore에 추가한후에 올리시길 바래요~

 


Local Notification 적용

진행중인 프로젝트는 오늘의 단어를 Push Notification을 사용해서 사용자가 설정한 시간에 알림이 오도록 하는 기능이 있습니다!

그전에 앱의 상태에 대해 알아보았는데요.

 

크게 3가지의 상태가 있습니다. 1. 앱이 실행중, 2.백그라운드, 3.앱이 꺼졌을때.

 

제가 졸업작품 프로젝트를 진행했을때엔, 서버에서 Push알림을 생성해 전송을 하여 APNs Apple Push Notification Service를 사용했었는데요, 지금은 간단한 서버리스 앱이라.. 하하 앱에서 알림을 주어야 겠죠??

 

 1. 앱이 실행중, 2.백그라운드, 3.앱이 꺼졌을때. 상태에서 Notificaion을 받아야 합니다.

따라서 Local Notification을 구현했습니다.

 

Local Notification

Local Notification은 앱이 백그라운드에 있거나 사용자가 앱을 사용하지 않을 때에 알림을 보내는 기능입니다.

 

저의 경우엔 알림을 끄고 키고, 시간대 설정을 하여 알림이 오게 되는 기능입니다.

 

로컬 알림을 사용하기 위해 UNUserNotificationCenter를 통해 알림 권한을 먼저 요청 해야합니다.

 

이를 위해 UserNotifications를 import하고 인스턴스를 가져옵니다.

import UserNotifications

let notificationCenter = UNUserNotificationCenter.current()

 

 

저장된 단어와 뜻을 불러와 상수에 할당하고, 알림on/off확인 변수, 알림 설정 시간 변수를 선언합니다.

앱에서 알림on/off를 확인할 변수 또한 선언합니다.

@Published var isAlertOccurred: Bool = false
@Published var notificationTime: Date = Date() {
    didSet {
        removeAllNotifications()
        addNotification(with: notificationTime)
    }
}
@Published var isToggleOn: Bool = UserDefaults.standard.bool(forKey: "hasUserAgreedNoti") {
    didSet {
        if isToggleOn {
            UserDefaults.standard.set(true, forKey: "hasUserAgreedNoti")
            requestNotiAuthorization()
        } else {
            UserDefaults.standard.set(false, forKey: "hasUserAgreedNoti")
            removeAllNotifications()
        }
    }
}

isToggleOn은 didSet을 활용하여 UserDefaults에 저장하고 가져옵니다.

isToggleOn이 True일때는 true를 저장하고, requestNotiAuthorization() 메소드를 호출하게 됩니다.

 

requestNotiAuthorization메서드는 Setting에 앱의 알림 설정을 확인하는 메소드 입니다.

func requestNotiAuthorization() {
        //noti 설정 가져오기
        //상태에 따라 다른 액션 수행
        notificationCenter.getNotificationSettings { settings in
            //승인이 되어있지 않은 경우 request
            if settings.authorizationStatus != .authorized {
                self.notificationCenter.requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
                    if let error = error {
                        print("notificationCenter Error: \(error.localizedDescription)")
                    }
                    
                    //노티피케이션 최초 승인
                    if granted {
                        self.addNotification(with: self.notificationTime)
                    } else {
                        //노티피케이션 최초 거부
                        DispatchQueue.main.async {
                            self.isToggleOn = false
                        }
                    }
                    
                }
            }
            
            //Notification이 거부되어 있는경우 alert
            if settings.authorizationStatus == .denied {
                DispatchQueue.main.async {
                    self.isAlertOccurred = true
                    
                }
            }
        }
    }

알림의 승인 거부에 따라 status로 관리를 하게 됩니다.

 

 

Toggle이 꺼져 있다면 removeAllNotificaions() 메소드를 호출하여 notificationCenter에 설정되어있는 알림들을 삭제합니다. 

func removeAllNotifications() {
    notificationCenter.removeAllDeliveredNotifications()
    notificationCenter.removeAllPendingNotificationRequests()
}

 

 

알림 설정을 승인했을때 즉 알림이 켜져 있고, Toggle이 On상태 이라면

addNotifiCation(with time: Date)의 메소드를 호출하게 됩니다!

이 메소드는 사용자가 설정한 시간대로 알림을 추가하게 되는 메소드 입니다.

//time에 반복되는 노티피케이션 추가
func addNotification(with time: Date) {
    //이 객체를 사용하여 알림의 제목과 메시지, 재생할 사운드 또는 앱의 배지에 할당할 값을 지정
    let content = UNMutableNotificationContent()

    if let todayWord = storedWord, let todayDescription = storedDefinition {
        content.title = "오늘의 단어: \(String(describing: todayWord))"
        content.subtitle = "뜻: \(String(describing: todayDescription))"
        content.sound = UNNotificationSound.default

        let dateComponent = Calendar.current.dateComponents([.hour, .minute], from: time)
        let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponent, repeats: true)
        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)

        notificationCenter.add(request) { (error) in
            if let error = error {
                print("Error adding notification: \(error.localizedDescription)")
            }
        }
    }
}

content의 title, subtitle, sound를 추가했습니다.

저는 위젯을 설정할때 단어와 뜻을 UserDefaults에 저장을 해두어서 가져와 사용했습니다.

상세 설명을 하면

content에 알림의 제목, 부제목, 소리를 설정

trigger = dateComponent 시간과 분을 기준으로 알림 트리거를 생성합니다.

request = 알림 요청을 생성하고,

notificationCenter.add(request)로 알림 센터에 추가하게 됩니다.

 

전체 코드

import Foundation
import UserNotifications
import UIKit

class NotificationManager: ObservableObject {
    
    let notificationCenter = UNUserNotificationCenter.current()
    let storedWord = UserDefaults.shared.string(forKey: "TodayWord")
    let storedDefinition = UserDefaults.shared.string(forKey: "TodayWordDefinition")
    
    @Published var isAlertOccurred: Bool = false
    @Published var notificationTime: Date = Date() {
        didSet {
            removeAllNotifications()
            addNotification(with: notificationTime)
        }
    }
    @Published var isToggleOn: Bool = UserDefaults.standard.bool(forKey: "hasUserAgreedNoti") {
        didSet {
            if isToggleOn {
                UserDefaults.standard.set(true, forKey: "hasUserAgreedNoti")
                requestNotiAuthorization()
            } else {
                UserDefaults.standard.set(false, forKey: "hasUserAgreedNoti")
                removeAllNotifications()
            }
        }
    }
    
    func requestNotiAuthorization() {
        //noti 설정 가져오기
        //상태에 따라 다른 액션 수행
        notificationCenter.getNotificationSettings { settings in
            //승인이 되어있지 않은 경우 request
            if settings.authorizationStatus != .authorized {
                self.notificationCenter.requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
                    if let error = error {
                        print("notificationCenter Error: \(error.localizedDescription)")
                    }
                    
                    //노티피케이션 최초 승인
                    if granted {
                        self.addNotification(with: self.notificationTime)
                    } else {
                        //노티피케이션 최초 거부
                        DispatchQueue.main.async {
                            self.isToggleOn = false
                        }
                    }
                    
                }
            }
            
            //Notification이 거부되어 있는경우 alert
            if settings.authorizationStatus == .denied {
                DispatchQueue.main.async {
                    self.isAlertOccurred = true
                }
            }
        }
    }
    
    func removeAllNotifications() {
        notificationCenter.removeAllDeliveredNotifications()
        notificationCenter.removeAllPendingNotificationRequests()
    }
    
    //time에 반복되는 노티피케이션 추가
    func addNotification(with time: Date) {
        //이 객체를 사용하여 알림의 제목과 메시지, 재생할 사운드 또는 앱의 배지에 할당할 값을 지정
        let content = UNMutableNotificationContent()
        
        if let todayWord = storedWord, let todayDescription = storedDefinition {
            content.title = "오늘의 단어: \(String(describing: todayWord))"
            content.subtitle = "뜻: \(String(describing: todayDescription))"
            content.sound = UNNotificationSound.default
            
            let dateComponent = Calendar.current.dateComponents([.hour, .minute], from: time)
            let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponent, repeats: true)
            let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
            
            notificationCenter.add(request) { (error) in
                if let error = error {
                    print("Error adding notification: \(error.localizedDescription)")
                }
            }
        }
    }
    
    //Setting으로 이동하는 메서드
    func openSettings() {
       if let bundle = Bundle.main.bundleIdentifier,
          let settings = URL(string: UIApplication.openSettingsURLString + bundle) {
            if UIApplication.shared.canOpenURL(settings) {
               UIApplication.shared.open(settings)
            }
        }
    }
        
}

 

전체 코드에 설명을 하지 않았던 openSettings()메소드는 앱에 알림 요청 Alert가 뜨게 되어서 Setting화면으로 연결해 주는 메소드입니다.

 

 

이후 위의 코드(뷰모델 코드) 를 뷰에 적용을 해야겠죠?

 

import SwiftUI

struct SettingView: View {
    
    @EnvironmentObject var notificationManager: NotificationManager
    var body: some View {
        NavigationStack {
            
            VStack(alignment: .leading, spacing: 10){
                Form{
                    //알림 설정 ex) 푸시알림, 효과음
                    Section(header: Text("알림 설정").font(.caption)) {
                        //NotiView
                        NotiView()
                        
                    }
                }
            }
        }
    }
}

 

Toggle버튼이 있는 푸시알림의 Section을 NotiView()로 빼서 작성하였습니다.

//MARK: - NotiView
struct NotiView: View {
    
    @EnvironmentObject var notificationManager: NotificationManager
    @State private var showDatePicker = false
    
    var body: some View {
        VStack(alignment: .leading){
            
            HStack {
                Text("푸시 알림")
                if notificationManager.isToggleOn {
                    DatePicker("", selection: $notificationManager.notificationTime, displayedComponents: .hourAndMinute)
                        .datePickerStyle(.graphical)
                        .onTapGesture {
                            showDatePicker.toggle()
                        }
                }
                Toggle("", isOn: $notificationManager.isToggleOn)
                    .onChange(of: notificationManager.isToggleOn) { oldValue, newValue in
                        if newValue {
                            showDatePicker = true
                        } else {
                            showDatePicker = false
                        }
                    }
            } //HStack
            .alert(isPresented: $notificationManager.isAlertOccurred) {
                //알림 설정이 꺼져있을 경우 설정창으로 이동
                Alert(title:Text("Notification Alert"), message: Text("알림 설정이 꺼져있습니다. 설정에서 알림을 켜주세요."), primaryButton: .cancel(Text("이동"), action: {
                    notificationManager.openSettings()
                }), secondaryButton: .destructive(Text("Cancel")))
            }
            .onTapGesture {
                if notificationManager.isToggleOn {
                    showDatePicker.toggle()
                }
            }
        } //VStack
    }
}

 

DatePicker로 알림 시간을 설정하고 Toggle버튼으로 알림을 끄고 킬수 있도록 하였습니다.

 

.alert에 알림이 꺼져있을경우 설정창으로 이동 할 수 있도록 하였습니다.

여기서 알림이 꺼져있고  "이동" 버튼을 누르면 openSettings()의 메소드가 호출되어 Setting으로 이동하게 됩니다.

 

 

 

 

 

 

 

 

 

 

마지막으로 Toggle을 On하고 시간대를 설정한다면?

 

4

 

 

 

이렇게 알림이 오게됩니다 하하...

 

 

 

 

 

 

 

아직 알림이 왔다 안왔다 하는데,, 그부분은 테스트 관련해서 더욱 학습을 한후에 해야할것같습니다 ㅜㅜ

그래도 알림은 오잖아요 한잔해~

 

 


 

현재 Combine에 대해 공부를 하고있는데 정확한 개념이 머리에 들어오지 않는거 있죠...

항상 느끼는 저의 문제점이지만, 왜? 에 중점을 맞추고 이해해 나가야 하지만,, 

대학교때 배우던 습관이 배어있어서 그런지 그냥 그런가보다.. 하고 머리에 집어넣게 되는것 같습니다.. (절때 대학탓 아님..교수탓아님.)

 

마지막으로 이후에 이 프로젝트에서 해야할것을 나열해보고 글 끝마치겠습니다~

 

1. 알림에 글 잘리는 현상

2. 퀴즈에 사진이 답을 스포하는 현상

3. 위젯 이쁘게...

4. 약관 작성

5. 테스트

 

를 하게 된다면 배포까지 할수있을것같습니다 하하 (저의생각)


도움주신분들 감사드립니다,,

https://velog.io/@j_aion/SwiftUI-Local-Notifications

 

[SwiftUI] Local Notifications

How to schedule local Push Notifications in SwiftUI | Continued Learning 실제 노티는 서버에서 푸시하는 게 일반적로컬 특정 조건이 만족된다면 서버를 사용하지 않아도 푸시 가능좋은 UI의 기본이 될 수 있는 방

velog.io

https://velog.io/@maddie/iOS-%EB%A1%9C%EC%BB%AC%EB%A1%9C-%EC%95%8C%EB%A6%BC-Notification-%EB%A7%8C%EB%93%A4%EA%B8%B0

 

[iOS][SwiftUI] 로컬로 알림 Notification 만들기

😤기능: alert 경고 표시, sound 소리 재생, badge app icon 아이콘 배지 표시사용자가 원하는 중요한 정보를 전달하기 위해 사용!앱이 실행되지 않았거나, 백그라운드: 시스템이 대신 사용자와 상호작

velog.io

 

https://2unbini.github.io/%F0%9F%93%82%20all/swift/swiftUI-Local-Notification/

 

SwiftUI, Local Notification 구현하기

back/foreground 둘 다 작동하는 로컬 푸시 알림 설정하기

2unbini.github.io

짱짱맨..bb

 

GitHub

https://github.com/jjwon2149/WordQuizDaily

 

GitHub - jjwon2149/WordQuizDaily: 단어 퀴즈 앱

단어 퀴즈 앱. Contribute to jjwon2149/WordQuizDaily development by creating an account on GitHub.

github.com

 

 

https://simth999wrld.tistory.com/78

 

우리말 퀴즈 앱)4. 위젯, Scheme 사용한 API KEY 숨기기

https://simth999wrld.tistory.com/77 우리말 퀴즈 앱)3.Alamofier, 네이버 검색 API 사용 https://simth999wrld.tistory.com/76 우리말 퀴즈 앱)2.퀴즈뷰 구성중... https://simth999wrld.tistory.com/75 우리말 퀴즈 앱)1.우리말샘 api

simth999wrld.tistory.com

 

오랜만에 보니 뭐라고 코드를 적었는지 하나도 이해가 가지 않았습니다.. 이는 제가 코드를 잘못짠 탓이겠지요...

 


미루고 미루던 urlSession부분에 async await을 적용 하였습니다.

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()
    }
}

 

보이시나요... 에너지파를 날리는 이전의 코드..

최신 강의를 보지않고 예전의 강의를 보아 저에겐 익숙하지만,, 보기 껄끄럽습니다.

저또한 저의 코드를 보면서 시간을 조금 보냈군요..ㅋㅋ

 

func asyncSearchWord(_ searchWord: String) async throws -> WordData? {
    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"
    guard let url = URL(string: urlString) else { return nil }

    do {
        let (data, response) = try await URLSession.shared.data(from: url)
        let decodedData = try JSONDecoder().decode(WordData.self, from: data)
        DispatchQueue.main.async {
            self.wordData = decodedData
        }
        return decodedData
    } catch {
        print(error)
        throw error
    }
}

async await을 사용해 바꾼 네트워크 코드입니다.

한눈에 봐도 정말 속이 시원해지는 코드입니다..

 

하지만 문제는 이후 입니다.

 

viewModel에서 코드를 고치다 보니 네트워크 요청을 중복으로 사용하고 있었습니다!

fetchData()와 generateChoice() 함수에서 중복으로 호출을 하고 있었습니다...

 

따라서 fetchData()에서는 네트워크 요청을 하는 함수인 setupNewQuiz()를 호출하여 data를 가져오는 함수로 작성을 하였고,

setupNewQuiz() 에선 비동기로 네트워킹을 합니다.

func fetchData() {
    Task {
        await setupNewQuiz()
    }
}

//퀴즈 셋업
func setupNewQuiz() async {
    isLoading = true
    correctWord = hardKoreanWords.hardWords.randomElement() ?? ""
    choiceWord = await generateChoices()

    do {
        let wordData = try await wordNetwork.asyncSearchWord(correctWord)
        await handleWordData(word: correctWord, wordData: wordData)
    } catch {
        handleNetworkError(error)
    }

    isLoading = false
}

 

그리고 엑티비티 인디케이터를 구분할수 있는 isLoading을 toggle하는 부분을  모두 메인 쓰레드에서 작동 하도록 바꾸었습니다.

DispatchQueue.main.async {
    self.isLoading = false
}


setupNewQuiz()에서 볼수 있는 handleNetworkError의 함수는 에러 핸들링을 위해 작성 하였습니다.

네트워크 요청이 실패 했을떄 호출되는 함수 입니다.

//MARK: - Error Handling
func handleNetworkError(_ error: Error) {
    DispatchQueue.main.async {
        self.errorMessage = error.localizedDescription
        self.isLoading = false
    }
}

 

에러발생

빌드를 진행하니

@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.

라는 에러가 발생하였습니다.

 

이는 백그라운드 쓰레드에서 UI를 업데이트를 하려고해 나타나는 오류라고 합니다!

-> @Published 속성을 백그라운드 쓰레드에서 변경하려고 시도하여 발생한다고 합니다.

이 문제를 해결하려면 변경되는 부분을 메인 쓰레드 에서 실행을 해야 합니다.

@MainActor
func setupNewQuiz() async {
    isLoading = true
    correctWord = hardKoreanWords.hardWords.randomElement() ?? ""
    choiceWord = await generateChoices()

    do {
        let wordData = try await wordNetwork.asyncSearchWord(correctWord)
        await handleWordData(word: correctWord, wordData: wordData)
    } catch {
        handleNetworkError(error)
    }

    isLoading = false
}

setupNewQuiz 메서드를 MainActor를 사용하여 메인 스레드에서 실행할 수 있도록 명시적으로 지정하였습니다.

 

 

 


저는 비동기 처리에 대한 개념을 더욱 확실하게 잡아야 합니다.. 네트워킹을 비동기로 바꾼후 적용하는 코드에서

많은 오류를 보았더랬죠..  언제쯤 수월하게 코드를 작성할 수 있게 될까요..




참조


https://developer.apple.com/videos/play/wwdc2021/10132/

 

Meet async/await in Swift - WWDC21 - Videos - Apple Developer

Swift now supports asynchronous functions — a pattern commonly known as async/await. Discover how the new syntax can make your code...

developer.apple.com

https://dev-mandos.tistory.com/28

 

[WWDC21] Use async/await with URLSession을 적용해보자..!

해당 글은 [WWDC21] Use async/await with URLSession 보고 작성했습니다. Swift Concurrency 에 대한 얘기가 몇몇 나왔었다. Swift Concurrency에 대해 간략하게 설명해보자면, 코드를 선형적이고 간결하게 만들고, Nat

dev-mandos.tistory.com


GitHub

https://github.com/jjwon2149/WordQuizDaily

 

GitHub - jjwon2149/WordQuizDaily: 단어 퀴즈 앱

단어 퀴즈 앱. Contribute to jjwon2149/WordQuizDaily development by creating an account on GitHub.

github.com

 

 

 

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