İçeriğe geç

HandySwift 4.0 ile Tanışın

Açık kaynağa yeniden zaman ayırıyorum: HandySwift'i tamamen yeniledim — çok daha iyi dokümantasyon ve uygulamalarımdan çıkardığım bir sürü kullanışlı özellik. En sık kullandığım yardımcıları öğrenmek için okumaya devam et!

HandySwift 4.0 ile Tanışın

Açık kaynak çalışması yapmayalı epey oldu. Son zamanlarda Indie kariyerimi uzun vadede sürdürebilmek için yeni uygulamalar yayınlamaya odaklanmıştım. 3 ay içinde 6 uygulama çıkardıktan sonra, en çok yeniden kullanılan kod parçalarını paylaşma zamanının geldiğini düşündüm. Bunun için zaten bir açık kaynak kütüphanem var: HandySwift.

Ama son sürümün üzerinden 2 yıldan fazla zaman geçmişti. Tabii ki zaman içinde main branch’e bazı işlevler eklemiştim, böylece uygulamalarım arasında kolayca yeniden kullanabiliyordum. Ama dokümante edilmemiş ve yeni bir sürümün parçası olmayan özellikler, API’leri public olarak işaretlenmiş olsa bile “dahili” sayılabilir.

Bu yüzden son birkaç gün tüm kodu temizlemek için vakit ayırdım — her şeyin Swift’in son eklemeleriyle tutarlı olduğundan emin oldum, kullanılmayan kodu kaldırdım, yeniden adlandırmalar için @available attribute’ları ekledim (böylece Xcode fix-it’ler sunabiliyor) ve bir sürü yeni API’yi dokümante ettim. Hatta tamamen yeni bir logo bile tasarladım!

Ayrıca Swift-DocC’u benimsemeye karar verdim — bu sayede README dosyamı minimuma indirip dokümantasyonumu Swift Package Index sitesinde barındırabildim. ChatGPT’nin biraz yardımıyla mevcut dokümantasyonumu bile genişletebildim ve şimdiye kadar yayınladığım en iyi dokümante edilmiş kütüphane ortaya çıktı!

Logo güncelleme

Taşıma detayları hakkında ayrı bir yazı yazacağım. Ama daha önce HandySwift hakkında hiç yazmadığım için, kullanarak elde edeceğin kolaylıkları anlatayım. Her projeye eklenmesini öneriyorum. Hiç bağımlılığı yok, kendisi de hafif, tüm platformları destekliyor (Linux ve visionOS dahil) ve platform desteği iOS 12’ye kadar geri gidiyor. Bence hiç düşünmeden eklenecek bir paket.


Extension’lar

Mevcut tiplere eklenen 100’den fazla fonksiyon ve property’nin bazı öne çıkanları, her biri doğrudan uygulamalarımdan pratik bir kullanım örneğiyle:

Güvenli Index Erişimi

Müzik çalar

FocusBeats’te bir müzik parçaları dizisine index ile erişiyorum. subscript(safe:) ile out of bounds çökmelerinden kaçınıyorum:

var nextEntry: ApplicationMusicPlayer.Queue.Entry? {
   guard let nextEntry = playerQueue.entries[safe: currentEntryIndex + 1] else { return nil }
   return nextEntry
}

Bunu Collection protokolüne uyan her tipte kullanabilirsin — Array, Dictionary ve String dahil. Index out of bounds olduğunda çökme yerine nil dönen array[safe: index] kullanarak, çöken array[index] subscript’inden kaçınıyorsun.

Boşluklu String’ler vs Boş String’ler

API anahtarları

Boş olmaması gereken metin alanlarındaki yaygın bir sorun, kullanıcıların yanlışlıkla boşluk veya satır sonu karakteri yazıp fark etmemesi. Eğer doğrulama kodu sadece .isEmpty‘yi kontrol ediyorsa sorun fark edilmez. Bu yüzden TranslateKit’te kullanıcılar bir API anahtarı girdiğinde, .isEmpty kontrolü yapmadan önce String’in başından ve sonundan boşluk ve satır sonu karakterlerini temizliyorum. Ve bunu birçok yerde çok sık yaptığım için bir yardımcı yazdım:

Image(systemName: self.deepLAuthKey.isBlank ? "xmark.circle" : "checkmark.circle")
   .foregroundStyle(self.deepLAuthKey.isBlank ? .red : .green)

Aynı davranışı elde etmek için isEmpty yerine isBlank kullanman yeterli!

Okunabilir Zaman Aralıkları

