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.
Ç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.
Şimdi bu işlemler bittikten sonra ilk yapmamız gereken şey biz ne istiyoruz, hangi yol ile istediğim şeyi yaparsak daha kolay olur.
- FirstVC deki Butona tıkladığım zaman SecondVC’ye gitmek
- firstTextFieldaki yazının secondLabel da yazılması
- 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
buttonTapDelegate
i buraya atamazsın çünkü o birButtonTapProtocol
görevlisi, temsilcisi, çalışanı istiyor. Ama eğer sende kendineButtonTapProtocol
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 firstButton
a 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)
}
Şimdi sıra geldi 2. Maddeyi gerçekleştirmeye.
firstTextField
’a girilmiştext
değerinisecondLabel
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ı vesecondView
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 bircompletion 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ındangetText
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 secondView
sin 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 SecondView
daki 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.
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()
}
}
}