Search code examples
iosswiftuiscrollviewscrollviewuipagecontrol

Swift ScrollView Layout Issue With PageController and Images iOS


I can't figure out how to set constraints for a scrollView with an imageView inside. I am using the scrollView with a pageConroller to swipe thru a bunch of images.

See my layout in the picture below.

// Code for imageView

for index in 0..<drinksImagesArray.count {
        frame.origin.x = scrollView.frame.size.width * CGFloat(index)
        frame.size = scrollView.frame.size
        
        let imageView = UIImageView(frame: frame)
        imageView.contentMode = .scaleAspectFit
        imageView.image = UIImage(named: imagesArray[index].name)
        self.scrollView.addSubview(imageView)
    }
    scrollView.contentSize = CGSize(width: scrollView.frame.size.width * CGFloat(imagesArray.count), height: scrollView.frame.size.height)
    scrollView.delegate = self

Any suggestions? Thank you!

Layout


Solution

  • You will have much better luck using auto-layout --- it can handle all of the frame sizes and .contentSize for you.

    Here's a quick example - it uses a view controller with a scroll view added in Storyboard, so it should be pretty easy for you to integrate with your code:

    class ScrollingImagesViewController: UIViewController {
        
        @IBOutlet var scrollView: UIScrollView!
        
        var drinksImagesArray: [String] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // however you're populating your array...
            drinksImagesArray = [
                "drink1",
                "drink2",
                "drink3",
                // etc...
            ]
    
            // create a horizontal stack view
            let stack = UIStackView()
            stack.axis = .horizontal
            stack.alignment = .fill
            stack.distribution = .fillEqually
            stack.spacing = 0
    
            // add the stack view to the scroll view
            stack.translatesAutoresizingMaskIntoConstraints = false
            scrollView.addSubview(stack)
            
            // use scroll view's contentLayoutGuide for content constraints
            let svCLG = scrollView.contentLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // stack view constrained Top / Bottom / Leading / Trailing of scroll view CONTENT guide
                stack.topAnchor.constraint(equalTo: svCLG.topAnchor),
                stack.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor),
                stack.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor),
                stack.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor),
                
                // stack view height == scroll view FRAME height
                stack.heightAnchor.constraint(equalTo: scrollView.frameLayoutGuide.heightAnchor),
                
            ])
    
            // create image views and add them to the stack view
            drinksImagesArray.forEach { imgName in
                let v = UIImageView()
                v.backgroundColor = .lightGray
                v.contentMode = .scaleAspectFit
                // make sure we load a valid image
                if let img = UIImage(named: imgName) {
                    v.image = img
                }
                stack.addArrangedSubview(v)
            }
            
            // stack distribution is set to .fillEqually, so we only need to set the
            // width constraint on the first image view
            
            // unwrap it
            if let firstImageView = stack.arrangedSubviews.first {
                firstImageView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor).isActive = true
            }
            
        }
    
    }
    

    Edit

    After reviewing your Storyboard...

    Auto-layout doesn't seem to like it when you add a UINavigationBar and a UIToolbar and a UIScrollView as subviews. In particular, it appears to confuse the scroll view's frame related constraints.

    The fix is to first add constraints for your scroll view:

    • Top to Navigation Bar Bottom
    • Bottom to Page Control Top
    • Leading and Trailing to safe-area

    Storyboard / Interface builder will complain that the scroll view is not configured properly. You can either ignore that, or select the scroll view and set Ambiguity to Never Verify:

    enter image description here

    Then, in your view controller class, we need to create a height constraint for the stack view we're adding to the scroll view, and set that height constant in viewDidLayoutSubviews().

    Here's the full code:

    //
    //  WasserhaushaltViewController.swift
    //  deSynthTheOceans
    //
    //  Created by robinsonhus0 on 24.03.20.
    //  Copyright © 2020 robinsonhus0. All rights reserved.
    //
    
    import UIKit
    import AVFoundation
    import Charts
    import FSCalendar
    import HealthKit
    
    
    struct WasserSpeicher: Codable {
        let wassermenge: Double
        let speicherdatum: String
        let speicherStelle: Double
    }
    
    class WasserhaushaltViewController: UIViewController, UIScrollViewDelegate {
        @IBOutlet weak var diagrammView: UIView!
        @IBOutlet weak var scrollView: UIScrollView!
        @IBOutlet weak var pageControl: UIPageControl!
        
        let drinksImagesArray = ["tapWater", "water", "milk", "cola", "coffee", "tea", "juice", "beer"]
    
        var imageIndex = Int()
        
        struct Drinks {
            var name: String
            var tagesMengeFactor: Double
            var gesamtMengeFactor: Double
        }
        
        var frame = CGRect(x: 0, y: 0, width: 0, height: 0)
        var pageNumber = CGFloat()
        
        @IBOutlet weak var todaysWaterConsumptionLabel: UILabel!
        @IBOutlet weak var waterGoalProgress: UIProgressView!
        @IBOutlet weak var waterGoalLabel: UILabel!
        @IBOutlet weak var wasserMengeStepper: UIStepper!
        @IBOutlet weak var motivationTextView: UITextView!
        @IBOutlet weak var wasserglasButton: UIBarButtonItem!
        @IBOutlet weak var kleineFlascheButton: UIBarButtonItem!
        @IBOutlet weak var grosseFlascheButton: UIBarButtonItem!
        @IBOutlet weak var overAllWaterConsumptionLabel: UILabel!
        
        // added
        let scrollingImagesStackView = UIStackView()
        var stackHeightConstraint: NSLayoutConstraint!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            pageControl.numberOfPages = drinksImagesArray.count
            setupDrinkImages()
        }
        
        
        func setupDrinkImages() {
            // set stack view properties
            scrollingImagesStackView.axis = .horizontal
            scrollingImagesStackView.alignment = .fill
            scrollingImagesStackView.distribution = .fillEqually
            scrollingImagesStackView.spacing = 0
            
            // add the stack view to the scroll view
            scrollingImagesStackView.translatesAutoresizingMaskIntoConstraints = false
            scrollView.addSubview(scrollingImagesStackView)
            
            // use scroll view's contentLayoutGuide for content constraints
            let svCLG = scrollView.contentLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // stack view constrained Top / Bottom / Leading / Trailing of scroll view CONTENT guide
                scrollingImagesStackView.topAnchor.constraint(equalTo: svCLG.topAnchor),
                scrollingImagesStackView.bottomAnchor.constraint(equalTo: svCLG.bottomAnchor),
                scrollingImagesStackView.leadingAnchor.constraint(equalTo: svCLG.leadingAnchor),
                scrollingImagesStackView.trailingAnchor.constraint(equalTo: svCLG.trailingAnchor),
                
            ])
            
            // create the stack view height constraint - it will be updated in viewDidLayoutSubviews
            stackHeightConstraint = scrollingImagesStackView.heightAnchor.constraint(equalToConstant: 0)
            stackHeightConstraint.isActive = true
            
            // create image views and add them to the stack view
            drinksImagesArray.forEach { imgName in
                let v = UIImageView()
                v.backgroundColor = .orange
                v.contentMode = .scaleAspectFit
                // make sure we load a valid image
                if let img = UIImage(named: imgName) {
                    v.image = img
                }
                scrollingImagesStackView.addArrangedSubview(v)
            }
            
            // stack distribution is set to .fillEqually, so we only need to set the
            // width constraint on the first image view
            
            // unwrap it
            if let firstImageView = scrollingImagesStackView.arrangedSubviews.first {
                firstImageView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor).isActive = true
            }
    
            scrollView.delegate = self
        }
        
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            
            // since we have a UINavigationBar and a UIToolBar in the view hierarchy,
            //  we need to set this here
            //  Note: if the view size changes
            // stack view height == scroll view FRAME height
            stackHeightConstraint.constant = scrollView.frame.height
            
        }
        
    //  func setupDrinkImages() {
    //      for index in 0..<drinksImagesArray.count {
    //          frame.origin.x = scrollView.frame.size.width * CGFloat(index)
    //          frame.size = scrollView.frame.size
    //
    //          let imageView = UIImageView(frame: frame)
    //          imageView.contentMode = .scaleAspectFit
    //          imageView.image = UIImage(named: drinksImagesArray[index])
    //          self.scrollView.addSubview(imageView)
    //      }
    //      scrollView.contentSize = CGSize(width: scrollView.frame.size.width * CGFloat(drinksImagesArray.count), height: scrollView.frame.size.height)
    //      scrollView.delegate = self
    //  }
        
        func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
            pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
            pageControl.currentPage = Int(pageNumber)
        }
    }
    

    Your (modified) Storyboard is too big to add here... if you have any trouble with the changes mentioned above, here it is: https://pastebin.com/2Q1uFUgL