Sitemap

Swift Debugging Part 2

7 min readMay 25, 2025

--

Press enter or click to view image in full size

Herkese Merhaba,

Bu yazı başladığım challenge’ın devamıdır. Aradan biraz vakit geçti. Ama başka konular hakkında yazılar paylaştım. Bunları profilimde inceleyebilirsiniz. Part 0 ve Part 1

Part 0'da da ediğim gibi bu kodlar internetten ya da ChatGPT’den kendimi geliştirmek için istediğim kodlardır.

Hadi Başlayalım

1. Swift Concurrency ile Güvenli Veri Çekimi: Task Yönetimi ve Actor Kullanımı

Soru 1: Bu yapıda, UserViewModel ve UserManager kullanılırken kullanıcı sayısının UI’da bazen güncellenmediği veya eski değerlerin gösterildiği gözlemleniyor. Muhtemel nedenler neler olabilir? Debug için nereleri kontrol edersin? Ayrıca bu problemi önlemek için Swift concurrency ve SwiftUI yapısına uygun ne gibi geliştirmeler yapılabilir?

UserManager:

struct User: Identifiable {
let id: Int
let name: String
}

class UserManager {
private var users: [User] = []

func fetchUsers() async {
let fetched = await API.fetchAllUsers()
users = fetched
}
}

enum API {
static func fetchAllUsers() async -> [User] {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return [User(id: 1, name: "Engin"), User(id: 2, name: "Ayşe")]
}
}

UserViewModel:

@MainActor
class UserViewModel: ObservableObject {
@Published private(set) var userCount: Int = 0
private let manager = UserManager()

func fetchUsers() async {
await manager.fetchUsers()
userCount = await manager.userCount()
}
}

ContentView:

struct ContentView: View {
@StateObject private var viewModel = UserViewModel()

var body: some View {
VStack {
Text("User count: \(viewModel.userCount)")
Button("Fetch Users") {
viewModel.onTappedButton()
}
}
}
}

Kodu incelemeye başlayalım. UserManager adında bir actor tanımlanmış. Actor Swift 5.5 ile gelen Concurrency yapısıyla uyumlu çalışır.. İlk önce actor’ü hakkında basit bir örnek ile açıklayalım.

Tanım Başlangıcı

Actor Nedir:

Swift’te class’e benzeyen ancak eşzamanlı erişimi kontrol eden özel bir referans türüdür.

Özellikleri:

  • İçindeki verilere yalnızca tek bir task aynı anda erişebilir.
  • Swift, actor içindeki kodları otomatik olarak izole eder ve veri yarışlarını önler.
  • Kodların manuel olarak DispatchQueue gibi GCD yapılarıyla korumak zorunda kalınmaz.

Neden Actor:

Manuel olarak DispatchQueue, lock, mutex gibi araçlar yerine Swift veri erişimini güvenli hale getirir.

Amaç:

Swift’te çoklu thread ortamlarında data race (veri yarışlarını) ve eşzamanlılıkla ilgili hataları önlemektir.

Actor Olmadan:

let counter = Counter()

DispatchQueue.concurrentPerform(iterations: 1000) { _ in
counter.increment()
}

print("Final value: \(counter.value)")

let counter = Counter()

DispatchQueue.concurrentPerform(iterations: 1000) { _ in
counter.increment()
}

print("Final value: \(counter.value)")

Burada belenen sonucun Final value: 1000 olmasıdır. Ancak çıktı çoğu zaman 1000'den küçük olur çünkü birden fazla thread aynı anda value’e erişip değiştirirken data race gerçekleşir.

Bunun çözüm yoluda actor kullanmakdır.

actor Counter {
private var value = 0

func increment() {
value += 1
}

func getValue() -> Int {
return value
}
}

let counter = Counter()

Task {
await withTaskGroup(of: Void.self) { group in
for _ in 0..<1000 {
group.addTask {
await counter.increment()
}
}
}

let result = await counter.getValue()
print("Final value: \(result)") // Her zaman 1000
}

withTaskGroup: Swift’in concurrency(eş zamanlılık) modelinde aynı anda birden fazla iş (task) başlatmak ve bunların hepsini takip edip bitmelerini beklemek için kullanılır.

Burada for döngüsü oluşturulmuş. Eğer withTaskGroup kullanılmasaydı. Bu döngü senkron sıralı çalışacaktı. Ancak withTaskGroup sayesinde 1000 tane eşzamanlı görev oluşturuyoruz. Her biri counter.increment() çağırmaktadır. Son olaraka tüm görevler tamamladığında devam edilmektedir. Yani kısaca asenkron çalışılması sağlanmaktadır.

Bu sayede beklenen sonuç alınmaktadır.

Tanım Bitişi

Debug kodumuza geri döner isek. Tanımda Counter için yapılan actor işlemi UserManager içinde gerçekleştirilecek.

Ancak UserManager içersinde flag (bayraklama) olarak adlandırılan bir işlemde gerçekleştirilecek.

