Swift'deki görünüm kontrolörleri ve diğer nesneler arasında verileri nasıl paylaşırsınız?


88

Diyelim ki Swift uygulamamda birden fazla görüntü denetleyicim var ve aralarında veri aktarabilmek istiyorum. Bir görünüm denetleyicisi yığınında birkaç düzey aşağıdaysam, verileri başka bir görünüm denetleyicisine nasıl aktarırım? Veya bir sekme çubuğu görünümü denetleyicisindeki sekmeler arasında mı?

(Not, bu soru bir "zil" dir.) O kadar çok soruluyor ki, konu hakkında bir eğitim yazmaya karar verdim. Aşağıdaki cevabıma bakın.


1
Delegeler için
Google'da arama yapmayı

4
Bunu, her gün SO'da bu sorunun 10.000 örneğine bir çözüm sağlayabilmek için gönderdim. Kendi kendime cevabımı görün. :)
Duncan C

Üzgünüm çok hızlı tepki verdim :) buna bağlanmak için iyi :)
milo526

2
Telaşa gerek yok. # 10.001 olduğumu düşündün, değil mi? <grin>
Duncan C

4
@DuncanC Cevabınızı beğenmedim. :( Her şeyi kapsayan bir senaryo cevabı olarak sorun değil ... insomuchas, her senaryo için işe yarayacak , ama aynı zamanda hemen hemen her senaryo için doğru yaklaşım da değil . konudaki herhangi bir soruyu bunun kopyası olarak işaretlemek iyi bir fikir mi lütfen, yapma.
nhgrif

Yanıtlar:


91

Sorunuz çok geniş. Her senaryoya tek bir basit çözüm olduğunu önermek biraz saflıktır. Öyleyse, bu senaryolardan bazılarına geçelim.


Deneyimlerime göre Stack Overflow'da sorulan en yaygın senaryo, bilgilerin bir görünüm denetleyicisinden diğerine basitçe aktarılmasıdır.

Film şeridi kullanıyorsak, ilk görünüm denetleyicimiz geçersiz kılabilir prepareForSegue, bu da tam olarak bunun için var. UIStoryboardSegueBu yöntem çağrıldığında bir nesne iletilir ve hedef görüntüleme denetleyicimize bir başvuru içerir. Burada geçmek istediğimiz değerleri ayarlayabiliriz.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "MySegueID" {
        if let destination = segue.destination as? SecondController {
            destination.myInformation = self.myInformation
        }
    }
}

Alternatif olarak, film şeridi kullanmıyorsak, görüntü denetleyicimizi bir uçtan yüklüyoruz. Kodumuz o zaman biraz daha basit.

func showNextController() {
    let destination = SecondController(nibName: "SecondController", bundle: nil)
    destination.myInformation = self.myInformation
    show(destination, sender: self)
}

Her iki durumda da, myInformationher görünüm denetleyicisinde bir görünüm denetleyicisinden diğerine geçirilmesi gereken verileri tutan bir özelliktir. Açıkça her kontrolörde aynı isme sahip olmaları gerekmiyor.


Ayrıca, bir UITabBarController.

Bu durumda, aslında potansiyel olarak daha da basit.

Öncelikle, bir alt sınıf oluşturalım UITabBarControllerve çeşitli sekmeler arasında paylaşmak istediğimiz bilgiler için ona özellikler verelim:

class MyCustomTabController: UITabBarController {
    var myInformation: [String: AnyObject]?
}

Biz film şeridinden bizim app inşa ediyoruz Şimdi, eğer biz sadece varsayılan bizim sekme çubuğu kontrolörün sınıfını değiştirmek UITabBarControlleriçin MyCustomTabController. Bir storyboard kullanmıyorsak, varsayılan UITabBarControllersınıf yerine bu özel sınıfın bir örneğini başlatır ve buna görünüm denetleyicimizi ekleriz.

Artık sekme çubuğu denetleyicisindeki tüm görünüm denetleyicilerimiz bu özelliğe şu şekilde erişebilir:

if let tbc = self.tabBarController as? MyCustomTabController {
    // do something with tbc.myInformation
}