Premium plan sona eriyor

TimeInterval (Double için bir typealias) bekleyen bir API kullandığımda, biriminin olmaması kodu daha az okunabilir yapıyordu çünkü birimin “saniye” olduğunu aktif olarak hatırlamak gerekiyordu. Ayrıca dakika veya saat gibi farklı bir birime ihtiyaç duyduğumda hesaplamayı elle yapmam gerekiyordu. HandySwift ile artık gerek yok!

Düz bir Double değeri olan 60 * 5 yerine, basitçe .minutes(5) geçirebilirsin. Örneğin TranslateKit’te kullanıcı aboneliğini iptal ettiğinde görünümü önizlemek için bunu kullanıyorum:

#Preview("Expiring") {
   ContentView(
      hasPremiumAccess: true,
      premiumExpiresAt: Date.now.addingTimeInterval(.days(3))
   )
}

Hatta birden fazla birimi + işaretiyle zincirleyerek “09:41 AM” gibi bir saat oluşturabilirsin:

let startOfDay = Calendar.current.startOfDay(for: Date.now)
let iPhoneRevealedAt = startOfDay.addingTimeInterval(.hours(9) + .minutes(41))

Bu API tasarımının, .milliseconds(250) gibi şeyleri zaten destekleyen Duration ve DispatchTimeInterval ile uyumlu olduğunu belirtmek isterim. Ama onlar saniye seviyesinde duruyor, daha yukarı çıkmıyorlar. HandySwift bu tipler için de dakika, saat, gün ve hatta hafta ekliyor. Yani şöyle bir şey yazabilirsin:

try await Task.sleep(for: .minutes(5))

⚠️ Zaman aralıklarıyla ilerleme, yaz saati uygulaması gibi karmaşıklıkları hesaba katmaz. Bunun için Calendar kullan.

Ortalama Hesaplama

Bulmaca oluşturma

CrossCraft’teki bulmaca oluşturma algoritmasında her iterasyonda bulmacanın genel kalitesini hesaplayan bir sağlık fonksiyonu var. İki farklı yön dikkate alınıyor:

/// 0 ile 1 arasında bir değer.
func calculateQuality() -> Double {
   let fieldCoverage = Double(solutionBoard.fields) / Double(maxFillableFields)
   let intersectionsCoverage = Double(solutionBoard.intersections) / Double(maxIntersections)
   return [fieldCoverage, intersectionsCoverage].average()
}

Önceki sürümlerde farklı ağırlıklarla deneyler yaptım — örneğin kesişimlere alan kapsamının iki katı ağırlık vermiştim. Bunu average() kullanarak son satırda şöyle de yapabilirdim:

return [fieldCoverage, intersectionsCoverage, intersectionsCoverage].average()

Kayan Noktalı Sayıları Yuvarlama

İlerleme çubuğu

CrossCraft’te bir bulmacayı çözerken ekranın üst kısmında mevcut ilerlemeyi görebilirsin. Sayısal değerler için yerleşik yüzde formatlayıcıyı (.formatted(.percent)) kullanıyorum, ama 0 ile 1 arasında bir Double gerektirir (1 = %100). 12 gibi bir Int geçmek beklenmedik şekilde 0% olarak gösteriyor, yani basitçe şunu yapamam:

Int(fractionCompleted * 100).formatted(.percent)  // => "0%" — "100%" arası

Ve sadece fractionCompleted.formatted(.percent) yapmak da bazen "0.1428571429" gibi çok uzun metin üretiyor.

Bunun yerine, Double’ı 2 anlamlı basamağa yuvarlamak için rounded(fractionDigits:rule:) kullanıyorum:

Text(fractionCompleted.rounded(fractionDigits: 2).formatted(.percent))

ℹ️ Bir değişkeni yerinde değiştirmek istersen mutating round(fractionDigits:rule:) fonksiyonu da var.

Simetrik Veri Şifreleme

Bulmaca paylaş

CrossCraft’te bir bulmacayı yüklemeden önce, teknoloji meraklısı kişilerin cevapları JSON’dan kolayca koklayamaması için şifreliyorum:

func upload(puzzle: Puzzle) async throws {
   let key = SymmetricKey(base64Encoded: "<base-64 encoded secret>")!
   let plainData = try JSONEncoder().encode(puzzle)
   let encryptedData = try plainData.encrypted(key: key)

   // yükleme mantığı
}

