๐ŸŒ™

[Swift] D-Day ์œ„์ ฏ ๋งŒ๋“ค๊ธฐ (2)

yeggrrr๐Ÿผ 2025. 12. 1. 20:31
728x90

 

 

์•ฑ์—์„œ ์ž…๋ ฅํ•œ ์„ค์ •๊ฐ’์„ ์œ„์ ฏ์— ๋ฐ˜์˜ํ•˜๊ธฐ

 

์ง€๋‚œ ๊ธ€์—์„œ๋Š” ๊ธฐ๋ณธ์ ์ธ D-Day ์œ„์ ฏ UI ๊ตฌ์„ฑ๊ณผ SwiftUI ๊ธฐ๋ฐ˜์˜ WidgetKit ๋™์ž‘ ๊ตฌ์กฐ๋ฅผ ์ •๋ฆฌํ•ด๋ดค๋‹ค.
์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๋ฉ”์ธ ์•ฑ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ D-Day ์ •๋ณด๋ฅผ ์œ„์ ฏ์— ๋ฐ˜์˜ํ•˜๋Š” ์ „์ฒด ํ๋ฆ„์„ ๊ตฌํ˜„ํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

ํ•ต์‹ฌ์€ ๋‹ค์Œ ์„ธ ๊ฐ€์ง€๋‹ค.

โ‘  App Group์„ ์ด์šฉํ•œ ๋ฐ์ดํ„ฐ ๊ณต์œ  (์ง€๋‚œ ๋ธ”๋กœ๊ทธ ๊ธ€์— ์žˆ์Œ)
- App Group ๊ธฐ๋ฐ˜์˜ SharedStore ์„ค๊ณ„
- D-Day ์„ค์ •๊ฐ’ ๋ชจ๋ธ ์ •์˜

โ‘ก ๋ฉ”์ธ ์•ฑ ViewController์—์„œ ์„ค์ •๊ฐ’ ์ €์žฅ

โ‘ข Widget TimelineProvider์—์„œ ํ•ด๋‹น ๊ฐ’์„ ๋ถˆ๋Ÿฌ์™€ ํ‘œ์‹œ

 

 


 

App Group ๊ธฐ๋ฐ˜์˜ SharedStore ์„ค๊ณ„

์œ„์ ฏ๊ณผ ๋ฉ”์ธ ์•ฑ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์„œ๋กœ ๋…๋ฆฝ๋œ ํ”„๋กœ์„ธ์Šค์—์„œ ๋™์ž‘ํ•œ๋‹ค.
๋”ฐ๋ผ์„œ ๋™์ผํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” App Group ๊ธฐ๋ฐ˜์˜ UserDefaults ๋˜๋Š” ํŒŒ์ผ ์ €์žฅ์ด ํ•„์š”ํ•˜๋‹ค.

์ด๋ฒˆ ์˜ˆ์ œ์—์„œ๋Š” ๊ตฌ์กฐ๊ฐ€ ๋‹จ์ˆœํ•˜๊ณ  ์•ˆ์ •์ ์ธ UserDefaults + Codable ์กฐํ•ฉ์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

import Foundation

enum SharedStore {
    // Signing & Capabilites์—์„œ ์„ค์ •ํ•œ App Group
    static let suiteName = "group.com.yegrcompany.ddaywidget"

    static var defaults: UserDefaults? {
        UserDefaults(suiteName: suiteName)
    }
}

extension SharedStore {
    private static let key = "dday_config"

    static func save(config: DDayConfig) {
        guard let data = try? JSONEncoder().encode(config) else { return }
        defaults?.set(data, forKey: key)
    }

    static func loadConfig() -> DDayConfig? {
        guard
            let data = defaults?.data(forKey: key),
            let config = try? JSONDecoder().decode(DDayConfig.self, from: data)
        else { return nil }

        return config
    }
}

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ์ ์€ App Group ID๊ฐ€ ์•ฑ ํƒ€๊ฒŸ๊ณผ ์œ„์ ฏ ํƒ€๊ฒŸ ๋ชจ๋‘์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

โ–ท Target → Signing & Capabilities → App Groups
โ–ท group.com.yegrcompany.ddaywidget ์ถ”๊ฐ€ (App + Widget Extension ๋™์ผํ•˜๊ฒŒ)

 

 


 

D-Day ์„ค์ •๊ฐ’ ๋ชจ๋ธ ์ •์˜

์•ฑ๊ณผ ์œ„์ ฏ ๋ชจ๋‘์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ„๋‹จํ•œ ๋ชจ๋ธ์„ ์ •์˜ํ•œ๋‹ค.

struct DDayConfig: Codable {
    let title: String
    let targetDate: Date
}


์ด ํŒŒ์ผ์€ ๋ฐ˜๋“œ์‹œ App Target + Widget Extension Target ๋ชจ๋‘์—์„œ Target Membership์ด ํ™œ์„ฑํ™”๋˜์–ด์•ผ ํ•œ๋‹ค.

 


 

ViewController ๊ตฌ์„ฑ ๋ฐ UI ๋ฐฐ์น˜

์•ฑ์—์„œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ D-Day ์ œ๋ชฉ๊ณผ ๋‚ ์งœ๋ฅผ ์ž…๋ ฅํ•˜๊ณ  "์ €์žฅ" ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋œ๋‹ค.

