본문 바로가기

iOS

📱iOS Stretchy header 오토 레이아웃으로 구현하기 (Snapkit)

새련된 느낌의 UI를 보여주는 Stretchy header입니다. 스크롤뷰를 아래쪽으로 당길수록 제일 위에있는 이미지가 늘어나는 뷰 입니다.
Snapkit을 사용한 간단한 오토레이아웃의 설정만으로 Stretchy header를 구현합시다.
(Snapkit을 사용하기 때문에 스토리보드 없이 UI를 그립니다.)
(아래에서 나타날 코드들은 위 예시의 간단버전입니다.)

간단 버전

화면 구성도

화면 구성

완성된 화면을 보았을때 스크롤이 되면서 UIImage가 늘어나기때문에 UIScrollView 안에 UIImage가 들어있을거라 생각했었는데 사실은  UIScrollView와 UIImageView는 같은 레이어에 위치시켜야합니다. 1️⃣UIImageView의 top은 스크롤 뷰와 함께 스크롤 되는것이 아니라 최상위 뷰의 top에 고정되어있어야하고(위 그림 UIImageVIew의 상단 빨간 선), 2️⃣bottom만 스크롤되도록 정의해야 합니다. 
그래서 UIImageView의 top은 변함없이 고정되어있고 bottom만 스크롤되면서 이미지의 높이가 늘어나게됩니다.
bottom만 스크롤되도록 하기위해 스크롤뷰 안에 보이지 않는 ImageContainerView를 생성하고 UIImageView의 bottom 을 ImageContainerView bottom과 동일하게 제약을 걸어줍니다!
자세한 구현 사항은 아래의 코드를 참고해서 구현하면 됩니다!

View 구현

// View.swift
import UIKit
import SnapKit

class View: UIView {
    
    // MARK: UIComponent 정의
    let scrollView: UIScrollView = {
        let scrollView = UIScrollView()
        
        scrollView.contentInsetAdjustmentBehavior = .never
        return scrollView
    }()
    
    let containerView = UIView()
    
    let image: UIImageView = {
        let imageView = UIImageView()
        
        imageView.image = UIImage(named: "169")
        imageView.contentMode = .scaleAspectFill
        return imageView
    }()
    
    let imageContainer = UIView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .white
        setup()
        bindConstraint()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: Layout 계층 정의
    private func setup() {
        containerView.addSubview(imageContainer)
        scrollView.addSubview(image)
        scrollView.addSubview(containerView)
        addSubview(scrollView)
    }
    
    // MARK: AutoLayout 정의
    private func bindConstraint() {
        scrollView.snp.makeConstraints { (make) in
            make.edges.equalTo(0)
        }
        
        imageContainer.snp.makeConstraints { (make) in
            make.left.right.top.equalToSuperview()
            make.height.equalTo(150)
        }
        
        image.snp.makeConstraints { (make) in
            make.left.right.equalToSuperview()
            make.top.equalTo(safeAreaLayoutGuide)
            make.bottom.equalTo(imageContainer)
        }
        
        containerView.snp.makeConstraints { (make) in
            make.edges.equalTo(0)
            make.width.equalTo(frame.width)
            make.left.right.top.equalToSuperview()
            make.height.equalTo(1000)
        }
    }
}

위코드는 "화면 구성도"에 그려진 화면을 코드로 나타낸 것입니다. 중요하게 봐야할 곳은 setup()함수와 bindConstrain()함수 입니다. setup()함수는 최상단에 정의된 UIComponent들을 계층구조를 정의하는 부분이고 bindConstraint()함수는 정의된 뷰들의 constraint를 정의해주는 함수입니다.

scrollView.snp.makeConstraints { (make) in
	make.edges.equalTo(0)
}

스크롤뷰는 최상위에 등록되어야할 뷰이고 전체화면 크기와 동일해야하고 움직이면 안되므로 모든 edge를 고정시켜줍니다.

imageContainer.snp.makeConstraints { (make) in
	make.left.right.top.equalToSuperview()
    make.height.equalTo(150)
}

imageContainer는 스크롤뷰 안에 있는 보이지 않는 자식 뷰로써 이미지뷰의 bottom constraint를 위해 생성합니다. 화면의 가장 위쪽에 이미지가 나타나야하기 때문에 left.right.top은 부모와 동일하게 제약을 생성하고 높이는 적당하게 150으로 잡았습니다.

image.snp.makeConstraints { (make) in
	make.left.right.equalToSuperview()
    make.top.equalTo(safeAreaLayoutGuide)
    make.bottom.equalTo(imageContainer)
}

image는 스크롤 진행도에 따라 늘어나야하는 이미지기때문에 최상단은 부모뷰(루트 뷰)의top으로 잡거나 위와같이  safeAreaLayoutGuide로 잡아서 고정시켜야합니다. bottom의 경우에는 스크롤뷰 안에있는 imageContainer의 bottom으로 잡아줘서 스크롤 되도록 설정합니다!

let image: UIImageView = {
	let imageView = UIImageView()
        
    imageView.image = UIImage(named: "169")
    imageView.contentMode = .scaleAspectFill
return imageView
}()

이미지뷰를 정의할때 설정해줘야할 속성은 contentMode입니다. 높이가 늘어남에따라 이미지도 늘어나야하기 때문에 contentMode를 scaleAspectFill로 설정합니다. scaleAspectFit으로 설정한다면 이미지가 폭에만 맞춰져서 늘어나지 않습니다!

 

ViewController 구현

// ViewController.swift
import UIKit

class ViewController: UIViewController {
    
    private lazy var photoView = View(frame: self.view.frame)
    
    static func instance() -> UIViewController {
        return ViewController(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view = photoView
        // Do any additional setup after loading the view.
    }
}

ViewController에서는 다른 기능을 하진 않기때문에 뷰만 설정해줍니다.