/Check-Your-Commit

Check Your Commit....πŸ™

Primary LanguageSwift

PJ2T2_CYC

λͺ©μ°¨

  1. μ†Œκ°œ
  2. μ£Όμš”κΈ°λŠ₯
  3. μ‹€ν–‰ν™”λ©΄
  4. νƒ€μž„λΌμΈ
  5. νŠΈλŸ¬λΈ”μŠˆνŒ…
  6. κ°œλ°œν™˜κ²½ 및 라이브러리
  7. Tree

μ†Œκ°œ

1일 1컀밋을 ν•˜κ³  싢은데 자꾸 κΉŒλ¨Ήμ–΄.. μ•Œλ¦Ό μžˆμ—ˆμœΌλ©΄ μ’‹κ² λ‹€..

μ—΄μ • λ§Žμ€ 개발자, push ν•΄μ•Όλ˜λŠ”λ° μžŠμ–΄λ²„λ¦° 마감 κΈ‰ν•œ 개발자 등등을 μœ„ν•œ 1일 1컀밋 κ°•μš”μ•±
저희 CYC(Check your commit)λŠ” 크게 commit μ•Œλ¦Ό κΈ°λŠ₯, κ°„λ‹¨ν•œ todo listλ₯Ό κ°–κ³  μžˆμŠ΅λ‹ˆλ‹€.

πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ νŒ€μ›

κ°•μΉ˜μš° κΉ€λͺ…ν˜„ 이민영 황민채 황성진

πŸ”— Links

μ£Όμš”κΈ°λŠ₯

  1. κΉƒν—ˆλΈŒ OAuthλ₯Ό ν†΅ν•œ 둜그인 연동
    • OAuth AcessToken을 λ°”νƒ•μœΌλ‘œ μœ μ € 정보λ₯Ό ν™œμš©
    • μœ μ € 정보λ₯Ό custom ProgressView, GrassView λ“± ν™œμš©
  2. λͺ©ν‘œλ‹¬μ„±μ„ λ„μ™€μ£ΌλŠ” μ±Œλ¦°μ§€ μ„€μ •
    • MainViewμ—μ„œ D-Dayλ₯Ό μ œκ³΅ν•¨μœΌλ‘œ, λͺ©ν‘œλ₯Ό κ°€μ‹œμ μœΌλ‘œ 확인
  3. 였늘 컀밋을 μœ„ν•΄ 할일을 κΈ°λ‘ν•˜λŠ” TodoList
  4. μ•ŒλžŒμ„ 톡해 μΌμ •μ‹œκ°„λ§ˆλ‹€ 컀밋 체크

μ‹€ν–‰ ν™”λ©΄

μ•± ν™”λ©΄
라이트 λͺ¨λ“œ 닀크 λͺ¨λ“œ

νƒ€μž„λΌμΈ

Step 1 νƒ€μž„λΌμΈ
  • 23.12.5 ~ 23.12.6
    • νŒ€λΉŒλ”©
    • 아이디어 ν† μ˜
    • 아이디어 κ΅¬ν˜„ λ°©μ•ˆ ν† μ˜
Step 2 νƒ€μž„λΌμΈ
  • 23.12.06 ~ 23.12.07
    • Figmaλ₯Ό κΈ°λ³Έ λ””μžμΈ ν”„λ‘œν† νƒ€μž… μ œμž‘
    • 각 κΈ°λŠ₯별 κ΅¬ν˜„ λ°©μ•ˆ ν† μ˜
    • 각 νŒŒνŠΈλ³„ μ—­ν•  λΆ„λ°°
    • ν”„λ‘œμ νŠΈ 개발 μ‹œμž‘
  • 23.12.12 ~ 23.12.13
    • μ•± μ•„μ΄μ½˜ μ œμž‘
Step 3 νƒ€μž„λΌμΈ
  • 23.12.06
    • κΈ°λ³Έ μ•± ꡬ쑰 μ œμž‘
    • μ»€μŠ€ν…€ 폰트, 컬러 Aseet 적용
  • 23.12.07 ~ 23.12.11
    • κΉƒν—ˆλΈŒ OAuth 둜그인 κ΅¬ν˜„
    • OAuth 데이터λ₯Ό 톡해 μœ μ € 정보 λ°›μ•„μ˜€λŠ” λΆ€λΆ„ κ΅¬ν˜„
  • 23.12.07 ~ 23.12.14
    • μ•Œλ¦ΌκΈ°λŠ₯ κ΅¬ν˜„
    • Todo List κ΅¬ν˜„
  • 23.12.11 ~ 23.12.14
    • κΉƒν—ˆλΈŒ APIλ₯Ό μ΄μš©ν•œ GrassView κ΅¬ν˜„
    • κΉƒν—ˆλΈŒ API둜 λ°›μ•„μ˜¨ μ»€λ°‹μΌμˆ˜λ‘œ D-day 계산기 κ΅¬ν˜„
  • 23.12.14
    • 라이트 λͺ¨λ“œ, 닀크λͺ¨λ“œ λ³€ν™˜ λ²„νŠΌ κ΅¬ν˜„

