Swift; Delegate Embedded View Controller and Parent

Swift; delegate embedded view controller and parent

What I ended up doing is adding this to the MainController:

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

if (segue.identifier == "mySegue") {
let vc = segue.destinationViewController as! EmbeddedController
vc.delegate = self
}
}

In storyboard I selected the segue from the MainController to the EmbeddedController, and set the identifier to "mySegue".

Without the code above the delegate kept returning nil. I didn't look into this solution at first as I thought segues were only for transitioning between view controllers, and in my mind I didn't see the embedded controller as a transition. Maybe someone more knowledgable than me (which is practically anyone on here at this point) can explain how this is all fitting together.

In any case, this is how I solved my issue and hopefully someone else can benefit from this as well :)

Can a child view controller be the delegate of its parent view controller?

The parent should be the child's delegate.

The parent, if it needs to tell the child something, should just invoke methods.

How to Delegate from Parent to Multiple Different Childs?

You could create an array of delegates... but this is really not the delegate / protocol design pattern.

You could also take the NotificationCenter addObserver / post notification approach... but that is more suitable to multiple objects that are not necessarily "under the control" of the current class.

For your case - multiple child view controllers which all need to "do something" based on the same event, a better approach may be to create a "Base" view controller and make each of your "Steps" subclasses of that Base.

Here's a quick example...

// this is our "Step" base view controller
// creates the "user" property
// defines the "userHasChanged()" method
class StepBaseViewController: UIViewController {

var user: String?
func userHasChanged(_ new_user: String) {
user = new_user
}

override func viewDidLoad() {
super.viewDidLoad()
// we can do anything that may be
// "common" to all "Steps"
}

}

Any view controller that you make a subclass of StepBaseViewController will now have a user property and a default method to handle userHasChanged. In addition, anything that is "common" to the steps (such as UI elements like labels, buttons, etc) can be setup in viewDidLoad().

Now your 4 "step" class become:

class Step1: StepBaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
// setup specific to this "Step"
}
override func userHasChanged(_ new_user: String) {
super.userHasChanged(new_user)
print("User changed to:", self.user, "in:", self)
// do something specific to Step 1
}
}
class Step2: StepBaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
// setup specific to this "Step"
}
override func userHasChanged(_ new_user: String) {
super.userHasChanged(new_user)
print("User changed to:", self.user, "in:", self)
// do something specific to Step 2
}
}
class Step3: StepBaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
// setup specific to this "Step"
}
override func userHasChanged(_ new_user: String) {
super.userHasChanged(new_user)
print("User changed to:", self.user, "in:", self)
// do something specific to Step 3
}
}
class Step4: StepBaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
// setup specific to this "Step"
}
override func userHasChanged(_ new_user: String) {
super.userHasChanged(new_user)
print("User changed to:", self.user, "in:", self)
// do something specific to Step 4
}
}

And here's a modified version of your NavigationController showing how to use this approach:

class NavigationController: UIViewController {

// we'll simulate the user changing
// so on first tap the user will become "User 1"
// on next tap user will become "User 2"
// on next tap user will become "User 3"
// and so on
var n: Int = 0
var user: String = ""

override func viewDidLoad() {

// all step childs are differents
let step1 = Step1()
addStep(step: step1)

let step2 = Step2()
addStep(step: step2)

let step3 = Step3()
addStep(step: step3)

let step4 = Step4()
addStep(step: step4)

}

func addStep(step: UIViewController){
self.addChild(step)
step.willMove(toParent: self)
view.addSubview(step.view)
step.didMove(toParent: self)
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// simulate the "user changed" event
n += 1
self.user = "User \(n)"
self.children.forEach { child in
if let vc = child as? StepBaseViewController {
vc.userHasChanged(self.user)
}
}
}

}

With NavigationController loaded, tapping 3 times results in this debug console output:

User changed to: Optional("User 1") in: <MyProj.Step1: 0x7fbd7a21caf0>
User changed to: Optional("User 1") in: <MyProj.Step2: 0x7fbd7a21c480>
User changed to: Optional("User 1") in: <MyProj.Step3: 0x7fbd7a21d480>
User changed to: Optional("User 1") in: <MyProj.Step4: 0x7fbd7a21dca0>
User changed to: Optional("User 2") in: <MyProj.Step1: 0x7fbd7a21caf0>
User changed to: Optional("User 2") in: <MyProj.Step2: 0x7fbd7a21c480>
User changed to: Optional("User 2") in: <MyProj.Step3: 0x7fbd7a21d480>
User changed to: Optional("User 2") in: <MyProj.Step4: 0x7fbd7a21dca0>
User changed to: Optional("User 3") in: <MyProj.Step1: 0x7fbd7a21caf0>
User changed to: Optional("User 3") in: <MyProj.Step2: 0x7fbd7a21c480>
User changed to: Optional("User 3") in: <MyProj.Step3: 0x7fbd7a21d480>
User changed to: Optional("User 3") in: <MyProj.Step4: 0x7fbd7a21dca0>

