ChatGPT解决这个技术问题 Extra ChatGPT

如何在 Swift 中的视图控制器和其他对象之间共享数据?

假设我的 Swift 应用程序中有多个视图控制器,我希望能够在它们之间传递数据。如果我在视图控制器堆栈中下降了几个级别,我如何将数据传递给另一个视图控制器?或者在标签栏视图控制器的标签之间?

(注意,这个问题是一个“响铃”。)它被问到太多以至于我决定写一篇关于这个主题的教程。请看下面我的回答。

尝试在谷歌上搜索代表
我发布了这个,以便我可以为每天在 SO 上出现的 10,000 个这个问题的实例提供解决方案。看我的自我回答。 :)
抱歉,我反应太快了 :) 很高兴能够链接到这个 :)
不用担心。你以为我是#10,001,不是吗? <咧嘴笑>
@DuncanC 我不喜欢你的回答。 :( 没关系 - 作为一个包罗万象的每个场景答案...... insomuchas,它适用于所有场景,但它也不是几乎任何场景的正确方法。尽管如此,我们现在已经把它放在我们的脑海中将有关该主题的任何问题标记为该问题的副本是个好主意?请不要。

R
Rob

你的问题非常广泛。建议每个场景都有一个简单的包罗万象的解决方案有点天真。所以,让我们来看看其中的一些场景。

根据我的经验,在 Stack Overflow 上被问到的最常见的场景是从一个视图控制器到下一个视图控制器的简单传递信息。

如果我们使用故事板,我们的第一个视图控制器可以覆盖 prepareForSegue,这正是它的用途。调用此方法时会传入一个 UIStoryboardSegue 对象,它包含对目标视图控制器的引用。在这里,我们可以设置我们想要传递的值。

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

或者,如果我们不使用故事板,那么我们将从 nib 加载我们的视图控制器。那么我们的代码稍微简单一些。

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

在这两种情况下,myInformation 都是每个视图控制器上的一个属性,它包含需要从一个视图控制器传递到下一个视图控制器的任何数据。它们显然不必在每个控制器上具有相同的名称。

我们可能还希望在 UITabBarController 中的选项卡之间共享信息。

在这种情况下,它实际上可能更简单。

首先,让我们创建一个 UITabBarController 的子类,并为其提供我们想要在各个选项卡之间共享的任何信息的属性:

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

现在,如果我们从情节提要构建我们的应用程序,我们只需将标签栏控制器的类从默认的 UITabBarController 更改为 MyCustomTabController。如果我们不使用情节提要,我们只需实例化此自定义类的实例而不是默认的 UITabBarController 类,并将我们的视图控制器添加到其中。

现在,标签栏控制器中的所有视图控制器都可以访问此属性:

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

通过以相同方式子类化 UINavigationController,我们可以采用相同的方法在整个导航堆栈中共享数据:

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

还有其他几种情况。这个答案绝不涵盖所有这些。


我还要补充一点,有时您希望通道将信息从目标视图控制器发送回源视图控制器。处理这种情况的常用方法是将委托属性添加到目标,然后在源视图控制器的 prepareForSegue 中,将目标视图控制器的委托属性设置为 self。 (并定义一个协议,定义目标 VC 用来向源 VC 发送消息的消息)
恩格里夫,我同意。对新开发者的建议应该是,如果您需要在情节提要的场景之间传递数据,请使用 prepareForSegue。太糟糕了,这个非常简单的观察在这里的其他答案和题外话中丢失了。
@Rob 是的。单例和通知应该是最后的选择。在几乎所有场景中,我们应该更喜欢prepareForSegue或其他直接信息传输,然后当新手出现这些情况不起作用的场景时,我们就可以接受教他们这些更全球化的方法。
这取决于。但我非常非常担心将应用程序委托作为我们不知道该放在哪里的代码的垃圾场。这里是通往疯狂的道路。
@nhgrif。谢谢你的回答。但是,如果您希望在 4 或 5 个视图控制器之间传递数据怎么办。如果我说 4-5 个视图控制器管理客户端登录名和密码等,并且我想在这些视图控制器之间传递用户的电子邮件,有没有比在每个视图控制器中声明 var 然后在 prepareforsegue 中传递它更方便的方法来做到这一点。有没有一种方法我可以声明一次并且每个视图控制器都可以访问它,但同时也是一种很好的编码习惯?
C
Community

这个问题无时无刻不在出现。

一个建议是创建一个数据容器单例:在应用程序的生命周期中创建一次且仅一次的对象,并在应用程序的生命周期中持续存在。

这种方法非常适合当您拥有需要在应用程序中的不同类之间可用/可修改的全局应用程序数据的情况。

其他方法,例如在视图控制器之间设置单向或双向链接,更适合您在视图控制器之间直接传递信息/消息的情况。

(有关其他选择,请参见下面的 nhgrif 的回答。)

使用数据容器单例,您可以将一个属性添加到存储对您的单例的引用的类中,然后在您需要访问时随时使用该属性。

您可以设置您的单例,以便将其内容保存到磁盘,以便您的应用程序状态在启动之间保持不变。

我在 GitHub 上创建了一个演示项目,演示如何做到这一点。链接在这里:

SwiftDataContainerSingleton project on GitHub 以下是该项目的自述文件:

SwiftDataContainerSingleton

使用数据容器单例保存应用程序状态并在对象之间共享的演示。

DataContainerSingleton 类是实际的单例。

它使用静态常量 sharedDataContainer 来保存对单例的引用。

要访问单例,请使用语法

DataContainerSingleton.sharedDataContainer

示例项目在数据容器中定义了 3 个属性:

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

要从数据容器加载 someInt 属性,您可以使用如下代码:

let theInt = DataContainerSingleton.sharedDataContainer.someInt

要将值保存到 someInt,您可以使用以下语法:

DataContainerSingleton.sharedDataContainer.someInt = 3

DataContainerSingleton 的 init 方法为 UIApplicationDidEnterBackgroundNotification 添加了一个观察者。该代码如下所示:

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

在观察者代码中,它将数据容器的属性保存到 NSUserDefaults。您还可以使用 NSCoding、Core Data 或各种其他方法来保存状态数据。

DataContainerSingleton 的 init 方法还尝试为其属性加载保存的值。

init 方法的那部分如下所示:

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

将值加载和保存到 NSUserDefaults 的键存储为字符串常量,它们是结构 DefaultsKeys 的一部分,定义如下:

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

您像这样引用这些常量之一:

DefaultsKeys.someInt

使用数据容器单例:

此示例应用程序对数据容器单例进行了简单的使用。

有两个视图控制器。第一个是 UIViewController ViewController 的自定义子类,第二个是 UIViewController SecondVC 的自定义子类。

两个视图控制器都有一个文本字段,并且都将数据容器单例的 someInt 属性中的值加载到其 viewWillAppear 方法中的文本字段中,并且都将文本字段中的当前值保存回 `someInt ' 的数据容器。

将值加载到文本字段中的代码位于 viewWillAppear: 方法中:

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

将用户编辑的值保存回数据容器的代码位于视图控制器的 textFieldShouldEndEditing 方法中:

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

您应该在 viewWillAppear 而不是 viewDidLoad 中将值加载到用户界面中,以便每次显示视图控制器时您的 UI 都会更新。


我不想对此投反对票,因为我认为您花时间创建问题和答案作为资源非常好。谢谢你。尽管如此,我认为我们对新开发人员提倡模型对象使用单例是非常不利的。我不属于“单身人士是邪恶的”阵营(尽管新手应该在谷歌上搜索该短语以更好地理解这些问题),但我确实认为模型数据是对单身人士的质疑/有争议的使用。
很想看到你写的关于双向链接的精彩文章
@Duncan C 你好,Duncan,我在每个模型中制作静态对象,所以我可以从任何正确的方法获取数据,或者我必须遵循你的路径,因为它看起来非常正确。
@VirendraSinghRathore,全局静态变量是跨应用程序共享数据的最糟糕的方式。它们将应用程序的各个部分紧密耦合在一起,并引入了严重的相互依赖关系。这与“非常正确”正好相反。
@DuncanC - 这种模式是否适用于 CurrentUser 对象 - 基本上是登录到您的应用程序的单个用户?谢谢
M
Mobile Team iOS-RN

斯威夫特 4

有很多方法可以快速传递数据。在这里,我添加了一些最好的方法。

1) 使用 StoryBoard Segue

Storyboard segues 对于在源视图控制器和目标视图控制器之间传递数据非常有用,反之亦然。

// 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) 使用委托方法

视图控制器D

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

视图控制器C

    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 答案的 Swift 代码片段的 Google 员工,似乎假设您应该始终知道他们推断代码的去向:我使用选项 1)从 ViewControllerA 发送到 ViewControllerB。我只是在最后一个花括号之前将代码片段粘贴在 ViewControllerA.swift 的底部(其中 ViewControllerA.swift 实际上是您的文件的名称,当然)。 “prepare”实际上是给定类中的一个特殊的内置预先存在的函数[什么都不做],这就是为什么你必须“override”它
D
Duncan C

另一种选择是使用通知中心(NSNotificationCenter)并发布通知。这是一个非常松散的耦合。通知的发送者不需要知道或关心谁在听。它只是发布通知并忘记它。

通知适用于一对多的消息传递,因为可以有任意数量的观察者在监听给定的消息。


请注意,使用通知中心会引入可能过于松散的耦合。它会使跟踪程序的流程变得非常困难,因此应谨慎使用。
K
Kristiina

我建议不要创建数据控制器单例,而是创建一个数据控制器实例并传递它。为了支持依赖注入,我将首先创建一个 DataController 协议:

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

然后我会创建一个 SpecificDataController (或任何当前合适的名称)类:

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

ViewController 类应该有一个字段来保存 dataController。请注意,dataController 的类型是协议 DataController。这样很容易切换数据控制器实现:

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

AppDelegate 我们可以设置 viewController 的 dataController

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

当我们移动到不同的 viewController 时,我们可以将 dataController 传递给:

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

现在,当我们希望为不同的任务切换数据控制器时,我们可以在 AppDelegate 中执行此操作,而无需更改任何其他使用数据控制器的代码。

如果我们只是想传递一个值,这当然是大材小用了。在这种情况下,最好使用 nhgrif 的答案。

通过这种方法,我们可以将视图与逻辑部分分开。


您好,这种方法是干净的、可测试的,并且我大部分时间在小型应用程序中使用它,但在较大的应用程序中,并非每个 VC(甚至可能甚至不是根 VC)都可能需要依赖项(例如,在这种情况下为 DataController)它对于每个 VC 来说,要求依赖项只是为了传递它似乎是一种浪费。此外,如果您使用不同类型的 VC(例如常规 UIVC 与 NavigationVC),那么您需要对这些不同类型进行子类化以添加该依赖变量。你如何处理这个问题?
D
Duncan C

正如@nhgrif 在他出色的回答中指出的那样,VC(视图控制器)和其他对象可以通过多种不同的方式相互通信。

我在第一个答案中概述的数据单例实际上更多是关于共享和保存全局状态,而不是直接通信。

nhrif 的回答让您可以将信息直接从源发送到目标 VC。正如我在回复中提到的,也可以将消息从目的地发送回源。

实际上,您可以在不同的视图控制器之间设置一个活动的单向或两向通道。如果视图控制器通过故事板 segue 链接,则设置链接的时间在 prepareFor Segue 方法中。

我在 Github 上有一个示例项目,它使用父视图控制器来托管 2 个不同的表视图作为子视图。子视图控制器使用嵌入 segue 链接,父视图控制器在 prepareForSegue 方法中与每个视图控制器连接 2 路链接。

您可以find that project on github(链接)。然而,我是用 Objective-C 编写的,并没有将它转换为 Swift,所以如果你对 Objective-C 不满意,可能会有点难以理解


M
Maxim

斯威夫特 3:

如果您有一个带有已识别 segues 的故事板,请使用:

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

尽管如果您以编程方式完成所有操作,包括不同 UIViewController 之间的导航,那么请使用以下方法:

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

注意:要使用第二种方式,你需要制作你的 UINavigationController,你正在推动 UIViewControllers,一个委托,它需要符合协议 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

    }
}

永远不要做 self.delegate = self
Y
Yusuf

这取决于您何时想要获取数据。

如果您想随时获取数据,可以使用单例模式。模式类在应用程序运行时处于活动状态。这是单例模式的示例。

class AppSession: NSObject {

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

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

        print(AppSession.shared.username)
    }
}

如果您想在任何操作后获取数据,可以使用 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")
}

N
NDCoder

我这样做的方式不是在视图控制器之间传递数据,而是在全局声明一个变量。你甚至可以用一个函数来做到这一点!

例如:

var a = "a"
func abc() {
   print("abc")
}
class ViewController: UIViewController {

}

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