Ve UINavigationControlleraynı şekilde alt sınıflandırma yaparak, verileri tüm gezinti yığınında paylaşmak için aynı yaklaşımı kullanabiliriz:

if let nc = self.navigationController as? MyCustomNavController {
    // do something with nc.myInformation
}

Birkaç başka senaryo var. Bu cevap hiçbir şekilde hepsini kapsamaz.


1
Ayrıca bazen bir kanalın bilgileri hedef görünüm denetleyicisinden kaynak görünüm denetleyicisine geri göndermesini istediğinizi de eklemek isterim. Bu durumu işlemenin yaygın bir yolu, hedefe bir temsilci özelliği eklemektir ve ardından kaynak görünümü denetleyicisinin readyForSegue'unda, hedef görünüm denetleyicisinin delegate özelliğini self olarak ayarlayın. (ve hedef VC'nin kaynak VC'ye mesaj göndermek için kullandığı mesajları tanımlayan bir protokol tanımlayın)
Duncan C

1
nhgrif, katılıyorum. Yeni geliştiricilerin tavsiyesi, film şeridindeki sahneler arasında veri aktarmanız gerekirse, şunu kullanmalısınız prepareForSegue. Bu çok basit gözlemin buradaki diğer cevaplar ve aralar arasında kaybolması çok kötü.
Rob

2
@Rob Yup. Tekli tonlar ve bildirimler son tercihler olmalıdır. Neredeyse her senaryoda prepareForSeguebaşka doğrudan bilgi aktarımlarını tercih etmeliyiz ve sonra acemiler bu durumların işe yaramadığı senaryoyu ortaya çıkardıklarında sorun olmayacak ve sonra onlara bu daha küresel yaklaşımları öğretmeliyiz.
nhgrif

1
Değişir. Ancak uygulama temsilcisini başka nereye koyacağımızı bilmediğimiz kod için çöplük alanımız olarak kullanmak konusunda çok endişeliyim. İşte deliliğe giden yol.
nhgrif

2
@nhgrif. cevabınız için teşekkürler. ne olursa olsun, verilerin örneğin 4 veya 5 görüntü denetleyicileri arasında aktarılmasını istiyorsanız İstemci oturum açma bilgilerini ve şifresini vb. yöneten 4-5 görüntü denetleyicisine sahipsem ve kullanıcının e-postasını bu görüntü denetleyicileri arasında iletmek istersem, bunu yapmanın her görüntü denetleyicisindeki var'ı bildirip daha sonra bunu prepareforsegue içinde geçirmekten daha uygun bir yolu var mı? Bir kez bildirebileceğim ve her görüntü denetleyicisinin ona erişebileceği bir yol var mı, ancak bu aynı zamanda iyi bir kodlama uygulaması mı?
lozflan

45

Bu soru her zaman ortaya çıkıyor.

Bir öneri, bir veri taşıyıcı tekli oluşturmaktır: Uygulamanızın ömrü boyunca bir kez ve yalnızca bir kez oluşturulan ve uygulamanızın ömrü boyunca varlığını sürdüren bir nesne.

Bu yaklaşım, uygulamanızdaki farklı sınıflarda kullanılabilir / değiştirilebilir olması gereken global uygulama verileriniz olduğu durumlar için çok uygundur.

Görünüm denetleyicileri arasında tek yönlü veya 2 yönlü bağlantılar kurma gibi diğer yaklaşımlar, bilgileri / mesajları doğrudan görünüm denetleyicileri arasında ilettiğiniz durumlar için daha uygundur.

