Erkag
Administrator
Yönetici
Giriş
2025’te SwiftUI, UIKit’yi neredeyse tamamen gölgede bırakmış durumda. Apple’ın Swift Concurrency (async/await, actors) ile getirdiği modern concurrency, CloudKit ile ücretsiz senkronizasyon ve Observation framework’ü ile MVVM çok daha temiz hale geldi.
Bu rehberde sıfırdan bir Kişisel Alışkanlık Takip Uygulaması (Habit Tracker) yapacağız:
- Habit ekleme / silme / tamamlama
- Günlük / haftalık ilerleme takibi
- iCloud senkronizasyonu (CloudKit)
- SwiftUI + MVVM + @Observable
- Async/await ile ağ işlemleri (simüle edilmiş API + yerel Core Data)
- Widget desteği için basit bir ipucu
1. Proje Yapısı (Clean MVVM)
Kod:
HabitTracker/
├── Models/
│ └── Habit.swift
├── ViewModels/
│ └── HabitViewModel.swift
├── Views/
│ ├── ContentView.swift
│ └── HabitDetailView.swift
├── Persistence/
│ └── PersistenceController.swift
├── Services/
│ └── CloudKitService.swift
└── HabitTrackerApp.swift
2. Model (@Observable ile 2025 stili)
Models/Habit.swift
Kod:
import Foundation
import SwiftData
@Model
@Observable
final class Habit {
var id: UUID = UUID()
var title: String
var goal: Int // günlük hedef (ör: 1 = tamamlandı, 5 = 5 kez)
var current: Int = 0
var createdAt: Date = Date()
var completedDates: [Date] = []
init(title: String, goal: Int) {
self.title = title
self.goal = goal
}
var isCompletedToday: Bool {
let calendar = Calendar.current
return completedDates.contains { calendar.isDateInToday($0) }
}
func completeToday() {
let now = Date()
if !isCompletedToday {
completedDates.append(now)
current += 1
}
}
}
3. Core Data + SwiftData Setup (Persistence)
SwiftData 2025’te varsayılan haline geldi, Core Data’dan çok daha basit:
Persistence/PersistenceController.swift
Kod:
import SwiftData
actor PersistenceController {
static let shared = PersistenceController()
let container: ModelContainer
private init() {
do {
container = try ModelContainer(for: Habit.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: false))
} catch {
fatalError("SwiftData container oluşturulamadı: \(error)")
}
}
}
HabitTrackerApp.swift içinde:
Kod:
import SwiftUI
import SwiftData
@main
struct HabitTrackerApp: App {
let persistence = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(persistence.container)
}
}
}
4. ViewModel (@Observable + async/await)
ViewModels/HabitViewModel.swift
Kod:
import SwiftUI
import SwiftData
import CloudKit
@Observable
@MainActor
final class HabitViewModel {
var habits: [Habit] = []
var errorMessage: String?
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
fetchHabits()
Task { await syncWithCloudKit() }
}
func fetchHabits() {
do {
let descriptor = FetchDescriptor<Habit>(sortBy: [SortDescriptor(\.createdAt, order: .reverse)])
habits = try modelContext.fetch(descriptor)
} catch {
errorMessage = "Habit'ler çekilemedi: \(error.localizedDescription)"
}
}
func addHabit(title: String, goal: Int) {
let habit = Habit(title: title, goal: goal)
modelContext.insert(habit)
fetchHabits() // refresh
Task { await saveToCloudKit(habit) }
}
func deleteHabit(_ habit: Habit) {
modelContext.delete(habit)
fetchHabits()
Task { await deleteFromCloudKit(habit) }
}
func completeHabit(_ habit: Habit) {
habit.completeToday()
try? modelContext.save()
fetchHabits()
Task { await saveToCloudKit(habit) }
}
}
5. CloudKit Senkronizasyonu (iCloud)
Services/CloudKitService.swift
Kod:
actor CloudKitService {
static let shared = CloudKitService()
private let container = CKContainer.default()
func saveHabit(_ habit: Habit) async {
let record = CKRecord(recordType: "Habit")
record["title"] = habit.title
record["goal"] = habit.goal
record["current"] = habit.current
record["completedDates"] = habit.completedDates.map { $0 as NSDate }
record["id"] = habit.id.uuidString
do {
_ = try await container.privateCloudDatabase.save(record)
} catch {
print("CloudKit save hatası: \(error)")
}
}
func fetchHabitsFromCloud() async -> [Habit] {
// Gerçek projede subscription + change notification eklenir
// Bu örnekte basit fetch
return []
}
// delete ve update için benzer fonksiyonlar...
}
ViewModel’de saveToCloudKit ve syncWithCloudKit çağrılarını ekle.
6. Ana Arayüz (SwiftUI)
Views/ContentView.swift
Kod:
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@State private var viewModel: HabitViewModel?
@State private var showingAddSheet = false
var body: some View {
NavigationStack {
if let vm = viewModel {
List {
ForEach(vm.habits) { habit in
HStack {
VStack(alignment: .leading) {
Text(habit.title)
.font(.headline)
Text("Hedef: \(habit.goal) - Bugün: \(habit.current)")
.font(.subheadline)
}
Spacer()
Button(action: { vm.completeHabit(habit) }) {
Image(systemName: habit.isCompletedToday ? "checkmark.circle.fill" : "circle")
.foregroundStyle(habit.isCompletedToday ? .green : .gray)
.font(.title2)
}
.buttonStyle(.plain)
}
}
.onDelete { indexSet in
for index in indexSet {
vm.deleteHabit(vm.habits[index])
}
}
}
.navigationTitle("Alışkanlık Takip")
.toolbar {
Button("Ekle") { showingAddSheet = true }
}
.sheet(isPresented: $showingAddSheet) {
AddHabitView(viewModel: vm)
}
} else {
ProgressView()
.task {
viewModel = HabitViewModel(modelContext: modelContext)
}
}
}
}
}
AddHabitView.swift (basit form):
Kod:
struct AddHabitView: View {
@Environment(\.dismiss) private var dismiss
let viewModel: HabitViewModel
@State private var title = ""
@State private var goal = 1
var body: some View {
NavigationStack {
Form {
TextField("Alışkanlık Adı", text: $title)
Stepper("Günlük Hedef: \(goal)", value: $goal, in: 1...10)
}
.navigationTitle("Yeni Alışkanlık")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Kaydet") {
if !title.isEmpty {
viewModel.addHabit(title: title, goal: goal)
dismiss()
}
}
}
}
}
}
}
7. İleri Seviye İpuçları (2025)
- Widget için App Intents + TimelineProvider kullan
- Observation ile @Bindable ve @ObservationIgnored
- Swift 6 concurrency checking’ini etkinleştir (strict mode)
- Test için XCTest + @MainActor preview