SwiftUI'da bir butona tıklıyorsun, sayaç artıyor. Basit görünüyor değil mi? Ama perde arkasında @State, SwiftUI'ın diffing engine'i, view identity ve memory management gibi karmaşık mekanizmalar çalışıyor. Property wrapper'ları gerçekten anlamadan performanslı SwiftUI kodu yazamazsın.
Bu rehberde her bir property wrapper'ı derinlemesine inceleyeceğiz. Sadece "ne yapar" değil, "nasıl çalışır" ve "ne zaman kullanılır" sorularını cevaplayacağız.
💡 Hızlı Not: Bu rehber Apple'ın WWDC23-24 session'ları ve resmi SwiftUI documentation'ından derlendi. iOS 17+ @Observable macro dahil tüm güncel bilgiler mevcut.
İçindekiler
- Property Wrapper Nedir? Perde Arkası
- @State: View-Local State
- @Binding: İki Yönlü Bağlantı
- @StateObject vs @ObservedObject
- @EnvironmentObject: Dependency Injection
- @Environment: Sistem Değerleri
- @AppStorage ve @SceneStorage
- iOS 17+: @Observable Macro Devrimi
- Karar Ağacı: Hangisini Kullanmalıyım?
- Production Best Practices
Property Wrapper Nedir? Perde Arkası {#property-wrapper-nedir}
Property wrapper, bir property'nin get/set davranışını özelleştiren Swift mekanizması. SwiftUI bunu state yönetimi için kullanıyor. Perde arkasında olan şu:
swift
1// @State kullandığında aslında olan:2@State private var count = 03 4// Derleyici bunu şuna çevirir:5private var _count = State<Int>(initialValue: 0)6var count: Int {7 get { _count.wrappedValue }8 nonmutating set { _count.wrappedValue = newValue }9}10 11// $ prefix ile projectedValue'ya erişirsin:12// $count → Binding<Int>Bu mekanizma sayesinde SwiftUI, hangi state'in değiştiğini takip edebilir ve sadece etkilenen view'ları yeniden çizer.
@State: View-Local State {#state}
@State, view'a ait ve view tarafından yönetilen state için kullanılır. SwiftUI, @State değerlerini view struct'ının dışında, özel bir storage'da tutar.
swift
1struct CounterView: View {2 // ✅ View-local state - sadece bu view yönetir3 @State private var count = 04 @State private var isAnimating = false5 6 var body: some View {7 VStack(spacing: 20) {8 Text("\(count)")9 .font(.largeTitle)10 .scaleEffect(isAnimating ? 1.2 : 1.0)11 12 Button("Artır") {13 count += 114 withAnimation(.spring()) {15 isAnimating = true16 }17 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {18 withAnimation { isAnimating = false }19 }20 }21 }22 }23}@State Kuralları
Kural | Açıklama |
|---|---|
**private olmalı** | Dışarıdan erişilmemeli |
**Value type kullan** | Struct, enum, temel tipler |
**init'te set etme** | Varsayılan değer ver veya parent'tan al |
**View-local olmalı** | Paylaşılan state için kullanma |
⚠️ Dikkat: @State'i class (reference type) ile kullanma! Değişiklikleri doğru takip edemez. Class için @StateObject veya @Observable kullan.
@Binding: İki Yönlü Bağlantı {#binding}
@Binding, parent view'ın state'ine referans verir. Child view, parent'ın state'ini okuyabilir ve yazabilir.
swift
1// Parent - state'in sahibi2struct ParentView: View {3 @State private var isOn = false4 5 var body: some View {6 // $isOn → Binding<Bool> oluşturur7 ToggleRow(title: "Bildirimler", isOn: $isOn)8 }9}10 11// Child - state'e referans12struct ToggleRow: View {13 let title: String14 @Binding var isOn: Bool // Parent'ın state'ine referans15 16 var body: some View {17 Toggle(title, isOn: $isOn)18 }19}20 21// ✅ Custom Binding oluşturma22struct SettingsView: View {23 @State private var volume: Double = 0.524 25 var body: some View {26 Slider(value: Binding(27 get: { volume },28 set: { newValue in29 volume = min(max(newValue, 0), 1) // Clamp 0-130 HapticManager.impact(.light) // Side effect31 }32 ))33 }34}@StateObject vs @ObservedObject {#stateobject-vs-observedobject}
Bu ikisi, yeni geliştiricilerin en çok karıştırdığı wrapper'lar. Fark basit ama kritik:
Özellik | @StateObject | @ObservedObject |
|---|---|---|
**Ownership** | View oluşturur ve sahiplenir | Dışarıdan geçirilir |
**Lifecycle** | View yeniden oluşturulsa bile korunur | Her rebuild'de yeniden oluşturulabilir |
**Ne zaman** | İlk kez oluşturan view'da | Alt view'lara geçirirken |
swift
1class UserViewModel: ObservableObject {2 @Published var name = ""3 @Published var email = ""4 @Published var isLoading = false5 6 func loadProfile() async {7 isLoading = true8 defer { isLoading = false }9 // API çağrısı...10 }11}12 13// ✅ DOĞRU: İlk oluşturan view'da @StateObject14struct ProfileScreen: View {15 @StateObject private var viewModel = UserViewModel()16 17 var body: some View {18 VStack {19 ProfileHeader(viewModel: viewModel) // Aşağı geçir20 ProfileForm(viewModel: viewModel)21 }22 .task { await viewModel.loadProfile() }23 }24}25 26// ✅ DOĞRU: Alt view'da @ObservedObject27struct ProfileHeader: View {28 @ObservedObject var viewModel: UserViewModel29 30 var body: some View {31 Text(viewModel.name).font(.title)32 }33}34 35// ❌ YANLIŞ: Alt view'da @StateObject - her rebuild'de yeni instance!36struct BrokenProfileHeader: View {37 @StateObject var viewModel = UserViewModel() // HER REBUILD'DE YENİ!38 var body: some View { Text(viewModel.name) }39}🎯 Altın Kural: "Kim oluşturuyorsa @StateObject, kim alıyorsa @ObservedObject."
@EnvironmentObject: Dependency Injection {#environmentobject}
Birden fazla view arasında paylaşılan state için kullanılır. Tüm view hierarchy boyunca erişilebilir.
swift
1class AppState: ObservableObject {2 @Published var currentUser: User?3 @Published var isAuthenticated = false4 @Published var theme: AppTheme = .system5}6 7// Root'ta inject et8@main9struct MyApp: App {10 @StateObject private var appState = AppState()11 12 var body: some Scene {13 WindowGroup {14 ContentView()15 .environmentObject(appState) // Tüm alt view'lar erişebilir16 }17 }18}19 20// Herhangi bir alt view'da kullan21struct SettingsView: View {22 @EnvironmentObject var appState: AppState23 24 var body: some View {25 Form {26 Text("Merhaba, \(appState.currentUser?.name ?? "Misafir")")27 Toggle("Dark Mode", isOn: Binding(28 get: { appState.theme == .dark },29 set: { appState.theme = $0 ? .dark : .light }30 ))31 }32 }33}⚠️ Dikkat: @EnvironmentObject inject edilmeden kullanılırsa crash olur! Preview'larda ve test'lerde unutma.
@Environment: Sistem Değerleri {#environment}
SwiftUI'ın sunduğu sistem değerlerine erişmek için kullanılır. Color scheme, locale, dismiss action gibi.
swift
1struct AdaptiveView: View {2 @Environment(\.colorScheme) var colorScheme3 @Environment(\.dynamicTypeSize) var typeSize4 @Environment(\.dismiss) var dismiss5 @Environment(\.horizontalSizeClass) var sizeClass6 7 var body: some View {8 VStack {9 Text("Tema: \(colorScheme == .dark ? "Karanlık" : "Aydınlık")")10 11 if sizeClass == .regular {12 // iPad veya büyük ekran13 HStack { content }14 } else {15 // iPhone16 VStack { content }17 }18 19 Button("Kapat") { dismiss() }20 }21 }22 23 @ViewBuilder var content: some View {24 Text("İçerik 1")25 Text("İçerik 2")26 }27}28 29// Custom EnvironmentKey oluşturma30struct AnalyticsKey: EnvironmentKey {31 static let defaultValue: AnalyticsService = DefaultAnalyticsService()32}33 34extension EnvironmentValues {35 var analytics: AnalyticsService {36 get { self[AnalyticsKey.self] }37 set { self[AnalyticsKey.self] = newValue }38 }39}40 41// Kullanım42struct TrackableView: View {43 @Environment(\.analytics) var analytics44 45 var body: some View {46 Button("Satın Al") {47 analytics.track("purchase_tapped")48 }49 }50}@AppStorage ve @SceneStorage {#appstorage}
UserDefaults ve scene-level persistence için:
swift
1struct SettingsView: View {2 // UserDefaults'a otomatik persist edilir3 @AppStorage("username") var username = "Misafir"4 @AppStorage("notifications_enabled") var notificationsEnabled = true5 @AppStorage("app_theme") var appTheme = "system"6 7 // Scene-level storage (tab, window bazlı)8 @SceneStorage("selected_tab") var selectedTab = 09 @SceneStorage("scroll_position") var scrollPosition: String?10 11 var body: some View {12 Form {13 TextField("Kullanıcı adı", text: $username)14 Toggle("Bildirimler", isOn: $notificationsEnabled)15 Picker("Tema", selection: $appTheme) {16 Text("Sistem").tag("system")17 Text("Aydınlık").tag("light")18 Text("Karanlık").tag("dark")19 }20 }21 }22}iOS 17+: @Observable Macro Devrimi {#observable}
iOS 17 ile gelen @Observable macro, property wrapper karmaşıklığının büyük bölümünü ortadan kaldırıyor.
swift
1// ✅ iOS 17+ Modern yaklaşım2@Observable3class UserStore {4 var name = ""5 var email = ""6 var isLoading = false7 var posts: [Post] = []8 9 func loadPosts() async {10 isLoading = true11 defer { isLoading = false }12 posts = try? await PostService.fetchAll() ?? []13 }14}15 16// View'da kullanım - @StateObject/@ObservedObject YOK!17struct ProfileView: View {18 @State private var store = UserStore() // Sadece @State yeterli!19 20 var body: some View {21 // SwiftUI otomatik olarak sadece kullanılan property'leri takip eder22 // name değişince BU view rebuild olur23 // email değişince bu view ETKİLENMEZ (kullanılmıyor çünkü)24 VStack {25 Text(store.name).font(.title)26 27 if store.isLoading {28 ProgressView()29 } else {30 PostList(posts: store.posts)31 }32 }33 .task { await store.loadPosts() }34 }35}36 37// Alt view'a geçirme - @ObservedObject YOK!38struct PostList: View {39 let posts: [Post] // Sadece let!40 41 var body: some View {42 ForEach(posts) { post in43 Text(post.title)44 }45 }46}Eski Yol | Yeni Yol (@Observable) |
|---|---|
`@StateObject var vm = VM()` | `@State var store = Store()` |
`@ObservedObject var vm: VM` | `let store: Store` |
`@EnvironmentObject var app: App` | `@Environment(AppStore.self) var app` |
`@Published var name = ""` | `var name = ""` (otomatik!) |
Karar Ağacı: Hangisini Kullanmalıyım? {#karar-agaci}
- Basit değer (Int, String, Bool)? →
@State - Child view'a geçirecek misin? →
@Binding - Class-based model, sen oluşturuyorsun? →
@StateObject(veya@State+@Observable) - Class-based model, dışarıdan geliyor? →
@ObservedObject(veyalet+@Observable) - Tüm app boyunca paylaşılacak? →
@EnvironmentObject(veya@Environment) - UserDefaults'a persist? →
@AppStorage - Scene bazlı geçici state? →
@SceneStorage
Production Best Practices {#best-practices}
🔑 Bu Yazıdan Çıkarımlar
- @State sadece view-local basit tipler için
- @StateObject oluşturan view'da, @ObservedObject alan view'da
- @Observable kullanabiliyorsan geç - iOS 17+ hedefliyorsan
- @EnvironmentObject inject kontrolü yap - crash önle
- Custom Environment key'ler dependency injection için harika
- @AppStorage hassas veri için kullanma - UserDefaults şifrelenmemiş
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.

