I have a View controller with an embedded Container View plus a controller
The Container View hosts a UIPageViewController
The View controller has a button, if its clicked I want to update a label in the current displayed page managed by the UIPageView Controller
I am getting the ContainerView Controller with this approach
#IBAction func sendButtonTouched(sender: AnyObject) {
if let vc = self.childViewControllers.last as? ContainerViewController{
vc.pageViewController.view.backgroundColor = UIColor.blueColor()
I get the UIPageViewController and set the color but it does not update
also if go deeper into the rabbit hole to get my currently viewed page I am able to get and set all values but my view never updates
what I really want to do is something like this
#IBAction func sendButtonTouched(sender: AnyObject) {
if let vc = self.childViewControllers.last as? ContainerViewController{
vc.pageViewController.view.backgroundColor = UIColor.blueColor()
print("make it blue baby")
if let pageItemController = vc.getCurrentViewController(){
print(pageItemController.indexLabel.text)
pageItemController.message = self.messageTextView.text
pageItemController.messageImage.image = UIImage()
pageItemController.reloadInputViews()
}
}
}
and in ContainerViewController
func getCurrentViewController()-> MIPViewController?
{
print("\(self.pageViewController.viewControllers!.count) view controllers")
if let vc = self.pageViewController.viewControllers!.first as? PageItemViewController
{
if vc.index < mipCount
// must be a MIPViewController
{
return vc as? MIPViewController
}
}
return nil
}
in my console output i see
make it blue baby
1 view controllers
Optional("This is a message of the number 0")
Optional("")
so everything is called but as stated no view ever updates
I am probably missing something really basic here, so thank you for your help
I also checked other questions e.g. Access Container View Controller from Parent iOS
but afaik using the childViewControllers is also valid
Related
I implemented a Hamburger Menu which gets called when the user taps a BarButtonItem.
When the user clicks an index of the menu a delegate method gets called and selects the correct row:
func rowTapped(index: MenuIndex) {
let vc1 = storyboard?.instantiateViewController(withIdentifier: "VC1") as! VC!
// lazy loading
_ = vc1.self.view
vc1.transitionToNew(index)
}
And in my VC1 the ** transitionToNew** method gets called and selects the correct index:
(Let´s assume that the user tapped index 1 which is associated to .a)
func transitionToNew(_ index : MenuIndex) {
switch index {
case .a:
addSubviewToContainer(asChildViewController: childVC)
...
}
Now the childVC should be added into the scrollView of my VC1.
The childVC is instantiated lazy:
private lazy var childVC: ChildVC = {
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "ChildVC") as! ChildVC
return viewController
}()
To add the childVC into the scrollView the addSubViewToContainer method gets called in the switch-case statement:
private func addSubviewToContainer(asChildViewController viewController: UIViewController)
{
viewController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
viewController.view.frame = scrollView.bounds
addChildViewController(viewController)
scrollView.addSubview(viewController.view)
viewController.didMove(toParentViewController: self)
}
I know that views gets instantiated lazy (sadly) so we have to input something like
_ = self.view
(although its a stupid hack) to instantiate the view which indeed shows me that my scrollView got instantiated (at least I think that because the preview in the debugger shows me the view)
Can someone tell me without seeing all of the code why my the childVC is not added or displayed (!?) in my scrollView?
I got the correct frame, the scrollView should be instantiated at the moment the user taps an index.
UPDATE
I also have some navigation buttons which the user can select:
#IBAction func navigateToChildVC(_ sender: UIButton) {
addSubviewToContainer(asChildViewController: childVC)
)
}
It is calling the exact same method but here it is working.
It looks like with your implementation the scroll view cannot determine its content size, so setting it explicitly might fix your issue. Something in the lines of scrollView.contentSize = scrollView.bounds.size sets the content size so that it fills the scroll view in both dimensions - which might not be what you want for a scroll view, but that is a different discussion.
There is also no need to call addChildViewController when lazily creating the child view controller, it is enough to have it called in addSubviewToContainer.
I have a Tabbed App with two tabs... the first tab has the main Action, and the second has Settings that can be updated. I am trying to pass some variables data from Settings to the Action tab. Based on some suggestions, I have used the following code for the Update button:
#IBAction func updateBut(_ sender: Any) {
let myVC = self.storyboard?.instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController
myVC.totTime = totalTime
myVC.timeInt = intTime
self.present(myVC, animated: true, completion: nil)
}
The data does pass to the first view controller, however, the tabs have disappeared on this view now. So, how can I get the tabs back on the screen? I am quite the beginner to any form of app development, and am just trying to learn by doing... the Tabbed App has been created using one of the Xcode New Project templates. Thanks.
try this way
(self.tabBarController?.viewControllers?[0] as! FirstViewController).totTime = totalTime
(self.tabBarController?.viewControllers?[0] as! FirstViewController).timeInt = intTime
self.tabBarController?.selectedIndex = 0
self.tabBarController?.tabBar.isHidden = false try this
A much better way to pass data by using protocols, you can define a protocol like below
protocol PassDataDelegate{
func updateFirstVc(totalTime:String)
}
and in your SecondViewController class have a delegate property like below
class SecondViewController:UIViewController{
myDelegate:PassDataDelegate?//declaration part
#IBAction func updateBut(_ sender: Any) {
myDelegate.updateFirstVc(totalTime:totalTime)
}
}
And finally in your UITabController class implement the protocol
class myTabController:UITabController,PassDataDelegate{
var firstController:FirstViewController? //declaration part
var secondController:SecondViewController?
override func viewDidLoad(){
super.viewDidLoad()
//initialize your view controller here
self.firstViewController = FirstViewController()
self.secondViewController = SecondViewController()
self.secondViewController.myDelegate = self //assign delegate to second vc
self.viewcontrollers = [firstController, secondController]
}
updateFirstVc(totalTime:totalTime){
self.firstViewController?.totTime = totalTime
self.selectedIndex = 0// here change the tab to first vc if you want to switch the tab after passing data
}
}
I am new to iOS correct me If I am wrong,
The reason why the TabBar is not visible since you are instantiating new FirstViewController which is present on top of your TabBar.
TabBar by default does this Job or Add the new viewController to the TabBar Stack.
tabBarController.viewControllers.append(myVC)
For passing the data TabBar holds the reference of all its ViewControllers. So you can set or get in each other ViewControllers like this
var yourData{
set{
let yourVC = self.tabBarController?.viewController[0] as? FirstViewController ?? ErrorClass
yourVC.dataObj = newValue
}
get{
let yourVC = self.tabBarController?.viewController[0] as? FirstViewController ?? ErrorClass
return yourVC.dataObj
}
I followed Ray Wenderlich tutorial on how to create an iOS book animation, now i'm trying to perform some changes to it.
Original project is composed of Navigation Controller, Books View Controller - showing an array of books, by clicking on a book you can open it - and Book View Controller - showing the selected book open.
What i've added: a View Controller and set it as initial VC; a UIButtonwhich perform a show segue to Navigation Controller.
I want to show View Controller in background after Books View Controller appears but apparently iOS removes the view controllers underneath it from the view hierarchy, this leading to a black background if i set clearColor in the Attributes inspector. I've then added the following code to ViewController.swift to have a transparent background.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let vc = self.storyboard!.instantiateViewControllerWithIdentifier("BooksViewController") as! BooksViewController
vc.view.backgroundColor = UIColor.clearColor()
vc.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
self.presentViewController(vc, animated: true, completion: nil)
}
It works well and i can see the initial VC in the background while seeing the array of books, but i can no longer perform segue to Book View Controller by clicking on a book. So apparently openBook in BooksViewController is never called:
func selectedCell() -> BookCoverCell? {
if let indexPath = collectionView?.indexPathForItemAtPoint(CGPointMake(collectionView!.contentOffset.x + collectionView!.bounds.width / 2, collectionView!.bounds.height / 2)) {
if let cell = collectionView?.cellForItemAtIndexPath(indexPath) as? BookCoverCell {
return cell
}
}
return nil
}
func openBook(book: Book?) {
let vc = storyboard?.instantiateViewControllerWithIdentifier("BookViewController") as! BookViewController
vc.book = selectedCell()?.book
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.navigationController?.pushViewController(vc, animated: true)
return
})
}
I cannot understand where the problem is, any help is really appreciated. Please be gentle, i'm still learning Swift and english is not my native language.
You are using prepareForSegue incorrectly. You do not want to present a view controller in your prepareForSegue. Use segue.destinationViewController as! YourViewController to reference it instead
My view controller hierarchy is the following:
The entry point is a UINavigationController, whose root view controller is a usual UITableViewController. The Table View presents a list of letters.
When the user taps on a cell, a push segue is triggered, and the view transitions to ContainerViewController. It contains an embedded ContentViewController, whose role is to present the selected letter on screen.
The Content View Controller stores the letter to be shown as a property letter: String, which should be set before its view is pushed on screen.
class ContentViewController: UIViewController {
var letter = "-"
#IBOutlet private weak var label: UILabel!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
label.text = letter
}
}
On the contrary, the Container View Controller should not know anything about the letter (content-unaware), since I'm trying to build it as reusable as possible.
class ContainerViewController: UIViewController {
var contentViewController: ContentViewController? {
return childViewControllers.first as? ContentViewController
}
}
I tried to write prepareForSegue() in my Table View Controller accordingly :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
// Not executed:
containerViewController.contentViewController?.letter = letter
}
}
but contentViewController is not yet created by the time this method is called, and the letter property is never set.
It is worth mentioning that this does work when the segue's destination view controller is set directly on the Content View Controller -- after updating prepareForSegue() accordingly.
Do you have any idea how to achieve this?
Actually I feel like the correct solution is to rely on programmatic instantiation of the content view, and this is what I chose after careful and thorough thoughts.
Here are the steps that I followed:
The Table View Controller has a push segue set to ContainerViewController in the storyboard. It still gets performed when the user taps on a cell.
I removed the embed segue from the Container View to the ContentViewController in the storyboard, and I added an IB Outlet to that Container View in my class.
I set a storyboard ID to the Content View Controller, say… ContentViewController, so that we can instantiate it programmatically in due time.
I implemented a custom Container View Controller, as described in Apple's View Controller Programming Guide. Now my ContainerViewController.swift looks like (most of the code install and removes the layout constraints):
class ContainerViewController: UIViewController {
var contentViewController: UIViewController? {
willSet {
setContentViewController(newValue)
}
}
#IBOutlet private weak var containerView: UIView!
private var constraints = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
setContentViewController(contentViewController)
}
private func setContentViewController(newContentViewController: UIViewController?) {
guard isViewLoaded() else { return }
if let previousContentViewController = contentViewController {
previousContentViewController.willMoveToParentViewController(nil)
containerView.removeConstraints(constraints)
previousContentViewController.view.removeFromSuperview()
previousContentViewController.removeFromParentViewController()
}
if let newContentViewController = newContentViewController {
let newView = newContentViewController.view
addChildViewController(newContentViewController)
containerView.addSubview(newView)
newView.frame = containerView.bounds
constraints.append(newView.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor))
constraints.append(newView.topAnchor.constraintEqualToAnchor(containerView.topAnchor))
constraints.append(newView.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor))
constraints.append(newView.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor))
constraints.forEach { $0.active = true }
newContentViewController.didMoveToParentViewController(self)
}
} }
In my LetterTableViewController class, I instantiate and setup my Content View Controller, which is added to the Container's child view controllers. Here is the code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
if let viewController = storyboard?.instantiateViewControllerWithIdentifier("ContentViewController"),
let contentViewController = viewController as? ContentViewController {
contentViewController.letter = letter
containerViewController.contentViewController = contentViewController
}
}
}
This works perfectly, with an entirely content-agnostic container view controller. By the way, it used to be the way one instantiated a UITabBarController or a UINavigationController along with its children, in the appDidFinishLaunching:withOptions: delegate method.
The only downside of this I can see: the UI flow ne longer appears explicitly on the storyboard.
The only way I can think of is to add delegation so that your tableViewController implements a protocol with one method to return the letter; then you have containerViewController setting its childViewController (the contentViewController) delegate to its parent. And the contentViewController can finally ask its delegate for the letter.
At your current solution the presenting object itself is responsible for working both with the "container" and the "content", it doesn't have to be changed, but such solution not only has the issues like the one you described, but also makes the purpose of the "container" not very clear.
Look at the UIAlertController: you are not configuring its child view controller directly, you are not even supposed to know it exists when using the alert controller. Instead of configuring the "content", you are configuring the "container" which is aware of the content interfaces, lifecycle and behavior and doesn't expose it. Following this approach you achieve a properly divided responsibility of the container and content, minimal exposure of the "content" allows you to update the "container" without a need to update the way it is used.
In short, instead of trying to configure everything from a single place, make it so you configure only the "container" and let it configure the "content" when and where it is needed. E.g. in the scenario you described the "container" would set data for the "content" whenever it initializes the child controllers. I'm using "container" and "content" instead of ContainerViewController and ContentViewController because the solution is not strictly based on the controllers because you might as well replace it wth NSObject + UIView or UIWindow.
I have 2 controllers: ViewController and GalleryViewController(with the collection view on it). From the storyboard I set for the collectionView in GalleryViewController Scrolling Enabled to false. Now, how can I change it to true from the ViewController?
I've tried this:
var vc: GalleryViewController?
vc.collectionView.scrollEnabled = true
but it does not work. Is there another solution with which I can change scrolling from another controller(ViewController)?
your code will create a new instance of GalleryViewController and you need to use the existing one.
You have a number of options, partly depending on how you navigate from ViewController to GalleryViewController.
If you are creating the Gallery View from your initial controller, then you should use prepareForSegue, something like this
override func prepareForSegue(segue: UIStoryboardSegue?, sender: AnyObject?) {
if segue!.identifier == "GallerySegueOrWhateverYouHaveCalledIt" {
let viewGalleryController:ViewGalleryController = segue!.destinationViewController as ViewGalleryController
let collectionViewLink = viewGalleryController.collectionView
}
}
If you're using a Tab Controller, and assuming you know the index of your GalleryView, let's call it indexGalleryView, then it's even easier
var vc = tabBarController!.viewControllers![indexGalleryView] as! GalleryViewController
vc.collectionView.scrollEnabled = true
And if you have a ViewController -> Container -> Embed GalleryViewController -> CollectionView, you can get a handle to the embedded ViewController in the viewDidLoad of the top level controller like this
for vc in self.childViewControllers
{
if vc.isKindOfClass(GalleryViewController)
{
myGalleryViewController = vc as! GalleryViewController
}
}
once you have myGalleryViewController you should be able to access everything on the child view