为什么使用委托传递数据在Swift 4中失败

时间:2017-11-20 11:35:50

标签: ios swift

这是我的协议

protocol PassDataDelegate {
    func passData(data: String)
}

我的第一个控制器

class FirstViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!

    var delegate: PassDataDelegate?

    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = SecondViewController()
    }

    @IBAction func sendDataButtonTapped(_ sender: Any) {
        delegate?.passData(data: textField.text!)
        performSegue(withIdentifier: "Go", sender: nil)
    }

}

第二,最后一个

class SecondViewController: UIViewController, PassDataDelegate {

    @IBOutlet weak var myLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    func passData(data: String) {
        print("This came from first: \(data). Will change UI.")
        myLabel.text = data
    }
}

应用程序在标签更改部分崩溃。在打开可选项时,它会显示为零。这有什么不对?

2 个答案:

答案 0 :(得分:1)

您的代码存在几个基本问​​题。 我认为,对于代表团和UIStoryboardSegue机制,您可能也会有一些误解。您应该阅读here (Delegation)here (Segues)

话虽如此,让我通过内联评论解释您的问题,解释发生了什么。

//  Has to be marked as a class protocol (`: class`) so that
//  `weak var delegate: PassDataDelegate?` can be weak.
protocol PassDataDelegate: class {
  func passData(data: String)
}

class FirstViewController: UIViewController {

  @IBOutlet weak var textField: UITextField!

  //  Important!
  //  Make this a `weak` var. In your case, you would fortunately not create a retain cycle
  //  but there is a big threat of creating those when using delegation patterns with non-weak delegates.
  //
  //  In your case, if you don't make this `weak`, `SecondViewController` would never be deallocated unless you
  //  cleared this var manually (setting it to `nil`).
  //
  //  Also note that, if you're using `PassDataDelegate` solely for forwarding some data to the next view controller,
  //  you can dismiss this var entirely. There is no need to have a reference to the second view controller hanging around.
  //  In fact, as mentioned above, it can be dangerous to do so.
  //  Additionally, you don't need to make the protocol `: class` then.
  private weak var delegate: PassDataDelegate?

  override func viewDidLoad() {
    super.viewDidLoad()

    //  It doesn't make any sense to create a `SecondViewController` here.
    //  The segue mechanism will create a new instance of `SecondViewController`.
//    delegate = SecondViewController()
  }

  @IBAction func sendDataButtonTapped(_ sender: Any) {
    //  `delegate?` is `nil` here.
//    delegate?.passData(data: textField.text!)
    performSegue(withIdentifier: "Go", sender: nil)
  }

  //  This is the proper 'hook' for you to forward data or do anything with a destination
  //  view controller presented using `UIStoryboardSegue`.
  //  This method will be called by the system following your call to `performSegue`.
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    super.prepare(for: segue, sender: sender)

    //  `UITextField.text` can be `nil`, so safeguard for that here.
    //  If the destination implements our protocol, we can forward data to it.
    if let text = textField.text, let delegate = segue.destination as? PassDataDelegate {

      //  This is optional. You can hang on to the destination view controller here, but
      //  review the comments above to reason about whether this makes sense or not.
      self.delegate = delegate

      //  We can safely forward the data (text) here.
      delegate.passData(data: text)
    }
  }
}

SecondViewController可以保持原样。

更新

关于Delegation

委托模式通常描述一个后退指针,它与一个发起实例进行对话。例如。使用UITableViewDataSourceUITableView与实现此协议的内容进行对话,以获取有关其数据的信息等。
您实际上是通过转发数据到SecondViewController。正如评论中所提到的,此代码甚至会中断,因为passDataSecondViewController的实施是使用尚未初始化的插座。

现在你可以在这里做三件事之一:

1

立即保留您正在使用的模式(授权是准确的)并更改SecondViewController以使其正常工作

class SecondViewController: UIViewController, PassDataDelegate {

  @IBOutlet weak var myLabel: UILabel!

  private var data: String?

  override func viewDidLoad() {
    super.viewDidLoad()

    //  It is safe to access `myLabel` in `viewDidLoad`. Outlets have been connected.
    if let data = data {
      myLabel.text = data
    }
  }


  func passData(data: String) {
    self.data = data

    //  Only access `myLabel` if the view is loaded.
    if isViewLoaded {
      print("This came from first: \(data). Will change UI.")
      myLabel.text = data
    }
  }
}

