Photo by Clay Banks on Unsplash

6 Ayda(!) Mid Olmak İsteyen Jr.lar için: Swift Delegation Pattern

Merhaba ve hoşgeldiniz arkadaşlar bu yazımda Delegation Patterni kendi anladığım şekilde anlatmayı düşünüyorum çünkü çoğu yerde aynı şekilde anlatılıyor. İşte bir araç protocolu varmış ve gitmiş araba bu araç protocolunu kendine tanımladığı için sürme yetisi gelmiş vs vs bunlar zaten tamamen syntaxlık işler zor olan bence bu değil, zor olan gerçek uygulamada bunu nerede kullanıcağımızı bilemememiz. Buda hep ezberden oluyor işte. Bu yazımda size Delegation Patternin mantığını anlatmayı amaçlıyorum.

Yasin Özmen
11 min readJun 15, 2023

--

Çayınızı kahvenizi alın uzun bir yazı sizi bekliyor ☕️

eğer bir noktada sıkılır veya yanlış görürseniz lütfen geri bildirimde bulunun.

Baslamadan önce eğer Protocol ve Clouser terimlerini daha önce duymadıysanız ve görünce korkup kapatıcaksınız sizi böyle alayım.👇🏼

Swift Book Resmi dökümantasyon Clouse anlatımı
- Closures / Swift ile iOS Programlama — Uğur KILIÇ
- Swift Closures Explained — Sean Allen

- Protocols: Giriş — Ufuk KÖŞKER
-Protocols: İleri Seviye — Ufuk KÖŞKER

Gerçek Uygulamalardan gitmek istediğimi söylemiştim o yüzden hadi 2 ekranlı veri aktarımı yaptığımız bir app tasarlayalım.

View kısımlarını hızlıca geçiyorum

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

var window: UIWindow?
// MARK: - UI Scene Caller
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else {return}
let window = UIWindow(windowScene: windowScene)
let FirstVC = FirstVC()
let navController = UINavigationController(rootViewController: FirstVC)
window.rootViewController = navController
self.window = window
self.window?.makeKeyAndVisible()
}
}
/// Burada uygulamamızı FirstVC'yi gorünüm denetleyicisi seçmiş bir
/// Navigation Controllerdan başlatacağımızı söylüyoruz basitçe.
final class FirstView: UIView {
// MARK: - UI Elements
private let firstLabel: UILabel = {...}()
private let firstTF: UITextField = {...}()
private let firstButton: UIButton = {...}()
/// Label, TextField ve Buttonu Clouser içinde özelleştiriyorum.

// MARK: - Life Clycle
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Function
private func configure() {
backgroundColor = .systemGray5
setupFirstLabel()/// Bu üç fonksiyonu bir extensiyon ile oluşturdum
setupFirstTf()/// ve snapkit ile ekrana hizaladım
setupFirstButton()/// konumuz olmadıgı için doldurmuyorum ekranı.
}

// MARK: - Action
@objc func buttonTapped(){
}
// bu fonksiyon butonumuza tıklanınca çalışacak olan fonksiyon
}

Eğer programatik ui oluşturmak istiyorsanız bu kaynakları öneriririm
- Çok güzel videoları olan yabancı bir abimiz
- Hem Nasıl yapılacağını anlatması hemde püf noklardan bahsetmesi açısından güzel bir playlist
- Programatik ui öğrendikten sonra öğreneceğiniz ilk Şey SnapKit, asla standart Constraints ile zaman kaybetmeyin

final class FirstVC: UIViewController{
// MARK: - Properties
var firstView: FirstView?

// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}

// MARK: - Functions
private func configureUI() {
firstView = FirstView()
self.view = firstView
}
}

Ekran ilk açılınca gösterilecek olan VC’nin viewvini bizim tasarladığımız viewe eşitliyoruz. Peki neden firstView’i ilk olarak optinal bir değer olarak yapıyoruz ? Ekranda gösterilecek olan View i firstView e eşitlediğimiz için ViewControllerın hayatı bittiği zaman Ekranda gösterdiğimiz Viewinde hayatı bitmeli ki ekranda yeni bir şey gösterebilelim o yüzden bu şekilde bir değişken olarak tanımlayıp sonra Viewe eşitliyoruz.