(Diğer alternatifler için aşağıdaki nhgrif'in cevabına bakın.)

Bir veri kapsayıcısı tekliyle, sınıfınıza tekliğinize bir başvuru depolayan bir özellik eklersiniz ve daha sonra bu özelliği, erişime ihtiyacınız olduğunda kullanırsınız.

Singleton'ınızı, içeriğini diske kaydedecek şekilde ayarlayabilirsiniz, böylece uygulama durumunuz başlatmalar arasında devam eder.

GitHub'da bunu nasıl yapabileceğinizi gösteren bir demo proje oluşturdum. Bağlantı burada:

SwiftDataContainerSingleton projesi GitHub'da İşte o projedeki README:

SwiftDataContainerSingleton

Uygulama durumunu kaydetmek ve nesneler arasında paylaşmak için bir veri kabı tekli kullanmanın bir gösterimi.

DataContainerSingletonSınıf fiili tekil olduğunu.

sharedDataContainerSingleton'a bir referans kaydetmek için statik bir sabit kullanır .

Singleton'a erişmek için sözdizimini kullanın

DataContainerSingleton.sharedDataContainer

Örnek proje, veri kabındaki 3 özelliği tanımlar:

  var someString: String?
  var someOtherString: String?
  var someInt: Int?

someIntMülkü veri kabından yüklemek için aşağıdaki gibi bir kod kullanırsınız:

let theInt = DataContainerSingleton.sharedDataContainer.someInt

BazıInt'e bir değer kaydetmek için sözdizimini kullanırsınız:

DataContainerSingleton.sharedDataContainer.someInt = 3

DataContainerSingleton inityöntemi UIApplicationDidEnterBackgroundNotification,. Bu kod şuna benzer:

goToBackgroundObserver = NSNotificationCenter.defaultCenter().addObserverForName(
  UIApplicationDidEnterBackgroundNotification,
  object: nil,
  queue: nil)
  {
    (note: NSNotification!) -> Void in
    let defaults = NSUserDefaults.standardUserDefaults()
    //-----------------------------------------------------------------------------
    //This code saves the singleton's properties to NSUserDefaults.
    //edit this code to save your custom properties
    defaults.setObject( self.someString, forKey: DefaultsKeys.someString)
    defaults.setObject( self.someOtherString, forKey: DefaultsKeys.someOtherString)
    defaults.setObject( self.someInt, forKey: DefaultsKeys.someInt)
    //-----------------------------------------------------------------------------

    //Tell NSUserDefaults to save to disk now.
    defaults.synchronize()
}

Gözlemci kodunda, veri kabının özelliklerini içine kaydeder NSUserDefaults. NSCodingDurum verilerini kaydetmek için Core Data veya diğer çeşitli yöntemleri de kullanabilirsiniz .

DataContainerSingleton inityöntemi ayrıca özellikleri için kaydedilmiş değerleri yüklemeye çalışır.

İnit yönteminin bu kısmı şuna benzer:

let defaults = NSUserDefaults.standardUserDefaults()
//-----------------------------------------------------------------------------
//This code reads the singleton's properties from NSUserDefaults.
//edit this code to load your custom properties
someString = defaults.objectForKey(DefaultsKeys.someString) as! String?
someOtherString = defaults.objectForKey(DefaultsKeys.someOtherString) as! String?
someInt = defaults.objectForKey(DefaultsKeys.someInt) as! Int?
//-----------------------------------------------------------------------------

Değerleri NSUserDefaults'a yüklemek ve kaydetmek için anahtarlar DefaultsKeys, aşağıdaki gibi tanımlanan bir yapının parçası olan dize sabitleri olarak saklanır :

struct DefaultsKeys
{
  static let someString  = "someString"
  static let someOtherString  = "someOtherString"
  static let someInt  = "someInt"
}

Bu sabitlerden birine şöyle referansta bulunuyorsunuz:

DefaultsKeys.someInt

Veri taşıyıcı tekli kullanarak:

Bu örnek uygulama, veri kabının tek değerlikli kullanımını sağlar.

İki görünüm denetleyicisi vardır. İlki, UIViewController'ın özel bir alt sınıfı ViewController, ikincisi ise UIViewController'ın özel bir alt sınıfıdır SecondVC.

Her iki görünüm denetleyicisinin de üzerinde bir metin alanı vardır ve her ikisi de veri kabı singlelton someIntözelliğinden kendi viewWillAppearyöntemlerinde metin alanına bir değer yükler ve her ikisi de metin alanından geçerli değeri veri kabının `` someInt '' içine geri kaydeder.

Değeri metin alanına yükleyen kod şu viewWillAppear:yöntemdedir:

override func viewWillAppear(animated: Bool)
{
  //Load the value "someInt" from our shared ata container singleton
  let value = DataContainerSingleton.sharedDataContainer.someInt ?? 0
  
  //Install the value into the text field.
  textField.text =  "\(value)"
}

Kullanıcı tarafından düzenlenen değeri veri taşıyıcısına geri kaydetme kodu, görünüm denetleyicilerinin textFieldShouldEndEditingyöntemlerindedir:

 func textFieldShouldEndEditing(textField: UITextField) -> Bool
 {
   //Save the changed value back to our data container singleton
   DataContainerSingleton.sharedDataContainer.someInt = textField.text!.toInt()
   return true
 }

Görünüm denetleyicisi her görüntülendiğinde kullanıcı arabiriminizin güncellenmesi için viewDidLoad yerine viewWillAppear'da kullanıcı arabiriminize değerler yüklemelisiniz.


8
Buna eksi oy vermek istemiyorum çünkü soruyu ve cevabı bir kaynak olarak oluşturmak için zaman ayırmanızın mükemmel olduğunu düşünüyorum. Teşekkür ederim. Buna rağmen, yeni geliştiricilere model nesneler için tekli tonları savunmak için büyük bir kötülük yaptığımızı düşünüyorum. "Singletons are evil" kampında değilim (gerçi çaylaklar, sorunları daha iyi anlamak için bu cümleyi google'da aramalı), ancak model verilerinin tekillerin tartışmalı / tartışmalı bir kullanımı olduğunu düşünüyorum.
Rob

2 yönlü bağlantılar hakkında sizinki gibi harika bir yazı görmek
isterim

@Duncan C Merhaba Duncan Her modelde statik obje yapıyorum bu yüzden doğru olan herhangi bir yerden Data alıyorum veya yolunuzu takip etmem gerekiyor çünkü çok doğru görünüyor.
Virendra Singh Rathore

@VirendraSinghRathore, Global statik değişkenler, uygulama genelinde veri paylaşmanın mümkün olan en kötü yoludur. Uygulamanızın parçalarını birbirine sıkıca bağlarlar ve ciddi karşılıklı bağımlılıklar sağlarlar. Bu "çok doğru" nun tam tersidir.
Duncan C

@DuncanC - bu kalıp bir CurrentUser nesnesi için çalışır mı - temelde uygulamanızda oturum açmış tek bir kullanıcı? thx
timpone

9

Swift 4

Hızlı bir şekilde veri aktarımı için pek çok yaklaşım var. Burada en iyi yaklaşımlarından bazılarını ekliyorum.

1) StoryBoard Segue Kullanımı

Film şeridi segmentleri, Kaynak ve Hedef Görünümü Denetleyicileri arasında veri aktarımı için çok kullanışlıdır ve bunun tersi de geçerlidir.

// If you want to pass data from ViewControllerB to ViewControllerA while user tap on back button of ViewControllerB.
        @IBAction func unWindSeague (_ sender : UIStoryboardSegue) {
            if sender.source is ViewControllerB  {
                if let _ = sender.source as? ViewControllerB {
                    self.textLabel.text = "Came from B = B->A , B exited"
                }
            }
        }

// If you want to send data from ViewControllerA to ViewControllerB
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if  segue.destination is ViewControllerB {
                if let vc = segue.destination as? ViewControllerB {
                    vc.dataStr = "Comming from A View Controller"
                }
            }
        }

