Tüm Yazılar
KategoriSwiftUI
Okuma Süresi
25 dk
Yayın Tarihi
...
Kelime Sayısı
1.744kelime

Kahveni hazırla - bu içerikli bir makale!

SwiftUI'ın tüm property wrapper'larını derinlemesine öğrenin. @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, @Environment ve iOS 17+ @Observable macro'su.

SwiftUI Property Wrappers Deep Dive: @State'den @Observable'a

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

  1. Property Wrapper Nedir? Perde Arkası
  2. @State: View-Local State
  3. @Binding: İki Yönlü Bağlantı
  4. @StateObject vs @ObservedObject
  5. @EnvironmentObject: Dependency Injection
  6. @Environment: Sistem Değerleri
  7. @AppStorage ve @SceneStorage
  8. iOS 17+: @Observable Macro Devrimi
  9. Karar Ağacı: Hangisini Kullanmalıyım?
  10. 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 = 0
3 
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önetir
3 @State private var count = 0
4 @State private var isAnimating = false
5 
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 += 1
14 withAnimation(.spring()) {
15 isAnimating = true
16 }
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 sahibi
2struct ParentView: View {
3 @State private var isOn = false
4 
5 var body: some View {
6 // $isOn → Binding<Bool> oluşturur
7 ToggleRow(title: "Bildirimler", isOn: $isOn)
8 }
9}
10 
11// Child - state'e referans
12struct ToggleRow: View {
13 let title: String
14 @Binding var isOn: Bool // Parent'ın state'ine referans
15 
16 var body: some View {
17 Toggle(title, isOn: $isOn)
18 }
19}
20 
21// ✅ Custom Binding oluşturma
22struct SettingsView: View {
23 @State private var volume: Double = 0.5
24 
25 var body: some View {
26 Slider(value: Binding(
27 get: { volume },
28 set: { newValue in
29 volume = min(max(newValue, 0), 1) // Clamp 0-1
30 HapticManager.impact(.light) // Side effect
31 }
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 = false
5 
6 func loadProfile() async {
7 isLoading = true
8 defer { isLoading = false }
9 // API çağrısı...
10 }
11}
12 
13// ✅ DOĞRU: İlk oluşturan view'da @StateObject
14struct ProfileScreen: View {
15 @StateObject private var viewModel = UserViewModel()
16 
17 var body: some View {
18 VStack {
19 ProfileHeader(viewModel: viewModel) // Aşağı geçir
20 ProfileForm(viewModel: viewModel)
21 }
22 .task { await viewModel.loadProfile() }
23 }
24}
25 
26// ✅ DOĞRU: Alt view'da @ObservedObject
27struct ProfileHeader: View {
28 @ObservedObject var viewModel: UserViewModel
29 
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 = false
4 @Published var theme: AppTheme = .system
5}
6 
7// Root'ta inject et
8@main
9struct 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şebilir
16 }
17 }
18}
19 
20// Herhangi bir alt view'da kullan
21struct SettingsView: View {
22 @EnvironmentObject var appState: AppState
23 
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 colorScheme
3 @Environment(\.dynamicTypeSize) var typeSize
4 @Environment(\.dismiss) var dismiss
5 @Environment(\.horizontalSizeClass) var sizeClass
6 
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 ekran
13 HStack { content }
14 } else {
15 // iPhone
16 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şturma
30struct 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ım
42struct TrackableView: View {
43 @Environment(\.analytics) var analytics
44 
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 edilir
3 @AppStorage("username") var username = "Misafir"
4 @AppStorage("notifications_enabled") var notificationsEnabled = true
5 @AppStorage("app_theme") var appTheme = "system"
6 
7 // Scene-level storage (tab, window bazlı)
8 @SceneStorage("selected_tab") var selectedTab = 0
9 @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şım
2@Observable
3class UserStore {
4 var name = ""
5 var email = ""
6 var isLoading = false
7 var posts: [Post] = []
8 
9 func loadPosts() async {
10 isLoading = true
11 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 eder
22 // name değişince BU view rebuild olur
23 // 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 in
43 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}

  1. Basit değer (Int, String, Bool)?@State
  2. Child view'a geçirecek misin?@Binding
  3. Class-based model, sen oluşturuyorsun?@StateObject (veya @State + @Observable)
  4. Class-based model, dışarıdan geliyor?@ObservedObject (veya let + @Observable)
  5. Tüm app boyunca paylaşılacak?@EnvironmentObject (veya @Environment)
  6. UserDefaults'a persist?@AppStorage
  7. Scene bazlı geçici state?@SceneStorage

Production Best Practices {#best-practices}

🔑 Bu Yazıdan Çıkarımlar

  1. @State sadece view-local basit tipler için
  2. @StateObject oluşturan view'da, @ObservedObject alan view'da
  3. @Observable kullanabiliyorsan geç - iOS 17+ hedefliyorsan
  4. @EnvironmentObject inject kontrolü yap - crash önle
  5. Custom Environment key'ler dependency injection için harika
  6. @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.

Etiketler

#swiftui#property-wrappers#state-management#observable#ios-development#tutorial#advanced
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz