Swift Closures Kullanımı: $0, @escaping ve [weak self]
5 dk okuma
![Swift Closures Kullanımı: $0, @escaping ve [weak self]](/assets/blog-covers/swift.webp)
Closure, bir değişkene atanabilen, fonksiyona parametre olarak verilebilen isimsiz bir kod bloğudur. Swift'te sorted, map, filter, ağ isteklerinin tamamlanma blokları ve SwiftUI'daki her Button aksiyonu closure'dır; yani dili kullanıp closure'dan kaçmak mümkün değil. Yeni başlayanları zorlayan şey kavramın kendisi değil, aynı şeyin altı farklı şekilde yazılabilmesidir. Bu yazıda en uzun yazımdan en kısasına adım adım iniyor, ardından bellek tarafına, yani @escaping ve [weak self] konularına geçiyoruz.
Fonksiyondan Closure'a
Bir fonksiyon ile closure arasındaki fark yalnızca isim ve yazım biçimidir:
func greet(name: String) -> String {
return "Merhaba, \(name)"
}
let greetClosure = { (name: String) -> String in
return "Merhaba, \(name)"
}
print(greet(name: "Ada")) // Merhaba, Ada
print(greetClosure("Ada")) // Merhaba, AdaClosure'da parametreler ve dönüş tipi süslü parantezin içine taşınır, in kelimesi de imza ile gövdeyi ayırır. greetClosure'ın tipi (String) -> String'dir. Çağırırken argüman etiketi yazılmadığına dikkat edin.
Tam Formdan $0'a Altı Adım
Aynı sıralama işlemini giderek kısaltalım:
let numbers = [4, 1, 7, 3]
// 1. Tam form
let s1 = numbers.sorted(by: { (a: Int, b: Int) -> Bool in
return a < b
})
// 2. Tipler bağlamdan çıkarılır
let s2 = numbers.sorted(by: { a, b in return a < b })
// 3. Tek ifadede return yazılmaz
let s3 = numbers.sorted(by: { a, b in a < b })
// 4. Kısa argüman adları
let s4 = numbers.sorted(by: { $0 < $1 })
// 5. Trailing closure
let s5 = numbers.sorted { $0 < $1 }
// 6. Operatörün kendisi de bir fonksiyondur
let s6 = numbers.sorted(by: <)Altısı da [1, 3, 4, 7] üretir. Derleyici sorted(by:)'ın (Int, Int) -> Bool beklediğini bildiği için tipleri yazmanıza gerek kalmaz. $0 ilk, $1 ikinci parametredir.
Hangisini seçmeli? Gövde tek kısa ifadeyse $0 okunaklıdır. Gövde birkaç satırsa ya da iç içe closure varsa parametreye isim verin; iç içe closure'larda $0 en içteki closure'a aittir ve hangisinden bahsettiğiniz hızla karışır.
Trailing Closure
Fonksiyonun son parametresi closure ise, closure parantezin dışına yazılabilir:
func repeatTask(times: Int, task: (Int) -> Void) {
for index in 1...times {
task(index)
}
}
repeatTask(times: 3) { index in
print("\(index). tekrar")
}SwiftUI kodunun bu kadar temiz görünmesinin sebebi budur: VStack { … }, Button("Kaydet") { … }, List { … } hep trailing closure'dır.
Değer Yakalama
Closure, tanımlandığı yerdeki değişkenleri yakalar ve o kapsam bittikten sonra da yaşatır:
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let next = makeCounter()
print(next()) // 1
print(next()) // 2
let another = makeCounter()
print(another()) // 1makeCounter bittiğinde count normalde yok olmalıydı. Ama dönen closure onu yakaladığı için yaşamaya devam ediyor. another kendi ayrı count'unu alıyor.
Önemli ayrıntı: closure değişkenin o anki değerini değil, değişkenin kendisini yakalar. Değeri dondurmak isterseniz capture list kullanılır:
var level = 1
let printLive = { print("Seviye: \(level)") }
let printFrozen = { [level] in print("Seviye: \(level)") }
level = 5
printLive() // Seviye: 5
printFrozen() // Seviye: 1Closure'lar referans tipidir. Bir closure'ı iki değişkene atarsanız ikisi aynı yakalanmış durumu paylaşır. Değer ve referans tipi ayrımını struct ve class farkı yazısında ayrıntılı anlattım.
@escaping: Fonksiyondan Sonra Yaşayan Closure
Varsayılan olarak parametre closure'ı non-escaping'dir: fonksiyon dönmeden önce çağrılır ve işi biter. Closure fonksiyon bittikten sonra çağrılacaksa, yani bir özelliğe saklanıyor ya da asenkron bir işe veriliyorsa, @escaping ile işaretlenmesi gerekir:
import Foundation
func fetchGreeting(completion: @escaping (String) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion("Merhaba")
}
}
fetchGreeting { text in
print(text) // 1 saniye sonra: Merhaba
}@escaping'i silerseniz derleyici Escaping closure captures non-escaping parameter 'completion' hatası verir. Bu işaret bir formalite değildir; "bu closure yakaladığı şeyleri uzun süre tutabilir, belleği düşün" uyarısıdır. Aynı nedenle class içindeki escaping closure'larda self. yazmanız istenir; yazmazsanız reference to property … in closure requires explicit use of 'self' to make capture semantics explicit hatası alırsınız.
Retain Cycle ve [weak self]
Sorun şöyle oluşur: nesne closure'ı bir özelliğinde tutar, closure da self'i yakalar. İkisi birbirini güçlü referansla tuttuğu için hiçbiri bellekten silinmez.
final class Stopwatch {
var seconds = 0
var onTick: (() -> Void)?
deinit {
print("Stopwatch bellekten silindi")
}
func start() {
onTick = {
self.seconds += 1 // self -> onTick -> self
}
}
}
var stopwatch: Stopwatch? = Stopwatch()
stopwatch?.start()
stopwatch = nil // deinit mesajı gelmezÇözüm, self'i zayıf yakalamaktır:
func start() {
onTick = { [weak self] in
guard let self else { return }
self.seconds += 1
}
}[weak self] ile self closure içinde opsiyonel olur; nesne silinmişse nil gelir. guard let self else { return } kalıbı bu opsiyoneli açmanın en temiz yoludur; mantığı guard let yazısındakiyle aynıdır. Bu değişiklikten sonra stopwatch = nil dediğinizde deinit mesajını görürsünüz.
[unowned self] de bir seçenektir ama nesne silindikten sonra closure çalışırsa uygulama çöker. Closure'ın nesneden uzun yaşamayacağından emin değilseniz weak kullanın.
Her closure'a [weak self] yazmak gerekmez. map, filter, sorted gibi non-escaping closure'lar fonksiyonla birlikte biter, döngü oluşturamaz.
map, filter, sorted ve reduce
Closure'ların en keyifli kullanım alanı koleksiyonlardır:
struct Student {
let name: String
let grade: Int
}
let students = [
Student(name: "Ada", grade: 85),
Student(name: "Linus", grade: 42),
Student(name: "Grace", grade: 97),
Student(name: "Alan", grade: 68)
]
let passedNames = students
.filter { $0.grade >= 50 }
.sorted { $0.grade > $1.grade }
.map { $0.name }
print(passedNames) // ["Grace", "Ada", "Alan"]
let total = students.reduce(0) { sum, student in sum + student.grade }
let average = Double(total) / Double(students.count)
print(average) // 73.0filter koşulu sağlayanları seçer, sorted sıralar, map her elemanı başka bir şeye dönüştürür, reduce hepsini tek değere indirir. reduce'ta iki parametrenin anlamı farklı olduğu için isim vermek $0 + $1.grade yazmaktan daha okunaklıdır. Sadece bir özelliği çekiyorsanız key path daha da kısadır: students.map(\.grade).
SwiftUI Buton Aksiyonları
import SwiftUI
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("\(count)")
.font(.largeTitle)
Button("Artır") {
count += 1
}
Button(action: { count = 0 }) {
Label("Sıfırla", systemImage: "arrow.counterclockwise")
}
}
}
}İlk butonda aksiyon trailing closure olarak verildi. İkincisinde iki closure var: action ve görünümü üreten label. SwiftUI view'ları struct olduğu için burada [weak self] yazılmaz; denerseniz 'weak' may only be applied to class and class-bound protocol types hatası alırsınız. Retain cycle riski view'da değil, view'ın kullandığı class tabanlı model nesnelerinde ortaya çıkar. Bu yapının tamamını sıfırdan görmek için SwiftUI ile ilk uygulama yazısına bakabilirsiniz.
Gerçekçi Senaryo: Arama Ekranının Modeli
import Foundation
final class SearchViewModel {
private(set) var results: [String] = []
var onResultsChanged: (([String]) -> Void)?
private let allItems = ["Swift", "SwiftUI", "Xcode", "Flutter", "Dart"]
func search(_ query: String) {
loadItems(matching: query) { [weak self] found in
guard let self else { return }
self.results = found
self.onResultsChanged?(found)
}
}
private func loadItems(matching query: String,
completion: @escaping ([String]) -> Void) {
let items = allItems
DispatchQueue.global().async {
let found = items.filter { $0.localizedCaseInsensitiveContains(query) }
DispatchQueue.main.async {
completion(found)
}
}
}
}
let viewModel = SearchViewModel()
viewModel.onResultsChanged = { print("Sonuçlar:", $0) }
viewModel.search("swift") // Sonuçlar: ["Swift", "SwiftUI"]Bu küçük sınıfta yazıdaki her şey var. completion arka plan kuyruğuna verildiği için @escaping. Kullanıcı sonuç gelmeden ekranı kapatabilir; [weak self] sayesinde model bellekte asılı kalmaz, closure sessizce çıkar. filter içindeki closure non-escaping olduğu için $0 ile kısa tutuldu. Arka plan closure'ına self yerine items kopyasını vermek de bilinçli bir tercih: dizi değer tipi olduğu için kuyruklar arasında güvenle taşınır.
Ne Zaman Kullanılır, Ne Zaman Kullanılmaz?
Closure şu durumlarda doğru araçtır: bir davranışı parametre olarak vermek (sorted, filter), kısa olay tepkileri (buton aksiyonu), bir nesnenin dışarıya tek bir olay bildirmesi (onResultsChanged).
Şu durumlarda alternatife bakın:
- Asenkron adımlar iç içe girmeye başladıysa, yani completion içinde completion varsa, async/await çok daha okunaklıdır. Geçişi async/await rehberinde anlattım.
- Closure gövdesi onlarca satıra ulaştıysa onu isimli bir metoda taşıyın ve metodu referans olarak verin:
Button("Ekle", action: addItem). - Bir nesne dışarıya üç dört farklı olay bildiriyorsa, ayrı ayrı closure özellikleri yerine bir protocol (delegate) daha düzenli olur.
Sık Yapılan Hatalar
1. @escaping yazmayı unutmak
Belirti: Escaping closure captures non-escaping parameter 'completion' ya da closure'ı bir özelliğe atarken assigning non-escaping parameter … to an '@escaping' closure. Çözüm: parametre tipinin önüne @escaping eklemek.
2. Saklanan closure'da self'i güçlü yakalamak
Belirti: derleme hatası yok, ama ekran kapatıldığı hâlde deinit hiç çalışmıyor, zamanlayıcılar ve dinleyiciler arkada devam ediyor. Çözüm: [weak self] ve guard let self else { return }. Şüphelendiğiniz sınıfa geçici bir deinit { print(…) } eklemek en hızlı teşhis yöntemidir.
3. Değerin dondurulduğunu sanmak
Belirti: closure, tanımlandığı andaki değil, çalıştığı andaki değeri yazdırıyor. Closure değişkeni yakalar, değeri değil. Çözüm: { [level] in … } şeklinde capture list kullanmak.
4. İç içe closure'larda $0 kullanmak
Belirti: $0'ın dıştaki elemanı gösterdiğini sanıyorsunuz ama içteki closure'ın parametresini gösteriyor; sonuç sessizce yanlış çıkıyor ya da tip hatası alıyorsunuz. Çözüm: dıştaki closure'da parametreye isim vermek. let matrix = [[1, 2], [3, 4]] için matrix.map { row in row.filter { $0 > row[0] } } yazdığınızda row dıştaki, $0 içteki elemandır ve karışmaz.
Sık Sorulan Sorular
Closure ile fonksiyon arasındaki fark nedir?
Fonksiyonlar aslında isim verilmiş closure'lardır. Closure isimsizdir, yerinde yazılır ve çevresindeki değişkenleri yakalayabilir; tipi ikisinde de (Parametreler) -> DönüşTipi biçimindedir.
$0 ne anlama gelir?
Closure'ın ilk parametresinin otomatik adıdır; ikincisi $1, üçüncüsü $2 olur. Parametrelere in ile isim vermediğinizde kullanılabilir ve kısa, tek ifadeli closure'larda tercih edilir.
Her closure'da [weak self] yazmalı mıyım?
Hayır. Yalnızca closure saklanıyor ya da uzun süre yaşıyorsa ve self bir class örneğiyse gerekir. map, filter gibi non-escaping closure'larda ve struct olan SwiftUI view'larında gerek yoktur.
@escaping ne zaman gerekir?
Closure, verildiği fonksiyon döndükten sonra çağrılacaksa: bir özelliğe ya da diziye saklanıyorsa veya asenkron bir işe aktarılıyorsa. Fonksiyon içinde hemen çağrılıp biten closure'lar için gerekmez.
İlgili Yazılar
SwiftUI @State, @Binding ve @Observable: Veri Akışı Rehberi
SwiftUI'da @State, @Binding, @Observable, @Bindable ve @Environment ne zaman kullanılır? ObservableObject ile farkı, karar tablosu ve sık görülen hatalar.
SwiftUI ile İlk Uygulama: Adım Adım Başlangıç Rehberi
Xcode'da proje açmaktan simülatörde çalıştırmaya kadar SwiftUI ile ilk uygulama: App, View, VStack, @State, List ve TextField ile yapılacaklar listesi.
Swift async/await Rehberi: Task, Actor ve MainActor Kullanımı
Completion handler'dan async/await'e geçiş: Task, async let, TaskGroup, @MainActor, actor, iptal ve URLSession ile JSON API çağrısı, SwiftUI örneğiyle.