# UIPageViewController with Dynamic Data

If your experience with `UIPageViewController` was anything like mine, some things seemed pretty straightforward forward and some things might not have. More specifically, the data source area. I needed to create view controllers as they’re needed and not all at once which is what most tutorials I’ve seen have shown.

In this article, I’m going to show you how I went about this in hopes that it might help you out as well. 🍻Cheers!

## Here’s the gist of the idea I’m going to use for this article:

I have a collection view with a bunch of square cells that show thumbnails of either photos or videos, could be either-or. Let’s call these items media items `[MediaItem]`.

```swift
struct MediaItem {
	let id: UUID
	let mediaType: MediaType
	let title: String?
	let mediaUrl: URL?
}

enum MediaType { case video, photo }
```

This data used to populate the collection view is the same data we’re going to need for the page view controller. ☜ This is what I was after.

**Side Note** - One small twist is that I have different view controllers that have different UI’s depending on if the media item’s `mediaType` is a `.video` or a `.photo`.

Here are our dummy view controllers that will be applied inside our page view controller. They are a subclass of `MediaViewController`, which is a subclass of `UIViewController`. I made it to handle functionality for both `VideoViewController` and `PhotoViewController` such as taking in a `MediaItem` and other things. You can also just use a protocol, but that’s not the point of this article. Check it out:

```swift
class MediaViewController: UIViewController {
	let mediaItem: MediaItem

	init(mediaItem: MediaItem) {
		self.mediaItem = mediaItem
	}

	required init(coder: NSCoder) {
		fatalError("Use init(mediaItem: MediaItem)")
	}

	// … More code
}


class VideoViewController: MediaViewController {
	...
}

class PhotoViewController: MediaViewController {
	...
}
```

In the collection view, navigating to the page view controller will probably look something like this:

```swift
// For one dimensional array
func collectionView(
    _ collectionView: UICollectionView, 
    didSelectItemAt indexPath: IndexPath
) {
	let item = mediaItems[indexPath.item]
	let pagedMediaViewController = PagedMediaController(
        mediaItem: item, 
        currentIndex: indexPath.item
    )
	pagedMediaViewController.modalPresentationStyle = .overFullScreen
	present(pagedMediaViewController, animated: true)
}

// In case you're dealing with a two dimensional array like I was 
// dealing with since the collection view's items had sections
func collectionView(
    _ collectionView: UICollectionView, 
    didSelectItemAt 
    indexPath: IndexPath
) {
	let item = mediaItems[indexPath.section][indexPath.item]
	let joinedItems = mediaItems.flatMap { $0 }
	guard let currentIndexForPagedMediaVC = joinedItems.firstIndex(of: item) else { return }
	let pagedMediaViewController = PagedMediaController(
        mediaItem: item, 
        currentIndex: currentIndexForPagedMediaVC
    )
	pagedMediaViewController.modalPresentationStyle = .overFullScreen
	present(pagedMediaViewController, animated: true)
}
```

Enough with the setup, let’s get to what we came here for, shall we?

---

#### Quick Note

Notice that I’m also passing in the current index of the item that was tapped. This is going to be helpful for us because we need to know which cell & media item was tapped as we want the page view controller to start there.

Let’s look at what’s happening inside `PagedMediaViewController`.

**Properties & Init**

```swift
class PagedMediaViewController: UIPageViewController {
	// MARK: - Properties
	private var mediaItems: [MediaItem]
	private var currentIndex: Int


	// MARK: - Init
	init(mediaItems: [MediaItem], currentIndex: Int) {
		self.mediaItems = mediaItems
	  	self.currentIndex = currentIndex
		super.init(
            transitionStyle: .scroll, 
            navigationOrientation: .horizontal, 
            options: nil
        )
	}

	required init(coder: NSCoder) {
		fatalError("Use init(mediaItems: [MediaItem], currentIndex: Int)")
	}
}
```

We now have our data and current index being injected.

Now we’re going to write a method that we can use any time we need a view controller inside `PagedMediaViewController`. It’s going to create one and return it to us so we can use it. This is going to be helpful especially when we need to create these on the fly, which is exactly what we’ll be doing. Also, in this case, I need to choose which view controller I need depending on the media type. Let’s see what this looks like:

```swift
private func mediaViewControllerAtIndex(_ index: Int) -> MediaViewController? {
	// returns nil if the index is out of bounds, this way the app won't crash.
	// You can also put a print statement before you return.
	guard (0...mediaItems.count).contains(index) else { return nil }

	let item = mediaItems[index]

	switch item.mediaType {  
	case .video:  
		return VideoViewController(mediaItem: item)  
	case .photo:  
		return PhotoViewController(mediaItem: item)
	}
}
```