νŠΈλŸ¬λΈ” μŠˆνŒ…

Step1

APIλ₯Ό 톡해 JSON μœ μ € 데이터가 μ •μƒμ μœΌλ‘œ λΆˆλŸ¬μ™€μ§€μ§€ μ•ŠμŒ
  • Git APIλ₯Ό 톡해 μœ μ € 데이터가 JSON ν˜•μ‹μœΌλ‘œ λΆˆλŸ¬μ™€μ§€μ§€ μ•ŠλŠ” 문제
func getUser() {
        let accessToken = KeychainSwift().get("accessToken") ?? ""
        let headers: HTTPHeaders = ["Accept": "application/vnd.github.v3+json",
                                    "Authorization": "token \(accessToken)"]
        
        AF.request(githubApiURL+ApiPath.USER.rawValue,
                   method: .get,
                   parameters: [:],
                   headers: headers).responseJSON(completionHandler: { (response) in
            switch response.result {
            case .success(let json):
                print(json as! [String: Any])
            case .failure:
                print("")
            }
        })
    }
  • κΉƒν—ˆλΈŒ μœ μ € API κ³΅μ‹λ¬Έμ„œ ν•΄λ‹Ή λ¬Έμ„œμ˜ ν˜•νƒœλ‘œ curl 을 μ‚¬μš©ν•˜λ©΄ μ •μƒμ μœΌλ‘œ JSON ν˜•νƒœμ˜ 데이터가 받아와 μ§€λŠ” 것을 확인
  • APIλ₯Ό λ°›μ•„μ˜€λŠ” κ³Όμ •μ—μ„œ responseJSON 의 ν˜•νƒœκ°€ μ•„λ‹ˆλΌ responseString ν˜Ήμ€ responseDecodable 으둜 μ‚¬μš©ν•˜λ©΄ μ •μƒμ μœΌλ‘œ 데이터가 받아와 μ§€λŠ” 것을 확인
  • structλ₯Ό 톡해 Userλ₯Ό μ„ μ–Έν•˜κ³  responseDecodable 둜 ν•΄λ‹Ή 데이터λ₯Ό ν• λ‹Ήμ‹œν‚€λŠ” λ°©λ²•μœΌλ‘œ ν™œμš©
    struct User: Decodable {
        let login: String
        let name: String
    }

    func getUser() {
        let headers: HTTPHeaders = ["Accept": "application/vnd.github+json",
                                    "Authorization": "Bearer \(access_token!)"]
        
        AF.request("https://api.github.com/user",
                   method: .get, parameters: [:],
                   headers: headers).responseDecodable(of: User.self) { response in
            switch response.result {
            case .success(let user):
                self.userLogin = user.login
                self.userName = user.name
                self.getCommitData()
            case .failure(let error):
                print("Error: \(error.localizedDescription)")
            }
        }
    }
  • REST API의 μ£Όμ†Œκ°€ λͺ…ν™•ν•œμ§€ ν™•μΈν•˜κΈ°μœ„ν•΄ curl의 ν™œμš©λ²•μ„ μ•Œκ²Œλ¨.
GitHub contribution graph에 SwiftSoup μ‚¬μš©ν•œ 이유
let parsedHtml = try SwiftSoup.parse(htmlURL)
let dailyContribution = try parsedHtml.select("td")

