Bagaimana Anda berbagi data antara pengontrol tampilan dan objek lain di Swift?

88

Katakanlah saya memiliki beberapa pengontrol tampilan di aplikasi Swift saya dan saya ingin dapat meneruskan data di antara mereka. Jika saya beberapa level di bawah dalam tumpukan pengontrol tampilan, bagaimana cara meneruskan data ke pengontrol tampilan lain? Atau di antara tab dalam pengontrol tampilan bilah tab?

(Catatan, pertanyaan ini adalah "nada dering".) Pertanyaan ini begitu banyak sehingga saya memutuskan untuk menulis tutorial tentang subjek tersebut. Lihat jawaban saya di bawah.

Duncan C
sumber
1
Coba googling untuk delegasi
milo526
4
Saya memposting ini sehingga saya dapat memberikan solusi untuk 10.000 contoh pertanyaan ini yang muncul setiap hari di SO. Lihat jawaban saya sendiri. :)
Duncan C
Maaf saya terlalu cepat bereaksi :) bagus untuk dapat ditautkan ke ini :)
milo526
2
Jangan khawatir. Anda pikir saya # 10.001, bukan? <grin>
Duncan C
4
@DuncanC Saya tidak suka jawaban Anda. :( Tidak apa-apa sebagai jawaban untuk semua skenario ... insomuchas, ini akan berhasil untuk setiap skenario, tetapi ini juga bukan pendekatan yang tepat untuk hampir semua skenario. Meskipun demikian, sekarang kita sudah memikirkannya bahwa menandai pertanyaan apa pun pada topik sebagai duplikat dari pertanyaan ini adalah ide yang bagus? Tolong, jangan.
nhgrif

Jawaban:

91

Pertanyaan Anda sangat luas. Untuk menyarankan bahwa ada satu solusi sederhana untuk semua skenario adalah sedikit naif. Jadi, mari kita lihat beberapa skenario ini.


Skenario paling umum yang ditanyakan tentang Stack Overflow dalam pengalaman saya adalah informasi yang lewat sederhana dari satu pengontrol tampilan ke yang berikutnya.

Jika kita menggunakan storyboard, pengontrol tampilan pertama kita dapat menimpa prepareForSegue, untuk itulah tepatnya. Sebuah UIStoryboardSegueobjek dilewatkan saat metode ini dipanggil, dan itu berisi referensi ke pengontrol tampilan tujuan kita. Di sini, kita dapat mengatur nilai yang ingin kita teruskan.

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

Alternatifnya, jika kita tidak menggunakan storyboard, maka kita memuat pengontrol tampilan kita dari ujung. Kode kami sedikit lebih sederhana.

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

Dalam kedua kasus, myInformationadalah properti pada setiap pengontrol tampilan yang menyimpan data apa pun yang perlu diteruskan dari satu pengontrol tampilan ke pengontrol berikutnya. Mereka jelas tidak harus memiliki nama yang sama di setiap pengontrol.


Kami mungkin juga ingin berbagi informasi antar tab di file UITabBarController.

Dalam hal ini, sebenarnya berpotensi lebih sederhana.

Pertama, mari buat subkelas UITabBarController, dan berikan properti untuk informasi apa pun yang ingin kita bagikan di antara berbagai tab:

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

Sekarang, jika kita membangun aplikasi kita dari storyboard, kita cukup mengubah kelas pengontrol tab bar kita dari default UITabBarControllermenjadi MyCustomTabController. Jika kita tidak menggunakan storyboard, kita cukup membuat instance dari kelas kustom ini daripada UITabBarControllerkelas default dan menambahkan pengontrol tampilan kita ke ini.

Sekarang, semua pengontrol tampilan kita dalam pengontrol bilah tab dapat mengakses properti ini seperti:

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

Dan dengan membuat subclass UINavigationControllerdengan cara yang sama, kita dapat mengambil pendekatan yang sama untuk berbagi data di seluruh tumpukan navigasi:

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

Ada beberapa skenario lainnya. Tidak berarti jawaban ini mencakup semuanya.

nhgrif
sumber
1
Saya juga menambahkan bahwa terkadang Anda ingin saluran mengirim informasi kembali dari pengontrol tampilan tujuan ke pengontrol tampilan sumber. Cara yang umum untuk menangani situasi tersebut adalah dengan menambahkan properti delegasi ke tujuan, dan kemudian di presenterForSegue pengontrol tampilan sumber, setel properti delegasi pengontrol tampilan tujuan ke self. (dan tentukan protokol yang menentukan pesan yang digunakan VC tujuan untuk mengirim pesan ke VC sumber)
Duncan C
1
nhgrif, saya setuju. Nasihat bagi pengembang baru adalah jika Anda perlu menyampaikan data antar adegan di papan cerita, gunakan prepareForSegue. Sayang sekali pengamatan yang sangat sederhana ini hilang di antara jawaban dan penyimpangan lain di sini.
Rob
2
@Rob Yup. Lajang dan notifikasi harus menjadi pilihan terakhir. Kita harus memilih prepareForSegueatau transfer informasi langsung lainnya di hampir setiap skenario dan kemudian baik-baik saja dengan para pemula ketika mereka muncul dengan skenario di mana situasi ini tidak berhasil dan kita kemudian harus mengajari mereka tentang pendekatan yang lebih global ini.
nhgrif
1
Tergantung. Tapi saya sangat, sangat prihatin tentang penggunaan delegasi aplikasi sebagai tempat pembuangan kode, kita tidak tahu harus meletakkannya di mana. Di sinilah letak jalan menuju kegilaan.
nhgrif
2
@tokopedia terima kasih atas jawaban Anda. bagaimana jika bagaimanapun Anda ingin data diteruskan antara katakanlah 4 atau 5 viewcontrollers. jika ive harus mengatakan 4-5 viewcontrollers mengelola login klien dan kata sandi dll dan saya ingin meneruskan email pengguna antara viewcontrollers ini, apakah ada cara yang lebih mudah untuk melakukan ini daripada mendeklarasikan var di setiap viewcontroller kemudian meneruskannya dalam preparasi. apakah ada cara saya dapat mendeklarasikan sekali dan setiap viewcontroller dapat mengaksesnya tetapi dengan cara yang juga merupakan praktik pengkodean yang baik?
lozflan
45

Pertanyaan ini selalu muncul setiap saat.

Salah satu sarannya adalah membuat penampung data tunggal: Objek yang dibuat sekali dan hanya sekali dalam kehidupan aplikasi Anda, dan terus berlanjut selama masa aktif aplikasi Anda.

Pendekatan ini sangat sesuai untuk situasi ketika Anda memiliki data aplikasi global yang perlu tersedia / dapat dimodifikasi di berbagai kelas dalam aplikasi Anda.

Pendekatan lain seperti menyiapkan tautan satu arah atau 2 arah antara pengontrol tampilan lebih cocok untuk situasi di mana Anda menyampaikan informasi / pesan secara langsung antara pengontrol tampilan.

(Lihat jawaban nhgrif, di bawah, untuk alternatif lain.)

Dengan penampung data tunggal, Anda menambahkan properti ke kelas Anda yang menyimpan referensi ke tunggal Anda, lalu menggunakan properti itu kapan pun Anda membutuhkan akses.

Anda bisa mengatur singleton Anda sehingga ia menyimpan isinya ke disk sehingga status aplikasi Anda tetap ada di antara peluncuran.

Saya membuat proyek demo di GitHub yang mendemonstrasikan bagaimana Anda dapat melakukan ini. Ini tautannya:

Proyek SwiftDataContainerSingleton di GitHub Berikut adalah README dari proyek itu:

SwiftDataContainerSingleton

Demonstrasi menggunakan wadah data tunggal untuk menyimpan status aplikasi dan membagikannya di antara objek.

The DataContainerSingletonkelas adalah tunggal yang sebenarnya.

Ini menggunakan konstanta statis sharedDataContaineruntuk menyimpan referensi ke singleton.

Untuk mengakses singleton, gunakan sintaks

DataContainerSingleton.sharedDataContainer

Proyek sampel mendefinisikan 3 properti dalam penampung data:

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

Untuk memuat someIntproperti dari penampung data, Anda akan menggunakan kode seperti ini:

let theInt = DataContainerSingleton.sharedDataContainer.someInt

Untuk menyimpan nilai ke someInt, Anda akan menggunakan sintaks:

DataContainerSingleton.sharedDataContainer.someInt = 3

Metode DataContainerSingleton initmenambahkan pengamat untuk UIApplicationDidEnterBackgroundNotification. Kode itu terlihat seperti ini:

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()
}

