์ฑ์์ ์ ๋ ฅํ ์ค์ ๊ฐ์ ์์ ฏ์ ๋ฐ์ํ๊ธฐ
์ง๋ ๊ธ์์๋ ๊ธฐ๋ณธ์ ์ธ 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
'๐' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [Swift] iOS WidgetKit์ ๊ฐ๋ฅ ๋ฒ์์ ํ๊ณ: ์ค์๊ฐ์ฑ, ๋ฐ์ดํฐ ๊ฐฑ์ ์ ๋ต (0) | 2025.12.14 |
|---|---|
| [Swift] D-Day ์์ ฏ ๋ง๋ค๊ธฐ (1) (0) | 2025.11.28 |
| [iOS] WKWebView์์์ ์คํฌ๋กคโBounceโSafeArea ๋์ (0) | 2025.11.17 |
| [Swift] Swift Concurrency ์์ ์ ๋ณต (0) | 2025.11.11 |
| iOS ์น๋ทฐ ์๋จ ๋ ธ์น ๋์ํ๊ธฐ (0) | 2025.11.07 |