let validCommits = dailyContribution.compactMap { element -> (String, String)? in
    guard
        let dateString = try? element.attr("data-date"),
        let levelString = try? element.attr("data-level"),
        !dateString.isEmpty
    else { return nil }

    return (dateString, levelString)
}
  1. Github profile에 μžˆλŠ” κΉƒν—™ μž”λ””μ— λŒ€ν•œ 데이터λ₯Ό api둜 μ œκ³΅ν•΄μ£Όμ§€ μ•ŠμŒ
  2. commits history만 μ œκ³΅ν•˜μ§€λ§Œ 각 repoλ³„λ‘œ history둜 μ œκ³΅ν•˜κ±°λ‚˜, user events둜 전체 commit을 λ³΅μž‘ν•œ ꡬ쑰둜 제곡
  3. ν•˜μ§€λ§Œ 제일 μ€‘μš”ν•œκ±΄ 무엇보닀 api의 μ—…λ°μ΄νŠΈκ°€ λŠλ €μ„œ commit을 ν•œ ν›„ μ΅œλŒ€ 8μ‹œκ°„ 후에 반영 됨
  4. κ·ΈλŸ¬λ―€λ‘œ κ°€λŠ₯ν•œ 빨리 λ°˜μ˜λ˜λŠ” λ©”μΈμ˜ contribution graphλ₯Ό 톡해 λ°›μ•„μ˜€κΈ° μœ„ν•΄ μ›Ή 크둀링 라이브러리λ₯Ό μ‚¬μš©ν•˜μ—¬ dataλ₯Ό λ°›μŒ

Step2

UserDefaults의 μ‚¬μš©
  • API λ₯Ό ν™œμš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” μ•‘μ„ΈμŠ€ν† ν° 값이 μ ˆλŒ€μ μœΌλ‘œ ν•„μš”, 앱을 μ’…λ£Œ μ‹œμΌœλ„ ν•΄λ‹Ή 값은 μœ νš¨ν•΄μ•Ό 됨
  • AppStorageλ₯Ό μ‚¬μš©ν•˜λ € ν–ˆμ§€λ§Œ λ‹€λ₯Έ λ·°μ—μ„œλ„ μ‚¬μš©ν•˜κ³  μ°Έμ‘°ν•΄μ•Ό 되기 λ•Œλ¬Έμ— μ‚¬μš©μ΄ 어렀움
class LoginModel: ObservableObject {

    static let shared = LoginModel()

    @Published var code: String?
    @Published var access_token: String?
    @Published var userLogin: String?
  • UserDefaults 둜 ν•΄λ‹Ή λ³€μˆ˜λ“€μ„ μ„ μ–Έν•˜κ³  extension을 톡해 set, get 뢀뢄을 적용
  • init() 뢀뢄을 톡해 μ„ μ–Έλœ λ³€μˆ˜λ₯Ό μ΄ˆκΈ°ν™”
    @Published var access_token: String? {
        didSet {
            UserDefaults.standard.setAccessToken(access_token ?? "")
        }
    }
    
    @Published var userName: String? {
        didSet {
            UserDefaults.standard.setUserName(userName ?? "")
        }
    }

    @Published var userLogin: String? {
        didSet {
            UserDefaults.standard.setUserLogin(userLogin ?? "")
        }
    }
    
    var results: [(String, String)] = []
    @Published var testCase:[String:Int] = [:]
    
    // UserDefaults둜 μ„ μ–Έλœ λ³€μˆ˜λ₯Ό μ‚¬μš©ν•˜κΈ° μœ„ν•œ init λΆ€λΆ„
    init() {
        self.userLogin = UserDefaults.standard.getUserLogin()
        self.access_token = UserDefaults.standard.getAccessToken()
        self.userName = UserDefaults.standard.getUserName()
    }


// UserDefaults의 extension λΆ€λΆ„ 
    extension UserDefaults {
        private static let userLoginKey = "userLoginKey"

        func setUserLogin(_ login: String) {
            set(login, forKey: UserDefaults.userLoginKey)
        }

        func getUserLogin() -> String? {
            return string(forKey: UserDefaults.userLoginKey)
        }
    }

    extension UserDefaults {
        private static let userAcessToken = "acessToken"

        func setAccessToken(_ token: String) {
            set(token, forKey: UserDefaults.userAcessToken)
        }

        func getAccessToken() -> String? {
            return string(forKey: UserDefaults.userAcessToken)
        }
    }

    extension UserDefaults {
        private static let userNickname = "userNickname"

        func setUserName(_ name: String) {
            set(name, forKey: UserDefaults.userNickname)
        }

        func getUserName() -> String? {
            return string(forKey: UserDefaults.userNickname)
        }
    }

Step3

FCMμ—μ„œ userNotifications둜 μ „ν™˜ν•œ 이유

처음 κ΅¬ν˜„ν•˜κ³ μž ν–ˆλ˜ κΈ°λŠ₯의 μˆœμ„œλŠ” λ‹€μŒκ³Ό κ°™μ•˜λ‹€.