We can return either of these view controller classes because the method says it’ll return their common superclass type `MediaViewController`.

---

## Now Let’s Start Wiring Everything Up!

We’re gonna implement `viewDidLoad()` which will set where the `dataSource` is coming from and who the `delegate` is, and we’ll set up the initial view controller to be displayed with the item that was tapped on:

```swift
override func viewDidLoad() {
	super.viewDidLoad()
	dataSource = self
	delegate = self
	
	if let firstMediaVC = mediaViewControllerAtIndex(currentIndex) {
		let viewControllers = [firstMediaVC]
		setViewControllers(
            viewControllers, 
            direction: .forward, 
            animated: false, 
            completion: nil
        )
	}
}
```

#### Let's go over what we did:

1. Don’t forget to set the data source and delegate as `self`
    
2. `mediaViewControllerAtIndex()` returns an optional `MediaViewController` so we’re unwrapping it
    
3. If we can create that view controller successfully by unwrapping it, we then create an array of view controllers with a single view controller `firstMediaVC`
    
4. Then we use the method `setViewControllers(…)` from `UIPageViewController` to set our initial view controller
    

This threw me off a little cause it’s asking for an array of view controllers, but I never ended up having to pass in an array of view controllers. Now, I haven’t used `UIPageViewController` too often, so I’m not entirely sure when you’d want to pass in an array of view controllers. But, I can imagine that if you have a simple enough use case for a page view controller such as displaying a few screens for onboarding or something similar, I can imagine you’d just initialize them all at once and pass them in there. Even then, I might still go the route we’re using here.

### Let's Get That Swipe Action Going!

There are two data source methods we need. We need to tell it which view controller comes before the current one being displayed, and which one comes after:

```swift
extension PagedMediaViewController: UIPageViewControllerDataSource {

	pageViewController(
        _ pageViewController: UIPageViewController, 
        viewControllerBefore viewController: UIViewController
    ) -> UIViewController? {
		guard currentIndex > 0 else { return nil }
		return mediaViewControllerAtIndex(currentIndex - 1)
	}


	pageViewController(
        _ pageViewController: UIPageViewController, 
        viewControllerAfter viewController: UIViewController
    ) -> UIViewController? {
		guard currentIndex < mediaItems.count - 1 else { return nil }
		return mediaViewControllerAtIndex(currentIndex + 1)
	}
}
```

Both of these methods’ signatures say they will return an optional `UIViewController`. Our method `mediaViewControllerAtIndex()` does just that.

1. First, we want to make sure that the index isn’t out of bounds. When asked for the view controller before we need to make sure the current index is greater than `0`. If it is, we know we can decrement the index. If not, we’ll just return `nil`.
    
2. We want to have similar logic when asked for the view controller after. We need to check if the `currentIndex` is less than the total number of items minus one (cause arrays start at `0` ). If not, we’ll just return `nil`.
    

We’re passing in the current index decremented by one to get the view controller before the current one that's being displayed and we’re incrementing the current index by one to give it the view controller we need after the current one that's being displayed. Now, you may be wondering how `currentIndex` stays updated. Great question! …and an important one. If we’re not updating the current index, then it will break things and give us unexpected and undesirable results.

## Keeping Things In Sync

Here we will implement the delegate method where we can update our current index:

```swift
extension PagedMediaViewController: UIPageViewControllerDelegate {
	
	func pageViewController(
        _ pageViewController: UIPageViewController, 
        didFinishAnimating finished: Bool, 
        previousViewControllers: [UIViewController], 
        transitionCompleted completed: Bool
    ) {
		guard
			let mediaVCs = pageViewController.viewControllers as? [MediaViewController],
			let currIndex = mediaItems.firstIndex(of: mediaVCs[0].mediaItem)
		else { return }

		currentIndex = currIndex
	}


	// If you want to specify the interface orientations you want to support, 
    // implement these as well:
	func pageViewControllerSupportedInterfaceOrientation(
        _ pageViewController: UIPageViewController
    ) -> UIInterfaceOrientationMask {
		// example
		.allButUpsideDown
	}

	func pageViewControllerPreferredInterfaceOrientationForPresentation(
        _ pageViewController: UIPageViewController
    ) -> UIInterfaceOrientationMask {
		// example
		.portrait
	}
}
```

#### So, let’s see what we did here:

1. Stub out the method that has `didFinishAnimating` in its signature
    
2. Inside that method we’re doing some unwrapping here and chaining them together
    
3. We first need the view controllers prior to the transition and we’re optionally casting them as an array of our `MediaViewController` type so we have access to the media item
    