์•„๋ž˜ ์˜ˆ์‹œ๋Š” SanpKit ๋ ˆ์ด์•„์›ƒ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌ์„ฑํ•œ ViewController ์ด๋ฉฐ,
์ €์žฅ ์‹œ์ ์— SharedStore๋ฅผ ํ†ตํ•ด ๊ฐ’์„ ์ €์žฅํ•˜๊ณ  ์œ„์ ฏ ๊ฐฑ์‹ ๊นŒ์ง€ ์š”์ฒญํ•œ๋‹ค.

import UIKit
import SnapKit
import WidgetKit

final class ViewController: UIViewController {
    private let titleTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "๊ธฐ๋…์ผ ์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”"
        textField.borderStyle = .roundedRect
        return textField
    }()

    private let datePicker: UIDatePicker = {
        let picker = UIDatePicker()
        picker.datePickerMode = .date
        picker.locale = Locale(identifier: "ko_KR")
        return picker
    }()

    private let saveButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("์ €์žฅ", for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
        setupConstraints()
    }

    private func setupViews() {
        view.addSubview(titleTextField)
        view.addSubview(datePicker)
        view.addSubview(saveButton)

        saveButton.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
    }

    private func setupConstraints() {
        titleTextField.snp.makeConstraints {
            $0.top.equalTo(view.safeAreaLayoutGuide).offset(24)
            $0.leading.trailing.equalToSuperview().inset(24)
            $0.height.equalTo(44)
        }

        datePicker.snp.makeConstraints {
            $0.top.equalTo(titleTextField.snp.bottom).offset(24)
            $0.leading.trailing.equalToSuperview().inset(24)
        }

        saveButton.snp.makeConstraints {
            $0.top.equalTo(datePicker.snp.bottom).offset(32)
            $0.centerX.equalToSuperview()
            $0.width.equalTo(120)
            $0.height.equalTo(44)
        }
    }

    @objc
    private func saveButtonTapped() {
        let title = (titleTextField.text?.isEmpty == false)
            ? titleTextField.text!
            : "๊ธฐ๋…์ผ"

        let date = datePicker.date

        let config = DDayConfig(title: title, targetDate: date)
        SharedStore.save(config: config)

        WidgetCenter.shared.reloadAllTimelines()
    }
}

์ด ์‹œ์ ์—์„œ ๋ฉ”์ธ ์•ฑ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ๊ฐ’์„ App Group ์ €์žฅ์†Œ์— ์ •์ƒ์ ์œผ๋กœ ๊ธฐ๋กํ•œ๋‹ค.

 


 

์œ„์ ฏ ํƒ€์ž„๋ผ์ธ์—์„œ ์„ค์ •๊ฐ’ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ

์ด์ œ WidgetKit์ด ์ฃผ๊ธฐ์ ์œผ๋กœ ํ˜ธ์ถœํ•˜๋Š” TimelineProvider ์—์„œ
SharedStore.loadConfig()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์œ„์ ฏ ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค.

import WidgetKit

struct DDayEntry: TimelineEntry {
    let date: Date
    let config: DDayConfig?
    let dayDiff: Int
}

struct DDayProvider: TimelineProvider {
    func placeholder(in context: Context) -> DDayEntry {
        DDayEntry(date: Date(), config: nil, dayDiff: 0)
    }

    func getSnapshot(in context: Context, completion: @escaping (DDayEntry) -> Void) {
        completion(makeEntry())
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<DDayEntry>) -> Void) {
        let entry = makeEntry()
        let nextUpdate = Date().addingTimeInterval(3600)
        completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
    }

    private func makeEntry() -> DDayEntry {
        let config = SharedStore.loadConfig()

        let today = Calendar.current.startOfDay(for: Date())
        let target = Calendar.current.startOfDay(for: config?.targetDate ?? Date())
        let diff = Calendar.current.dateComponents([.day], from: today, to: target).day ?? 0

        return DDayEntry(
            date: Date(),
            config: config,
            dayDiff: diff
        )
    }
}

 


 

์œ„์ ฏ View์—์„œ ์„ค์ • ๋‚ด์šฉ ํ‘œ์‹œ

์•ž์—์„œ ๊ตฌ์„ฑํ•œ Entry๋ฅผ ๊ทธ๋Œ€๋กœ UI์— ๋ฐ˜์˜ํ•œ๋‹ค.

struct DDayWidgetView: View {
    var entry: DDayEntry

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(entry.config?.title ?? "D-Day ๋ฏธ์„ค์ •")
                .font(.headline)
                .foregroundColor(.white)

            Text("D-\(entry.dayDiff)")
                .font(.system(size: 32, weight: .bold))
                .foregroundColor(.white)

            if let targetDate = entry.config?.targetDate {
                Text(targetDate, style: .date)
                    .font(.caption)
                    .foregroundColor(.white.opacity(0.9))
            }
        }
        .padding()
        .containerBackground(for: .widget) {
            LinearGradient(
                gradient: Gradient(colors: [.blue.opacity(0.8), .purple]),
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
        }
    }
}


๊ฒฐ๊ณผ์ ์œผ๋กœ ์œ„์ ฏ์€ ๋‹ค์Œ ๊ทœ์น™์œผ๋กœ UI๋ฅผ ํ‘œ์‹œํ•œ๋‹ค.

โ–ท ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์ œ๋ชฉ → ์œ„์ ฏ์˜ ํ—ค๋“œ๋ผ์ธ
โ–ท D-Day ๊ณ„์‚ฐ ๊ฐ’ → ์ค‘์•™ ํ…์ŠคํŠธ
โ–ท ๋ชฉํ‘œ ๋‚ ์งœ ํ‘œ์‹œ → ํ•˜๋‹จ ์ž‘์€ ๊ธ€์”จ


๊ฒฐ๊ณผ๋ฌผ



0

 


 

728x90