  1. APNs에 λ””λ°”μ΄μŠ€ 토큰을 μš”μ²­
  2. APNsμ—μ„œ 받은 λ””λ°”μ΄μŠ€ 토큰을 Push server에 λ„˜κΉ€
  3. APNs에 ν‘Έμ‰¬μ•Œλ¦Όμ„ 보낼 데이터λ₯Ό 전달
  4. APNs에 μžˆλŠ” 데이터λ₯Ό λ°›μ•„μ„œ μœ μ €μ˜ ν°μ—μ„œ μ•Œλ¦Ό 전달
import SwiftUI
import FirebaseCore
import FirebaseMessaging

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        FirebaseApp.configure()

        // 원격 μ•Œλ¦Ό 등둝
        if #available(iOS 10.0, *) {
            // For iOS 10 display notification (sent via APNS)
            UNUserNotificationCenter.current().delegate = self

            let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
            UNUserNotificationCenter.current().requestAuthorization(
                options: authOptions,
                completionHandler: { _, _ in }
            )
        } else {
            let settings: UIUserNotificationSettings =
            UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
            application.registerUserNotificationSettings(settings)
        }

        application.registerForRemoteNotifications()

        // Firebase κ°€ ν‘Έμ‹œ λ©”μ‹œμ§€λ₯Ό λŒ€μ‹  전솑할 수 μžˆλ„λ‘ λŒ€λ¦¬μžλ₯Ό μ„€μ •ν•˜λŠ” κ³Όμ • (MessagingDelegate)
        Messaging.messaging().delegate = self


        // ν‘Έμ‹œ ν¬κ·ΈλΌμš΄λ“œ μ„€μ •
        UNUserNotificationCenter.current().delegate = self

        return true
        //Messaging에 λ“±λ‘λœ 토큰은 messaging:didReceiveRegistrationToken ν”„λ‘œν† μ½œ λ©”μ„œλ“œλ₯Ό 1회 ν˜ΈμΆœν•¨ - μƒˆλ‘œ λ“±λ‘λœ 토큰이라면 μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„œλ²„λ‘œ 전솑/ μ•„λ‹ˆλΌλ©΄ λ“±λ‘λœ 토큰을 ꡬ독 μ²˜λ¦¬ν•΄μ€Œ
    }


    // fcm 토큰이 등둝 λ˜μ—ˆμ„ λ•Œ
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Messaging.messaging().apnsToken = deviceToken
    }
}

@main
struct CYCApp: App {
struct YourApp: App {
    // register app delegate for Firebase setup
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    

    var body: some Scene {
        WindowGroup {
            AboutCYC()
        }
    }
}
extension AppDelegate : MessagingDelegate {

    // fcm 등둝 토큰을 λ°›μ•˜μ„ λ•Œ
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        print("Firebase registration token: \(String(describing: fcmToken))")
        let dataDict: [String: String] = ["token": fcmToken ?? ""]
        NotificationCenter.default.post(
            name: Notification.Name("FCMToken"),
            object: nil,
            userInfo: dataDict
        )
    }
}

extension AppDelegate : UNUserNotificationCenterDelegate {

    // ν‘Έμ‹œλ©”μ„Έμ§€κ°€ 앱이 켜져 μžˆμ„λ•Œ λ‚˜μ˜¬λ•Œ
    // completionHandler둜 "UNNotificationPresentationOptions"λ₯Ό λ°˜ν™˜ν•¨
    // μ‚¬μš©μžκ°€ 머무λ₯΄κ³  μžˆλŠ” 화면에 따라 ν¬κ·ΈλΌμš΄λ“œ μƒνƒœμ—μ„œμ˜ ν‘Έμ‹œλ₯Ό 보여쀄지 아닐지에 λŒ€ν•œ λΆ„κΈ°μ²˜λ¦¬κ°€ κ°€λŠ₯(ex.μΉ΄ν†‘μ±„νŒ…λ°©μ—μ„œ ν‘Έμ‹œλ₯Ό λ„μš°μ§€ μ•ŠλŠ” λ“±)
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

        let userInfo = notification.request.content.userInfo

        print("willPresent: userInfo: ", userInfo)

        completionHandler([.banner, .sound, .badge])

        // Notification λΆ„κΈ°μ²˜λ¦¬
        if userInfo[AnyHashable("Check Your Commit")] as? String == "project" {
            print("CYC project")
        }else {
            print("NOTHING")
        }
    }

    // ν‘Έμ‹œλ©”μ„Έμ§€λ₯Ό λ°›μ•˜μ„ λ•Œ
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        print("didReceive: userInfo: ", userInfo)
        completionHandler()
    }
}