4. Next, we need to unwrap the index as the `firstIndex(of:)` method returns an optional index as it might be there or it might now. We’re just grabbing the first view controller in `mediaVCs` and passing in its media item so we can get the index of it
    
5. Once we have the index, we set that as the new value for our `currentIndex` property.
    

---

## Let’s See What The Whole File Looks Like!

```swift
class PagedMediaViewController: UIPageViewController {

	// MARK: - Properties
	private var mediaItems: [MediaItem]
	private var currentIndex: Int


	// MARK: - Init
	init(mediaItems: [MediaItem], currentIndex: Int) {
		self.mediaItems = mediaItems
	  	self.currentIndex = currentIndex
		super.init(
            transitionStyle: .scroll, 
            navigationOrientation: .horizontal, 
            options: nil
        )
	}

	required init(coder: NSCoder) {
		fatalError("Use init(mediaItems: [MediaItem], currentIndex: Int)")
	}


	// MARK: - Lifecycle
	override func viewDidLoad() {
		super.viewDidLoad()
		dataSource = self
		delegate = self
	
		if let firstMediaVC = mediaViewControllerAtIndex(currentIndex) {
			let viewControllers = [firstMediaVC]
			setViewControllers(
                viewControllers, 
                direction: .forward, 
                animated: false, 
                completion: nil
            )
		}
	}


	// MARK: - Helper Methods
	private func mediaViewControllerAtIndex(_ index: Int) -> MediaViewController? {
		// returns nil if the index is out of bounds, this way the app won't crash.
		// You can also put a print statement before you return.
		guard (0...mediaItems.count).contains(index) else { return nil }

		let item = mediaItems[index]

		switch item.mediaType {  
		case .video:  
			return VideoViewController(mediaItem: item)  
		case .photo:  
			return PhotoViewController(mediaItem: item)
		}
	}
}


// MARK: - DataSource Methods
extension PagedMediaViewController: UIPageViewControllerDataSource {

	pageViewController(
        _ pageViewController: UIPageViewController, 
        viewControllerBefore viewController: UIViewController
    ) -> UIViewController? {
		guard currentIndex > 0 else { return nil }
		return mediaViewControllerAtIndex(currentIndex - 1)
	}


	pageViewController(
        _ pageViewController: UIPageViewController, 
        viewControllerAfter viewController: UIViewController
    ) -> UIViewController? {
		guard currentIndex < mediaItems.count - 1 else { return nil }
		return mediaViewControllerAtIndex(currentIndex + 1)
	}
}


// MARK: - Delelgate Methods
extension PagedMediaViewController: UIPageViewControllerDelegate {
	
	func pageViewController(
        _ pageViewController: UIPageViewController, 
        didFinishAnimating finished: Bool, 
        previousViewControllers: [UIViewController], 
        transitionCompleted completed: Bool
    ) {
		guard
			let mediaVCs = pageViewController.viewControllers as? [MediaViewController],
			let currIndex = mediaItems.firstIndex(of: mediaVCs[0].mediaItem)
		else { return }

		currentIndex = currIndex
	}


	// If you want to specify the interface orientations you want to support, 
    // implement these as well:
	func pageViewControllerSupportedInterfaceOrientation(
        _ pageViewController: UIPageViewController
    ) -> UIInterfaceOrientationMask {
		// example
		.allButUpsideDown
	}

	func pageViewControllerPreferredInterfaceOrientationForPresentation(
        _ pageViewController: UIPageViewController
    ) -> UIInterfaceOrientationMask {
		// example
		.portrait
	}
}
```

---

## Well, That's It!

I hope this helps anyone out that’s looking to implement a `pageViewController` with an unknown amount of data. My first go was creating all view controllers at once even though I knew there was a better way. There was a noticeable lag and it takes away from the user experience. The route we went in this article allows the `pageViewController` to pop up immediately without any lag and things run smoothly.

A good thing to note is that since you’re just swiping the different view controllers on/off-screen, the life cycle methods you’re used to like `viewDidLoad` or `viewWillAppear` still get called as expected. I haven’t tested when `deinit` gets called exactly. I know it gets called when the whole `UIPageViewController` is dismissed, but I haven’t noticed `deinit` getting called just by swiping. I’m not sure at this moment if so many view controllers get cached or something like that to help out with memory.

Well, if you have any further questions, feel free to reach out to me. I will do my best to answer all questions. Also, if you notice any mistakes, please let me know so I can update this article as quickly as possible. And if you have a better or different way of implementing this, then share it with everyone in the comments down below.

Happy coding!
