[내일배움캠프] 260105 TIL - layoutSubviews(), 아키텍처

Bambu·2026년 1월 5일

내배캠 TIL

목록 보기
12/52

1. 미니프로젝트

⁉️ 코드 리뷰

1) 원형 버튼 만들기

기존 코드

func roundButton() {
	personalButtons.forEach {
    	$0.layer.cornerRadius = min($0.bounds.height, $0.bounds.widht) / 2
        $0.clipsToBounds = true
    }
}

override func viewDidLayoutSubviews() {
	super.viewDidLayoutSUbviews()
    roundButton()
}

→ 함수를 통해 viewDidLayoutSubviews() 시점에 버튼을 원형으로 만듦

💡 피드백

  • viewDidLayoutSubviews() 시점이라도 오토레이아웃이 정확히 잡히지 않는 경우도 간혹 있음

    layoutSubviews()를 사용하면 크기가 확정된 시점에 원형으로 만들 수 있으므로 더 정확할 것

변경 코드

class CircularButton: UIButton {
	private var aspectConstraint: NSLayoutConstraint?
    
    init() {
    	super.init(frame: .zero)
        setConstraints()
    }
    
    required init?(coder: NSCoder) { ... }
    
    // 동그란 원을 만들기 위한 제약
    private func setConstraints() {
    	translatesAutoresizingMAskIntoConstraints = false
        asepctConstraint = heightAnchor.constraint(equalTo: widthAnchor)
        aspectConstraint?.priority = .required
        asepctConstraint?.isActive = true
    }
    
    // 원형으로 자르기
    override func layoutSubviews() {
    	super.layoutSubviews()
        layer.cornerRadius = min(bounds.height, bounds.width) / 2
        clipsToBounds = true
    }
}

// override func viewDidLayoutSubviews() 삭제

→ 커스텀 버튼 CircularButton을 생성하여 원형 버튼 생성

2) 완전한 원형 버튼 만들기

⚠️ 버튼이 타원형으로 그려지는 문제 발생

❗️원인: 오토레이아웃 충돌

CircularButton.heightAnchor.constraint(equalTo: widthAnchor)

buttonStack.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, constant: -30)

rowButtonStack.distribution = .fillEquarly
// firstButtonStack, secondButtonStack
// rowButtonStack 넓이 = (buttonStack.width - spacing) / 2

rowButtonStack.distribution에 따라 CircularButton.width = (buttonStack.width - spacing) / 2가 되어야 함

원형 버튼의 넓이 = 높이 조건도 맞춰야 함

⇒ 즉, CircularButton.height = (buttonStack.width - spacing) / 2가 되어야하는데, buttonStack.height는 2개의 원형 버튼 높이만큼 충분하지 않음

➡︎ 넓이 조건만 맞추고 높이는 부족한 채로 버튼이 생성

✅ 해결 방법
1️⃣ 버튼 높이 제한하기

func setUI() {
	...
    
    NSLayoutConstraint.active([
    ...
    ])
    
    personalButtons.forEach {
    	$0.widthAnchor.constraint(lessThanOrEqualTo: firstButtonStack.heightAnchor).isActive = true
    }
}

→ 원형 버튼의 높이를 rowButtonStack과 동일하거나 더 작도록 맞춤


➡︎ 부족한 높이만큼 가로 길이가 줄어들어 버튼의 전체적인 크기는 작아졌지만 동그란 원형 버튼 생성 가능

2️⃣ rowButtonStack.distribution 변경하기

firstButtonStack.distribution = .equalSpacing
secondButtonStack.distribution = .equalSpacing

→ 두 스택의 분배를 .fillEqually가 아닌 .equalSpacing으로 변경

.equalSpacing : 스택 뷰 객체 사이를 동일한 빈 공간으로 채움

➡︎ 원형 버튼이 rowButtonStack 넓이만큼 늘어나지 않고, 원형 버튼의 넓이 = 높이을 충족하는 범위에서 생성됨
➡︎ rowButtonStack의 높이 제한 함수가 없어도 1️⃣과 같은 형태의 버튼 생성 가능

3️⃣ buttonStack.width를 객체가 있는 범위만큼만 잡기

func setUI() {
	...
    
    NSLayoutConstraint.activate ([
    	...
        buttonStack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        
     // buttonStack.widthAnchor.constraint(equalTo: view.safeAreLayoutGuide.widthAnchor, constant: -30),
        
        buttonStack.leadingAnchor.constraint(greaterThanOrEqualTo: view.safeAreLayoutGuide.leadingAnchor, constant: 15)
        buttonStack.trailingAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -15)
    ])
}

→ 위 코드를 제외한 나머지는 기존 최초 코드 유지

buttonStackleadingAnchortrailingAnchorgreaterThanOrEqualTo, lessThanOrEqualTo로 맞춤으로써 buttonStack.width를 가변으로 만듦