μœ„ μ½”λ“œλ‘œ 토큰을 λ°›μ•„ μˆ˜λ™μœΌλ‘œ Firebase messiging μ„œλ²„μ— 직접 λ“±λ‘ν•˜κ³  앱에 μ•Œλ¦Όμ„ λ°›λŠ”λ°μ— μ„±κ³΅ν–ˆλ‹€.ν•˜μ§€λ§Œ λ¬Έμ œλŠ” λ‹€μˆ˜ μœ μ €μ˜ 토큰을 μ–΄λ–»κ²Œ λ°›μ•„μ„œ λ©”μ‹œμ§• μ„œλ²„μ— μ˜¬λ €μ£ΌλŠλƒμ˜€λ‹€. μ„œλ²„μ—†μ΄ FCM만 μ‚¬μš©ν•˜μ—¬ λ‹€μŒ 두 쑰건을 λ™μ‹œμ— λ§Œμ‘±ν•˜λŠ” μœ μ €μ—κ²Œλ§Œ μ•Œλ¦Όμ„ 쀄 수 μžˆλŠ” 방법을 μƒκ°ν•˜μ—¬μ•Ό ν–ˆλ‹€.

  • μ‚¬μš©μžκ°€ 일정 μ‹œκ°„μ— μ»€λ°‹ν•˜μ˜€λŠ”κ°€
  • μ‚¬μš©μžκ°€ μ•Œλ¦Ό μ„€μ • 토글을 on ν•˜μ˜€λŠ”κ°€

μ‚¬μš©μžμ˜ 정보λ₯Ό μ„œλ²„κ°€ μ €μž₯ν•˜κ³  μžˆμ–΄μ•Ό μœ„ 두 쑰건을 λ§Œμ‘±ν•˜λŠ” κΈ°λŠ₯을 κ΅¬ν˜„ν•  수 μžˆλ‹€κ³  결둠을 λ‚΄λ Έκ³ , 이번 개발 κΈ°κ°„μ—λŠ” μ‚¬μš©μžκ°€ μ•Œλ¦Ό μ„€μ • 토글을 on ν•˜μ˜€μ„ λ•Œ 7μ‹œ 이후 맀 μ‹œκ°„λ§ˆλ‹€ μ•Œλ¦Όμ„ μ£ΌλŠ” κΈ°λŠ₯λ§Œμ„ κ΅¬ν˜„ν•˜κΈ°λ‘œ ν•˜μ˜€λ‹€. 이 κΈ°λŠ₯을 κ΅¬ν˜„ν•˜λŠ”λ°μ— FCM을 ꡳ이 μ‚¬μš©ν•˜μ§€ μ•Šκ³  λ‚΄λΆ€ 라이브러리인 userNotifications 을 μ‚¬μš©ν•˜μ˜€λ‹€.

  • AppDelegate.swift
import SwiftUI
import UserNotifications

class AppDelegate: NSObject, UIApplicationDelegate {
    
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        
        // μ•± μ‹€ν–‰ μ‹œ μ‚¬μš©μžμ—κ²Œ μ•Œλ¦Ό ν—ˆμš© κΆŒν•œμ„ λ°›μŒ
        UNUserNotificationCenter.current().delegate = self
        
        
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] // ν•„μš”ν•œ μ•Œλ¦Ό κΆŒν•œμ„ μ„€μ •
        UNUserNotificationCenter.current().requestAuthorization(
            options: authOptions,
            completionHandler: { _, _ in }
        )
        return true
    }
}

extension AppDelegate: UNUserNotificationCenterDelegate {
    
    // Foreground(μ•± μΌœμ§„ μƒνƒœ)μ—μ„œλ„ μ•Œλ¦Ό μ˜€λŠ” μ„€μ •
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.list, .banner])
    }
}

μ•±λΈλ¦¬κ²Œμ΄νŠΈμ—μ„œ μ•Œλ¦ΌκΆŒν•œμ„ μ„€μ •ν•΄μ£Όμ—ˆλ‹€.

  • NotificationHelper.swift
import Foundation
import UIKit
import UserNotifications

//
// - Note: μ‹±κΈ€ν„΄μœΌλ‘œ κ΅¬ν˜„ `LocalNotificationHelper.shared`λ₯Ό 톡해 μ ‘κ·Ό
class LocalNotificationHelper {
    static let shared = LocalNotificationHelper()
    