2) Temsilci Yöntemlerini Kullanma

ViewControllerD

//Make the Delegate protocol in Child View Controller (Make the protocol in Class from You want to Send Data)
    protocol  SendDataFromDelegate {
        func sendData(data : String)
    }

    import UIKit

    class ViewControllerD: UIViewController {

        @IBOutlet weak var textLabelD: UILabel!

        var delegate : SendDataFromDelegate?  //Create Delegate Variable for Registering it to pass the data

        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            textLabelD.text = "Child View Controller"
        }

        @IBAction func btnDismissTapped (_ sender : UIButton) {
            textLabelD.text = "Data Sent Successfully to View Controller C using Delegate Approach"
            self.delegate?.sendData(data:textLabelD.text! )
            _ = self.dismiss(animated: true, completion:nil)
        }
    }

ViewControllerC

    import UIKit

    class ViewControllerC: UIViewController , SendDataFromDelegate {

        @IBOutlet weak var textLabelC: UILabel!

        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
        }

        @IBAction func btnPushToViewControllerDTapped( _ sender : UIButton) {
            if let vcD = self.storyboard?.instantiateViewController(withIdentifier: "ViewControllerD") as?  ViewControllerD  {
                vcD.delegate = self // Registring Delegate (When View Conteoller D gets Dismiss It can call sendData method
    //            vcD.textLabelD.text = "This is Data Passing by Referenceing View Controller D Text Label." //Data Passing Between View Controllers using Data Passing
                self.present(vcD, animated: true, completion: nil)
            }
        }

        //This Method will called when when viewcontrollerD will dismiss. (You can also say it is a implementation of Protocol Method)
        func sendData(data: String) {
            self.textLabelC.text = data
        }

    }

