I'm new to swift and am trying to write an app with it.
I have a UIViewController that I am transitioning to. I have designed the UI in interface builder and I intend to use segues to manage the transition. However, the view controller relies on data that is passed into the view controller from the previous view controller.
If I have properties on my view controller then I will need to redefine my init method. But I wouldn't normally call the init method; it would be called for me before prepareForSegue. So I see a few possible solutions:
Make my variables optional (so I can pass them in prepareForSegue
and update the view then).
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let viewController: EventViewController = segue.destinationViewController as! EventViewController
viewController.event = self.event
}
Manually init my view controller and present it programmatically instead.
???
Is there a third option? If not, which of the previously mentioned 2 is better practice?
There is two possible options as you mentioned:
The first one is the easiest which is to pass the data in prepareForSegue. which you don't have to care about dismissing the controller or keeping a track of inner view controllers,because storyboard will take care of it.
The second way is to set a Storyboard ID in storyboard,for the controller you need to present programmatically, which need more things to handle, like to dismiss the controller or keep track of inner presented controllers.
let nextViewControllerName = storyboard?.instantiateViewControllerWithIdentifier("Storyboard ID") as! nextViewControllerName
nextViewControllerName.event = self.event
self.presentViewController(nextViewControllerName, animated: true, completion: nil).
At the end they does the same purpose.
Note: You should always pass the data before presenting the controller.
Related
I have a Detail View Controller and I want to show the same Detail View Controller but with a different item.
var nexttype : Type!
let nexttypeVC = TypeDetailVC()
#IBAction func buttonTapped(_ sender: UITapGestureRecognizer) {
nexttype = type.Array![0]
nexttypeVC.type = next.type
self.navigationController?.pushViewController(nexttypeVC, animated: true)
}
When I print the name or a identifier of the item I get the correct one, but when I launch the simulator I get error. The values of the labels and everything is nil.
How can I tell the view controller to reload again but with a different item?
Call performSegue(withIdentifier:) rather than pushViewController. You'll need to programmatically register a segue or add one via storyboard. Then use prepare(for segue: sender:) to give the destination ViewController the item you wish. Note that the destination's viewDidLoad() is called after the prepareForSegue.
This is the standard Apple-recommended way to seed the destination ViewController with data it needs prior to its load.
Instead of pushing the same view controller onto the navigation stack. Try to arrange all the logic to update UI elements into a method. Call this method in viewDidLoad(). Calling self.viewDidLoad() is not a good idea.
Use observer pattern and call the method that updates UI elements.
I have three views, each with its own view controller: VC1, VC2, VC3.
The user will frequently switch back and forth between each of the three views, both forward and backward.
Each view contains data: both shared from the previous view and data unique to that view.
When the user goes back to a View that he has already visited, the data displayed on that view needs to be retained (the same data as he saw the last time he visited that view), and not set to the default values the first time he visited the view.
In the first view controller, VC1, I am using a prepare for segue to push data from VC1 to VC2 or VC3:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueToVC2” {
let destinationViewController: VC2 = segue.destination as! VC2;
destinationViewController.passedData1 = firstAmount
destinationViewController.passedData2 = secondAmount
destinationViewController.passedData3 = thirdAmount
} else {
let destinationViewController: VC3 = segue.destination as! VC3;
destinationViewController.passedData1 = firstAmount
destinationViewController.passedData2 = secondAmount
destinationViewController.passedData3 = thirdAmount
destinationViewController.passedData4 = fourthAmount
}
By tapping the GO BACK button on each view, I return to the previous view:
#IBAction func goBackButtonPressed(_ sender: Any) {
print("Back Button Pressed!")
self.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
}
I am having trouble passing data backwards. And when I return to VC2 from VC1, data has been reset to 0. I have no segues going back from VC2 to VC1 or from VC3 to VC2. Would that be the cleanest way to pass the data back: to create another segue in Main.storyboard from VC2 to VC1 and then add another ‘if’ to my prepare for segue that checks for VC1?
I am passing ALL these variables back and forth between view controllers but only using some of them in each view controller. It seems like a waste and I don't think I am on the right track here.
Any help or suggestions?
View controllers should never store data. They are responsible for coordinating between model objects and view objects. That's their whole point. The pattern you're looking for is called MVC (Model-View-Controller) and it's a core part of iOS development.
Move your data out of the view controllers and put it into model classes. Each view controller should fetch data out of the model, and send updates into the model. The only thing the view controllers should pass between themselves is what model objects to work on, and most of the time that only needs to pass in one direction (down the stack).
Delegation can be a useful tool here, and you can also investigate "unwind segues" which are built to help you send data upstream. But again, the data you should be sending is mostly references to the model, and the model itself needs to live outside the view controllers.
It's in Objective-C, but still one of the best simple examples from Apple on MVC design is TheElements, and is worth exploring as a basis. Even without reading the Objective-C, you can see how the various pieces fit together.
I haven't studied it as much as TheElements, but Lister claims to be a good demonstration of MVC patterns in Swift using modern iOS techniques.
Why don't you call a delegate which passes the data to the view controller when you press back button.
Or if the data shared by all view controllers reflect the same value. Make a singleton class and use those values across the app.
example singleton class:
class SomeModel {
static let shared = SomeModel()
private init() {}
}
The issue I'm having is this.
I have a navigation controller with 3 viewController. In the 1st controller, I have the user select an image. This image is passed to 2nd and 3rd controller via prepareForSegue.
At the 3rd controller, I have a button that takes the user back to the 1st view controller. I explored 2 ways in doing this:
1) use performSegue, but I don't like this because it just push the 1st controller to my navigation stack. So I have this weird "Back" button at the 1st Viewcontroller now, which is not what I want. I want the app to take user directly to 1st viewcontroller without the back button.
2) I tried Poptorootviewcontroller. This solves the issue of the "back" button. But, when I pop back to the 1st viewcontroller, the user's selected image is still on screen. I want to clear this image when the user goes from the 3rd viewcontroller back to the 1st viewcontroller.
So with approach 2), how do I make sure all memory is refreshed and the image becomes nil in the 1st viewcontroller? Since I'm not using performSegue, 3rd viewcontroller does not have access to the 1st Viewcontroller.
For refresh, you'd have to clear it in viewWillAppear but I find this rather dangerous. Best you can do there is to create a new copy of the view controller everytime and Swift will take care of the rest. I don't know if you are using the storyboard but I would recommend using the class UIStoryboard and the function instiantiateViewControllerWithIdentifier("something") as! YourCustomVC
As long as you stay in the navigation stack, you'll not lose any of the current configurations of previous View Controllers.
As for passing data back to the first controller. You can either just throw it in the global scope which is the easiest way but might be difficult to know when it was updated or if the data is fresh. But you can always just:
var something: String = ""
class someView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
something = "foo"
}
}
Something will be availabe everywhere then.
You could make a protocol and pass the delegate along the 3 view controllers. So when you are starting it you could do:
func someAction() {
let v = SomeViewController()
v.delegate = self
self.navigationController?.pushViewController(v, animated: true)
}
And then with each following view:
func someOtherAction() {
let v = SomeOtherViewController()
v.delegate = self.delegate
self.navigationController?.pushViewController(v, animated: true)
}
Although personally I find it hard to keep track of this.
Lastly you could use the NSNotificationCenter to pass an object along with all the data and catch it in a function on your first controller.
To do this you first register your VC for the action in viewDidLoad() or something:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "someAction:", name: "someNotification", object: nil)
Then when you are done in the 3rd view make some object or a collection of string and send it back as follows:
NSNotificationCenter.defaultCenter().postNotificationName("someNotification", object: CustomObject())
And then lastly you'll catch it in the function "someAction"
func someAction(note: NSNotification) {
if let object = note.object as? CustomObject {
//Do something with it
}
}
Hope this helps!
Use an unwind segue which provides the functionality to unwind from the 3rd to the 1st (root) view controller.
The unwind segue is tied to an action in the root view controller. Within this action, you simply nil the image:
#IBAction func unwindToRootViewController(sender: UIStoryboardSegue)
{
let sourceViewController = sender.sourceViewController
// Pull any data from the view controller which initiated the unwind segue.
// Nil the selected image
myImageView.image = nil
}
As you can see in the action, segues also let you pass data back from the source view controller. This is a much simpler approach than needing to resort to using delegates, notifications, or global variables.
It also helps keep things encapsulated, as the third view controller should never need to know specifics about a parent view controller, or try to nil any image that belongs to another view controller.
In general, you pass details to a controller, which then acts on it itself, instead of trying to manipulate another controller's internals.
I would like to use a custom initializer (with passed parameters, for dependency injection) for a view controller that is initialized in prepareForSegue. I don't understand exactly how the view controller is initialized in prepareForSegue, so not sure the correct pattern for this.
Here is the prepareForSegue code in my view controller:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "FilterPopover" {
let vc = segue.destinationViewController as! FilterViewController
vc.popoverPresentationController!.delegate = self
}
}
I would like to pass data into the FilterViewController when it is created, so that I can make the property a constant (let, not var), and do not have to use an implicit unwrapped optional. The view controller that has the above method has the data to pass into the FilterViewController custom init.
Is there a pattern for using a custom init for segue.destinationViewController so that I can pass parameters?
By the time prepareForSegue is called the destination view controller is already initialized. This is done for you by the Storyboard system which will eventually call initWithCoder: on your view controller. You could initialize your let properties here.
If you want to use a custom initializer you would have to create the controller in code without using storyboards in that situation.
To complete that Joris said, in that case, you can additionnaly use a .xib separated file for your ViewController
I have a complex iPad view that I manage by having several view controllers. I previously (before iOS6/Xcode 4.5) did this by allocating my view controllers in code, and hooked up the various views to them though links to the master view.
What I would like to do is use the new UIContainerView container views to embed the view controllers in the storyboard file. I don't seem to be able to make an IBOutlet link to the embedded view controller to the master controller.
Is it possible to do this? Or to retrieve the embedded controller via a tag or something in the code?
This question is SPECIFICALLY about using container views
Another option for some cases is to capture the embedded controller using -prepareForSegue:sender:.
For example, if I have a UINavigationController embedded within a CustomContainerViewController, I can name the embed segue embedContentStack in the storyboard and capture it in CustomContainerViewController via
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"embedContentStack"]) {
// can't assign the view controller from an embed segue via the storyboard, so capture here
_contentStack = (UINavigationController *)segue.destinationViewController;
}
}
I'm not sure what you mean by "retrieve the embedded controller". When you want to use a controller you use the UIStoryboard method instantiateViewControllerWithIdentifier:, using the identifier that you give to the controller in IB. You can also use the performSegueWithIdentifier:sender: method (which also instantiated the view controller). You should check out the "Using View Controllers in Your App" section in the Apple docs. It also makes reference to the fact that child view controllers are instantiated at the same time as the container controller.
After edit: If you embed a container view in another view controller, that embedded view's controller can be referenced from the containing controller with self.childViewControllers (which will be an array, so if there is just one, you can get it with lastObject).
Here is another thread about it: Access Container View Controller from Parent iOS
They propose to keep a reference in prepareForSegue or search for the embedded viewController in self.childViewControllers
Note of Caution
Before proceeding to use an answer to this question, you may wish to reflect whether the embedded things really need to be view controllers.
Eg, if you're embedding a UICollectionViewController subclass, could you instead embed a UICollectionView subclass? Or, even better, could you embed a UIView subclass that hides away the UICollectionView behind a simple ViewModel?
In the code base I'm currently working on, I'm embedding two view controllers in to another view controller. Both could fairly easily be plain views instead, and could then be more easily bound to in the storyboard, without this messy code.
Unfortunately, they are currently view controllers and I'm not in a position to simplify them in to plain views right now, so this will have to do.
Background
I'm using the approach of picking up the embed segue in prepare(for segue:, sender:) as suggested by Playful Geek here.
I wanted to show the swift I'm using for this, as it seems to be fairly tidy…
class EditionLandingViewController: UIViewController {
fileprivate var titlesView: SectionTitlesViewController!
fileprivate var sectionsView: SectionsViewController!
}
//MARK:-
extension EditionLandingViewController {
private enum SegueId: String {
case embedTitles
case embedSections
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
guard
let segueRawId = segue.identifier,
let segueId = SegueId(rawValue: segueRawId)
else { return }
switch segueId {
case .embedTitles:
self.titlesView = segue.destination as! SectionTitlesViewController
case .embedSections:
self.sectionsView = segue.destination as! SectionsViewController
}
}
}
Discussion
I've chosen to name segues as action methods.
Using an enum cases for segue identifiers means you've got the compiler and tooling on your side, so its much harder to get a segue name wrong.
Keeping the segue ids in a private enum within the extension scope seems appropriate in this case as these segues are not needed anywhere else (they can't be performed, for example).
I'm using implicitly unwrapped types for the embedded view controllers because (in my case anyway) it's a logic error if they are missing.
Similarly, I'm also happy to force cast the destination view controller types. Again, it would be a logic error if these types are not the same.
Swift version of the top-voted Answer. Years later, Interface Builder still does not seem to support dragging IBOutlets to embedded Container Views.
Instead, set the outlets in prepare(for:sender:):
#IBOutlet var someViewController: SomeViewController!
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "EmbedSomeViewController", let destination = segue.destination as? SomeViewController {
someViewController = destination
}
}
You must also set up the UIContainerView on your Storyboard. Xcode will generate an embed segue automatically; set the segue's Identifier.