
1์ธ ๊ฐ๋ฐ๋ก SoDoIt(์๋์)์ด๋ผ๋ ํฌ๋ ์ฑ์ App Store์ ์ถ์ํ๋ฉด์ ๋ง์ฃผ์น ํธ๋ฌ๋ธ์ํ 4๊ฐ์ง๋ฅผ ์ ๋ฆฌํด ๋ณด์์ต๋๋ค!
Color(hex:)์ ํจ์ : Swift ์ฐ์ฐ์ ์ฐ์ ์์SoDoIt(์๋์) ์ ์ค๋ ํ ์ผ์ ๊น๋ํ๊ฒ ๊ด๋ฆฌํ๋ ํฌ๋ ์ฑ์ ๋๋ค.
UNUserNotificationCenter ๋ก์ปฌ ํธ์App Store์ v1.0์ผ๋ก ์ถ์ํ๊ณ , ์ ๋ถ 1์ธ ๊ฐ๋ฐ + Apple ๊ณต์ ํ๋ ์์ํฌ๋ง ์ฌ์ฉํ์ต๋๋ค! (์ธ๋ถ ์์กด์ฑ 0)
| ํญ๋ชฉ | ์ ํ | ์ด์ |
|---|---|---|
| UI ํ๋ ์์ํฌ | SwiftUI | ์ ์ธํ ํจ๋ฌ๋ค์์ ์ต์ํด์ง๊ณ ์ถ์์ |
| ๋ฐ์ดํฐ ์ ์ฅ | CoreData (NSPersistentContainer) | ์ธ๋ถ ์์กด์ฑ ์์ด ๋ก์ปฌ ์์ํ |
| ๋ฐ์ํ ๊ฐฑ์ | NSFetchedResultsController + @Observable | SwiftUI์ ์์ฐ์ค๋ฝ๊ฒ ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ํ |
| ์ํคํ ์ฒ | MVVM + Repository | View / ๋น์ฆ๋์ค ๋ก์ง / ์์ํ ์ฑ ์ ๋ถ๋ฆฌ |
| ์์ ฏ | WidgetKit + App Group | ๋ฉ์ธ ์ฑ๊ณผ ๋ฐ์ดํฐ ๊ณต์ |
| ํฐํธ | Pretendard (๋ฐํ์ ๋ฑ๋ก) | ํ๊ตญ์ด ๊ฐ๋ ์ฑ |
View (SwiftUI)
โ @Observable
ViewModel
โ ์์กด์ฑ ์ฃผ์
Repository (CoreData CRUD ์บก์ํ)
โ
CoreDataManager (NSPersistentContainer)
SwiftUI๋ฅผ ์ฐ๋ค ๋ณด๋ฉด "๋ฐ์ดํฐ๊ฐ ๋ฐ๋๋ฉด UI๊ฐ ์๋์ผ๋ก ๋ค์ ๊ทธ๋ ค์ง๋ค"๊ฐ ์์ฐ์ค๋ฝ๊ฒ ๋๊ปด์ง๋๋ฐ, CoreData NSManagedObject๋ฅผ ๊ฐ์ด ์ฌ์ฉํ๋ฉด ํท๊ฐ๋ฆฌ๋ ๋ถ๋ถ์ด ๋ง์์ง๋๋ผ๊ตฌ์. ๊ทธ๋์ ์ด ๋ถ๋ถ์ ๋ํด ์์ฑํด ๋ณด์์ต๋๋ค.
SwiftUI + CoreData๋ฅผ ์ฒ์ ๋ง์ง๋ฉด ๊ฑฐ์ ๋ฌด์กฐ๊ฑด ๊ฒช๋ ํจ์ ์ด๋ผ๊ณ ์๊ฐํ๋ ๋ถ๋ถ์ ๋๋ค. ์ด ๋ถ๋ถ์ ํ ๋ฒ ์ ๋๋ก ์ดํดํด๋๋ฉด ๋น์ทํ ๋ฅ์ ๋ฌธ์ ๋ ๋ค ํ ์ ์์ต๋๋ค.
์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก์์ ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์ค์์ดํ๋ก ์ญ์ ํ๋ฉด ์ฑ์ด ๊ทธ๋๋ก ๊บผ์ก์ต๋๋ค. (ใ ใ )
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x19744d754)
Xcode ์ฝ์์๋ ๋ณ๋ค๋ฅธ ์๋ฌ ๋ฉ์์ง๋ ์์ด ๊ทธ๋ฅ ํธ๋ฉ์ด ๋จ์ด์ง๋๋ค. ์ฒ์์ "๋ด๊ฐ ๋ญ ์๋ชป ์ ๋ ฅํ๋?" ์ถ์๋๋ฐ ๊ฐ์ ์ฝ๋๊ฐ ๋ค๋ฅธ ๊ธฐ๊ธฐ์์ ๋ฉ์ฉกํ ๋์ํ์ด์. ์ฌํ์ 100%์๋๋ฐ ๋๋ฒ๊ฑฐ ๋ฉ์ถ๋ ์์น๊ฐ ๋งค๋ฒ ๋ฌ๋์ต๋๋ค.
struct CategoryListView: View {
@State private var viewModel = CategoryListViewModel()
var body: some View {
List {
ForEach(viewModel.categories) { category in
CategoryRowView(category: category)
.swipeActions {
Button("์ญ์ ", role: .destructive) {
viewModel.delete(category)
}
}
}
}
}
}
struct CategoryRowView: View {
@ObservedObject var category: Category // NSManagedObject
var body: some View {
Label(category.name, systemImage: category.iconName)
}
}
CoreData ๋ชจ๋ธ์ ํ๋ฒํ NSManagedObject ์๋ธํด๋์ค์ ๋๋ค:
@objc(Category)
public class Category: NSManagedObject {}
extension Category {
@NSManaged public var id: UUID // โ non-optional!
@NSManaged public var name: String // โ non-optional!
@NSManaged public var iconName: String // โ non-optional!
@NSManaged public var colorHex: String // โ non-optional!
}
EXC_BREAKPOINT subcode๊ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฃผ์์ฒ๋ผ ๋ณด์ฌ์ ์ฒ์์ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฌธ์ ์ธ ์ค ์์๋๋ฐ, ์ธ๋ถ ์์กด์ฑ์ด ์๋ ํ๋ก์ ํธ๋ผ ๊ทธ๋ด ๋ฆฌ๊ฐ ์์์ด์.
๊ฐ์ค 1: @ObservedObject var category: Category๊ฐ KVO๋ก ๊ฐ์ฒด ๋ณ๊ฒฝ์ ๊ฐ์งํ๋๋ฐ, ๊ฐ์ฒด๊ฐ ์ญ์ ๋๋ ์๊ฐ KVO ๋ฐํ โ SwiftUI ๋ฆฌ๋ ๋ โ ์ญ์ ๋ ๊ฐ์ฒด์ ํ๋กํผํฐ ์ ๊ทผ โ ํฌ๋์?
์ด ๊ฐ์ค์ ํ์ธํ๋ ค๋ฉด KVO์ KVC๊ฐ ๋ฌด์์ธ์ง๋ถํฐ ์ง์ด์ผ ํฉ๋๋ค.
[obj valueForKey:@"name"] ๊ฐ์ Obj-C ์ฝ๋๊ฐ ๊ทธ๊ฒ.CoreData์ @NSManaged ํ๋กํผํฐ๋ ์ปดํ์ผ ํ์์ด ์๋๋ผ ๋ฐํ์์ KVC๋ฅผ ํตํด ๊ฐ์ ์ฝ๊ณ ์๋๋ค. ์ฆ:
let name = category.name // ์ปดํ์ผ๋ฌ๊ฐ ๋ณด๊ธฐ์ ์ผ๋ฐ ํ๋กํผํฐ ์ ๊ทผ
// โ ์ค์ ๋ก๋
[category valueForKey:@"name"] // KVC ํธ์ถ โ CoreData๊ฐ store์์ ๊ฐ ์กฐํ
๊ทธ๋ฆฌ๊ณ SwiftUI์ @ObservedObject๋ ObservableObject์ objectWillChange ํผ๋ธ๋ฆฌ์
๋ฅผ ๊ตฌ๋
ํฉ๋๋ค. NSManagedObject๋ iOS 14+์์ ObservableObject๋ฅผ ์๋ ์ฑํํ๊ณ , KVO ์๋ฆผ์ ๋ฐ์ผ๋ฉด objectWillChange.send()๋ฅผ ํธ์ถํฉ๋๋ค.
๊ทธ๋์ ์นดํ ๊ณ ๋ฆฌ๊ฐ ์ญ์ ๋๋ฉด ์ผ์ด๋๋ ์ผ:
viewModel.delete(category) โ ๋ด๋ถ์์ context.delete(category) + save()category๋ฅผ deleted ์ํ๋ก ๋งํน (isDeleted = true)@ObservedObject๊ฐ ์๋ฆผ ์์ โ SwiftUI์๊ฒ "์ด ๋ทฐ ๋ค์ ๊ทธ๋ ค"viewModel.categories ๋ฐฐ์ด์ ์์ง ๊ฐฑ์ ์ (NSFetchedResultsController.controllerDidChangeContent๊ฐ ์์ง ํธ์ถ ์ ๋จ)body๋ฅผ ํ ๋ฒ ๋ ํ๊ฐcategory.name ์ ๊ทผ โ KVC ํธ์ถ โ deleted ๊ฐ์ฒด๋ผ nil ๋ฐํname์ String(non-optional)์ผ๋ก ์ ์ธํ์ผ๋ฏ๋ก nil์ ๋ฐ์ ์ ์์์ฌ๊ธฐ์ ํต์ฌ์ @NSManaged var name: String์ด non-optional์ด๋ผ๋ ๊ฒ์
๋๋ค. ๋ง์ฝ String?์ด์๋ค๋ฉด nil์ ๋ฐ์๋ ํฌ๋์ ์ ๋ฌ์ ๊ฑฐ์์. CoreData ์ฝ๋์ ์ด ๋ชจ๋ธ์ "Optional" ์ฒดํฌ๋ฐ์ค๋ฅผ ๊ธฐ์ค์ผ๋ก ์ต์
๋ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ๋๋ฐ, ๊ธฐ๋ณธ๊ฐ์ด non-optional๋ก ๋๊ธฐ ์ฝ์ต๋๋ค.
๊ฐ์ค 2 โ ForEach์ identity
์ ๊ฐ์ค๋ก ๋ทฐ ๋ณธ๋ฌธ์ ๊ฐ๋๋ฅผ ์ถ๊ฐํ์ต๋๋ค:
struct CategoryRowView: View {
@ObservedObject var category: Category
var body: some View {
if category.isDeleted || category.managedObjectContext == nil {
EmptyView()
} else {
Label(category.name, systemImage: category.iconName)
}
}
}
isDeleted๋ NSManagedObject์ ์์ฒด ํ๋กํผํฐ๋ผ deleted ์ํ์์๋ ์์ ํฉ๋๋ค. ์ด ๊ฐ๋๋ฉด ๋์ผ ์ค ์์๋๋ฐโฆ
์ฌ์ ํ ํฌ๋์.
๊น์ด ๋ณด๋ ๋ ๋ค๋ฅธ ๊ฒฝ๋ก๊ฐ ์์์ต๋๋ค. ForEach๊ฐ ๋ฐฐ์ด diffing์ ํ ๋ ๊ฐ ์์์ Identifiable.id๋ฅผ ํ๊ฐํฉ๋๋ค. Category๋ ์ด๋ ๊ฒ Identifiable์ conform๋์ด ์์์ด์:
extension Category: Identifiable {
// id๋ @NSManaged var id: UUID ๋ฅผ ์๋ ์ฌ์ฉ
}
๋ฐฐ์ด์ด ๊ฐฑ์ ๋๋ ์๊ฐ(viewModel.categories = newCategories) ForEach๋ ์ ๋ฐฐ์ด๊ณผ ์ ๋ฐฐ์ด์ id๋ฅผ ๋น๊ตํฉ๋๋ค. ๊ทธ๋ฐ๋ฐ ์ ๋ฐฐ์ด์๋ deleted Category๊ฐ ๋ค์ด์๊ณ , ๊ทธ ๊ฐ์ฒด์ id(@NSManaged UUID)๋ฅผ KVC๋ก ์ฝ์ผ๋ฉด nil โ ํธ๋ฉ.
์ด ํฌ๋์๋ row์ body๊ฐ ํธ์ถ๋๊ธฐ ์ , ForEach ๋ด๋ถ diffing ๋จ๊ณ์์ ์ผ์ด๋ฉ๋๋ค. ๊ทธ๋์ body ๊ฐ๋๊ฐ ๋ฟ์ง ๋ชปํฉ๋๋ค.
objectID๋ก ์ฐํ: NSManagedObjectID๋ ๊ฐ์ฒด์ ๋ผ์ดํ์ฌ์ดํด๊ณผ ๋ฌด๊ดํ๊ฒ ํญ์ ์ ํจํฉ๋๋ค (๋ฉ๋ชจ๋ฆฌ์์ ์ฌ๋ผ์ง ๋๊น์ง).body ์ง์
๊ฐ๋ ์ ์ง: KVO race๋ก ์ธํ row ๋ฆฌ๋ ๋ ์ ์์ ๋ง ์ญํ .struct CategoryListView: View {
@State private var viewModel = CategoryListViewModel()
var body: some View {
List {
// โ
id: \.objectID ๋ก ๋ณ๊ฒฝ
ForEach(viewModel.categories, id: \.objectID) { category in
CategoryRowView(category: category)
.swipeActions {
Button("์ญ์ ", role: .destructive) {
viewModel.delete(category)
}
}
}
}
}
}
struct CategoryRowView: View {
@ObservedObject var category: Category
var body: some View {
// โ
body ์ง์
์์ deleted ์ํ ์ฐจ๋จ
if category.isDeleted || category.managedObjectContext == nil {
EmptyView()
} else {
Label(category.name, systemImage: category.iconName)
}
}
}
์ด ๋ ์ฅ์น๋ฅผ ์ ์ฉํ ๋ค๋ก๋ ํฌ๋์๊ฐ ๋จ ํ ๋ฒ๋ ๋์ง ์์์ต๋๋ค! ๐
@NSManaged ํ๋กํผํฐ๋ KVC๋ฅผ ํตํด ๋์ ์ผ๋ก ์ฝ๊ณ ์ฐ๋ฏ๋ก, deleted ์ํ์์ ์ ๊ทผํ๋ฉด ์ํํฉ๋๋ค.@ObservedObject๋ KVO๋ฅผ ๊ตฌ๋
ํ๊ธฐ ๋๋ฌธ์ deleted ๊ฐ์ฒด๊ฐ ์ ๊น ๋ทฐ์ ๋ค์ ๋ค์ด์ฌ ์ ์์ต๋๋ค.objectID๋ฅผ ์ฐ์ธ์. Identifiable.id(@NSManaged UUID)๋ ๊ฐ์ฒด ๋ผ์ดํ์ฌ์ดํด์ ๋ฌถ์ด์ง๋ง, objectID๋ ํญ์ ์์ ํฉ๋๋ค.๐ก ๋ ๊ทผ๋ณธ์ ์ธ ํด๋ฒ: ๋ชจ๋ธ์
name/colorHex๋ฑ์ Optional๋ก ์ ์ธํ๊ณwrappedName๊ฐ์ ์์ ์ธ๋ํ ๊ณ์ฐ ํ๋กํผํฐ๋ก ๋ ธ์ถํ๋ ํจํด. CLAUDE.md ๊ฐ์ด๋์ฒ๋ผ ์ปจ๋ฒค์ ์ ๋ฏธ๋ฆฌ ์ ํด๋๋ฉด ๊ฐ์ ํจ์ ์ ๋ฐ๋ณตํ์ง ์์ ์ ์์ต๋๋ค.
ํ ํ๋ฉด ์์ ฏ์ ์ถ๊ฐํ๋ ค๊ณ ๋ณด๋ ์์ ฏ์์ ๋ฉ์ธ ์ฑ์ CoreData๋ฅผ ์ง์ ๋ชป ์ฝ์์ต๋๋ค.
// SoDoItWidget ํ๊น์ Provider ์์์
let context = CoreDataManager.shared.viewContext
let request = TodoItem.fetchRequest()
let todos = try context.fetch(request)
// โ ์ปดํ์ผ์ ๋์ง๋ง ์คํํ๋ฉด ๋น ๊ฒฐ๊ณผ
// ํน์ ๋ค๋ฅธ sqlite ํ์ผ์ ๋ณด๊ณ ์์
์์ ฏ์์ fetchedObjects๊ฐ ํญ์ 0๊ฐ๋ก ๋์์ต๋๋ค. ๋ฉ์ธ ์ฑ์๋ ๋ถ๋ช ํ ์ผ์ด ์๋๋ฐ๋์...
์์ ฏ Extension์ ๋ฉ์ธ ์ฑ๊ณผ ๋ถ๋ฆฌ๋ ๋ณ๊ฐ์ ํ๋ก์ธ์ค์ ๋๋ค. ๊ฐ์ ์ฝ๋๋ฅผ ๋น๋ํด์ ๊ฐ์ ํด๋์ค๋ฅผ ํธ์ถํด๋, ์ค์ ๋์ํ๋ ์ปจํ ์คํธ๋ ๋ค๋ฆ ๋๋ค:
App.app/Library/Application Support/SoDoIt.sqlite ๊ฐ์ ๊ฒฝ๋ก์ ๋ฐ์ดํฐ ์ ์ฅCoreDataManager.shared๋ ๊ฐ ํ๋ก์ธ์ค์์ ๋ฐ๋ก ์ธ์คํด์ค๊ฐ ์์ฑ๋๋ฉฐ, ์๋ก ๋ค๋ฅธ sqlite ํ์ผ์ ๋ด
๋๋ค.
Apple์ด ์ ๊ณตํ๋ ์ ์ ํด๋ฒ์ App Group์ ๋๋ค. ๋ ํ๊น์ ๊ฐ์ App Group์ ๋ฌถ์ผ๋ฉด ๊ทธ ๊ทธ๋ฃน์ฉ ๊ณต์ ์ปจํ ์ด๋์ ์ ๊ทผํ ์ ์์ต๋๋ค.
1๋จ๊ณ: Xcode์์ App Group ํ์ฑํ
๋ฉ์ธ ์ฑ + ์์ ฏ Extension ๋ ํ๊น ๋ชจ๋์ group.sso.SoDoIt ๊ฐ์ ๊ทธ๋ฃน ID ์ถ๊ฐ. Entitlements ํ์ผ์ ์๋์ผ๋ก ๋ค์ด๊ฐ๋๋ค.
2๋จ๊ณ: ์์ ฏ์ฉ ๋ฐ์ดํฐ ๋ชจ๋ธ ์ ์ (Codable)
CoreData NSManagedObject๋ ์ง๋ ฌํยท์ญ์ง๋ ฌํ์ ์ ํฉํ์ง ์์ผ๋, ์์ ฏ ์ ์ฉ ๊ฐ๋ฒผ์ด struct๋ฅผ ๋ง๋ค์์ต๋๋ค.
struct WidgetTodoItem: Identifiable, Codable {
let id: UUID
let title: String
let priority: Int16
let dueDate: Date?
let categoryName: String?
let categoryColorHex: String?
}
3๋จ๊ณ: ๋ฉ์ธ ์ฑ์์ ์ฐ๊ธฐ
enum AppGroup {
static let identifier = "group.sso.SoDoIt"
static let sharedDefaults = UserDefaults(suiteName: identifier)
static let todosKey = "widget.todos"
}
final class WidgetDataManager {
static let shared = WidgetDataManager()
func updateWidgetData() {
let context = CoreDataManager.shared.viewContext
let request = TodoItem.fetchRequest()
// ์์ ฏ์ ๋ณด์ฌ์ค ์ ๋น๋๋ง (์ค๋ / ๋ค๊ฐ์ค๋ ๋ง๊ฐ์ผ ์์ฃผ)
request.fetchLimit = 5
do {
let todos = try context.fetch(request)
let snapshot = todos.map { todo in
WidgetTodoItem(
id: todo.id,
title: todo.title,
priority: todo.priority,
dueDate: todo.dueDate,
categoryName: todo.category?.name,
categoryColorHex: todo.category?.colorHex
)
}
let data = try JSONEncoder().encode(snapshot)
AppGroup.sharedDefaults?.set(data, forKey: AppGroup.todosKey)
// ์์ ฏ์๊ฒ "์ง๊ธ ์ ๋ฐ์ดํฐ ์์ด, ์ฆ์ ๊ฐฑ์ ํด" ์ ํธ
WidgetCenter.shared.reloadTimelines(ofKind: "TodoListWidget")
} catch {
print("์์ ฏ ๋ฐ์ดํฐ ๊ฐฑ์ ์คํจ: \(error)")
}
}
}
CRUD ์งํ๋ง๋ค WidgetDataManager.shared.updateWidgetData()๋ฅผ ํธ์ถํ์ต๋๋ค.
4๋จ๊ณ: ์์ ฏ์์ ์ฝ๊ธฐ
struct TodoWidgetProvider: TimelineProvider {
func getSnapshot(in context: Context,
completion: @escaping (TodoWidgetEntry) -> Void) {
let todos = loadTodos()
completion(TodoWidgetEntry(date: Date(), todos: todos))
}
func getTimeline(in context: Context,
completion: @escaping (Timeline<TodoWidgetEntry>) -> Void) {
let todos = loadTodos()
let entry = TodoWidgetEntry(date: Date(), todos: todos)
// 30๋ถ๋ง๋ค ์์ฒด ๊ฐฑ์ (์ฑ์ด ๊บผ์ ธ ์์ด๋)
let next = Date().addingTimeInterval(60 * 30)
completion(Timeline(entries: [entry], policy: .after(next)))
}
private func loadTodos() -> [WidgetTodoItem] {
guard let data = AppGroup.sharedDefaults?.data(forKey: AppGroup.todosKey)
else { return [] }
return (try? JSONDecoder().decode([WidgetTodoItem].self, from: data)) ?? []
}
}
WidgetCenter.reloadTimelines๊ฐ ํธ๋ฆฌ๊ฑฐ๋๋ฉด ์์ ฏ์ด ๊ฑฐ์ ์ค์๊ฐ์ผ๋ก ๊ฐฑ์ ๋ฉ๋๋ค..after(next)๋ก ๋๋ฉด ์์คํ
์ด ์์์ ๊นจ์์ค๋๋ค.WidgetTodoItem ๋ณํ ๋จ๊ณ์์ ํก์ ๊ฐ๋ฅ.๐ก ๋ ๋ฌด๊ฑฐ์ด ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํ๋ค๋ฉด
NSPersistentContainer์ store URL์ App Group ์ปจํ ์ด๋ ๊ฒฝ๋ก๋ก ์ค์ ํด CoreData ์์ฒด๋ฅผ ๊ณต์ ํ๋ ๋ฐฉ์๋ ์์ต๋๋ค. ๋ค๋ง ๋์ ์ฐ๊ธฐ ๋๊ธฐํ๊ฐ ๊น๋ค๋ก์์, ์์ ฏ์ฉ ๊ฐ๋ฒผ์ด snapshot ํจํด์ด ๋ ์์ ํ ์ ํ์ด๋ผ๊ณ ๋ด ๋๋ค.
Color(hex:)์ ํจ์ : Swift ์ฐ์ฐ์ ์ฐ์ ์์์นดํ ๊ณ ๋ฆฌ์ ์์์ ์ ํ๋ ค๊ณ hex string โ SwiftUI Color ๋ณํ ํจ์๋ฅผ ๋ง๋ค์๋๋ฐ, ์นดํ ๊ณ ๋ฆฌ๋ค์ด ์ฃ๋ค ๋น์ทํ ํค์ผ๋ก ๋ณด์์ต๋๋ค. ๋นจ๊ฐ์ ๊ฒ์๋น ๋๋ ํ๋ น์, ํ๋์ ํํฌ๋น ๋ง์ ํ๋ก ๋ ๋๋ง๋๋ ์์ด์์ด์.
extension Color {
init?(hex: String) {
var hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines)
if hexString.hasPrefix("#") { hexString.removeFirst() }
guard hexString.count == 6,
let hexNumber = UInt64(hexString, radix: 16) else {
return nil
}
let r = Double(hexNumber & 0xFF0000 >> 16) / 255.0 // โ ๏ธ
let g = Double((hexNumber & 0x00FF00) >> 8) / 255.0
let b = Double(hexNumber & 0x0000FF) / 255.0
self.init(red: r, green: g, blue: b)
}
}
์ธ๋ป ๋ณด๋ฉด ๋ฉ์ฉกํฉ๋๋ค. ๋นจ๊ฐ ์ฑ๋์ 0xFF0000์ผ๋ก ๋ง์คํนํ๊ณ 16๋นํธ ์ค๋ฅธ์ชฝ ์ํํธ, ๋
น์์ 8๋นํธ ์ํํธ, ํ๋์ ๊ทธ๋๋ก. ๊ต๊ณผ์์ ์ด์ฃ .
๋ฌธ์ ๋ ๋นจ๊ฐ ๋ผ์ธ์ ๊ดํธ์ ๋๋ค. Swift์์:
>> (BitwiseShiftPrecedence): ์ฐ์ ์์ ๋์& (MultiplicationPrecedence): ์ฐ์ ์์ ๋ฎ์๊ทธ๋์ hexNumber & 0xFF0000 >> 16์ ๋ค์๊ณผ ๊ฐ์ด ํ๊ฐ๋ฉ๋๋ค:
hexNumber & (0xFF0000 >> 16)
= hexNumber & 0xFF // โ ํ๋ ๋ฐ์ดํธ๋ฅผ ์ถ์ถ!
์ฆ ๋นจ๊ฐ ์ฑ๋์ด ํ๋ ๋ฐ์ดํธ์ ๊ฐ์ ๊ฐ๊ฒ ๋ฉ๋๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก ๋ชจ๋ ์์์ด R == B์ธ ๋ณ์ข
์ผ๋ก ๋ ๋๋ง๋์ด ์์ํ์ ๋ค์์ฑ์ด ์ฌ๋ผ์ง๋๋ค.
์๋ฅผ ๋ค์ด ์์คํ
๋นจ๊ฐ #FF3B30์ ํต๊ณผ์ํค๋ฉด:
โ ์ฐ์ค์ถฉํ ๋คํฌ ๊ทธ๋ ์ด๊ฐ ๋ฉ๋๋ค. ๋นจ๊ฐ์ด ์๋์ฃ .
๋
น์ยทํ๋ ๋ผ์ธ์ด ๋ฉ์ฉกํ ์ด์ ๋ ๋ ๋ค ๋ช
์์ ๊ดํธ (hexNumber & 0x00FF00) >> 8 ํํ๋ก ๋์ด ์์ด์์
๋๋ค. ๋นจ๊ฐ ๋ผ์ธ๋ง ๊ดํธ๊ฐ ๋น ์ง ๊ฒ ํจ์ ์ด์์ด์.
๊ดํธ ํ๋ ์ถ๊ฐํ๋ฉด ๋๋ฉ๋๋ค. (ํ๋ฌดํ์ง๋ง ์์ํ์ด์....!!ใ ใ )
extension Color {
init?(hex: String) {
var hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines)
if hexString.hasPrefix("#") { hexString.removeFirst() }
guard hexString.count == 6,
let hexNumber = UInt64(hexString, radix: 16) else {
return nil
}
let r = Double((hexNumber & 0xFF0000) >> 16) / 255.0 // โ
let g = Double((hexNumber & 0x00FF00) >> 8) / 255.0
let b = Double(hexNumber & 0x0000FF) / 255.0
self.init(red: r, green: g, blue: b)
}
}
func test_Color_hexParsing() {
let red = Color(hex: "#FF3B30")
XCTAssertNotNil(red)
// UIColor๋ก ๋ณํํด์ r, g, b ์ฑ๋์ ์ง์ ๊ฒ์ฆ
// ...
}
์ด๋ฒ PR์์ ๊ฐ๋ ๋ ์ฅ์น๋ก ๋ง์์ง๋ง, ๊ทผ๋ณธ์ ์ผ๋ก name: String? + wrappedName: String ํจํด์ด ๋ ์์ ํฉ๋๋ค. CLAUDE.md ๊ฐ์ด๋์๋ ๋ช
์๋์ด ์๋๋ฐ ์ ์ ๋ชจ๋ธ์ ์ ์ฉ์ ์ ํ์ด์. v2 ๋ง์ด๊ทธ๋ ์ด์
์์ ์ ๊ฐ์ด ์ฒ๋ฆฌํ ์์ .
App Store์ ์ถ์ํ ํ ๋ชจ๋ธ์ ๋ฐ๊พธ๋ฉด ์ฌ์ฉ์ ๋ฐ์ดํฐ๊ฐ ๋ ์๊ฐ ์ ์์ต๋๋ค. ๋ผ์ดํธ์จ์ดํธ ๋ง์ด๊ทธ๋ ์ด์ ์ด ์๋์ผ๋ก ๋์ํ๊ธด ํ์ง๋ง, ์ถ์ ์ ์ ์๋์ ์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์ ์ ํ๋ฒ ๋๋ ค๋ณด๋ ๊ฒ์ฆ ๋จ๊ณ๋ฅผ ๋๋ ๊ฒ ์ข๊ฒ ๋ค๋ ์๊ฐ.
์ง๊ธ์ JSON ์ง๋ ฌํ ๋ฐฉ์์ธ๋ฐ, ๋ฐ์ดํฐ๊ฐ ์ปค์ง๋ฉด ๋นํจ์จ์ ์ ๋๋ค. NSPersistentContainer์ store URL์ App Group ์ปจํ ์ด๋๋ก ์ค์ ํด CoreData ์์ฒด๋ฅผ ๊ณต์ ํ๋ ๋ฐฉ์๋ ๊ฒํ ํด๋ณผ ๋งํฉ๋๋ค. ๋ค๋ง ๋์ ์ฐ๊ธฐ ๋๊ธฐํ๊ฐ ๊น๋ค๋ก์์ ์ ์คํ.
UI ํ๊ท๋ฅผ ์ก์ผ๋ ค๋ฉด ์ค๋ ์ท ํ ์คํธ๊ฐ ํ์์ธ๋ฐ ์ด๋ฒ ์ถ์์ ๋ชป ๋ฃ์์ต๋๋ค. point-free์ swift-snapshot-testing์ ๋ค์ ๋ฒ์ ์ ์ถ๊ฐํ ๊ณํ.
๊ฐ์ฅ ํฌ๊ฒ ๋ฐฐ์ด ๊ฑด "์ ํธ๋ฅผ ์์ฌํ๋ ๋ฒ"์ด์์ต๋๋ค.
์ฒ์์๋ EXC_BREAKPOINT๋ฅผ ๋ณด๋ฉด "์ด๋๊ฐ ์ฝ๋๊ฐ ์๋ชป๋๊ฒ ์ง" ์ ๋๋ก ๋ง์ฐํ๊ฒ ๋ดค์ด์. ๊ทธ๋ฐ๋ฐ ๊ทธ ์ ํธ ๋ค์ KVOยทKVCยทNSManagedObject ๋ผ์ดํ์ฌ์ดํดยทSwiftUI ํธ๋์ญ์ ๊ฐ์ ์ฌ๋ฌ ๋ ์ด์ด์ ์ํธ์์ฉ์ด ์์์ต๋๋ค. ์ ์ ์ ๊ทธ ๋ ์ด์ด ๋ชจ๋๋ฅผ ๊น์ด ์ ์ ์์ง๋ง, "์ด ํจ์ ์ X์ Y์ ๊ฒฝ๊ณ์์ ์ผ์ด๋ฌ๊ฒ ๊ตฌ๋" ๋ผ๋ ๊ฐ๊ฐ๋ง ๊ธธ๋ฌ๋ ๊ฐ์ ๋ฅ์ ๋ฌธ์ ๋ ๋น ๋ฅด๊ฒ ํด๊ฒฐํ ์ ์๋๋ผ๊ณ ์.
ํฌ๋ ์ฑ์ ํํ์ง๋ง, ํผ์์ ์ฒ์๋ถํฐ ๋๊น์ง ๋ง๋ค๊ณ App Store๊น์ง ์ฌ๋ฆฐ ๊ฒฝํ์ ํํ์ง ์์ต๋๋ค. ์ ์ ๊ฐ๋ฐ์๋ผ๋ฉด ํ ๋ฒ์ฏค ์๊ธฐ ์ด๋ฆ์ด ๋ฐํ ์ฑ์ ์ถ์ํด๋ณด๊ธธ ๊ฐ๋ ฅ ์ถ์ฒํฉ๋๋ค. ํํ ๋ฆฌ์ผ๋ง์ผ๋ก๋ ์ ๋ ๋ฐฐ์ธ ์ ์๋ ๊ฒ ๋๋ฌด ๋ง์์.