    private init() {}
    
    ///Push Notification에 λŒ€ν•œ 인증 μ„€μ • ν•¨μˆ˜
    func setAuthorization() {
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] // ν•„μš”ν•œ μ•Œλ¦Ό κΆŒν•œμ„ μ„€μ •
        UNUserNotificationCenter.current().requestAuthorization(
            options: authOptions,
            completionHandler: { _, _ in }
        )
    }
    // ν•˜λ£¨λ₯Ό 주기둜 νŠΉμ • μ‹œκ°„μ— Notification을 λ³΄λ‚΄λŠ” μ½”λ“œ
    func pushScheduledNotification(title: String, body: String, hour: Int, identifier: String) {
        
        assert(hour >= 0 || hour <= 24, "μ‹œκ°„μ€ 0이상 24μ΄ν•˜λ‘œ μž…λ ₯ν•΄μ£Όμ„Έμš”.")
        
        let notificationContent = UNMutableNotificationContent()
        notificationContent.title = title
        notificationContent.body = body
        
        var dateComponents = DateComponents()
        dateComponents.hour = hour  // μ•Œλ¦Όμ„ 보낼 μ‹œκ°„ (24μ‹œκ°„ ν˜•μ‹)
        
        let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
        let request = UNNotificationRequest(identifier: identifier,
                                            content: notificationContent,
                                            trigger: trigger)
        
        UNUserNotificationCenter.current().add(request) { error in
            if let error = error {
                print("Notification Error: ", error)
            }
        }
    }
    
    /// λŒ€κΈ°μ€‘μΈ Push Notification을 좜λ ₯
    func printPendingNotification() {
        UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
            for request in requests {
                print("Identifier: \(request.identifier)")
                print("Title: \(request.content.title)")
                print("Body: \(request.content.body)")
                print("Trigger: \(String(describing: request.trigger))")
                print("---")
            }
        }
    }
    //μ•Œλ¦Ό μ „μ²΄μ‚­μ œ
    func removeAllNotifications() {
        UNUserNotificationCenter
            .current().removeAllDeliveredNotifications()
        UNUserNotificationCenter
            .current().removeAllPendingNotificationRequests()
    }
}

NotificationHelper ν΄λž˜μŠ€μ—μ„œ μ•Œλ¦Όμ— ν•„μš”ν•œ ν•¨μˆ˜λ₯Ό κ΅¬ν˜„ν•˜μ˜€λ‹€.

  • NotificationView
class NotificationSettings: ObservableObject {
    @Published var isOnNotification: Bool {
        didSet {
            UserDefaults.standard.set(isOnNotification, forKey: "isOnNotification")
        }
    }
    