Delegation Among Container View Controllers

Unfortunately, I don't know how the dragging and dropping works in Xcode, I do everything in code. However, when your parent view controller instantiates another view controller, just set the parent as the container's delegate.

Create the protocol:

protocol SomeProtocol: AnyObject {
func passX(a: String?)
func passY(b: String?)
}

And the containers will have delegates of type that protocol:

class FirstContainerVC: UIViewController {
weak var delegate: SomeProtocol?
}

class SecondContainerVC: UIViewController {
weak var delegate: SomeProtocol?
}

The parent must conform to the protocol so that it can become the delegate. Then when you instantiate the containers (which you must only do once in this scenario), set self as their delegates:

class ParentViewController: UIViewController, SomeProtocol {

// make the containers instance properties so that you
// can access them from the protocol methods
weak var firstContainerVC = FirstContainerVC()
weak var secondContainerVC = SecondContainerVC()

// set the delegates at some point
func setDelegates() {
firstContainerVC?.delegate = self
secondContainerVC?.delegate = self
}

func passX(a: String?) {
guard let a = a else {
return
}
secondContainerVC?.getFromFirst(a: a)
}

func passY(b: String?) {
//
}

}

Then when you want to go from first to second, go through the delegate from the first container to the parent, and from the parent to the second container.

class FirstContainerVC: UIViewController {

weak var delegate: SomeProtocol?

func sendToSecond() {
delegate?.passX(a: "slick")
}

}

class SecondContainerVC: UIViewController {

weak var delegate: SomeProtocol?

func getFromFirst(a: String) {
print(a)
}

}

This is a somewhat crude example. You should code your implementation how you feel most comfortable (i.e. gracefully unwrapping, how/where you instantiate, etc.). Also, if all of the view controllers are permanent view controllers (meaning that they are never deallocated), no need to make them weak var. However you do it, the concepts are all the same. The parent is the delegate for the containers, and the containers communicate with each other through the parent.

Some people may suggest using notification observers or singletons for the containers to communicate with each other, but I find that to be overkill when you have a parent right there.

How can I pass data from a parent view controller to an embedded view controller in Swift?

A way to achiеve this is to get the child view controller instance in the parent's viewDidLoad. It appears that the parent's viewDidLoad: gets called after the child's viewDidLoad:, which means the label is already created in the child's view.

override func viewDidLoad() {
super.viewDidLoad()

if let childVC = self.childViewControllers.first as? ChildVC {
childVC.someLabel.text = "I'm here. Aye-aye."
}
}

Passing data from embedded PageViewController to parent View Controller

The proper way would be to create a delegate protocol and have the parent conform to it (which the child then has a reference to as a weak property) or pass in a completion handler callback block to the child and have the child call that. Generally, you don't want to be directly referencing a parent from its child.

call a method of a parent view from a view embedded in a UIContainerView. swift

You can either use delegation pattern or NSNotification.

Delegation

Set the parentVC as the pageVC's delegate and remember that the parentVC must conform to page view controller's delegate protocol

class ParentClass: UIViewController, UIPageViewControllerDelegate {
// ...
pageInstanceVC.delegate = self
}

and then implement its delegate method (this is where you change the button's color), you might want to implement it in
- pageViewController:willTransitionToViewControllers: or - pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted: Complete docs can be found here

Notification

Set parentVC to listen to page change notification and implement the required method when the notification is received

// Parent VC 
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "changeButtonColor", name: "kPageChangeNotif", object: nil)
}

func changeButtonColor(notification: NSNotification) {
let userInfo = notification.userInfo as Dictionary
let pageNumber = userInfo["PageNumber"]

// Change the button color
// .....
}

Then sent out notification when page is changed

// PageVC
NSNotificationCenter.defaultCenter().postNotificationName("kPageChangeNotif", object: nil, userInfo: ["PageNumber" : 2])

Remember to remove parentVC from observing NSNotificationCenter's (removeObserver) when appropriate



Related Topics



Leave a reply



Submit