Get Engin Gülek’s stories in your inbox

Join Medium for free to get updates from this writer.

İlk önce düzenlemiş kodu görelim sonra açıklamaları yapalım.

actor UserManager {
private var users: [User] = []
private var isFetching = false

func fetchUsers() async {
guard !isFetching else {
print("Zaten fetch işlemi çalışıyor.")
return
}

isFetching = true
defer { isFetching = false }

do {
print("Kullanıcılar getiriliyor...")
users = await API.fetchAllUsers() // Burada gerçek API çağrısı yapılıyor
print("Fetch tamamlandı.")
} catch {
print("Fetch hatası: \(error)")
}
}

func getUserCount() -> Int {
users.count
}
}

enum API {
static func fetchAllUsers() async -> [User] {
try? await Task.sleep(nanoseconds: 1_000_000_000)
return [User(id: 1, name: "Engin"), User(id: 2, name: "Ali")]
}
}

UserManager actor olarak tanımladık. Bu actor sayesinde kullanıcı listesini tutuyor ve dışarıdan güvenli bir şekilde güncellenebiliyor.

  • users : çekilen kullanıcıları saklıyor.
  • isFetching: değişkeni ise şu an fetch işlemlerinin devam edip etmediğini takip ediyor. Böylece aynı anda birden fazla fetch başlanmasının önüne geçilmekte.
func fetchUsers() async {
guard !isFetching else {
print("Zaten fetch işlemi çalışıyor.")
return
}

isFetching = true
defer { isFetching = false }

do {
print("Kullanıcılar getiriliyor...")
users = await API.fetchAllUsers()
print("Fetch tamamlandı.")
}
}
  • Öncelikle isFetching kontrolü yapıyoruz: Eğer halihazırda bir fetch işlemi varsa, yenisinin başlamasını engelliyoruz.
  • isFetching true yapıldıktan sonra defer ile fonksiyon bittiğinde tekrar false olacak şekilde ayarlanıyor.

Özet:

  • UserManager actor’ü ile kullanıcı listesi eş zamanlı erişim sorunları olmadan yönetilebilinmektedir.
  • Aynı anda birden fazla fetch çağrısının yapılması önlenmektedir

UserManager içerisinde yapılan değişiklikler böyledir.

Şimdi sıra UserViewModelda

@MainActor
class UserViewModel: ObservableObject {
@Published private(set) var userCount: Int = 0
private let manager = UserManager()
private var fetchTask: Task<Void, Never>? = nil

func onTappedButton() {
fetchTask?.cancel()

fetchTask = Task {
await manager.fetchUsers()
let count = await manager.getUserCount()
await MainActor.run { [weak self] in
self?.userCount = count
}
}
}
}

Burada yer alan @ MainActor bu sınıfta yer alan tüm kodların main thread’de çalışılması gerektiğini belirtir. UI ile ilgli işlemler mutlaka ana iş parçacığında yapılmalıdr. Yani DispathQueueMain.async yeri kullanılmaktadır.

fetchTask ise yapılan async işlemleri takip etmek ve daha önce başlatılmışsa iptal edebilmek için bir referanstır.

fetchTask?.cancel() ifadesi, daha önce başlatılan bir task’ı iptal eder. Bu sayede çakışmaların önüne geçilir.

    fetchTask = Task {
await manager.fetchUsers()
let count = await manager.getUserCount()
await MainActor.run { [weak self] in
self?.userCount = count
}
}
}

Yeni bir Task işlemi başlatılır. Kullanıcıları çekilme işlemi ve sayılarının alınam işlemi gerçekleştirilir. MainActor.run ile ana iş parçacığına geçilir. Çünkü UI üzerinde bir güncelleme işlemi gerçekleştirilecektir.

Evet debug işlemi bu kadardı. Eğer bir özet geçer isek;

Kullanıcı verilerini güvenli ve verimli şekilde yönetmek için actor tabanlı bir UserManager tanımlandı. Aynı anda birden fazla veri çekme işlemini engellemek amacıyla isFetching kontrolü eklendi. Böylece gereksiz API çağrılarının ve olası veri tutarsızlıklarının önüne geçildi.

Bu durum, gerçek hayatta bir kullanıcının beğeni butonuna art arda hızlıca basması gibi senaryolara benzetilebilir. Eğer her tıklamada aynı işlem tekrar tekrar yapılırsa, arka arkaya ağ istekleri gönderilir ve veri çakışmaları yaşanabilir. Bu yapıda, ilk işlem tamamlanmadan ikinci işlem başlatılmaz; yani kullanıcı ne kadar hızlı tıklarsa tıklasın sadece tek bir işlem gerçekleşir.

2. SwiftUI’de ViewModel Yönetimi: @StateObject ve @ObservedObject ile Performans Optimizasyonu

