Weekoding

[Swift] Snapkit 맛보기(코드 UI, layoutIfNeeded) 본문

공부 노트(Swift)

[Swift] Snapkit 맛보기(코드 UI, layoutIfNeeded)

Weekoding 2022. 4. 27. 12:57

⌘ 이번 포스팅은 지난 포스팅과 이어지는 포스팅이다.

 

[Swift] Snapkit Setting(+ UIWindow, Xcode 13.0 (iOS 15))

최근 공부하는 프로젝트들이 Storyboard(Interface Builder)를 일절 사용하지 않고(삭제한다), Snapkit과 코드를 이용하여 UI를 만든다. " 시각적으로 뚜렷하게 보여 layout 걸기 편리할 것 같은 Storyboard를 왜.

weekoding.tistory.com

 

저번 글에서 Snapkit이 무엇이고, 어떻게 사용환경을 구축하는지 알아보았다.

이제 간단하게 맛을 보자.

 

Snapkit을 import하고, View형 변수를 선언 해주자. 3개의 View를 이용할 예정이다.

그리고 viewDidLoad()에서 setViews()를 실행시켜줄 것이다.

    let myView = UIView()
    let myBtn = UIButton()
    let myLabel = UILabel()
    
        override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = .systemBackground
        self.setViews()
    }

 

setViews()는

  1) View 변수들의 속성을 정의하고,

  2) addSubView()를 통해 View에 추가하고,

  3) constraint를 걸어줄 메소드이다.

    private func setViews(){
        //View들의 속성 정의 함수
        self.viewStyle()
        
        [myView, myBtn, myLabel].forEach({ view.addSubview($0) })
        
        self.myView.snp.makeConstraints({
            $0.leading.trailing.top.bottom.equalToSuperview()
            // = $0.edges.equalToSuperView()
        })
        self.myBtn.snp.makeConstraints({
            $0.height.width.equalTo(60)
            $0.trailing.bottom.equalToSuperview().inset(30)
        })
        
        self.myLabel.snp.makeConstraints({
            $0.centerX.centerY.equalToSuperview()
        })
    }

 