StackOverflow yanıtlarını nereye koyacakları konusunda tamamen ve tamamen kaybolan Google çalışanları için, benim gibi Swift kod snippet'lerini her zaman kodun nereye gittiğini bilmeniz gerektiği varsayılıyordu: Göndermek için Seçenek 1) ' ViewControllerAi kullandım ViewControllerB. Kod parçacığını son küme parantezinin hemen önüne yapıştırdım ViewControllerA.swift( ViewControllerA.swiftaslında dosyanızın adı ne olursa olsun, tabii ki). " prepare" aslında belirli bir Sınıfta önceden var olan özel bir yerleşik işlevdir [hiçbir şey yapmaz], bu yüzden " override" onu " " yapmanız gerekir
velkoon

8

Diğer bir alternatif, bildirim merkezini (NSNotificationCenter) kullanmak ve bildirimleri yayınlamaktır. Bu çok gevşek bir bağlantı. Bir bildirimi gönderenin kimin dinlediğini bilmesi veya umursaması gerekmez. Sadece bir bildirim gönderir ve bunu unutur.

Belirli bir mesajı dinleyen rastgele sayıda gözlemci olabileceğinden, bildirimler birden çoğa mesaj geçişi için iyidir.


3
Bildirim merkezini kullanmanın belki de çok gevşek olan bir bağlantıya neden olduğunu unutmayın . Programınızın akışının izini sürmeyi çok zorlaştırabilir, bu nedenle dikkatli kullanılmalıdır.
Duncan C

2

Tek bir veri denetleyicisi oluşturmak yerine, bir veri denetleyici örneği oluşturmanızı ve bunu iletmenizi öneririm. Bağımlılık enjeksiyonunu desteklemek için önce bir DataControllerprotokol oluşturmalıyım :

protocol DataController {
    var someInt : Int {get set} 
    var someString : String {get set}
}

Sonra bir SpecificDataController(veya şu anda uygun olan ad ne olursa olsun) sınıf oluştururdum:

class SpecificDataController : DataController {
   var someInt : Int = 5
   var someString : String = "Hello data" 
}

Daha ViewControllersonra sınıfın dataController. Tipin dataControllerprotokol olduğuna dikkat edin DataController. Bu şekilde, veri denetleyici uygulamalarını değiştirmek kolaydır:

class ViewController : UIViewController {
   var dataController : DataController?
   ...
}

İçinde AppDelegateviewController'ı ayarlayabiliriz dataController:

 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    if let viewController = self.window?.rootViewController as? ViewController {
        viewController.dataController =  SpecificDataController()
    }   
    return true
}

Farklı bir viewController'a geçtiğimizde, şunu aktarabiliriz dataController:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    ...   
}

Şimdi, veri denetleyicisini farklı bir görev için değiştirmek istediğimizde, bunu şu şekilde yapabiliriz: AppDelegate için değiştirmek istediğimizde yapabiliriz ve veri denetleyicisini kullanan başka herhangi bir kodu değiştirmemiz gerekmez.

Tek bir değerin etrafından dolaşmak istiyorsak, bu elbette abartılıdır. Bu durumda nhgrif'in cevabıyla gitmek en iyisidir.

Bu yaklaşımla mantık kısmından görünümü ayırabiliriz.


