Search code examples
iosswiftmemoryuipageviewcontroller

add another page to UIpageviewcontroller when scroll and remove the previous one to prevent memory leak


I have a problem with big memory leaks when I use UIPageViewController. I have a view and I scroll it. I see that memory grows very fast.

i have an array of ViewController (about 350+ vc), the UIPageViewcontroller shows the every VC in a page, the problem is :

to append 350+ Viewcontroller in array is cause memory to grow up fast so is there a way to append VC in array by viewController Before and viewController After method when scrolling ?

so the array will always be 2 index for example

    arraypage1 = [vc1,vc2,vc3]

when scrolling to the next page arraypage1 well be [vc2,vc3,vc4] so the index 0 remove but a new page will add at index 2

here is what i did so far :

at TestVC :

var TodayDate: String?
var data1 : String? , data2: String?
var data3: String? , data4: String?
var data5: String? , data6: String?
var imagee: UIImage? 

at the UIpageViewcontroller :

let vc1 = self.storyboard?.instantiateViewController(withIdentifier: "page1")as! TestPageVC
vc1.TodayDate = "06.09.2022"
vc1.data1 = "welcome"
vc1.data2 = "The first data..."
vc1.data3 = "The first data..."
vc1.data4 = "The first data..."
vc1.data5 = "The first data..."
vc1.data6 = "The first data..."
vc1.imagee = UIImage(named: "0001")

let vc2 = self.storyboard?.instantiateViewController(withIdentifier: "page1")as! TestPageVC
vc2.TodayDate = "07.09.2022"
vc2.data1 = "welcome"
vc2.data2 = "The second data..."
vc2.data3 = "The second data..."
vc2.data4 = "The second data..."
vc2.data5 = "The second data..."
vc2.data6 = "The second data..."
vc2.imagee = UIImage(named: "0002")

let vc3 = self.storyboard?.instantiateViewController(withIdentifier: "page1")as! TestPageVC
vc3.TodayDate = "08.09.2022"
vc3.data1 = "welcome"
vc3.data2 = "The third data..."
vc3.data3 = "The third data..."
vc3.data4 = "The third data..."
vc3.data5 = "The third data..."
vc3.data6 = "The third data..."
vc3.imagee = UIImage(named: "0003")


let vc4 = self.storyboard?.instantiateViewController(withIdentifier: "page1")as! TestPageVC
vc4.TodayDate = "08.09.2022"
vc4.data1 = "welcome"
vc4.data2 = "The fourth data..."
vc4.data3 = "The fourth data..."
vc4.data4 = "The fourth data..."
vc4.data5 = "The fourth data..."
vc4.data6 = "The fourth data..."
vc4.imagee = UIImage(named: "0004")


let vc5 = self.storyboard?.instantiateViewController(withIdentifier: "page1")as! TestPageVC
vc3.TodayDate = "08.09.2022"
vc3.data1 = "welcome"
vc3.data2 = "The fifth data..."
vc3.data3 = "The fifth data..."
vc3.data4 = "The fifth data..."
vc3.data5 = "The fifth data..."
vc3.data6 = "The fifth data..."
vc3.imagee = UIImage(named: "0005")

arraypage1.append(contentsOf:[vc1, vc2, vc3])

// here i want add vc4 and remove vc1 when go to next page 

 func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {



    guard let currentIndex = arraypage2.firstIndex(of: viewController) else {
        return nil
    }
    
    UserDefaults.standard.removeObject(forKey: "emptyView1")

    let previousIndex = currentIndex - 1
    guard previousIndex >= 0 else {
        return nil
    }
    

    
    return arraypage2[previousIndex]
    
    }



  func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {

    guard let currentIndex = arraypage2.firstIndex(of: viewController) else {
        return nil
    }
    
    let afterIndex = currentIndex + 1

 
    let afterIndex3 = currentIndex + 2
     if afterIndex3 == arraypage2.count {
      UserDefaults.standard.set("next", forKey: "emptyView1")
    }
    
    
    guard afterIndex < arraypage2.count else {
        return nil
    }


    return arraypage2[afterIndex]
    
}

