Tüm Yazılar
KategoriArchitecture
Okuma Süresi
22 dk okuma
Yayın Tarihi
...
Kelime Sayısı
1.590kelime

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

Feature flags ile iOS uygulamanızda A/B test, kademeli yayın, kill switch ve deneysel özellik yönetimini profesyonelce uygulayın.

iOS Feature Flags Stratejisi: Güvenli ve Kontrollü Yayın

# iOS Feature Flags Stratejisi: Güvenli ve Kontrollü Yayın

Bir özelliği App Store'a gönderdikten sonra geri almak istediğiniz oldu mu? Ya da yeni bir UI'ı sadece kullanıcılarınızın yüzde onuna göstermek? Feature flags, modern mobil geliştirmenin vazgeçilmez aracıdır. Bu rehberde, basit boolean flag'lerden karmaşık A/B test senaryolarına kadar her şeyi production-ready kodlarla öğreneceksiniz.

Uyarı: Feature flags yanlış yönetildiğinde teknik borca dönüşür. Bu rehberdeki lifecycle yönetimi bölümünü mutlaka okuyun.

İçindekiler


1. Feature Flags Temelleri

Feature flag (ya da feature toggle), kodunuzdaki bir özelliğin çalışma zamanında açılıp kapatılmasını sağlayan mekanizmadır.

Flag Türleri

Tür
Ömür
Kullanım Amacı
Örnek
**Release Flag**
Kısa (1-2 sprint)
Tamamlanmamış özelliği gizleme
Yeni profil sayfası
**Experiment Flag**
Orta (2-4 hafta)
A/B testing
Checkout akışı varyantları
**Ops Flag**
Uzun
Operasyonel kontrol
Maintenance modu
**Kill Switch**
Kalıcı
Acil kapatma mekanizması
Ödeme sistemi devre dışı
**Permission Flag**
Kalıcı
Kullanıcı bazlı erişim
Premium özellikler

2. Mimari Tasarım

İyi bir feature flag sistemi, temiz mimari prensiplerine uygun olmalıdır.

Protocol-Based Tasarım

swift
1// MARK: - Feature Flag Protocol
2protocol FeatureFlagProviding {
3 func isEnabled(_ flag: FeatureFlag) -> Bool
4 func value<T>(for flag: FeatureFlag, defaultValue: T) -> T
5 func refresh() async throws
6}
7 
8// MARK: - Feature Flag Tanımları
9enum FeatureFlag: String, CaseIterable, Codable {
10 // Release Flags
11 case newProfilePage = "new_profile_page"
12 case redesignedCheckout = "redesigned_checkout"
13 case darkModeV2 = "dark_mode_v2"
14 
15 // Experiment Flags
16 case onboardingVariantB = "onboarding_variant_b"
17 case pricingExperiment = "pricing_experiment"
18 
19 // Kill Switches
20 case paymentSystemEnabled = "payment_system_enabled"
21 case pushNotificationsEnabled = "push_notifications_enabled"
22 
23 // Ops Flags
24 case maintenanceMode = "maintenance_mode"
25 case debugMenuEnabled = "debug_menu_enabled"
26 
27 var defaultValue: Bool {
28 switch self {
29 case .paymentSystemEnabled, .pushNotificationsEnabled:
30 return true // Kill switch'ler varsayilan acik
31 case .maintenanceMode:
32 return false
33 default:
34 return false
35 }
36 }
37 
38 var flagType: FlagType {
39 switch self {
40 case .newProfilePage, .redesignedCheckout, .darkModeV2:
41 return .release
42 case .onboardingVariantB, .pricingExperiment:
43 return .experiment
44 case .paymentSystemEnabled, .pushNotificationsEnabled:
45 return .killSwitch
46 case .maintenanceMode, .debugMenuEnabled:
47 return .ops
48 }
49 }
50}
51 
52enum FlagType: String {
53 case release, experiment, killSwitch, ops, permission
54}

Feature Flag Service

