Forum Xamarin.iOS

Adding a UICollectionView as a SubView on a UICollectionViewController blocks ItemSelected event?

DaveCarsonDaveCarson GBUniversity ✭✭✭

I have a UICollectionViewController which displays a number of cells based on the ViewModel I pass in to it - when a cell is clicked the ViewController loads and pushes another UIViewController onto it's NavigationController - all this works fine........until I add another UICollectionView as a SubView.

I'm adding the other UICollectionViewController as a 'footer' to the main view and it contains a number of tappable images which will take the user Back Home, and to specified areas of the app - a max of 4 options. This too works fine - even when I add it as a SubView to the main UICollectionViewController.

The problem is than when I add this Footer view the Cells in the main UICollectionView (the one that's part of the UICollectionViewController) no longer responds to tap events - the ItemSelected event is not fired. The Footer view works fine and I've checked that it's not picking up and swallowing the 'lost taps'.

Both views are in Frame which do not overlap and setting the BackgroundColor confirms this. I've tried messing around with Z-Indexes but that made no difference.

Can anyone shed any light on why this maybe happening?

Best Answer

  • adamkempadamkemp US mod
    Accepted Answer

    You're doing multiple things wrong here. You should rethink your whole approach.

    First, you are trying to make a footer view by just adding a subview directly to the collection view. The collection view has support for headers and footers built in. More importantly, it has support for sections, which allows for rendering different sets of items differently. You can read more about both of those here. Alternatively, you could have two separate UICollectionViews (one above and one below).

    That brings me to the next issue. You're using UICollectionViewController, not UICollectionView. There's nothing inherently wrong with that, but if you use the view controller then you need to let it manage its views completely. Don't go messing around with the view hierarchy, even adding your own views, to a view controller that already manages views for you. That's just going to confuse it. If you want to control the layout of the views then inherit from UIViewController and then create a UICollectionView directly and add it as a subview and handle layout yourself.

    Thirdly, you are adding a view controller's View as a subview of something. That violates the contract for view controllers. There is a view controller hierarchy that should parallel the view hierarchy. If you add a view controller's view as a subview then you also should be adding that view controller as a child view controller, and there's a whole API for that (see AddChildViewController, RemoveFromParentViewController, DidMoveToParentViewController and WillMoveToParentViewController). That API is tricky to use, and you really shouldn't use it unless you know what you're doing. The better thing to do here is to not use view controllers and just use views directly instead.

    Fourthly, you are creating custom subclasses of the layout classes, which is very likely to be unnecessary. The built in layout classes are very flexible, and I would bet that you could customize them to do what you want without subclassing.

    Now, the direct cause of your bug is that you're not doing layout right. Look at these lines:

            var footerFrame = new CGRect(new CGPoint(0, View.Bounds.Bottom - 90), new CGSize(View.Bounds.Width, 60));
            var footerNav = new FooterNavigationViewController(new FooterLayout(footerFrame), footerFrame);
    
            // COMMENT THIS OUT TO GET THE ITEMSELECTED EVENT IN THIS CONTROLLER TO FIRE
            View.AddSubview(footerNav.View);
    

    Where do you set ``footerNav.View.Frame`? You don't. Therefore it has the default size, which is the size of the screen. So your "footer" fills the screen. The fact that it draws the footer views where you want at all is a result of the fact that you're using a whole view controller, which expects to fill the screen. If you fix it like this:

    footerNav.View.Frame = footerFrame;
    

    Then you end up with the view in the right spot (and now the other items are tappable), but your footer items are missing. That's because they're drawing way below the bottom of the screen because your whole footer view controller is based on the assumption of filling the screen. You can see this kind of problem by showing the border:

            footerNav.View.Layer.BorderColor = UIColor.Red.CGColor;
            footerNav.View.Layer.BorderWidth = 2;
    

    My advice is to scrap this code and start over. Read more about how UICollectionView works, how to deal with multiple sections, how to do footers, and how to tweak layouts. Then read this post to get a better understanding of layout (you shouldn't be even attempting to set a Frame in ViewDidLoad).

Answers

  • adamkempadamkemp USInsider, Developer Group Leader mod

    Could you share an example project showing what you are trying to do?

  • DaveCarsonDaveCarson GBUniversity ✭✭✭
    edited July 2015

    Ok - this is not working code but I think it contains the relevant parts (formatting seems off though).

    I've also attached a screen shot of the resulting UI when the footer navigation is added (obviously this is a work in progress so ignore the rawness of it - I need to get the functionality working before looking to make it pretty).


    public class SubSectionGridViewController : UICollectionViewController
    {
    public SubSectionGridViewController (UICollectionViewFlowLayout layout, CGRect frame, SubSectionGridViewModel viewModel)
    : base (layout)
    {
    _viewModel = viewModel;
    _loader = new ViewModelLoader (new ConfigFileLoader ("configuration"));

            // DEBUG COLOUR
            var foo = new UIColor (66.0f / 255.0f, 79.0f / 255.0f, 91.0f / 255.0f, 0.5f);
            CollectionView.BackgroundColor = foo;
    
            CollectionView.Frame = new CGRect (new CGPoint (0, 100), new CGSize (View.Bounds.Width, View.Bounds.Height - 195));
        }
    
        public override void ViewDidLoad ()
        {
            CollectionView.RegisterClassForCell (typeof(SubSectionNavigationCell), cellToken);
    
            SetBackground (UIScreen.MainScreen.Bounds.Size);
    
            // Footer Navigation
            // TODO: Refactor path to footer navigation to constant?
            FooterNavigationViewModel fNav = _loader.LoadConfiguration<FooterNavigationViewModel> ("general/footer_navigation.json");
    
            var footerNavFrame = new CGRect (0, View.Bounds.Bottom - 90, View.Bounds.Width, 60);
            footerNavigation = new FooterNavigationViewController (new SingleRowLayout (footerNavFrame), footerNavFrame, fNav);
    
            footerNavigation.OnSectionSelected += (object sender, NavigationSelectionEventArgs e) => {
    
                var sectionVM = _loader.LoadConfiguration<SectionViewModel> (e.ConfigurationFilePath);
                if (sectionVM == null) {
                    UIAlertView dlg = new UIAlertView ("Not Implemented", "Section is not yet implemented", null, "OK", null);
                    dlg.Show ();
                } else {
                    var sectionVC = new SectionViewController (sectionVM);
                    this.NavigationController.PushViewController (sectionVC, false);
                }
            };
    
            footerNavigation.OnHomeSelected+= (sender, e) => {
                this.NavigationController.PopToRootViewController(false);
            };
    
            footerNavigation.OnBackSelected += (sender, e) => {
                this.NavigationController.PopViewController(false);
            };
    
            //View.AddSubview (footerNavigation.View);          // The ItemSelected event will fire if this line is commented out - it will not otherwise
    
    
            base.ViewDidLoad ();
        }
    
        public override void ItemSelected (UICollectionView collectionView, NSIndexPath indexPath)
        {
            var selectedItem = _viewModel.SubSectionCells [indexPath.Row];
    
            switch (selectedItem.Type) {
                .
                .
                .
                .
    
            }
        }
    }
    

  • adamkempadamkemp USInsider, Developer Group Leader mod

    When I say "project" I mean a .zip of a solution I can open and run. This code snippet isn't useful to me.

  • DaveCarsonDaveCarson GBUniversity ✭✭✭

    The zip will be a little on the large zip (so may not be able to upload it here) and I don't really think I can post it publicly anyway.

    Will upload to a Dropbox folder and message you the link if that's OK.

  • adamkempadamkemp USInsider, Developer Group Leader mod

    Delete the bin and obj directories and all subdirectories in the packages directory. Then zip it up, and it shouldn't be bigger than a few hundred Kb.

    To be clear, I'm not asking for you to give me your whole real project. Just a sample that shows this one thing.

  • DaveCarsonDaveCarson GBUniversity ✭✭✭

    Ok - here is a very simple example but it demonstrates the problem.

    Run the application up and you will see two large 'tiles' in the middle of the screen and three home buttons at the bottom.

    Tap on one of the Home buttons and an alert is displayed - tap on either of the central tiles and nothing happens, despite there being code in the ItemSelected event handler.

    Now comment out the addition of the footer view - (line 30 of the MainViewController) and run the application. The central tiles now respond to taps.

  • adamkempadamkemp USInsider, Developer Group Leader mod
    Accepted Answer

    You're doing multiple things wrong here. You should rethink your whole approach.

    First, you are trying to make a footer view by just adding a subview directly to the collection view. The collection view has support for headers and footers built in. More importantly, it has support for sections, which allows for rendering different sets of items differently. You can read more about both of those here. Alternatively, you could have two separate UICollectionViews (one above and one below).

    That brings me to the next issue. You're using UICollectionViewController, not UICollectionView. There's nothing inherently wrong with that, but if you use the view controller then you need to let it manage its views completely. Don't go messing around with the view hierarchy, even adding your own views, to a view controller that already manages views for you. That's just going to confuse it. If you want to control the layout of the views then inherit from UIViewController and then create a UICollectionView directly and add it as a subview and handle layout yourself.

    Thirdly, you are adding a view controller's View as a subview of something. That violates the contract for view controllers. There is a view controller hierarchy that should parallel the view hierarchy. If you add a view controller's view as a subview then you also should be adding that view controller as a child view controller, and there's a whole API for that (see AddChildViewController, RemoveFromParentViewController, DidMoveToParentViewController and WillMoveToParentViewController). That API is tricky to use, and you really shouldn't use it unless you know what you're doing. The better thing to do here is to not use view controllers and just use views directly instead.

    Fourthly, you are creating custom subclasses of the layout classes, which is very likely to be unnecessary. The built in layout classes are very flexible, and I would bet that you could customize them to do what you want without subclassing.

    Now, the direct cause of your bug is that you're not doing layout right. Look at these lines:

            var footerFrame = new CGRect(new CGPoint(0, View.Bounds.Bottom - 90), new CGSize(View.Bounds.Width, 60));
            var footerNav = new FooterNavigationViewController(new FooterLayout(footerFrame), footerFrame);
    
            // COMMENT THIS OUT TO GET THE ITEMSELECTED EVENT IN THIS CONTROLLER TO FIRE
            View.AddSubview(footerNav.View);
    

    Where do you set ``footerNav.View.Frame`? You don't. Therefore it has the default size, which is the size of the screen. So your "footer" fills the screen. The fact that it draws the footer views where you want at all is a result of the fact that you're using a whole view controller, which expects to fill the screen. If you fix it like this:

    footerNav.View.Frame = footerFrame;
    

    Then you end up with the view in the right spot (and now the other items are tappable), but your footer items are missing. That's because they're drawing way below the bottom of the screen because your whole footer view controller is based on the assumption of filling the screen. You can see this kind of problem by showing the border:

            footerNav.View.Layer.BorderColor = UIColor.Red.CGColor;
            footerNav.View.Layer.BorderWidth = 2;
    

    My advice is to scrap this code and start over. Read more about how UICollectionView works, how to deal with multiple sections, how to do footers, and how to tweak layouts. Then read this post to get a better understanding of layout (you shouldn't be even attempting to set a Frame in ViewDidLoad).

  • DaveCarsonDaveCarson GBUniversity ✭✭✭

    Wow - thanks for the insight and the detailed response.

    Looks like there's a lot wrong here - basically the result of trying to learn as you go, not a good approach but sometimes it's the hand your dealt.

    Thanks again for the feedback - File > New Project is probably the next step, never too late to scrap it all and start over to do it right.

  • DaveCarsonDaveCarson GBUniversity ✭✭✭

    Wasn't quite as severe as a File > New Project but I've got everything working now and after reading a bit more into the way iOS works I think I have a better handle on things now.

    Thanks again for taking the time to provide such a detailed response - that's why you are an MVP I suppose :-)

  • adamkempadamkemp USInsider, Developer Group Leader mod

    I'm glad you got it working! I hate to be the guy to say "throw this all away", but sadly sometimes that's the right answer. :)

Sign In or Register to comment.