Solution

  • As you've seen, if you are using a UIPageViewController with many pages, you'll run into memory issues rather quickly if you keep an array of instantiated view controllers.

    To avoid that, we want to dynamically instantiate the controllers when the Page View Controller requests them.

    Here's a quick example...

    First, we'll define a data structure (you'll likely have more properties):

    struct MyPageData {
        var title: String = "Title"
        var subtitle: String = "Subtitle"
        var imgName: String = "0.circle.fill"
        var bkgColor: UIColor = .white
    }
    

    Next we'll lay out a "Page" view controller in Storyboard, so it looks something like this:

    enter image description here

    And set its Custom Class to TestPageVC and give it an Identifier of "page1":

    class TestPageVC: UIViewController {
        
        public var pageIndex: Int = 0
        
        @IBOutlet var titleLabel: UILabel!
        @IBOutlet var subtitleLabel: UILabel!
        @IBOutlet var imageView: UIImageView!
        
        func fillData(_ mpd: MyPageData) {
            titleLabel.text = mpd.title
            subtitleLabel.text = mpd.subtitle
            if let img = UIImage(systemName: mpd.imgName) {
                imageView.image = img
            }
            view.backgroundColor = mpd.bkgColor
        }
    }
    

    Note that we added a pageIndex property, which we'll use to track the "pages" as they're displayed.

    In our UIPageViewController, instead of an array of view controllers, we'll use an array of data structures:

        // array of data -- NOT view controllers
        var pageData: [MyPageData] = []
    

    and in viewDidLoad() we'll generate 350 of those:

    class MyDynamicViewController: UIPageViewController {
        
        // array of data -- NOT view controllers
        var pageData: [MyPageData] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
        
            let colors: [UIColor] = [
                .red, .green, .blue,
                .cyan, .magenta, .yellow,
                .systemRed, .systemGreen, .systemYellow,
            ]
            
            // let's create 350 "pages"
            var mpd: MyPageData
            for i in 0..<350 {
                mpd = MyPageData()
                mpd.title = "Page \(i)"
                mpd.subtitle = "Subtitle for page \(i)"
                mpd.imgName = "\(i % 20).circle.fill"
                mpd.bkgColor = colors[i % colors.count]
                pageData.append(mpd)
            }
            
            dataSource = self
            delegate = nil
            
            // instantiate the first "page"
            guard let sb = storyboard,
                  let vc = sb.instantiateViewController(withIdentifier: "page1") as? TestPageVC
            else {
                fatalError("Could not instantiate view controller!")
            }
            vc.loadViewIfNeeded()
            vc.fillData(pageData[0])
            vc.pageIndex = 0
    
            setViewControllers([vc], direction: .forward, animated: false, completion: nil)
        }
        
    }
    

    As we see at the end of viewDidLoad(), we instantiated the first "page" view controller and called setViewControllers([vc], ...)

    Now we write the UIPageViewControllerDataSource to instantiate and return new instances of "page" view controllers, rather than keeping them in a giant array:

    // typical Page View Controller Data Source
    extension MyDynamicViewController: UIPageViewControllerDataSource {
        
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            
            guard let vc = viewController as? TestPageVC else { return nil }
            
            if vc.pageIndex == 0 { return nil }
            
            guard let sb = storyboard,
                  let newVC = sb.instantiateViewController(withIdentifier: "page1") as? TestPageVC
            else {
                fatalError("Could not instantiate view controller!")
            }
            
            let n = vc.pageIndex - 1
            
            newVC.loadViewIfNeeded()
            newVC.fillData(pageData[n])
            newVC.pageIndex = n
    
            return newVC
            
        }
        
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
    
            guard let vc = viewController as? TestPageVC else { return nil }
            
            if vc.pageIndex >= pageData.count - 1 { return nil }
            
            guard let sb = storyboard,
                  let newVC = sb.instantiateViewController(withIdentifier: "page1") as? TestPageVC
            else {
                fatalError("Could not instantiate view controller!")
            }
            
            let n = vc.pageIndex + 1
            
            newVC.loadViewIfNeeded()
            newVC.fillData(pageData[n])
            newVC.pageIndex = n
            
            return newVC
    
        }
        
    }
    
    // typical Page View Controller Delegate
    extension MyDynamicViewController: UIPageViewControllerDelegate {
        
        // if you do NOT want the built-in PageControl (the "dots"), comment-out these funcs
        
        func presentationCount(for pageViewController: UIPageViewController) -> Int {
            return pageData.count
        }
        
        func presentationIndex(for pageViewController: UIPageViewController) -> Int {
            guard let firstVC = pageViewController.viewControllers?.first as? TestPageVC else {
                return 0
            }
            return firstVC.pageIndex
        }
        
    }