swift
1// MARK: - Feature Flag Service
2final class FeatureFlagService: FeatureFlagProviding {
3 static let shared = FeatureFlagService()
4 
5 private var remoteFlags: [String: Any] = [:]
6 private var localOverrides: [String: Any] = [:]
7 private let cache: UserDefaults
8 private let lock = NSLock()
9 
10 private init(cache: UserDefaults = .standard) {
11 self.cache = cache
12 loadCachedFlags()
13 }
14 
15 func isEnabled(_ flag: FeatureFlag) -> Bool {
16 lock.lock()
17 defer { lock.unlock() }
18 
19 // 1. Local override (debug menusunden)
20 if let override = localOverrides[flag.rawValue] as? Bool {
21 return override
22 }
23 
24 // 2. Remote deger
25 if let remote = remoteFlags[flag.rawValue] as? Bool {
26 return remote
27 }
28 
29 // 3. Cache
30 if cache.object(forKey: "ff_\(flag.rawValue)") != nil {
31 return cache.bool(forKey: "ff_\(flag.rawValue)")
32 }
33 
34 // 4. Default
35 return flag.defaultValue
36 }
37 
38 func value<T>(for flag: FeatureFlag, defaultValue: T) -> T {
39 lock.lock()
40 defer { lock.unlock() }
41 
42 if let override = localOverrides[flag.rawValue] as? T {
43 return override
44 }
45 if let remote = remoteFlags[flag.rawValue] as? T {
46 return remote
47 }
48 return defaultValue
49 }
50 
51 func refresh() async throws {
52 let url = URL(string: "https://api.example.com/v1/feature-flags")!
53 let (data, _) = try await URLSession.shared.data(from: url)
54 let flags = try JSONDecoder().decode([String: AnyCodable].self, from: data)
55 
56 lock.lock()
57 remoteFlags = flags.mapValues { item in item.value }
58 cacheFlags()
59 lock.unlock()
60 
61 NotificationCenter.default.post(name: .featureFlagsUpdated, object: nil)
62 }
63 
64 // MARK: - Debug
65 func setOverride(_ value: Bool, for flag: FeatureFlag) {
66 lock.lock()
67 localOverrides[flag.rawValue] = value
68 lock.unlock()
69 }
70 
71 func clearOverrides() {
72 lock.lock()
73 localOverrides.removeAll()
74 lock.unlock()
75 }
76 
77 private func loadCachedFlags() {
78 for flag in FeatureFlag.allCases {
79 let key = "ff_\(flag.rawValue)"
80 if cache.object(forKey: key) != nil {
81 remoteFlags[flag.rawValue] = cache.bool(forKey: key)
82 }
83 }
84 }
85 
86 private func cacheFlags() {
87 for (key, value) in remoteFlags {
88 cache.set(value, forKey: "ff_\(key)")
89 }
90 }
91}
92 
93extension Notification.Name {
94 static let featureFlagsUpdated = Notification.Name("featureFlagsUpdated")
95}

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?


3. Local vs Remote Flags

Kriter
Local Flags
Remote Flags
Hybrid
**Hız**
Anında
Network gecikmesi
Cached + async
**Esneklik**
App update gerekli
Anlık değişiklik
En iyi ikisi
**Offline**
Her zaman çalışır
Cache'e bağlı
Cache fallback
**Maliyet**
Ücretsiz
Servis maliyeti
Servis maliyeti
**Kontrol**
Geliştirici
Product/Ops team
Her iki taraf
**Güvenlik**
Reverse engineer riski
Sunucu tarafı güvenli
Sunucu tarafı güvenli

4. A/B Testing Entegrasyonu