class SecondView: UIView{
// MARK: - UI Elements
private var secondLabel: UILabel = {...}()
private let secondButton: UIButton = {...}()

// MARK: - Life Clycle
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Function
func configure() {
backgroundColor = .systemGray5
setupSecondLabel()//Bu fonksıyonlarda SnapKit kullanarak
setupSecondButton()//Elementlerı ekrana yerlestırmeye yarıyorlar
}
// MARK: - Action
@objc func buttonTapped(){
}
///Bu view de FirstView ile aynı mantıkda elementlerimizi tanımlıyoruz ve
///Init anında bu elementleri configure ediyoruz.
class SecondVC: UIViewController{
// MARK: - Varaibles
var secondView: SecondView?

// MARK: - Life Cycle
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
configureUI()
}

// MARK: - Functions
func configureUI() {
navigationItem.hidesBackButton = true
secondView = SecondView()
view = secondView
}
}// Buradaki her şey de FirstVC ile aynı o yüzden devam ediyorum.
Evet Viewlerimizi ve ViewControllerlarımızı tanımladık uygulama şu anda şöyle gözüküyor.
Evet Viewlerimizi ve ViewControllerlarımızı tanımladık uygulama şu anda şöyle gözüküyor.

Şimdi bu işlemler bittikten sonra ilk yapmamız gereken şey biz ne istiyoruz, hangi yol ile istediğim şeyi yaparsak daha kolay olur.

  1. FirstVC deki Butona tıkladığım zaman SecondVC’ye gitmek
  2. firstTextFieldaki yazının secondLabel da yazılması
  3. secondButton’a tıklandığı zaman FirstVC’ye gitmek

Şimki sıra sıra isteklerimizi yerine getirelim.

Öncelikle bir Protocol yazalım ve ismi ButtonTapProtocol olsun ve AnyObject olarak işaretleyelim çünkü bu protocolü sadece sınıfların benimsemesini istiyoruz.
Ayrıca 1.maddede istediğimiz görevi yerine getirecek bir fonksiyon tanımlayalım protocolümüzün içine

protocol ButtonTapProtocol: AnyObject {
func didTapButton()
}

Bütün bu tanımlamanın nerede yapıldığının hiç bir önemi yok çok büyük bir app yapıyorsanız ve delegate pattern kullanıyorsanız sırf
protocoller için ayrı bir swift dosyası açıp hepsini orada bile tutabilirsiniz. Protocoller de standart sınıflar ve structlar gibi her yerden çağırılabiliyor

Şimdi FirstViewimize gidelim çünkü butona tıklandığı zaman ilk uyarılan fonksiyonumuz orada.

Bir Nesneyi-Değişkeni eğerki protocol cinsinden oluşturursak o değişken bizim protocolün fonksiyonlarına/içeriğine erişmemize olanak sağlar.
Nasıl yani? Böyle ⬇️

weak var buttonTapDelegate: ButtonTapProtocol?

weak ne ayak derseniz buraya bakabilirsiniz

Biz de bu sayede Butona tıklanınca çalışan buttonTapped() fonksiyonunun içinde protocolümüzdeki didTapButton fonksiyonunu çağıracağız. Neden orada çağırıyoruz?
Çünkü didTapButton Foksiyonunun Butona tıklandıktan sonra
çalışmasını / çağrılmasını istiyoruz. buttonTapped() fonksiyonunun son hali bu şekilde

@objc func buttonTapped(){
buttonTapDelegate?.didTapButton()
}