Dalam kode pengamat itu menyimpan properti penampung data ke NSUserDefaults. Anda juga dapat menggunakan NSCodingCore Data, atau berbagai metode lain untuk menyimpan data status.

Metode DataContainerSingleton initjuga mencoba memuat nilai yang disimpan untuk propertinya.

Bagian dari metode init tersebut terlihat seperti ini:

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?
//-----------------------------------------------------------------------------

Kunci untuk memuat dan menyimpan nilai ke NSUserDefaults disimpan sebagai konstanta string yang merupakan bagian dari sebuah struct DefaultsKeys, didefinisikan seperti ini:

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

Anda mereferensikan salah satu konstanta ini seperti ini:

DefaultsKeys.someInt

Menggunakan wadah data tunggal:

Aplikasi sampel ini membuat penggunaan penampung data tunggal secara trival.

Ada dua pengontrol tampilan. Yang pertama adalah subkelas khusus UIViewController ViewController, dan yang kedua adalah subkelas khusus UIViewController SecondVC.

Kedua pengontrol tampilan memiliki bidang teks di atasnya, dan keduanya memuat nilai dari properti penampung data singlelton someIntke bidang teks dalam viewWillAppearmetode mereka , dan keduanya menyimpan nilai saat ini dari bidang teks kembali ke `someInt 'penampung data.

Kode untuk memuat nilai ke dalam bidang teks ada di viewWillAppear:metode:

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)"
}

Kode untuk menyimpan nilai yang diedit pengguna kembali ke penampung data ada di textFieldShouldEndEditingmetode pengontrol tampilan :

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

Anda harus memuat nilai ke antarmuka pengguna Anda di viewWillAppear daripada viewDidLoad sehingga UI Anda diperbarui setiap kali pengontrol tampilan ditampilkan.

Duncan C
sumber
8
Saya tidak ingin meremehkan ini karena menurut saya sangat baik Anda menginvestasikan waktu untuk membuat pertanyaan dan jawaban sebagai sumber daya. Terima kasih. Meskipun demikian, saya pikir kami melakukan tindakan merugikan yang besar bagi pengembang baru untuk menganjurkan lajang untuk objek model. Saya tidak berada di kamp "lajang yang jahat" (meskipun noobs harus google frase itu untuk lebih menghargai masalah), tapi menurut saya data model adalah penggunaan lajang dipertanyakan / diperdebatkan.
Rob
ingin melihat tulisan yang luar biasa seperti milik Anda tentang tautan 2 arah
Cmag
@ Duncan C Halo Duncan Saya membuat objek statis di setiap model jadi saya mendapatkan Data dari mana saja pendekatan yang benar atau saya harus mengikuti jalan Anda karena Tampaknya sangat benar.
Virendra Singh Rathore
@VirendraSinghRathore, Variabel statis global adalah cara terburuk untuk berbagi data di seluruh aplikasi. Mereka sangat erat menggabungkan bagian-bagian aplikasi Anda dan memperkenalkan saling ketergantungan yang serius. Ini adalah kebalikan dari "sangat benar".
Duncan C
@DuncanC - apakah pola ini akan berfungsi untuk objek CurrentUser - pada dasarnya satu pengguna yang masuk ke aplikasi Anda? thx
timpone
9

Cepat 4

Ada begitu banyak pendekatan untuk pengiriman data dengan cepat. Di sini saya menambahkan beberapa pendekatan terbaik darinya.

1) Menggunakan Segue StoryBoard

Segmen papan cerita sangat berguna untuk meneruskan data di antara Pengontrol Tampilan Tujuan dan Sumber dan juga sebaliknya.

// 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) Menggunakan Metode Delegasi

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
        }

    }
Tim iOS
sumber
Untuk Googler yang sama sekali dan benar-benar bingung di mana harus meletakkan cuplikan kode Swift jawaban StackOverflow seperti saya, karena tampaknya Anda harus selalu tahu ke mana mereka menyimpulkan kode tersebut: Saya menggunakan Opsi 1) untuk mengirim dari ViewControllerAke ViewControllerB. Saya hanya menempelkan potongan kode di bagian bawah saya ViewControllerA.swift(di mana ViewControllerA.swiftsebenarnya apa pun nama file Anda, tentu saja) tepat sebelum kurung kurawal terakhir. " prepare" sebenarnya adalah fungsi khusus yang sudah ada sebelumnya di Kelas tertentu [yang tidak melakukan apa-apa], itulah sebabnya Anda harus " override" melakukannya
velkoon
8

Alternatif lain adalah dengan menggunakan pusat pemberitahuan (NSNotificationCenter) dan pemberitahuan posting. Itu adalah kopling yang sangat longgar. Pengirim notifikasi tidak perlu tahu atau peduli siapa yang mendengarkan. Itu hanya memposting pemberitahuan dan melupakannya.

Notifikasi bagus untuk penyampaian pesan satu-ke-banyak, karena bisa saja terdapat sejumlah pengamat yang mendengarkan pesan tertentu.

Duncan C
sumber
2
Perhatikan bahwa menggunakan pusat notifikasi memperkenalkan kopling yang mungkin terlalu longgar. Hal ini dapat membuat penelusuran aliran program Anda menjadi sangat sulit, sehingga harus digunakan dengan hati-hati.
Duncan C
2

Alih-alih membuat pengontrol data singelton, saya akan menyarankan untuk membuat contoh pengontrol data dan menyebarkannya. Untuk mendukung injeksi ketergantungan, saya pertama-tama akan membuat DataControllerprotokol:

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

Kemudian saya akan membuat kelas SpecificDataController(atau nama apa pun yang saat ini sesuai):

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

The ViewControllerkelas maka harus memiliki lapangan untuk memegang dataController. Perhatikan bahwa tipe dataControlleradalah protokol DataController. Dengan cara ini, mudah untuk mengganti implementasi pengontrol data:

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

Di dalam AppDelegatekita dapat mengatur viewController dataController:

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

Saat kita pindah ke viewController yang berbeda, kita bisa meneruskannya dataControllerdi:

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

Sekarang ketika kita ingin mengganti pengontrol data untuk tugas lain, kita dapat melakukannya di AppDelegatedan tidak perlu mengubah kode lain yang menggunakan pengontrol data.

Ini tentu saja berlebihan jika kita hanya ingin meneruskan satu nilai. Dalam hal ini yang terbaik adalah menggunakan jawaban nhgrif.

Dengan pendekatan ini kita dapat memisahkan tampilan dari bagian logika.

Kristiina
sumber
1
Halo, pendekatan ini bersih, dapat diuji, dan apa yang saya gunakan sebagian besar waktu dalam aplikasi kecil, tetapi dalam aplikasi yang lebih besar, di mana tidak setiap VC (bahkan mungkin bukan root VC) mungkin memerlukan ketergantungan (misalnya DataController dalam kasus ini) tampaknya boros untuk setiap VC yang membutuhkan ketergantungan hanya untuk menyebarkannya. Selain itu, jika Anda menggunakan jenis VC yang berbeda (misalnya UIVC biasa versus NavigationVC), Anda perlu membuat subkelas jenis yang berbeda tersebut hanya untuk menambahkan variabel dependensi itu. Bagaimana Anda mendekati ini?
RobertoCuba
1

Seperti yang ditunjukkan @nhgrif dalam jawabannya yang sangat bagus, ada banyak cara berbeda yang dapat digunakan VC (pengontrol tampilan) dan objek lain untuk berkomunikasi satu sama lain.

Data tunggal yang saya uraikan dalam jawaban pertama saya lebih banyak tentang berbagi dan menyelamatkan keadaan global daripada tentang berkomunikasi secara langsung.

Jawaban nhrif memungkinkan Anda mengirim informasi langsung dari sumber ke VC tujuan. Seperti yang saya sebutkan dalam balasan, itu juga memungkinkan untuk mengirim pesan kembali dari tujuan ke sumbernya.

Faktanya, Anda dapat mengatur saluran satu arah atau 2 arah yang aktif antara pengontrol tampilan yang berbeda. Jika pengontrol tampilan ditautkan melalui segmen storyboard, waktu untuk menyiapkan tautan berada dalam metode preparedFor Segue.

Saya memiliki proyek sampel di Github yang menggunakan pengontrol tampilan orang tua untuk menjadi tuan rumah 2 tampilan tabel yang berbeda sebagai anak-anak. Pengontrol tampilan anak ditautkan menggunakan segue sematan, dan pengontrol tampilan induk menyambungkan tautan 2 arah dengan setiap pengontrol tampilan dalam metode preparedForSegue.

Anda dapat menemukan proyek itu di github (tautan). Saya menulisnya di Objective-C, bagaimanapun, dan belum mengubahnya menjadi Swift, jadi jika Anda tidak nyaman dengan Objective-C, mungkin akan sedikit sulit untuk diikuti.

Duncan C
sumber
1

SWIFT 3:

Jika Anda memiliki storyboard dengan segues teridentifikasi, gunakan:

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

Meskipun jika Anda melakukan semuanya secara terprogram termasuk navigasi antara UIViewControllers yang berbeda, gunakan metode ini:

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

Catatan: untuk menggunakan cara kedua, Anda perlu membuat UINavigationController, Anda mendorong UIViewControllers, sebuah delegasi, dan harus menyesuaikan dengan protokol UINavigationControllerDelegate:

   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

    }
}
Pepatah
sumber
tidak pernah melakukan self.delegate = self
malhal
1

Itu tergantung kapan Anda ingin mendapatkan data.

Jika ingin mendapatkan data kapanpun Anda mau, bisa menggunakan pola singleton. Kelas pola aktif selama runtime aplikasi. Berikut adalah contoh pola singleton.

class AppSession: NSObject {

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

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

        print(AppSession.shared.username)
    }
}

Jika Anda ingin mendapatkan data setelah tindakan apa pun, bisa menggunakan NotificationCenter.

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")
}
Yusuf
sumber