> ## Content Index
> Fetch the complete content index at: https://williamboles.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Hosting ViewControllers in Cells
- URL: https://williamboles.com/hosting-viewcontrollers-in-cells/
- Published: 2018-04-11T20:09:59.000Z
- Updated: 2026-04-12T21:09:18.000Z
- Description: In the fairytale 🧚 version of UICollectionView, a cell has a simple view inside it. In the nightmarish reality, a cell sometimes needs to host a UIViewController. Here, we look at how to stuff a UIViewController instance inside a UICollectionViewCell, avoiding various gotchas.
- Author: William Boles
- Tags: UIKit

Recently, I've been experiencing the iOS equivalent of the movie [Inception](http://www.imdb.com/title/tt1375666/?ref=williamboles.com) \- putting a collection view inside a collection view. While exploring possible solutions, I stumbled upon this very informative [post](http://khanlou.com/2015/04/view-controllers-in-cells/?ref=williamboles.com) by [Soroush Khanlou](https://twitter.com/khanlou?ref=williamboles.com) and his suggestion that the best way to implement a collection view inside a collection view was by using child view controllers - with each child view controller implementing its own collection view and having its view added as a subview on one of the parent view controller's cells.

If you haven't read that post, I recommend doing so, as it presents the argument for why you would want to put view controllers inside cells very well. And even if you don't need to be convinced, I would still recommend it, as the rest of this post won't make sense without it 😉.

![Photo of stained glass spiral](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/2019/11/stained-glass-spiral-min-1.jpg)

The post itself is a few years old, written in Objective-C, so I converted it to Swift and integrated the solution into my project. I ended with the following collection view cell subclass:

```
class HostedViewCollectionViewCell: UICollectionViewCell {

    // MARK: - HostedView

    // 1
    weak var hostedView: UIView? {
        didSet {
          // 2
          if let oldValue = oldValue {
              oldValue.removeFromSuperview()
          }

          // 3
          if let hostedView = hostedView {
              hostedView.frame = contentView.bounds
              contentView.addSubview(hostedView)
          }
        }
    }

    // MARK: - Reuse

    // 4
    override func prepareForReuse() {
        super.prepareForReuse()

        hostedView = nil
    }
}
```

1. Each cell holds a reference to a (child) view controller's view via the `hostedView`. Whenever that `hostedView` property is set, the above `didSet` observer code is triggered.
2. If `hostedView` was previously set, that old `hostedView` is removed from the cell's view hierarchy.
3. If the current `hostedView` value is non-nil, it is added as a subview of the cell's `contentView`.
4. To improve performance, a collection view will only create enough cells to fill the visible UI and then a few more to allow for smooth scrolling. When a cell scrolls off-screen, it is marked for reuse on a different index path. `prepareForReuse()` is called just before the cell is reused and allows us to reset that cell. In the above `prepareForReuse()`, `hostedView` is set to `nil`, so triggering its removal from the cell's view hierarchy.

To begin with, this solution worked well. However, I started noticing that occasionally a cell would *forget* its content. It was infrequent and could be resolved by scrolling the collection view. However, I was quite dissatisfied with this experience and wanted to understand what was causing this UI breakdown.

Looking over that `prepareForReuse()` method, you can see that `removeFromSuperview()` is called to do the removing. What's interesting about `removeFromSuperview()` is that it takes no arguments and instead uses the soon-to-be-removed view's `superview` value to determine what view to remove the caller from. A view can only have one `superview`. If a view that already has a superview is added as a subview to a different view, the original connection to the first superview is broken and replaced with this new connection. For the most part, this `1-to-M` mapping between a superview and its subviews works just fine, as most views once added as subviews do not tend to move around. However, cells are designed to be reused. The reusable nature of cells lies at the root of my cells *forgetfulness*. By using the solution above, we end up with the following unintended associations:

![A diagram showing how multiple cells can hold a reference to the same view controller's view but that view can only have one cell as a superview](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/2020/05/hosting_viewcontrollers_in_cells.jpg)

The diagram above shows how multiple cells can be associated (shown in purple) with the same view controller's view, but only one of those cells has the view of `ViewController` as a subview (shown in green). Because of the multiple references kept to the same view of `ViewController`, it's possible for any of `Cell A`, `Cell B` or `Cell C` to remove the `hostedView` from `Cell C` by calling `removeFromSuperview()` in their own `prepareForReuse()` method. Of course, it was not intentional for multiple cells to have an active reference to a view controller's view if that view was no longer part of the cell's view hierarchy.

Once those unintended left-over `hostedView` references were spotted, the solution for the bug became straightforward - only remove the `hostedView` if it is still in the cell's view hierarchy:

```
class HostedViewCollectionViewCell: UICollectionViewCell {
    var hostedView: UIView? {
        didSet {
            if let oldValue = oldValue {
                if oldValue.isDescendant(of: self) { //Make sure that hostedView hasn't been added as a subview to a different cell
                    oldValue.removeFromSuperview()
                }
            }

            //Omitted rest of observer
        }
    }

    //Omitted methods
}
```

Now, the cell will only remove the `hostedView` from its `superview` if that `superview` is part of the cell itself. This additional `if` statement addresses the *forgetfulness* that I was seeing. However, if you queried `hostedView` on `Cell A` or `Cell B` from the above diagram, you would still get back a reference to the view that is from `Cell C`. With the above `if` statement, we have only resolved part of the bug. Let's make the cell only return a `hostedView` value if that `hostedView` is *actually* part of its view hierarchy:

```
class HostedViewCollectionViewCell: UICollectionViewCell {

    // MARK: - HostedView

    // 1
    private weak var _hostedView: UIView? {
        didSet {
            if let oldValue = oldValue {
                if oldValue.isDescendant(of: self) { //Make sure that hostedView hasn't been added as a subview to a different cell
                    oldValue.removeFromSuperview()
                }
            }

            if let _hostedView = _hostedView {
                _hostedView.frame = contentView.bounds
                contentView.addSubview(_hostedView)
            }
        }
    }

    // 2
    weak var hostedView: UIView? {
        // 3
        get {
            guard _hostedView?.isDescendant(of: self) ?? false else {
                _hostedView = nil
                return nil
            }

            return _hostedView
        }
        //4
        set {
            _hostedView = newValue
        }
    }

    //Omitted methods
}
```

1. The private `_hostedView` property assumes the responsibilities of the previous `hostedView` property implementation and acts as the *backing-store* property to the new `hostedView` property implementation. The `_hostedView` now actually holds a reference to the view controller's view, even though the outside world still thinks it is `hostedView` that holds the reference. Just like before, the `didSet` observer checks if the `oldValue` of the `_hostedView` isn't `nil` and if it isn't, removes that view from the cell's view hierarchy if `_hostedView` is still a subview. If the current value of `_hostedView` is not nil, `_hostedView` is added as a subview of the current cell.
2. The externally accessible `hostedView` property now has a custom `get` and `set`.
3. The `get` only returns the `_hostedView` value if that view is still a subview of the cell. If `hostedView` isn't a subview, the `get` sets `_hostedView` to nil (which will cause it to be removed from this cell's view hierarchy) and returns `nil`. Dropping the reference once something tries to access `hostedView` feels a little strange. However, as the `superview` property of `UIView` isn't [KVO compliant](https://developer.apple.com/documentation/swift/cocoa%5Fdesign%5Fpatterns/using%5Fkey-value%5Fobserving%5Fin%5Fswift?ref=williamboles.com) (and we don't want to get involved in the dark-arts of [method swizzling](https://nshipster.com/method-swizzling/?ref=williamboles.com) to make it so) there is no way for us to know that the `hostedView` has a new `superview` without querying the `hostedView` and there is no point in querying the `hostedView` until something tries to access it - so a little self-contained strangeness is the only viable option here.
4. The `set` takes the new value and assigns it to the backing-store property.

> With a backing-store property, it is essential that you practice good access hygiene - `_hostedView` should only be directly accessed in either `hostedView` or `_hostedView`; everywhere else, you should use `hostedView`.

You can see that by truthfully returning `hostedView`, we have added a lot of complexity to our cell. Still, none of that complexity leaks out. We can have confidence that hosting a view controller's view won't lead to unintended consequences.

## Seeing the 🐛 for yourself

If you want to see this bug in the wild, you can download the example project from my [repo](https://github.com/wibosco/ViewControllerInCell-Example?ref=williamboles.com). In that example project, I've added logging to track when the view controller's view isn't a subview on that cell anymore, so that you can easily see when that bug would have happened by watching the console. If you want to see the bug's impact on the UI, comment out the `isDescendant(of:_)` check in `HostedViewCollectionViewCell`.