Peki şimdi butona tıklanınca didTapButton çalışacak tamam ama nasıl çalışacak bu fonksiyonun içeriğini tanımlamadık ki ??
Aynen öyle bu fonksiyonun içeriğini tanımlamadık ayrıca hatırlarsınız ki
Delegatimiz daha nil, onu optional olarak tanımlamıştık.
Sıra geldi delegatimizin anlamını doldurmaya yani onu. Temsilci, vekil yapmaya.

Şimdi FirstVC’ye gidelim ve delegateImpemantent adında bir fonksiyon oluşturalım.
Oluşturalım oluşturalım ama niye oluşturalım?
Yukarıda da dediğim gibi firstView sayfamızdaki buttonTapDelegate şu anda nil durumda ve onu altını doldurmalıyız, eğer Delegate kelimesinin türkçe anlamıyla anlatmak gerekirse ona bir vekil, temsilci, sorumlu kişi vermeliyiz.

private func delegateImplemantent() {
firstView?.buttonTapDelegate = self
}

Ben bu kısmı kafamda tam anlamıyla oturttuktan sonra bazı seylerı anlamak benim için daha kolaydı o yüzden burada biraz zaman geçirebiliriz.

firstView.buttonTapDelegate = self derken ne diyoruz tane tane gidelim.
Öncelikle firstView dediğimiz zaman zaten bizim view sınıfımızdan bahsettiğini biliyoruz ve diyoruz ki firstView içindeki buttonTapDelegate değişkeni ben olmak istiyorum.
Hmm nasıl yani ben olmak istiyorum ?
Değişken değil de temsilci diyelim o zaman daha iyi oluyor.
Bak şimdi firstView sınıfında buttonTapDelegate temsilcisi ben olmak istiyorum. YanibuttonTapDelegate aslında bizim FirstVC miz ama biz buttonTapDelegate’i ButtonTapProtocol’den türettiğimiz için FirstVC Bizim ButtonTapProtocol ile ilgili olan işlerimizden sorumlu olacak.

Yani biz butona tıkladığımızda didTapButton fonksiyonunun çalışması gerekiyordu ya işte bu çalışma/çalıştırma görevini FirstVC ben üsteliyorum dedi. E tamam üstlensin ama nasıl üstlenecek onda bu fonksiyon yok, çok doğru zaten ben bunları demeye kalmadan Xcode❤️ bana kızdı ve böyle diyor ⬇️

Yani çevirmek gerekirse diyor ki ⬇️

Sen buttonTapDelegatei buraya atamazsın çünkü o bir ButtonTapProtocol görevlisi, temsilcisi, çalışanı istiyor. Ama eğer sende kendine ButtonTapProtocol eşlersen ve gerekli fonksiyonları kullanırsan, temsilci sen olabilirsin.

E süper bizimde istediğimiz buydu zaten diyoruz ve gerekli işlemleri yapıyoruz.

final class FirstVC: UIViewController, ButtonTapProtocol {
...
private func delegateImplemantent() {
firstView?.buttonTapDelegate = self
}
func didTapButton() {

}
...
}

Yukarıda da gördüğümüz gibi FirstVC sınıfımıza Protocolü işliyoruz ve ardından gerekli fonksiyonları edindikten sonra Xcode bize kızmayı bırakıyor ve Artık tescilli olarak FirstView’in bir Temsilcisi, çalışanı, görevlisi olmuş oluyoruz.

Hani o firstButtona tıklandığı zaman çalışan didTapButton vardı ya işte biz artık buttonTapDelegate olduğumuz için ve didTapButton fonksiyonuna da bizim sayemizde ulaşıldığı için artık o fonksiyon FirstVC içindeki didTapButton fonksiyonunun ta kendisiymiş gibi düşünebiliriz.

Şimdi 1. Maddemizi gerçekleştirebiliriz. firstButton’a tıklandığı zaman SecondVC’ye gitmek için aşağıdaki işlemleri yapmamız yeterli oluyor.

func didTapButton() {
navigationController?.pushViewController(secondVC, animated: true)
}
Ekran geçişimizi sağladık

Şimdi sıra geldi 2. Maddeyi gerçekleştirmeye.