Yukarıdaki kodun iki extension kullandığını belirtmek isterim. Önce anahtarı başlatmak için init(base64Encoded:) kullanılıyor, sonra encrypted(key:) dahili olarak güvenli CryptoKit API’lerini kullanarak veriyi şifreliyor — detaylarla uğraşman gerekmiyor.

Başka bir kullanıcı aynı bulmacayı indirdiğinde, decrypted(key:) ile şifresini çözüyorum:

func downloadPuzzle(from url: URL) async throws -> Puzzle {
   let encryptedData = // indirme mantığı

   let key = SymmetricKey(base64Encoded: "<base-64 encoded secret>")!
   let plainData = try encryptedPuzzleData.decrypted(key: symmetricKey)
   return try JSONDecoder().decode(Puzzle.self, from: plainData)
}

ℹ️ HandySwift ayrıca String için encrypted(key:) ve decrypted(key:) fonksiyonlarını da barındırıyor — bunlar şifrelenmiş verinin base-64 kodlanmış String temsilini döndürüyor. String API’leriyle çalışırken bunları kullan.


Yeni Tipler

Mevcut tipleri genişletmenin yanı sıra, HandySwift 7 yeni tip ve 2 global fonksiyon da sunuyor. İşte neredeyse her uygulamada kullandıklarım:

Gregorian Day ve Time

Yıl, ay ve günden bir Date oluşturmak mı istiyorsun? Çok kolay:

GregorianDay(year: 1960, month: 11, day: 01).startOfDay() // => Date

Elinde bir Date var ve tarihten sadece gün kısmını saklamak mı istiyorsun, saati değil? Modelinde GregorianDay kullan:

struct User {
   let birthday: GregorianDay
}


let selectedDate = // DatePicker'dan geliyor
let timCook = User(birthday: GregorianDay(date: selectedDate))
print(timCook.birthday.iso8601Formatted)  // => "1960-11-01"

Sadece bugünün tarihini saat olmadan mı istiyorsun?

GregorianDay.today

.yesterday ve .tomorrow ile de çalışıyor. Daha fazlası için:

let todayNextWeek = GregorianDay.today.advanced(by: 7)

ℹ️ GregorianDay, beklediğin tüm protokollere uyum sağlıyor — Codable, Hashable ve Comparable gibi. Encoding/decoding için “2014-07-13” gibi ISO formatını kullanıyor.

GregorianTimeOfDay ise bunun karşılığı:

let iPhoneAnnounceTime = GregorianTimeOfDay(hour: 09, minute: 41)
let anHourFromNow = GregorianTimeOfDay.now.advanced(by: .hours(1))


let date = iPhoneAnnounceTime.date(day: GregorianDay.today)  // => Date

Delay ve Debounce

Hiç bir kodu geciktirmek istedin de bu API’yi hatırlaması ve yazması zor buldun mu?

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(250)) {
   // kodun
}

HandySwift hatırlaması daha kolay, daha kısa bir versiyon sunuyor:

delay(by: .milliseconds(250)) {
   // kodun
}

DispatchQueue gibi farklı Quality of Service sınıflarını da destekliyor (varsayılan main queue):

delay(by: .milliseconds(250), qosClass: .background) {
   // kodun
}

Geciktirme tek seferlik görevler için harika olsa da, bazen hızlı girdi performans veya ölçeklenebilirlik sorunlarına yol açabiliyor. Örneğin, bir kullanıcı arama alanına hızlı yazabilir. Arama sonuçlarını güncellemeyi geciktirmek ve yeni bir girdi geldiğinde eski girdileri iptal etmek yaygın bir pratik. Buna “Debouncing” deniyor. Ve HandySwift ile çok kolay:

@State private var searchText = ""
let debouncer = Debouncer()


var body: some View {
    List(filteredItems) { item in
        Text(item.title)
    }
    .searchable(text: self.$searchText)
    .onChange(of: self.searchText) { newValue in
        self.debouncer.delay(for: .milliseconds(500)) {
            // 500 milisaniyelik kullanıcı hareketsizliğinden sonra güncellenmiş arama metniyle arama işlemi yap
            self.performSearch(with: newValue)
        }
    }
    .onDisappear {
        debouncer.cancelAll()
    }
}

Debouncer’ın bir property’de saklandığına dikkat et — böylece disappear’da temizlik için cancelAll() çağrılabilir. Ama asıl sihir delay(for:id:operation:)’da gerçekleşiyor — ve detaylarla uğraşmana gerek yok!


Bu yazıyı beğendin mi? Swift ipuçları ve indie geliştirici güncellemeleri için Bluesky ve Mastodon üzerinden takip et.