원형 버튼의 넓이 = 높이을 충족하는 범위에서 버튼들이 생성됨

rowStackButton 생성시 설정한 .spacing = 10을 제외하고는 스택뷰 내 객체 간 공백 없음

➡︎ 원형 버튼들이 1️⃣, 2️⃣ 방법과 달리 중앙에 모여있음

3) SFSafariViewController와 WebView의 차이

  • SFSafariViewController
    : Safari 앱과 거의 동일한 기능 제공
    : Safari의 쿠키, 사용자 로그인 정보 등을 앱과 공유 (단, 읽기만 가능. 앱에서 사용은 불가능)
    : 앱과 Safari 간의 데이터 공유와 같은 상호작용 불가능
    : 커스텀 불가능

  • WebView
    : 앱의 UI에 웹의 콘텐츠를 통합
    : url 대신 HTML, CSS, JavaScript 콘텐츠르 만들어 사용 가능
    : 필요한 기능들을 개발자가 직접 상세히 구현 가능 - UI 커스텀 등
    : 앱과 웹의 상호작용 가능

→ 간단하게 블로그 페이지만 보여주는 이번 프로젝트에서는 SFSafriViewController를 사용하는 것이 적합했다고 판단한다.

참고: [Swift/TIL #36] Web 보여주기 (Safri 앱, SFSafariViewController, WKWebView
참고: [iOS/Swift] WKWebView (+ SFSafariViewController)

4) .first(where:)

기존 코드

@objc private func navigateTo(_ sender: UIButton) {
	guard let name = sender.currentTitle else { return }
    let vc = persons.filter { $0.name == name } [0].vc
    
    navigationController?.pushViewController(vc, animated: true)
}

persons.filter { $0.name == name }[0] 이 빈 배열일 경우와 같이 인덱스 조회 오류가 날 가능성 있음

⇒ 해당 프로젝트에서는 persons.name으로 버튼의 title을 생성했기 때문에 오류가 나지 않으리라 판단하고 위험 부담이 있는 코드 그대로 사용

💡 피드백

  • 인덱스 조회 오류가 발생할 가능성 있음
  • .first(where:) 메소드로 가독성도 높이고 오류 방지 가능
  • .filter 메소드는 필터링하기 위해 모든 요소를 순회함
    → 반면, .first(where:)는 첫 요소를 찾으면 종료되므로 성능 면에서도 효율적

변경 코드

@objc private func navigateTo(_ sender: UIButton) {
	guard let name = sender.currentTitle else { return }
    
    if let person = persons.first(where: { $0.name == name }) {
    	navigationController?.pushViewController(person.vc, animated: true)
    }
}

.first(where:) 코드 적용

5) shadowPath 설정 관련

기존 코드

final class PetCardViewController: UIViewController {
	private var isShadowPathSet = false

	override func viewDidLayoutSubviews() {
		super.viewDidLayoutSubviews()
    
    	guard !isShadowPathSet else { return }
        shadowView.layer.shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: shadowView.layer.cornerRadius).cgPath
        isShadowPathSet = true
    }
	...
}

isShadowPathSet 변수를 통해 shadowPath를 1번만 설정하도록 함

💡 피드백

  • viewDidLayoutSubviews()가 많이 호출되는 함수가 아니므로 shadowPath를 1번만 설정하도록 하지 않아도 성능에는 큰 차이 없음

  • shadowPath 설정을 최초 시점으로만 제한할 경우, 화면 방향이 바뀐다거나 하는 이벤트 발생 시 올바른 shadowPath를 설정할 수 없음
    → shadowPath를 사용하는 목적인 캐싱이 불가능

➡︎ 추후 프로젝트 진행 시 참고!

2. 아키텍처 (MVC, MVVM)

MVC 모델

  • Model, View, ViewController로 구분
  • 소규모 프로젝트에 적합
  • ViewController가 매우 커질 가능성이 있음
    → 대안으로 MVVM 등장
  • Model : 데이터 및 비즈니스 로직 담당
  • View : UI 요소 및 화면 표시
  • Model : Model과 View를 연결하고 UI 이벤트 처리

MVVM 모델

  • Model, View(Controller), ViewModel로 구분
  • 중,대규모 프로젝트에 적합
  • ViewModel을 분리시켜줌으로써 ViewController의 부담을 덜어줌
  • 유지 보수성이 좋음
  • 반응형 UI 프로젝트에 적합
  • Model : 데이터 및 비즈니스 로직 관리
  • ViewModel : UI 로직 처리, View에 필요 데이터 제공
    → Model에서 가능한가? 싶은 걸 여기서 미리 판단
    → 데이터 관련된 동작을 Model에게 시키는 역할
  • View(controller) : UI 담당, ViewModel과 바인딩
    → UI 관련한 동작을 하는 역할 (UI 업데이트 등)
profile
안녕하세요, iOS 개발을 공부하고 있는 Bambu입니다. (프로필: Swifticons)

0개의 댓글