firstTextField’a girilmiş text değerini secondLabel da göstermek.

1.Maddeden hatırladıklarımızla neler yapmamız gerektiğini düşünelim şimdi.

  • Girilen text değerini almalıyız.
  • Text değerini bir yerde tutmalıyız.
  • Diğer ekran yüklenmeden önce bu text değerini secondLabel'da göstermeliyiz

Girilen text değerini almak için zaten textin girildiği yerde çalışan bir fonksiyonumuz var didTapButton, Bu fonksiyonu String bir deger isteyecek sekilde bir parametre ekleyelim.

func didTapButton(text: String?) {
navigationController?.pushViewController(secondVC, animated: true)
}

Optional yapalım çünkü belki ileride başka şekillerde kullanılmak ister ve text değerini nil olarak girebilelim. 😉

Tamam şimdi text değerini alabiliyoruz bir diğer adım bu değeri SecondVC’de bir yerde tutmak ve göstermek için çalışması gereken diğer fonksiyonların çalışmasını beklemek. Eğer beklememiz gerekiyorsa biz yazıyı getirme işlemini ikinci ekrana gittikten sonra yapamayız, ekranımız gösterilmeden, ekranda ne yazacağını ekrana söylemiş olmamız lazım o zaman önce yazıyı götürelim, yazıyı depolayalım ve ne yazacağını söyleyelim sonra ekrana gidelim.

Bunları yapmak içinde bize protocol ve yeni bir temsilci lazım. Hadi bunları nerelerde yapmamız gerektiğini düşünelim.

  • Dediğimiz gibi Protocolün nerede tanımlandığının bir önemi yoktu o yüzden onu istediğimiz yerde oluşturabiliriz ve ismi de PrepareChangeLabelNameProtocol olsun. Hazırlamak diyorum çünkü önce texti bir yerde tutmalı ve secondView i hazırlamalıyız
protocol PrepareChangeLabelNameProtocol: AnyObject {
}
  • Peki Protocolümüzün fonksiyonları neler olmalı, düşünelim ne yapmak istiyoruz.
    Yazıyı getirmek, depolamak ve göstermek içinSecondView’i hazırlamak istiyoruz. O zaman fonksiyonlarımız şu şekilde olmalı. ⬇️
protocol PrepareChangeLabelNameProtocol: AnyObject {
func getText(text: String?, handler: (()->()))
func prepareChangeLabelText()
}
  • getText Fonksiyonunda bir completion handler var çünkü once yazıyı getirip kaydetmek ardından 2. ekranı görmek istiyorum.
  • Sırasıyla fonksiyonlarımızı yerleştirelim ve oluşturalım. Düzen sağlamak adına bir extension yazacağım.
extension SecondVC: PrepareChangeLabelNameProtocol{

func prepareChangeLabelText() {
}

func getText(text: String?, handler: (()->())){
}

}
  • SecondVC’nin üst tarafındagetText Fonksisyonu çalıştığı zaman aldığı değeri depolayabileceği bir String değişkeni oluşturalım ve ardından getText fonksiyonunun içini dolduralım.
class SecondVC: UIViewController{
// MARK: - Varaibles
var labelText:String = ""
...
}
extension SecondVC: PrepareChangeLabelNameProtocol{

func prepareChangeLabelText() {
}

func getText(text: String?, handler: (()->())){
if let text = text{
self.labelText = text
handler()
}
}

}

getText metodumuz labelText değişkenimize paremetresinde aldığı text değerini atadıktan sonra handlerda yazılmış değerleri çalıştırmaya gidiyor. Peki bu paremetreyi ve handlerı nerde alıyoruz tabiki de kendimize en uygun yerde ve bu şimdi bizim senaryomuz için FirstVC deki didTapButton fonksiyonun içi oluyor çünkü didTapButton fonksiyonumuz hem text paremetresini istiyor hem de SecondVC ye gitmeden önceki son fonksiyonumuz. Ama önce getText fonksiyonunu orda kullanabilmek icin bir delegate islemi yapmamız lazım bunu zaten 2 kere yaptığımız için artık detaylı anlatmıyorum sınıfımızın son halı bu şekilde ⬇️

