Search code examples
iosswiftscrolluikit

How to make scroll in Swift, UIKit


enter image description here

I made this app like that above image. but Scroll is not working. I want make scrollView fully down.

I have asked a question related of this and I was worked. But I need make some function, and Strategy is...

  1. set views (The order is Title Label, Date Label, Image Label(option), Article Label).
  2. anchors are follow above view like this: NSLayoutConstraint.activate([...topAnchor...(equalTo: aboveView.bottomAnchor, constrant: 20)
  3. scrollView and UIView(Container)'s height will be update after setting views height union.

Code is down Bellow. All Views are setting by code.

DetailViewController.Swift

import UIKit

class DetailViewController: UIViewController {
    
    private let titleLabel: UILabel = {
        let view = UILabel()
        view.numberOfLines = 2
        return view
    }()
    
    private let dateLabel: UILabel = {
        let view = UILabel()
        return view
    }()
    
    private let scroll: UIScrollView = {
        let scroll = UIScrollView()
        scroll.backgroundColor = .brown
        return scroll
    }()
    
    private let container: UIView = {
        let container = UIView()
        container.backgroundColor = .lightGray
        return container
    }()
    
    private let image: UIImageView = {
        let image = UIImageView()
        return image
    }()
    
    private let controller: UIPageControl = {
        let controller = UIPageControl()
        return controller
    }()
    
    private let content: UITextView = {
        let content = UITextView()
        return content
    }()
    
    var fileName: String!
    var id: String!
    var entryNumber: Int!
    var dataSize: Int!
    var entry: Entry!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        // Get contents id and parse file name
        parsingFileName(id: id)
                
        addScrollView()
        scroll.contentSize = CGSize(width: self.view.frame.width, height: self.view.frame.height)
        
        addTopLabels()
        
        content.text = entry. Article
        
        // isImage is true or false
        addContent(isImage: entry.isImage)
        
        // For debugging
        content.backgroundColor = .yellow
        scroll.updateContentSize()
        content.contentSize = CGSize(width: self.scroll.frame.width, height: self.scroll.frame.height)
        
    }

}

Extension 1: Union Function

extension UIScrollView {
    func updateContentSize() {
        let unionTotal = recursiveUnion(view: self)
        
        self.contentSize = CGSize(width: self.frame.width, height: unionTotal.height + 50)
    }
    
    private func recursiveUnion(view: UIView) -> CGRect {
        var totalRect: CGRect = .zero
        
        for v in view.subviews {
            totalRect = totalRect.union(recursiveUnion(view: v))
        }
        print("Updated content Size: \(totalRect.union(view.frame))")
        return totalRect.union(view.frame)
    }
}

Extension 2: Set views and constraints

extension DetailViewController {
    
    fileprivate func parsingFileName(id: String) {
        // parse ID and get filename
        // ref.: https://ssooyn.tistory.com/22
        let s = id.index(id.startIndex, offsetBy: 0)
        let e = id.index(id.startIndex, offsetBy: 7)
        let r = s...e
        
        fileName = "entry-" + String(id[r])
        let dataFromFile = DataLoader(fileName: fileName, fileType: ".json").entry
        
        // Find the entry
        for i in 0 ..< dataFromFile.count {
            if dataFromFile[i].id == id {
                entry = dataFromFile[i]
                break
            }
        }
    }
    
    fileprivate func addScrollView() {
        // Setting views - ScrollView and Container
        view.addSubview(scroll)
        scroll.translatesAutoresizingMaskIntoConstraints = false
        
        scroll.addSubview(container)
        container.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            scroll.topAnchor.constraint(equalTo: self.view.topAnchor),
            scroll.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
            scroll.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            scroll.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
            
            container.topAnchor.constraint(equalTo: self.scroll.topAnchor),
            container.bottomAnchor.constraint(equalTo: self.scroll.bottomAnchor),
            container.leadingAnchor.constraint(equalTo: self.scroll.leadingAnchor),
            container.trailingAnchor.constraint(equalTo: self.scroll.trailingAnchor),
            
            container.widthAnchor.constraint(equalTo: self.scroll.widthAnchor, multiplier: 1),
            container.heightAnchor.constraint(equalTo: self.scroll.heightAnchor, multiplier: 1)
        ])
    }
 
    fileprivate func addTopLabels() {
        // Setting Views - Top Labels

        titleLabel.textAlignment = .left
        titleLabel.text = entry.title
        titleLabel.font = UIFont(name: "Pretendard-SemiBold", size: 17)
        
        dateLabel.textAlignment = .right
        dateLabel.text = String(entry.year) + "." + String(entry.month) + "." + String(entry.day)
        
        container.addSubview(titleLabel)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        
        container.addSubview(dateLabel)
        dateLabel.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: container.topAnchor),
            titleLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            titleLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
            titleLabel.heightAnchor.constraint(equalToConstant: 20),
            titleLabel.widthAnchor.constraint(equalTo: container.widthAnchor),
            
            dateLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor),
            dateLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            dateLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor),
            dateLabel.heightAnchor.constraint(equalToConstant: 20),
            dateLabel.widthAnchor.constraint(equalTo: container.widthAnchor),
        ])
    }
    
    fileprivate func addContent(isImage: Bool) {
        if isImage == false {
            print("No Image")
            image.backgroundColor = .darkGray
            image.isHidden = true
            
            container.addSubview(content)
            content.translatesAutoresizingMaskIntoConstraints = false
            content.sizeToFit()
            content.isScrollEnabled = false
            
            NSLayoutConstraint.activate([
                content.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: 20),
                content.leadingAnchor.constraint(equalTo: container.leadingAnchor),
                content.trailingAnchor.constraint(equalTo: container.trailingAnchor),
//                content.bottomAnchor.constraint(equalTo: container.bottomAnchor),
                
                content.heightAnchor.constraint(equalTo: container.heightAnchor),
                content.widthAnchor.constraint(equalTo: container.widthAnchor),
            ])
        } else {
            print("There a image")
            image.backgroundColor = .red
            image.image = UIImage(named: "202310172204-1-1.jpeg")
            
            container.addSubview(image)
            content.translatesAutoresizingMaskIntoConstraints = false
            content.sizeToFit()
            content.isScrollEnabled = false
            
            container.addSubview(content)
            image.translatesAutoresizingMaskIntoConstraints = false
            
            print(content.contentSize.height)
            
            NSLayoutConstraint.activate([
                image.topAnchor.constraint(equalTo: dateLabel.bottomAnchor, constant: +20),
                image.leadingAnchor.constraint(equalTo: container.leadingAnchor),
                image.trailingAnchor.constraint(equalTo: container.trailingAnchor),
                image.heightAnchor.constraint(equalTo: container.widthAnchor),
                image.widthAnchor.constraint(equalTo: container.widthAnchor),
                
                content.topAnchor.constraint(equalTo: image.bottomAnchor, constant: +20),
                content.leadingAnchor.constraint(equalTo: container.leadingAnchor),
                content.trailingAnchor.constraint(equalTo: container.trailingAnchor),
//                content.bottomAnchor.constraint(equalTo: container.bottomAnchor),
                content.heightAnchor.constraint(equalTo: container.heightAnchor),
                content.widthAnchor.constraint(equalTo: container.widthAnchor),
            ])
        }
    }
}