viewStyle()은 View들의 속성을 정의하고, 버튼에 이벤트를 추가하는 메소드.

    private func viewStyle(){
        self.myLabel.font = .systemFont(ofSize: 30.0, weight: .bold)
        self.myView.backgroundColor = .systemGray
        self.myBtn.backgroundColor = .systemBlue
        self.myBtn.layer.cornerRadius = 10
        
        //Btn 이벤트 추가
        self.myBtn.addTarget(self, action: #selector(myBtnTapped(_:)), for: .touchUpInside)
    }

 

myBtnTapped()는, #selector 안에 들어갈 메소드로, 버튼이 눌렸을 때의 동작을 정의한 메소드.

@objc private func myBtnTapped(_ sender: UIButton){
	//remakeConstraint는 기존의 Constraint를 삭제하고, 새로운Constriant를 만드는 함수.
        self.myBtn.snp.remakeConstraints({
            $0.top.leading.trailing.equalToSuperview()
            $0.height.equalTo(80)
        })
        
        self.myView.snp.remakeConstraints({
            $0.bottom.leading.trailing.equalToSuperview()
            $0.height.equalTo(80)
        })
        
        self.myLabel.text = "Tada!"
        
        //animation
        UIView.animate(withDuration: 1.0, animations: {
            self.view.layoutIfNeeded()
        }, completion: nil)
    }

단순한 예제였기에, 코드는 끝이다. layoutIfNeeded()에 대해 공부해 보았다. 그전에 알아야 할 것이 있었는데...

 

 

🔄  Main Run Loop ?

: 이벤트들을 관찰하고, 처리하는 역할. 이벤트들에 맞는 핸들러를 찾아 그들에게 처리 권한을 위임한다. 

  이러한 과정 때문에 예시로 Button 터치 시, @IBAction 메소드가 이를 처리할 수 있게 되는 것.

  (이벤트로는 터치이벤트, 위치 변화, 디바이스 회전 등이 있다.)

 

앱 실행 시, iOS의 UIApplication은 'Main run loop'를 main thread에서 실행한다.

이벤트 발생 시 처리 권한이 넘어가고, 처리가 완료되면 당연하게도 권한이 다시 돌아올 것이다.

이를 Update Cycle이라고 한다.

 

 

🔄  Update Cycle ?

: Event Handler를 통해 변화되는 View를 체크한 뒤, Main run loop로 권한이 다시 돌아올 때

  체크했던 View들의 값이 바뀌는 시점.

 

→ position, layout같은 값을 변경하는 코드와, 실제로 값이 반영되는 시점에 시간차가 존재할 수 있다.

시간차가 있긴 하지만 너무 짧아서 사용자는 잘 모른다.

 

아무리 짧은 시간차여도, 이 미묘한 차이가 결과를 가른다. 이와 관련된 메소드가 존재한다.

 

layoutSubViews()

: View의 값을 호출한 즉시 변경시켜주는 메소드. update cycle에서 자동으로 호출된다.

그러나, 호출 시 해당 View의 모든 Subview들의 layoutSubViews()도 전부 호출한다.

그래서 자원이 많이 소모되기 때문에 직접 호출은 지양하고, 아래의 두 메소드를 주로 사용한다.

 

  setNeedsLayout() layoutIfNeeded()
공통점
  layoutSubViews() 메소드를 수동으로 호출하는 예약 메소드

차이점   • 비동기 실행:
    View에 대한 layoutSubView()를 예약한다.
    호출된 후 바로 반환된다.


  →  view는 Update cycle에 진입할 때 바뀐다.
  • 동기 실행:
     layoutSubView()를 예약함과 동시에 바로 실행시킨다.



  →  view는 Update cycle을 기다리지 않고, 즉시 바뀐다.

위와 같은 차이점으로, layoutIfNeeded()는 그 즉시 값이 변경되어야 하는 animation 효과에서 많이 사용된다.

 

setNeedsLayout()을 사용하게 되면, animate 코드 블록에서 view의 값이 변경되는 것이 아니기 때문이다.

update cycle에서 값이 반영되기 때문에 후에 view 값의 변경은 이루어지지만 animation 효과는 볼 수 없다.

 

 

📌  결과:  짠!

📂  전체 코드

class MyViewController: UIViewController{
    
    let myView = UIView()
    let myBtn = UIButton()
    let myLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .systemBackground
        self.setViews()
    }
    
    private func setViews(){
        self.viewStyle()
        
        [myView, myBtn, myLabel].forEach({ view.addSubview($0) })
        
        self.myView.snp.makeConstraints({
            $0.leading.trailing.top.bottom.equalToSuperview()
        })
        self.myBtn.snp.makeConstraints({
            $0.height.width.equalTo(60)
            $0.trailing.bottom.equalToSuperview().inset(30)
        })
        
        self.myLabel.snp.makeConstraints({
            $0.centerX.centerY.equalToSuperview()
        })
    }
    
    
    private func viewStyle(){
        self.myLabel.font = .systemFont(ofSize: 30.0, weight: .bold)
        self.myView.backgroundColor = .systemGray
        self.myBtn.backgroundColor = .systemBlue
        self.myBtn.layer.cornerRadius = 10
        
        self.myBtn.addTarget(self, action: #selector(myBtnTapped(_:)), for: .touchUpInside)
    }
    
    
    @objc private func myBtnTapped(_ sender: UIButton){
        self.myBtn.snp.remakeConstraints({
            $0.top.leading.trailing.equalToSuperview()
            $0.height.equalTo(80)
        })
        
        self.myView.snp.remakeConstraints({
            $0.bottom.leading.trailing.equalToSuperview()
            $0.height.equalTo(80)
        })
        
        self.myLabel.text = "Tada!"
        
        UIView.animate(withDuration: 1.0, animations: {
            self.view.layoutIfNeeded()
        }, completion: nil)
    }
}

 

 

 

 

오류 및 지적사항은 댓글로 남겨주시면 감사하겠습니다!

 

참고 :

https://github.com/SnapKit/SnapKit

https://baked-corn.tistory.com/105

 

Comments