这种方法实际上非常麻烦,因为你需要在任何时候调用passData这个事实。因此,您不知道您的网点是否已经初始化,这会导致代码膨胀和重复。坏。

2

完全剥离协议并使用更简单的方法

class FirstViewController: UIViewController {

  @IBOutlet weak var textField: UITextField!

  //  This is the proper 'hook' for you to forward data or do anything with a destination
  //  view controller presented using `UIStoryboardSegue`.
  //  This method will be called by the system following your call to `performSegue`.
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    super.prepare(for: segue, sender: sender)

    //  `UITextField.text` can be `nil`, so safeguard for that here.
    //  If the destination is a `SecondViewController`, we know that is has `public var data: String` and we can forward data to it.
    if let text = textField.text, let destination = segue.destination as? SecondViewController {

      //  We can safely forward the data (text) here.
      destination.data = text
    }
  }
}

class SecondViewController: UIViewController {

  @IBOutlet weak var myLabel: UILabel!

  //  Deliberatly marking this a `public` to make clear that
  //  you're intented to set this from the 'outside'.
  public var data: String? {
    didSet {
      if isViewLoaded {
        myLabel.text = data
      }
    }
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    //  It is safe to access `myLabel` in `viewDidLoad`. Outlets have been connected.
    if let data = data {
      myLabel.text = data
    }
  }
}

同样,有些事情我们不喜欢他的做法:

  • 仍然重复代码并且必须检查isViewLoaded
  • 您特别想使用协议,我们不在这里

我们可以通过在init SecondViewController中提供数据来解决重复性代码问题。但是,由于您正在使用segue,故事板将为您实例化目标视图控制器,您无法控制它。现在你可以使用segue剥离,但这很快就会远离原始问题,并且是一种完全不同的仅代码方法。所以这也不好。

3

使用协议但正确应用委托模式。

protocol DataProvider: class {
  func provideData() -> String?
}

protocol DataHandler: class {
  var providerDelegate: DataProvider? { get set }
}

class FirstViewController: UIViewController, DataProvider {

  @IBOutlet weak var textField: UITextField!

  func provideData() -> String? {
    return textField.text
  }

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    super.prepare(for: segue, sender: sender)

    //  If the destination is a `DataHandler`, we can set yourselves as its provider.
    if let destination = segue.destination as? DataHandler {
      destination.providerDelegate = self
    }
  }
}

class SecondViewController: UIViewController, DataHandler {

  @IBOutlet weak var myLabel: UILabel!

  weak var providerDelegate: DataProvider?

  override func viewDidLoad() {
    super.viewDidLoad()

    if let data = providerDelegate?.provideData() {
      //  Safe to access `myLabel`, because we are in `viewDidLoad`.
      myLabel.text = data
    }
  }
}

这种方法是最通用的。双方都不关心处理程序和提供程序究竟是什么。请注意,在经典委托模式中,您可能没有DataHandler协议,并在SecondViewController中检查具体类型(此处为prepareForSegue)。但是,这种方法更灵活,同时仍然将代理编入其中。从SecondViewController的角度来看,这种方法也是最强大的。它不必在任何时候处理passData,而是可以自己决定何时向其代理人(DataProvider)询问数据。通过这种方式,SecondViewController可以推断出所有出口等都已初始化并且处理数据是安全的。

答案 1 :(得分:1)

SecondViewController() 故事板中设计的控制器。这是一个没有连接插座的全新实例(这是崩溃的原因)。您需要对SecondViewController实例的真实引用。

假设SecondViewController实例是segue的目标视图控制器,则不需要protocol / delegate,通过segue传递数据

class FirstViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!

    @IBAction func sendDataButtonTapped(_ sender: Any) {
        performSegue(withIdentifier: "Go", sender: nil)
    }

    func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "Go" {
           let secondController = segue.destination as! SecondViewController 
           controller.passedData = textField.text!
        }
    }
}
class SecondViewController: UIViewController, PassDataDelegate {

    @IBOutlet weak var myLabel: UILabel!

    var passedData = ""

    override func viewDidLoad() {
        super.viewDidLoad()
        print("This came from first: \(passedData). Will change UI.")
        myLabel.text = passedData
    }
}