Thank you for reading long story and code. :) Have a nice day!


Solution

  • First, you are doing a whole lot of unnecessary stuff...

    Second, get in the practice of naming properties / objects / etc in a "readable" way, e.g. content doesn't tell you (or someone else looking at your code) what it is. Using contentTextView is obvious.

    Third, start simple - especially while you're learning.

    Take a look at this code:

    class DetailViewController: UIViewController {
        
        private let titleLabel: UILabel = {
            let view = UILabel()
            view.numberOfLines = 2
            return view
        }()
        
        private let dateLabel: UILabel = {
            let view = UILabel()
            return view
        }()
        
        private let scrollView: UIScrollView = {
            let scroll = UIScrollView()
            scroll.backgroundColor = .brown
            return scroll
        }()
        
        private let topImageView: UIImageView = {
            let image = UIImageView()
            return image
        }()
        
        private let contentTextView: UITextView = {
            let content = UITextView()
            content.isScrollEnabled = false
            return content
        }()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
            
            titleLabel.text = "This is the Title Label"
            
            //image.image = UIImage(named: "202310172204-1-1.jpeg")
            if let img = UIImage(systemName: "swift") {
                topImageView.image = img
            }
    
            dateLabel.text = "This is the Date Label"
    
            // let's create a string of 50 lines for the "content" text field
            var sTemp: String = "Line 1"
            for i in 2...50 {
                sTemp += "\nLine \(i)"
            }
            contentTextView.text = sTemp
    
            // let's put all those elements into a vertical UIStackView
            let stackView = UIStackView()
            stackView.axis = .vertical
            stackView.spacing = 8
            
            [titleLabel, topImageView, dateLabel, contentTextView].forEach { v in
                stackView.addArrangedSubview(v)
            }
            
            // top image view needs a height constraint
            topImageView.heightAnchor.constraint(equalToConstant: 160.0).isActive = true
            
            // add the stack view to the scroll view
            stackView.translatesAutoresizingMaskIntoConstraints = false
            scrollView.addSubview(stackView)
            
            // add the scroll view to self's view
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(scrollView)
            
            let g = view.safeAreaLayoutGuide
            let cg = scrollView.contentLayoutGuide
            let fg = scrollView.frameLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // constrain all 4 sides of scroll view
                //  we'll inset by 20-points so we can see the framing
                scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
    
                // constrain all 4 sides of stack view
                //  to the scroll view's Content Layout Guide
                //  we'll inset by 12-points so we can see the framing
                stackView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 12.0),
                stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 12.0),
                stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -12.0),
                stackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: -12.0),
    
                // we'll make the stack view width 24-points less than
                //  the scroll view's Frame Layout Guide
                //  (24 because we have 12-points on each side)
                stackView.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -24.0),
    
            ])
            
            // let's set some background colors so we can see the framing
            titleLabel.backgroundColor = .cyan
            topImageView.backgroundColor = .red
            dateLabel.backgroundColor = .green
            contentTextView.backgroundColor = .yellow
    
            // let's use a larger font for the content text view
            //  for demonstration purposes
            contentTextView.font = .systemFont(ofSize: 20.0, weight: .light)
        }
        
    }
    

    Here's the result:

    enter image description here

    enter image description here

    enter image description here

    Absolutely NO "calculating sizes" ... far, far fewer constraints needed ... everything scrolls as expected ... etc.