1
Merhaba, bu yaklaşım temiz, test edilebilir ve çoğu zaman küçük uygulamalarda kullandığım şeydir, ancak her VC'nin (belki de kök VC'nin bile) bağımlılığa ihtiyaç duymadığı daha büyük uygulamalarda (örneğin, bu durumda DataController) her VC için bağımlılığı sadece onu geçmek için gerekli kılmak savurgan görünüyor. Ayrıca, farklı VC türleri kullanıyorsanız (örneğin, normal UIVC'ye karşı NavigationVC), yalnızca bu bağımlılık değişkenini eklemek için bu farklı türleri alt sınıflara ayırmanız gerekir. Buna nasıl yaklaşıyorsun?
RobertoCuba

1

@Nhgrif'in mükemmel cevabında belirttiği gibi, VC'lerin (görüntü denetleyicileri) ve diğer nesnelerin birbirleriyle iletişim kurmasının birçok farklı yolu vardır.

İlk cevabımda ana hatlarıyla belirttiğim veri tekli, doğrudan iletişimden çok küresel durumu paylaşmak ve kurtarmakla ilgili.

nhrif'in cevabı, bilgileri doğrudan kaynaktan hedef VC'ye göndermenizi sağlar. Cevapta bahsettiğim gibi, mesajları hedeften kaynağa geri göndermek de mümkün.

Aslında, farklı görüntü denetleyicileri arasında etkin bir tek yönlü veya 2 yönlü kanal kurabilirsiniz. Görünüm denetleyicileri bir film şeridi segmenti aracılığıyla bağlanırsa, bağlantıları kurma zamanı readyFor Segue yöntemindedir.

Github'da alt öğe olarak 2 farklı tablo görünümünü barındırmak için bir üst görünüm denetleyicisi kullanan örnek bir projem var. Alt görünüm denetleyicileri, gömme segmentleri kullanılarak bağlanır ve üst öğe görünüm denetleyicisi, readyForSegue yönteminde her görünüm denetleyicisiyle 2 yönlü bağlantıları bağlar.

Bu projeyi github'da bulabilirsiniz (link). Ancak bunu Objective-C'de yazdım ve Swift'e dönüştürmedim, bu yüzden Objective-C'de rahat değilseniz, takip etmek biraz zor olabilir


1

SWIFT 3:

Tanımlanmış bölümleri olan bir film şeridiniz varsa şunları kullanın:

func prepare(for segue: UIStoryboardSegue, sender: Any?)

Farklı UIViewControllers arasında gezinme dahil olmak üzere her şeyi programlı olarak yapsanız da, şu yöntemi kullanın:

func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)

Not: UINavigationController'ınızı yapmak için ihtiyaç duyduğunuz ikinci yolu kullanmak için, UIViewControllers'ı bir temsilciye zorluyorsunuz ve UINavigationControllerDelegate protokolüne uyması gerekiyor:

   class MyNavigationController: UINavigationController, UINavigationControllerDelegate {

    override func viewDidLoad() {
        self.delegate = self
    }

    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {

     // do what ever you need before going to the next UIViewController or back
     //this method will be always called when you are pushing or popping the ViewController

    }
}

asla self.delegate yapma = self
malhal

1

Ne zaman veri almak istediğinize bağlıdır.

İstediğiniz zaman veri almak istiyorsanız, tekli model kullanabilirsiniz. Kalıp sınıfı, uygulama çalışma süresi boyunca etkindir. İşte tekli modelin bir örneği.

class AppSession: NSObject {

    static let shared = SessionManager()
    var username = "Duncan"
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        print(AppSession.shared.username)
    }
}

Herhangi bir eylemden sonra veri almak istiyorsanız, NotificationCenter'ı kullanabilirsiniz.

extension Notification.Name {
    static let loggedOut = Notification.Name("loggedOut")
}

@IBAction func logoutAction(_ sender: Any) {
    NotificationCenter.default.post(name: .loggedOut, object: nil)
}

NotificationCenter.default.addObserver(forName: .loggedOut, object: nil, queue: OperationQueue.main) { (notify) in
    print("User logged out")
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.