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

 

+ Recent posts