Soru : Aşağıdaki SwiftUI uygulamasında, kullanıcı “Yenile” butonuna bastığında viewModel’in fetchData fonksiyonu çağrılıyor. Ancak ekran her seferinde tüm viewModel’i yeniden oluşturuyor gibi davranıyor. Bu durum onAppear çağrısının tekrar çalışmasına ve fetchData fonksiyonunun iki kez çalışmasına neden oluyor.

import SwiftUI

class CartViewModel: ObservableObject {
@Published var items: [String] = []

init() {
print("ViewModel init")
}

func fetchData() {
print("Fetching data...")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.items = ["Elma", "Armut", "Muz"]
}
}
}

struct CartView: View {
@StateObject var viewModel = CartViewModel()

var body: some View {
VStack {
List(viewModel.items, id: \.self) { item in
Text(item)
}

Button("Yenile") {
viewModel.fetchData()
}
}
.onAppear {
viewModel.fetchData()
}
}
}

Kodu incelemeye başlayalım. CartViewModel adında ObservableObject ile bir viewModel oluşturulmuş. Ayrıca @ Published ile [String] ile items adında bir değer oluşturularak viewmodel’in oluşturulduğu view de dinlenebilinmesi sağlanmıştır. Bu durumu CartView içerisinde yer alan @ StateObject var viewModel = CartViewModel() satırından anlayabiliriz

@StateObject yalnızca view ilk kez oluşturulduğunda çalışır. Ancak CartView her NavigationLink tetiklendiğinde yeniden oluşturulduğu için, @StateObject de yeniden tetiklenir. Bu da ViewModel'in init()fonksiyonunun tekrar çalışmasına ve fetchData()’nın iki kez çağrılmasına neden olabilir. Bu durum genelede CartView’in dışarıdan sürekli yeniden yaratıldığı durumlarda gerçekleşir. Örneğin başka bir view tarafından .navigationDestination gibi navigation ile tetikleniyorsa.

Ayrıca StateObject yerine ObservedObject kullanılırsa, init her tetiklenişte çalışır. Bu da yaygın bir hatadır.

CartView bu gibi bir navigation ile çağrılıyor olabilir.

NavigationLink(destination: CartView()) {
Text("Git")
}

Bu durumda her NavigationLink tıklamasında yeni bir CartView yaratılır, bu da yeni bir @StateObject yaratılmasına neden olur.

Peki bu durum nasıl çözülecek ?

Ana bir Navigation oluşturarak CartView bir kez oluşturulmasını sağlanabilir.

Modular uygulamam için oluşturduğum HPNavigationKit ve yazını yazdığım yapı burada kullanım için uygundur. Link. Gerekli oluşturulmaların hepsi orada anlatılmıştır. Burada hepsi anlatılmauacaktır.

public class CartViewModule :CartModuleProrocol {
public init() { }

public func createCartModuleView() -> AnyView {
let view = CartView(viewModel: CartViewModel())
return AnyView(view)
}
}
// The Swift Programming Language
// https://docs.swift.org/swift-book

import SwiftUI
import DependencyKit
import HPModularProtocolsKit

//MARK: Page
/// Pages on the application
/// An enum representing pages in the application.
public enum Page : Hashable {
case cartView
}

//MARK: Navigation
/// Navigation class for managing the navigation stack.
public class Navigation : ObservableObject {
public init() { }
@Published public var path = NavigationPath()

/// Push a new page into the navigation stack.
public func push(_ page:Page){
path.append(page)

}
/// Pop the last page from the navigation stack.
public func pop() {
path.removeLast()
}
/// Pop all the pages, going back to the root page.
public func popToRoot(){
path.removeLast(path.count)
}

/// Build the appropriate view for a given page.
@ViewBuilder
public func build(page:Page) -> some View {
switch page {
case .cartView:
/// Dependency resolution and view creation for the home page
let cartViewModule:CartModuleProrocol = DependencyRegister.shared.resolve(CartViewModuleProrocol.self)
cartViewModule.createCartViewModuleView()

}
}
}

Artık CartView dışardan oluşturulacaktır. CartView navigation işlemi ise bunun yerine;

NavigationLink(destination: CartView()) {
Text("Git")
}

Bu şekilde olacaktır.

   @EnvironmentObject var navigation:Navigation
Button("Git") {
navigation.push(.cartView)
}

CartViewModel, CartView'e dışarıdan verildiği için bu referansı korumak için @ObservedObject kullanılmalıdır. @StateObject ,view içinde bir ViewModel’i ilk kez ve yalnızca o View içinde yaratmak için kullanılır. Dışarıdan gelen referanslar için @ObservedObject daha uygundur.

struct CartView: View {
@ObservedObject var viewModel: CartViewModel

var body: some View {
VStack {
List(viewModel.items, id: \.self) { item in
Text(item)
}

Button("Yenile") {
viewModel.fetchData()
}
}
.onAppear {
viewModel.fetchData()
}
}
}

Özet:

ViewModel’i @StateObject ile dışarda bir kere yarat ve referans olarak alt view’lere geçirir. Bu SwiftUI’de doğru ve performanslı ViewModel yönetimidir.

--

--