final class FirstVC: UIViewController, ButtonTapProtocol {
// MARK: - Properties
let secondVC = SecondVC()
var firstView: FirstView?
var secondVCDelegate: PrepareChangeLabelNameProtocol?

// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
delegateImplemantent()
}

// MARK: - Functions
private func configureUI() {
firstView = FirstView()
self.view = firstView
}

private func delegateImplemantent() {
firstView?.buttonTapDelegate = self
secondVCDelegate = secondVC
}

func didTapButton(text: String?) {
secondVCDelegate?.getText(text: text){
navigationController?.pushViewController(secondVC, animated: true)
}
}
}

didTapButton methodumuzu da güncelledim ve secondVCDelegate ile eriştiğimiz getText metoduna firstTextField’dan aldığımız text değerini veriyoruz aynen.

Artık getText metodumuz biz butona bastığımız zaman çalışıyor ve text field a girdiğimiz değeri labelText değişkenine atıyor.

Şimdi sırada LabelText değişkenindeki değeri secondLabel da göstermek. Bunu ayarlamak için viewWillAppear metodumuzu kullanıcaz çünkü ekran görünmeden hazırlıklarımızı yapmak istiyoruz ayrıca viewWillAppear ekran her görünmeden önce çalışır.

SecondView’a bir protocol yapıyoruz bu da asıl işimizi, labelın değerini değiştirecek olan metodu barındırıcak ve bu metodu delegate yöntemiyle SecondVC nin viewWillAppear methodunda erişeceğiz ve istediğimiz gibi daha ekran görünmeden label hazırlanmış olacak.

protocol ChangeLabelTextProtocol:AnyObject {
func changeLabelText(text:String)
}
class SecondView: UIView,ChangeLabelTextProtocol {
......

func changeLabelText(text: String) {
secondLabel.text = text
}
}

Tammamm şimdi delegasyon atamalarımızı yapmamız lazım

class SecondVC: UIViewController, ButtonTapProtocol {
// MARK: - Varaibles
var changeLAbelDelegate: ChangeLabelTextProtocol?
var secondView: SecondView?
var labelText:String = ""

// MARK: - Life Cycle
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
configureUI()
prepareChangeLabelText()
}

// MARK: - Functions
func configureUI() {
navigationItem.hidesBackButton = true
secondView = SecondView()
view = secondView
}
}
extension SecondVC: PrepareChangeLabelNameProtocol{

func prepareChangeLabelText() {
changeLAbelDelegate = secondView
changeLAbelDelegate?.changeLabelText(text: labelText)
}

func getText(text: String?, handler: (()->())){
if let text = text{
self.labelText = text
handler()
}
}

}

Burda changeLabelDelegate bizim çalışanımız, yardımcımız, valimiz olacak ve biz ona prepareChangeLabelText metodunun içinde sen aslında secondViewsin diyorsuz çünkü labela erişim secondView’da daha kolay ve fonksiyonumuzu orada oluşturduk

Delegasyon atamasını yaptıktan hemen sonra delegasyon üzrinden changeLabelText fonksiyonunu çağırıyoruz yani SecondViewdaki changeLabelText fonksiyonunu çalıştırıyoruz ardından secondLabelın text değerine ona verdiğimiz labelText değerini işliyor.

Ve ekran göründüğü anda 🎉. firstTextFielda yazdığımız değer ekranda görünüyor.

Evet ben çok çok çok uzattım ve kesinlikle bu yaptığımız işlemin best practicesi bu değil ama iş ne kadar karmaşıklaşırsa ve bu karmaşıklığı ne kadar iyi anlatabilirsem sizde daha iyi oturacağını düşündüm bu yüzden bu kadar uzattım ve dolangaçlı biz yol izledim.

Yazımızı taşıyabiliyoruz