    init() {
        self.isOnNotification = UserDefaults.standard.bool(forKey: "isOnNotification")
    }
}
.
.
VStack(alignment: .leading) {
    Toggle(isOn: $isOnNotification, label: {
        
        // MARK: - μ•Œλ¦Ό μ„€μ • ν† κΈ€
        Text("μ•Œλ¦Ό μ„€μ •")
            .font(.pretendardBold_25)
    }).onChange(of: isOnNotification, initial: false, techNotification)
.
.
func techNotification() {
    if isOnNotification {
      LocalNotificationHelper.shared.printPendingNotification()
      LocalNotificationHelper
        .shared
        .pushScheduledNotification(title: "Check Your Commit",
                                   body: "μ»€λ°‹ν•΄μ€˜μ—¬..🫢",
                                   hour: 18,
                                   identifier: "SCHEDULED_NOTI18")
    } else if {
        LocalNotificationHelper.shared.removeAllNotifications()
    }
}
.
.

μ•Œλ¦Ό μ„€μ •λ·°μ—μ„œ 토글값이 on일 λ•Œ μ•Œλ¦Όμ΄ μ•Œλ¦Όμ„Όν„°μ— μ˜¬λΌκ°€λ„λ‘ κ΅¬ν˜„ν•˜κ³ , off μ‹œμ—” μ•Œλ¦Όμ„Όν„°μ˜ μ•Œλ¦Όμ„ λͺ¨λ‘ μ‚­μ œν•˜λ„λ‘ κ΅¬ν˜„ν•˜μ˜€λ‹€.

Step4

OnTapGestureμ‚¬μš©
  • TodoList μ‚¬μš© μ‹œ 빈 ν™”λ©΄ ν„°μΉ˜ ν–ˆμ„λ•Œ, ν…μŠ€νŠΈν•„λ“œλ₯Ό μƒμ„±ν•˜λ €ν–ˆμ§€λ§Œ 리슀트 μŠ€μ™€μ΄ν”„ μ‚­μ œ ν•  λ•Œλ„ ν…μŠ€νŠΈν•„λ“œκ°€ 생성됨.
@State var isTextFieldShown = false

.onTapGesture {
        if !isTextFieldShown {
              isTextFieldShown.toggle()
            }
        }
  • TodoList μ‚¬μš© μ‹œ ν…μŠ€νŠΈν•„λ“œμ— ν…μŠ€νŠΈλ₯Ό μž…λ ₯ν•˜κ³  빈 화면을 ν„°μΉ˜ν•˜λ©΄ ν…μŠ€νŠΈ μ €μž₯을 κ΅¬ν˜„ν•˜λ € ν–ˆμ§€λ§Œ, 리슀트 μŠ€μ™€μ΄ν”„ μ‚­μ œ ν•  λ•Œλ„ ν•¨μˆ˜κ°€ μž‘λ™.
func addTodo() {
        withAnimation {
            let newTodo = TodoModel(title: textFieldText)
            if !newTodo.title.isEmpty {
                modelContext.insert(newTodo)
                isTextFieldShown.toggle()
            }
        }
    }


.onTapGesture {
    withAnimation{
        addTodo()   // ν…μŠ€νŠΈ μΆ”κ°€ ν•¨μˆ˜
        textFieldText = ""  // μΆ”κ°€ ν›„ ν…μŠ€νŠΈν•„λ“œ λΉ„μ›Œμ£ΌκΈ°
    }
}
DispatchQueue.main.async둜 UIView 속도 ν–₯상
// μ€€λΉ„λ˜λ©΄ λ°”λ‘œ μ—°μ†μΌμˆ˜ 뿌리기, 곡룑 움직이기 -> MainViewμ—μ„œ λ°”λ‘œ 처리
DispatchQueue.main.async {
    if self.dataToDictionary(validCommits){
        self.commitDay = self.findConsecutiveDates(withData: self.testCase)
        ModalView().moveDinosaur() // ν”„λ‘œκ·Έλž˜μŠ€λ°”μ˜ 곡룑이 μ›€μ§μ΄λŠ” ν•¨μˆ˜
    }
}
  1. onAppear에 UIView의 μ—…λ°μ΄νŠΈ ν•¨μˆ˜λ₯Ό λ„£μ—ˆμ§€λ§Œ, 컀밋 연속 μΌμˆ˜μ™€ ν”„λ‘œκ·Έλž˜μŠ€λ°”κ°€ λ‹€λ₯Έ 뷰에 λ“€μ–΄κ°”λ‹€κ°€ λ‚˜μ™€μ•Όμ§€λ§Œ μ œλŒ€λ‘œ λ‚˜μ˜€λŠ” λ¬Έμ œκ°€ μžˆμ—ˆμŒ
  • Alamofire둜 api μš”μ²­ ν•¨μˆ˜λŠ” μžλ™μœΌλ‘œ 비동기 μ²˜λ¦¬λ˜λ―€λ‘œ main threadμ—μ„œ 데이터λ₯Ό κ°€μ Έμ˜€μ§€ μ•Šμ•˜κ³ 
  • UIViewκ°€ onAppearλ˜λŠ” μ‹œμ κ³Ό 데이터가 λ“€μ–΄μ˜€λŠ” μ‹œμ  차이가 μƒκΈ°λ©΄μ„œ λ‹€λ₯Έ 뷰에 λ“€μ–΄κ°”λ‹€κ°€ UIviewλ₯Ό λ‹€μ‹œ ν‘œμ‹œν• λ•Œ μ œλŒ€λ‘œ μƒκΈ°λŠ” 것이 발견됨
  1. UIViewλ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” 데이터 ν•¨μˆ˜λŠ” DispatchQueue.main.async둜 λΉΌμ„œ μ‚¬μš©ν•΄μ£Όκ³  await μ‚¬μš©μ΄ λ―Έμˆ™ν•΄ if문으둜 데이터가 λ“€μ–΄μ™”λŠ”μ§€ νŒλ³„ν•¨

κ°œλ°œν™˜κ²½ 및 라이브러리

SwiftUI
Xcode 15.1
iOS 17.1
Language - Swift 5.5.3
μ•ŒλžŒ - UserNotification
API - Alamofire
Todo - SwiftData
GrassView - SwiftSoup

Tree

πŸ“¦CYC
 ┣ πŸ“‚ Main
 ┃ β”— πŸ“œ MainView.swift
 ┣ πŸ“‚ Login
 ┃ ┃ ┣ πŸ“‚ extension
 ┃ ┃ β”— πŸ“œ extensionOfUserDefaults.ttf
 ┃ ┣ πŸ“œ OnboardingTabView.swift
 ┃ ┣ πŸ“œ LoginView.swift
 ┃ β”— πŸ“œ LoginModel.swift
 ┃ ┃ ┣ πŸ“‚ Font
 ┣ πŸ“‚ Setting
 ┃ ┣ πŸ“‚ PersonProfile
 ┃ ┃ ┣ πŸ“‚ View
 ┃ ┃ ┃ ┣ πŸ“œ PersonGridView.swift
 ┃ ┃ ┃ β”— πŸ“œ AboutCYC.swift
 ┃ ┃ ┣ πŸ“‚ Model
 ┃ ┃ ┃ β”— πŸ“œ PersonModel.swift
 ┃ ┣ πŸ“‚ ViewModel
 ┃ ┃ ┣ πŸ“œ LicenseViewModel.swift
 ┃ ┃ β”— πŸ“œ SettingViewModel.swift
 ┃ ┣ πŸ“‚ View
 ┃ ┃ ┣ πŸ“œ LicenseView.swift
 ┃ ┃ ┣ πŸ“œ NotificationView.swift
 ┃ ┃ β”— πŸ“œ SettingView.swift
 ┃ ┣ πŸ“‚ Model
 ┃ ┃ ┣ πŸ“œ LicenseModel.swift
 ┃ ┃ β”— πŸ“œ SettingModel.swift
 ┣ πŸ“‚ Grass
 ┃ ┣ πŸ“‚ View
 ┃ ┃ β”— πŸ“œ CommitView.swift
 ┣ πŸ“‚ Todo
 ┃ ┣ πŸ“‚ View
 ┃ ┃ ┣ πŸ“œ TodoView.swift
 ┃ ┃ β”— πŸ“œ TodoPreView.swift
 ┃ ┣ πŸ“‚ Model
 ┃ ┃ β”— πŸ“œ TodoModel.swift
 ┣ πŸ“‚ Progress
 ┃ ┣ πŸ“‚ View
 ┃ ┃ ┣ πŸ“œ ProgressView.swift
 ┃ ┃ ┣ πŸ“œ ModalView.swift
 ┃ ┃ ┣ πŸ“œ ProgressBarView.swift
 ┃ ┃ ┣ πŸ“œ DdayButtonView.swift
 ┃ ┃ β”— πŸ“œ ProgressTextView.swift
 ┣ πŸ“‚ Helper
 ┃ ┣ πŸ“‚ NotificationHelper
 ┃ ┃ β”— πŸ“œ LocalNotificationHelper.swift
 ┃ ┣ πŸ“‚ DarkLightMode
 ┃ ┃ ┣ πŸ“œ DLMode.swift
 ┃ ┃ β”— πŸ“œ UIButton.swift
 ┃ ┣ πŸ“‚ Extensions
 ┃ ┃ ┣ πŸ“œ fontExtension.swift
 ┃ ┃ ┣ πŸ“œ CustomSpacing.swift
 ┃ ┃ ┣ πŸ“œ colorExtension.swift
 ┃ ┃ β”— πŸ“œ DismissGesture.swift
 ┃ ┣ πŸ“‚ Fonts
 ┃ ┃ ┣ πŸ“œ Pretendard-Black.otf
 ┃ ┃ ┣ πŸ“œ Pretendard-Bold.otf
 ┃ ┃ ┣ πŸ“œ Pretendard-ExtraBold.otf
 ┃ ┃ ┣ πŸ“œ Pretendard-ExtraLight.otf
 ┃ ┃ ┣ πŸ“œ Pretendard-Light.otf
 ┃ ┃ ┣ πŸ“œ Pretendard-Medium.otf
 ┃ ┃ ┣ πŸ“œ Pretendard-Regular.otf
 ┃ ┃ ┣ πŸ“œ Pretendard-SemiBold.otf
 ┃ ┃ β”— πŸ“œ Pretendard-Thin.otf.swift
 ┣ πŸ“œ CYCAPP.swift
 ┣ πŸ“œ AppDelegate.swift
 ┣ πŸ“œ StartView.swift
 β”— πŸ–ΌοΈ Assets