swift
1// MARK: - Experiment Manager
2final class ExperimentManager {
3 static let shared = ExperimentManager()
4 
5 private let flagService: FeatureFlagProviding
6 private let analytics: AnalyticsProviding
7 private var assignments: [String: String] = [:]
8 
9 init(
10 flagService: FeatureFlagProviding = FeatureFlagService.shared,
11 analytics: AnalyticsProviding = AnalyticsService.shared
12 ) {
13 self.flagService = flagService
14 self.analytics = analytics
15 }
16 
17 func variant(for experiment: Experiment) -> ExperimentVariant {
18 // Daha once atanmis mi kontrol et
19 if let cached = assignments[experiment.rawValue],
20 let variant = ExperimentVariant(rawValue: cached) {
21 return variant
22 }
23 
24 // Deterministic assignment (kullanici ID hash'i ile)
25 let userId = UserSession.current.userId
26 let hash = stableHash("\(experiment.rawValue)_\(userId)")
27 let bucket = hash % 100
28 
29 let variant: ExperimentVariant
30 if bucket < experiment.controlPercentage {
31 variant = .control
32 } else {
33 variant = .treatment
34 }
35 
36 // Cache'le ve analytics'e kaydet
37 assignments[experiment.rawValue] = variant.rawValue
38 analytics.track("experiment_assigned", properties: [
39 "experiment": experiment.rawValue,
40 "variant": variant.rawValue
41 ])
42 
43 return variant
44 }
45 
46 private func stableHash(_ input: String) -> Int {
47 var hash = 5381
48 for char in input.utf8 {
49 hash = ((hash << 5) &+ hash) &+ Int(char)
50 }
51 return abs(hash)
52 }
53}
54 
55enum Experiment: String {
56 case onboardingFlow = "onboarding_flow_v2"
57 case checkoutRedesign = "checkout_redesign"
58 case homeFeedAlgorithm = "home_feed_algo"
59 
60 var controlPercentage: Int {
61 switch self {
62 case .onboardingFlow: return 50
63 case .checkoutRedesign: return 70
64 case .homeFeedAlgorithm: return 80
65 }
66 }
67}
68 
69enum ExperimentVariant: String, Codable {
70 case control
71 case treatment
72}

5. Kill Switch Mekanizması

Kill switch, production'daki bir özelliği anında devre dışı bırakmanızı sağlar. Ödeme sisteminiz çöktüğünde, üçüncü parti bir servis hata verdiğinde veya güvenlik açığı tespit ettiğinizde hayat kurtarır.

swift
1// MARK: - Kill Switch
2struct KillSwitchMiddleware {
3 static func check(
4 feature: FeatureFlag,
5 fallback: () -> Void,
6 action: () -> Void
7 ) {
8 if FeatureFlagService.shared.isEnabled(feature) {
9 action()
10 } else {
11 LogService.warning("Kill switch aktif: \(feature.rawValue)")
12 fallback()
13 }
14 }
15}
16 
17// Kullanim
18// KillSwitchMiddleware.check(
19// feature: .paymentSystemEnabled,
20// fallback: { showMaintenanceScreen() },
21// action: { proceedWithPayment() }
22// )

6. Flag Lifecycle Yönetimi

Feature flag'lerin en büyük tehlikesi: temizlenmemesi. Zamanla onlarca kullanılmayan flag birikir ve kod okunmaz hale gelir.

Lifecycle Kuralları

  1. Release flag: Özellik stable olduktan 1 sprint sonra kaldır
  2. Experiment flag: Sonuçlar analiz edildikten 2 hafta sonra kaldır
  3. Kill switch: Kalıcı olabilir ama yılda bir gözden geçir
  4. Ops flag: İhtiyaç oldukça kalabilir

swift
1extension FeatureFlag {
2 var expiryDate: Date? {
3 switch self {
4 case .newProfilePage:
5 return DateComponents(calendar: .current, year: 2026, month: 4, day: 1).date
6 case .redesignedCheckout:
7 return DateComponents(calendar: .current, year: 2026, month: 5, day: 15).date
8 default: return nil // Kill switch ve ops flag'ler suresi dolmaz
9 }
10 }
11 
12 var owner: String {
13 switch self {
14 case .newProfilePage: return "profile-team"
15 case .redesignedCheckout: return "payments-team"
16 case .paymentSystemEnabled: return "platform-team"
17 default: return "unassigned"
18 }
19 }
20 
21 // CI'da calistirin: FeatureFlag.warnExpired()
22 static func warnExpired() {
23 for flag in allCases {
24 guard let expiry = flag.expiryDate, expiry < Date() else { continue }
25 print("WARNING: Feature flag '\(flag.rawValue)' suresi dolmus! Owner: \(flag.owner)")
26 }
27 }
28}

ALTIN İPUCU

Bu yazının en değerli bilgisi

Bu ipucu, yazının en önemli çıkarımını içeriyor.

Okuyucu Ödülü

Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:

Sonuç ve Öneriler

Feature flags modern iOS geliştirmenin temel taşlarından biridir. Doğru uygulandığında deployment riskini azaltır, A/B test yapmanızı sağlar ve production'da anlık kontrol verir. Ancak lifecycle yönetimini ihmal etmeyin — flag borcu, teknik borcun en sinsi halidir.

Etiketler

#Feature Flags#A/B Testing#Architecture#Remote Config#iOS#DevOps
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