3. Aşama secondButton’a tıklayınca geri dönmek

Bunu da anlatmama gerek yoktur diye düşünüyorum görüp zaten ne yaptığımız anlarsınız.

Kodlari kesik kesik ve farklı zamanlarda verdiğim için kafanızda canlanmamışsa eğer bütün kodları en aşşağıya gereksiz yerleri kesilmiş şekilde koyuyorum.

Ve eğer indirip bakmak isteseniz GitHub linkim burada bir yıldızınızı alkışınızı ve takipinizi alırım 😂

Twitterdan takip edin, en az kendiminki kadar(!) güzel gördüğüm makaleleri sürekli paylaşıyorum 🥳

final class FirstView: UIView {
// MARK: - Varaibles
weak var buttonTapDelegate: ButtonTapProtocol?

// MARK: - UI Elements
private let firstLabel: UILabel = {...}()
private let firstTF: UITextField = {...}()
private let firstButton: UIButton = {...}()

// MARK: - Life Clycle
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Function
private func configure() {
backgroundColor = .systemGray5
setupFirstLabel()
setupFirstTf()
setupFirstButton()
}

// MARK: - Action
@objc func buttonTapped(){
buttonTapDelegate?.didTapButton(text: firstTF.text)
}

}
// MARK: - UI Configure Function With SnapKit
extension FirstView {
...
}

protocol ButtonTapProtocol: AnyObject {
func didTapButton(text: String?)
}
final class FirstVC: UIViewController, ButtonTapProtocol {
// MARK: - Properties
let secondVC = SecondVC()
var firstView: FirstView?
var secondVCDelegate: PrepareChangeLabelNameProtocol?

// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
delegateImplemantent()
}

// MARK: - Functions
private func configureUI() {
firstView = FirstView()
self.view = firstView
}

private func delegateImplemantent() {
firstView?.buttonTapDelegate = self
secondVCDelegate = secondVC
}

func didTapButton(text: String?) {
secondVCDelegate?.getText(text: text) {
navigationController?.pushViewController(secondVC, animated: true)
}
}
}
protocol ChangeLabelTextProtocol:AnyObject {
func changeLabelText(text:String)
}
class SecondView: UIView,ChangeLabelTextProtocol {
let secondVC = SecondVC()
weak var buttonTapDelegate: ButtonTapProtocol?

// MARK: - UI Elements
private var secondLabel: UILabel = {...}()
private let secondButton: UIButton = {...}()

// MARK: - Life Clycle
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Function
func configure() {
backgroundColor = .systemGray5
setupSecondLabel()
setupSecondButton()
}
// MARK: - Action
@objc func buttonTapped(){
buttonTapDelegate?.didTapButton(text: nil)
}
func changeLabelText(text: String) {
secondLabel.text = text
}
}
// MARK: - UI Configure Function With SnapKit
extension SecondView {
...
}
protocol PrepareChangeLabelNameProtocol: AnyObject {
func getText(text: String?, handler: (()->()))
func prepareChangeLabelText()
}
class SecondVC: UIViewController, ButtonTapProtocol {
// MARK: - Varaibles
var changeLAbelDelegate: ChangeLabelTextProtocol?
var secondView: SecondView?
var labelText:String = ""

// MARK: - Life Cycle
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
configureUI()
delegateImplemantent()
prepareChangeLabelText()
}

// MARK: - Functions
func configureUI() {
navigationItem.hidesBackButton = true
secondView = SecondView()
view = secondView
}

func delegateImplemantent(){
secondView?.buttonTapDelegate = self
}

func didTapButton(text: String?) {
navigationController?.popViewController(animated: true)
}
}

extension SecondVC: PrepareChangeLabelNameProtocol{

func prepareChangeLabelText() {
changeLAbelDelegate = secondView
changeLAbelDelegate?.changeLabelText(text: labelText)
}

func getText(text: String?, handler: (()->())){
if let text = text{
self.labelText = text
handler()
}
}
}

--

--