ProgrammingView

Kyu hyunSung·2025년 4월 20일

IOS

목록 보기
6/9
post-thumbnail

이제는 프로그래밍으로도 View를 짜는법을 해볼거

LayoutGuide

  • 오토레이아웃을 위한 사각형 영역

  • 10개의 앵커(Anchor)를 가지고 있다.
    • 8개의 앵커는 좌표를 의미하며
    • 2개(wdithAnchor, heightAnchor)는 크기를 의미함

  • LayoutGuide를 이용한 설정
    – 각 Anchor는 constraint() 함수가 있어 이를 이용하여 아래와 같이 설정

프로그래밍 Constraint 적용

  • 프로그래밍 원칙
  1. 반드시 constraint를 적용하자고 하는 뷰(A)나 적용 당하는 뷰(B)는 계층적 뷰에 속해야 한다.
// 올바른 접근법
let myButton = UIButton(type: .system)
view.addSubview(myButton) // 계층에 추가
myButton.translatesAutoresizingMaskIntoConstraints = false
// 이제 제약조건 설정 가능

// 잘못된 접근법
let wrongButton = UIButton(type: .system) 
// 계층에 추가하지 않음 - 제약조건 적용 불가
wrongButton.translatesAutoresizingMaskIntoConstraints = false
  1. 오토리사이징 속성을 disable하여야 한다.
    • Interface Builder에서는 오토레이아웃을 적용하면 자동으로 오토리사이징 속성이 disable되나 프로그래밍에서는 일일이 해줘야 함
    A.translatesAutoresizingMaskIntoConstraints = false
  2. 생성된 컨트롤(A)의 LayoutGuide에서 10개의 Anchor중 (x, y), (w, h)를 결정할 수 있는 앵커를 선택하여 설정
    • 예) leftAnchor, topAnchor, widthAnchor, heightAnchor
// 기본 레이아웃 설정 예제
NSLayoutConstraint.activate([
    // 위치(x, y) 설정
    myView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
    myView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
    
    // 크기(w, h) 설정
    myView.widthAnchor.constraint(equalToConstant: 100),
    myView.heightAnchor.constraint(equalToConstant: 50)
])

참고사항

  • Autoresizing: 초기 iOS에서는 다양한 Device 크기를 만족하는 App을 만들기 위해서 오토리사이징 개념 을 도입하였음. 그러나 곧 다양성을 지원하기에는 한계가 있음을 알고 autolayout를 추가함. 그래서 하 나의 View에 대한 constraint가 두 방식을 양립함
  • Interface Builder: 인터페이스 빌더에서 컨트롤을 놓으면, 자동으로 Autoresizing은 리셋됨, 즉 autolayout만 적용됨
  • Programming: autoresizing이 기본적으로 동작하도록 설정됨, 그래서 이를 해제하기 위해서 translatesAutoresizingMaskIntoConstraints를 false로 설정함

프로그래밍 레이아웃

  1. 고정 레이아웃

– “Hello, Autolayout” Label

class ProgrammingViewController: UIViewController {
   
   override func viewDidLoad() {
       super.viewDidLoad()
       
       // 위치와 크기를 지정하여 UILabel을 생성
       let helloLabel = UILabel(frame: CGRect(x: 100, y: 100, width: 200, height: 30))
       
       // 생성된 UILabel을 꾸민다.
       helloLabel.text = "Hello, Autolayout"         // 레이블에 표시할 텍스트 설정
       helloLabel.backgroundColor = UIColor.green    // 배경색을 녹색으로 설정
       helloLabel.font = UIFont.systemFont(ofSize: 30, weight: .bold)  // 폰트를 30pt 크기, 볼드체로 설정
       helloLabel.textAlignment = .center            // 텍스트 정렬을 가운데로 설정
       
       // 계층적 뷰에 연결한다.
       view.addSubview(helloLabel)    // 생성한 레이블을 현재 뷰의 하위 뷰로 추가
   }
}

참고로 클래스 연결해야함

  • 결과


2. 동적 방법: 고정 레이아웃과 동일한 효과이다

override func viewDidLoad() {
   super.viewDidLoad()
   
   //let helloLabel = UILabel(frame: CGRect(x: 100, y: 100, width: 200, height: 30))
   let helloLabel = UILabel() // 위의 것을 대신하여 생성한다.
   
   // 생성된 UILabel을 꾸민다.
   helloLabel.text = "Hello, Autolayout"
   helloLabel.backgroundColor = UIColor.green
   helloLabel.font = UIFont.systemFont(ofSize: 30, weight: .bold)
   helloLabel.textAlignment = .center
   
   // 계층적 뷰에 연결한다.
   view.addSubview(helloLabel)
   // 오토리사이징을 해제한다.
   helloLabel.translatesAutoresizingMaskIntoConstraints = false
   
   // 좌, 상, 너비, 높이에 대한 제약 조건을 생성한다.
   let leadingConstraint = helloLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100)
   let topConstraint = helloLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 100)
   let widthConstraint = helloLabel.widthAnchor.constraint(equalToConstant: 200)
   let heightConstraint = helloLabel.heightAnchor.constraint(equalToConstant: 30)
   
   // 좌, 상, 너비, 높이에 대한 제약 조건을 활성화한다.
   leadingConstraint.isActive = true
   topConstraint.isActive = true
   widthConstraint.isActive = true
   heightConstraint.isActive = true
}
  • 앞의 방법에 대한 간략화
override func viewDidLoad() {
   super.viewDidLoad()
   
   //let helloLabel = UILabel(frame: CGRect(x: 100, y: 100, width: 200, height: 30))
   let helloLabel = UILabel() // 위의 것을 대신하여 생성한다.
   
   // 생성된 UILabel을 꾸민다.
   helloLabel.text = "Hello, Autolayout"
   helloLabel.backgroundColor = UIColor.green
   helloLabel.font = UIFont.systemFont(ofSize: 30, weight: .bold)
   helloLabel.textAlignment = .center
   
   // 계층적 뷰에 연결한다.
   view.addSubview(helloLabel)
   // 오토리사이징을 해제한다.
   helloLabel.translatesAutoresizingMaskIntoConstraints = false
   
   // 좌, 상, 너비, 높이에 대한 제약 조건을 생성한다.
   let leadingConstraint = helloLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100)
   let topConstraint = helloLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 100)
   let widthConstraint = helloLabel.widthAnchor.constraint(equalToConstant: 200)
   let heightConstraint = helloLabel.heightAnchor.constraint(equalToConstant: 30)
   
   // 좌, 상, 너비, 높이에 대한 제약 조건을 활성화한다.
   leadingConstraint.isActive = true
   topConstraint.isActive = true
   widthConstraint.isActive = true
   heightConstraint.isActive = true
}
  1. iOS 오토레이아웃 제약조건 설명
  • NSLayoutConstraint.activate 사용법
NSLayoutConstraint.activate([
    // view와 helloLabel 사이의 제약조건 (view에 속함)
    helloLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100),
    helloLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
    
    // helloLabel 자체의 제약조건 (helloLabel에 속함)
    helloLabel.widthAnchor.constraint(equalToConstant: 200),
    helloLabel.heightAnchor.constraint(equalToConstant: 30)
])
  • 예제
// 1. 레이블 생성
let helloLabel = UILabel()
helloLabel.text = "Hello, Autolayout"

// 2. 뷰 계층에 추가
view.addSubview(helloLabel)

// 3. 오토리사이징 비활성화 (필수)
helloLabel.translatesAutoresizingMaskIntoConstraints = false

// 4. 제약조건 설정 및 활성화
NSLayoutConstraint.activate([
    helloLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100),
    helloLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
    helloLabel.widthAnchor.constraint(equalToConstant: 200),
    helloLabel.heightAnchor.constraint(equalToConstant: 30)
])

예제

Todo

– 사진을 assets에 복사한다.
– UIImageView를 생성(imageView)한다.
– imageView.image = UIImage(named: “사진이름적으셈”)를 하여 이미지를 삽입한다.
– imageView를 view의 자식으로 등록한다.
– imageView의 translatesAutoresizingMaskIntoConstraints를 fasle한 다
– imageView의 상하좌우 여백을 0로 설정한다.

Asset에 사진 놓기

import UIKit

class ProgrammingViewController: UIViewController {

    override func viewDidLoad() {
       super.viewDidLoad()

       // 이미지 뷰를 위한 뷰를 생성한다
       let imageView = UIImageView()
       imageView.image = UIImage(named: "image") // assets에 있는 이미지를 객채화
       view.addSubview(imageView) // 자식으로 등록한다

       imageView.translatesAutoresizingMaskIntoConstraints = false

       // 상하좌우에 대한 0으로 여백을 설정한다
       NSLayoutConstraint.activate([
           imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
           imageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
           imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
           imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
       ])
    }
}

예제 2

  • view대신에 view.safeAreaLayoutGuide를 사용하라

import UIKit

class ProgrammingViewController: UIViewController {

    override func viewDidLoad() {
       super.viewDidLoad()

       // 이미지 뷰를 위한 뷰를 생성한다
       let imageView = UIImageView()
       imageView.image = UIImage(named: "image") // assets에 있는 이미지를 객채화
       view.addSubview(imageView) // 자식으로 등록한다

       imageView.translatesAutoresizingMaskIntoConstraints = false

       // 상하좌우에 대한 제약조건을 설정한다
       NSLayoutConstraint.activate([
           imageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), 
           // view -> safeAreaLayoutGuide로 변경
           imageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 
           // view -> safeAreaLayoutGuide로 변경, constant 제거
           imageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), 
           // view -> safeAreaLayoutGuide로 변경
           imageView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), 
           // view -> safeAreaLayoutGuide로 변경
       ])
    }
}

희미하게 간격 다른게 보일거임

그래도 모르겟으면

대충 이렇게됨

프로그래밍 허깅, 압축저항 우선순위

우선순위

  • 두개 이상의 UIView에 대하여 여백이 늘어나거나 줄어들때 UIView의 크기 제어
  1. setContentHuggingPriority
    설명: 뷰가 자신의 내용을 "꼭 감싸려는(hugging)" 우선순위를 설정합니다.
    • 높은 우선순위 = 뷰가 콘텐츠 크기보다 커지지 않으려고 함
    • 낮은 우선순위 = 뷰가 필요시 콘텐츠보다 쉽게 확장됨
UIView.setContentHuggingPriority(
    priority: UILayoutPriority,    // 높고/낮음 설정
    for: NSLayoutConstraint.Axis   // 수직/수평 설정
)
  1. setContentCompressionResistancePriority
    설명: 뷰가 자신의 내용이 "압축되는 것을 저항하는" 우선순위를 설정합니다.
    • 높은 우선순위 = 뷰가 콘텐츠 크기보다 작아지지 않으려고 함
    • 낮은 우선순위 = 뷰가 필요시 내용이 잘리더라도 쉽게 축소됨
UIView.setContentCompressionResistancePriority(
    priority: UILayoutPriority,    // 높고/낮음 설정
    for: NSLayoutConstraint.Axis   // 수직/수평 설정
)
  1. Ex
// 두 레이블이 나란히 있는 상황
let titleLabel = UILabel()
titleLabel.text = "제목"

let descriptionLabel = UILabel() 
descriptionLabel.text = "이 설명은 매우 길어질 수 있습니다..."

// 제목은 크기를 유지하고, 설명은 필요시 늘어나도록 설정
titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
descriptionLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)

// 제목은 압축되지 않고, 설명은 필요시 압축될 수 있도록 설정
titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
descriptionLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

예제 : 두개의 뷰

▪ 두개의 UIView에 대한 코딩

  • nameLabel: UILabel, nameTextField: UITextField를 생성하라.
  • nameLabel
    • text, backgroundColor을 각각 “로그인 ID”, .green으로 하라 • 상(safeArea),좌 (safeArea),우(nameTextField) 여백을 100, 10, 10으로 설정하라
  • nameTextField
    • placeholder, backgroundColor을 각각 “아이디 입력”, .cyan으로 하라 • 우(safeArea), 여백을 10으로 하라.
  • nameLabel, nameTextField의 centerYAnchor 을 같게 하라
  • 코드
// 레이블과 텍스트필드 생성
let nameLabel = UILabel()
nameLabel.text = "로그인 ID"                // 레이블에 표시할 텍스트 설정
nameLabel.backgroundColor = .green        // 레이블 배경색 초록색으로 설정

let nameTextField = UITextField()
nameTextField.placeholder = "아이디 입력"    // 텍스트필드 placeholder 설정
nameTextField.backgroundColor = .cyan     // 텍스트필드 배경색 청록색으로 설정

// 뷰 계층에 추가 (추가 순서는 중요하지 않음)
view.addSubview(nameTextField)  // add하는 순서는 중요하지 않다
view.addSubview(nameLabel)

// 오토레이아웃 사용을 위해 오토리사이징 비활성화
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameTextField.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
   // 레이블 위치 설정
   nameLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),  // 상단에서 100pt 떨어진 위치
   nameLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),  // 왼쪽에서 10pt 떨어진 위치
   
   // 레이블과 텍스트필드 관계 설정
   nameLabel.trailingAnchor.constraint(equalTo: nameTextField.leadingAnchor, constant: -10),  // 레이블의 오른쪽은 텍스트필드의 왼쪽에서 10pt 간격
   nameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),  // 텍스트필드의 오른쪽은 화면 오른쪽에서 10pt 간격
   nameTextField.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor)  // 텍스트필드와 레이블의 세로 중앙 정렬
])

  • nameLabel이 너무크게 나온다.
    – 여백으로 늘어난 상태이다. 허깅 우선순위로 제어할 수 있다.
    nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)

다음 코드를 추가하여 nameLabel에 고무밴드를 더 감아보자

그러나 막 입력을 하면 nameLabel이 줄여든다.

– 다음과 같이 코드 추가

nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) 
nameLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal
  • 효과가 없음
import UIKit

class ProgrammingViewController: UIViewController {

    override func viewDidLoad() {
       super.viewDidLoad()

        // 레이블과 텍스트필드 생성
        let nameLabel = UILabel()
        nameLabel.text = "로그인 ID"                // 레이블에 표시할 텍스트 설정
        nameLabel.backgroundColor = .green        // 레이블 배경색 초록색으로 설정

        let nameTextField = UITextField()
        nameTextField.placeholder = "아이디 입력"    // 텍스트필드 placeholder 설정
        nameTextField.backgroundColor = .cyan     // 텍스트필드 배경색 청록색으로 설정

        // 뷰 계층에 추가 (추가 순서는 중요하지 않음)
        view.addSubview(nameTextField)  // add하는 순서는 중요하지 않다
        view.addSubview(nameLabel)

        // 오토레이아웃 사용을 위해 오토리사이징 비활성화
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        nameTextField.translatesAutoresizingMaskIntoConstraints = false

        // 우선순위 설정 - nameLabel이 크기를 유지하도록 함
        nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
        // 압축 저항 우선순위도 높게 설정하여 내용이 짤리지 않도록 함
        nameLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
        // 주의: 위 두 코드를 추가해도 적절한 제약조건이 없으면 효과가 없을 수 있음

        NSLayoutConstraint.activate([
           // 레이블 위치 설정
           nameLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),  // 상단에서 100pt 떨어진 위치
           nameLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),  // 왼쪽에서 10pt 떨어진 위치

           // 레이블과 텍스트필드 관계 설정
           nameLabel.trailingAnchor.constraint(equalTo: nameTextField.leadingAnchor, constant: -10),  // 레이블의 오른쪽은 텍스트필드의 왼쪽에서 10pt 간격
           nameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),  // 텍스트필드의 오른쪽은 화면 오른쪽에서 10pt 간격
           nameTextField.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor)  // 텍스트필드와 레이블의 세로 중앙 정렬
        ])
    }
}
  • 문제확인하기
    print() 갈기기

  • 완성코드

import UIKit

class ProgrammingViewController: UIViewController {

    override func viewDidLoad() {
       super.viewDidLoad()

        // 레이블과 텍스트필드 생성
        let nameLabel = UILabel()
        nameLabel.text = "로그인 ID"                // 레이블에 표시할 텍스트 설정
        nameLabel.backgroundColor = .green        // 레이블 배경색 초록색으로 설정

        let nameTextField = UITextField()
        nameTextField.placeholder = "아이디 입력"    // 텍스트필드 placeholder 설정
        nameTextField.backgroundColor = .cyan     // 텍스트필드 배경색 청록색으로 설정

        // 뷰 계층에 추가 (추가 순서는 중요하지 않음)
        view.addSubview(nameTextField)  // add하는 순서는 중요하지 않다
        view.addSubview(nameLabel)

        // 오토레이아웃 사용을 위해 오토리사이징 비활성화
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        nameTextField.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
           // 레이블 위치 설정
           nameLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),  // 상단에서 100pt 떨어진 위치
           nameLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),  // 왼쪽에서 10pt 떨어진 위치

           // 레이블과 텍스트필드 관계 설정
           nameLabel.trailingAnchor.constraint(equalTo: nameTextField.leadingAnchor, constant: -10),  // 레이블의 오른쪽은 텍스트필드의 왼쪽에서 10pt 간격
           nameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),  // 텍스트필드의 오른쪽은 화면 오른쪽에서 10pt 간격
           nameTextField.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor)  // 텍스트필드와 레이블의 세로 중앙 정렬
        ])

        // 우선순위 설정 - 레이아웃 제약조건 활성화 이후에 설정됨
        nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)  // 레이블이 자신의 내용 크기를 유지하려는 성질 강화
        nameTextField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)  // 텍스트필드가 필요시 쉽게 압축될 수 있도록 설정
    }
}

프로그래밍 UIStackView

UIStackView의 장점

  • 자식 뷰들에 대한 배치를 자동으로 해준다.
    • 적용해야할 Constraints를 많이 줄여주어 코딩을 간단하게 해준다

AddArrangedSubView

  • addSubview도 존재하나 이것을 이용하여 자식뷰을 등록하면 스택뷰의 장점을 이용할 수 없음
// 스택뷰 생성
let stackView = UIStackView()

// 스택뷰 속성 설정
stackView.axis = .horizontal     // 가로 방향으로 요소들 배치
stackView.alignment = .center    // 요소들을 중앙에 정렬
stackView.distribution = .fill   // 가용 공간 채우기
stackView.spacing = 10           // 요소 사이 간격은 10포인트

// 요소 추가하기
stackView.addArrangedSubview(view1)
...
stackView.addArrangedSubview(viewN)

// 스택뷰를 메인 화면에 추가
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false  // 자동 크기 조정 비활성화

// 이제 stackView에 대한 제약조건 설정 가능

예제: UIStackView 이용

// UIStackView 기본 예제 코드 (iOS)
// UIStackView는 여러 UI 요소들을 쉽게 배치할 수 있게 해주는 컨테이너입니다.

import UIKit

class ProgrammingViewController: UIViewController  {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupStackView()
    }
    
    func setupStackView() {
        // 1. UIStackView 객체 생성
        let stackView = UIStackView()
        
        // 2. 스택뷰 기본 설정
        stackView.backgroundColor = UIColor(red: 0, green: 1, blue: 1, alpha: 1) // 청록색 배경
        stackView.translatesAutoresizingMaskIntoConstraints = false // 자동 크기 조정 비활성화
        
        // 3. 스택뷰 속성 설정
        stackView.axis = .horizontal      // 가로 방향으로 요소들을 배치 (수평축으로 설정)
        stackView.alignment = .center     // 요소들을 중앙에 정렬
        stackView.distribution = .fill    // 요소들이 공간을 채우도록 설정
        stackView.spacing = 10            // 요소들 사이의 간격은 10포인트
        
        // 4. UI 요소 생성 (라벨과 텍스트필드)
        // 4-1. 라벨 생성 및 설정
        let nameLabel = UILabel()
        nameLabel.text = "Name"
        nameLabel.backgroundColor = .green
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        
        // 4-2. 텍스트필드 생성 및 설정
        let nameTextField = UITextField()
        nameTextField.backgroundColor = .yellow
        nameTextField.translatesAutoresizingMaskIntoConstraints = false
        
        // 5. 허깅 우선순위 설정 (컨텐츠 크기 유지를 위한 설정)
        // 허깅 우선순위가 높을수록 자신의 고유 크기를 유지하려는 성향이 강해짐
        nameLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
        nameTextField.setContentHuggingPriority(.defaultLow, for: .horizontal)
        // 라벨은 텍스트 크기만큼만 공간을 차지하고, 텍스트필드는 남은 공간을 모두 차지
        
        // 6. stackView에 UI 요소 추가
        // 주의: addSubview가 아니라 addArrangedSubview 사용해야 스택뷰 특성이 적용됨
        stackView.addArrangedSubview(nameLabel)
        stackView.addArrangedSubview(nameTextField)
        
        // 7. 스택뷰를 메인 뷰에 추가
        view.addSubview(stackView)
        
        // 8. 스택뷰의 제약조건(Constraints) 설정
        NSLayoutConstraint.activate([
            // 왼쪽 여백: 스택뷰의 왼쪽을 안전영역 왼쪽에서 10포인트 떨어지게
            stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
            
            // 오른쪽 여백: 스택뷰의 오른쪽을 안전영역 오른쪽에서 -10포인트 떨어지게 (안쪽으로 10포인트)
            stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
            
            // 상단 여백: 스택뷰의 상단을 안전영역 상단에서 100포인트 떨어지게
            stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100),
            
            // 높이: 스택뷰의 높이를 120포인트로 고정
            stackView.heightAnchor.constraint(equalToConstant: 120)
        ])
        
        // 참고: 여기서 제약조건을 설정하는 이유는 스택뷰 자체의 위치와 크기를 지정하기 위함
        // 스택뷰 내부의 요소들은 스택뷰의 속성(axis, alignment, distribution, spacing)에 따라 자동 배치됨
    }
}

IB에서의 제한조건 Outlet

  • IB에서의 제한조건 Outlet
    – Interface Builder에서 설정한 Constraints도 UIView처럼 프로그램에서 변수로 액 세스할 수 있음
    – Document Outline에서 원하는 Constraint를 Control+Drag하여 ViewController 에 Drop하여 연결 생성

예제2

  1. IImageView 및 설정
    – Images 폴더에서 bird.png를 프로젝트에 복사
    • Navigator 판넬에서 asset폴더를 오픈하라
    • bird.png를 그곳에 drag & drop하라

  2. Object Library에서 UIImageView를 선택하여 ViewController에 드롭하라

  3. UIImageView 설정
    • 속성 Inspector의 Image에 bird를 선택하라.
    • 왼쪽여백 0, 윗여백 0, 크기(40, 20)으로 설정하라.

• 이를 ViewController의 birdImageView로 Outlet하라
4. Constraints와 ViewController연결
• Main.storyboard와 ViewController.swift를 동시에 오픈하라
• Document Outline에서 왼쪽여백의 Constraint를 Control+Drag하여 ViewController.swift에서 drop하라. 이름을 birdLeadingConstraint로 하라
• Document Outline에서 윗여백의 Constraint를 Control+Drag하여 ViewController.swift에서 drop하라. 이름을 birdTopConstraint로 하라

//
// 날아가는 새 애니메이션 구현 코드 (iOS)
//

import UIKit

class ProgrammingViewController: UIViewController {
    
    // MARK: - 속성
    
    // 새 이미지를 표시할 이미지뷰
    @IBOutlet weak var birdImageView: UIImageView!
    
    // 새의 위치 제약조건 (왼쪽 여백)
    @IBOutlet weak var birdLeadingConstraint: NSLayoutConstraint!
    
    // 새의 위치 제약조건 (위쪽 여백)
    @IBOutlet weak var birdTopConstraint: NSLayoutConstraint!
    
    // 타이머 객체
    var timer: Timer!
    
    // 새의 이동 방향 (1: 오른쪽, -1: 왼쪽)
    var direction = 1
    
    // MARK: - 뷰 라이프사이클
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 새 이미지뷰 초기 설정
        setupBirdImageView()
        
        // 타이머 생성 및 시작
        startBirdAnimation()
    }
    
    // MARK: - 설정 및 초기화
    
    func setupBirdImageView() {
        // 이미지뷰에 bird.png 이미지 설정
        birdImageView.image = UIImage(named: "bird")
        
        // 초기 위치 및 크기 설정 (스토리보드에서 설정해도 됨)
        // 왼쪽 여백: 0, 위쪽 여백: 0, 크기: 40x20
    }
    
    func startBirdAnimation() {
        // 타이머 생성 - 0.1초마다 timerAction 함수 실행
        timer = Timer.scheduledTimer(
            timeInterval: 0.1,        // 호출 주기 (0.1초)
            target: self,             // 호출 주체 (현재 클래스)
            selector: #selector(timerAction), // 실행할 함수
            userInfo: nil,            // 사용자 정보 (없음)
            repeats: true             // 반복 실행 여부 (계속 반복)
        )
    }
    
    // MARK: - 타이머 동작
    
    // @objc 어노테이션: Objective-C 런타임에 노출시키기 위해 필요함
    @objc func timerAction() {
        // 1. 새의 위치 업데이트 (왼쪽 여백 변경)
        birdLeadingConstraint.constant += CGFloat(direction * 10)
        
        // 2. 화면 경계 체크 및 방향 전환
        // 2-1. 오른쪽 경계 도달 확인
        if birdLeadingConstraint.constant + birdImageView.frame.size.width >= view.frame.size.width {
            // 오른쪽 끝에 도달하면 왼쪽으로 방향 전환
            direction = -1
            // 새의 이미지를 수평 방향으로 뒤집기 (왼쪽 방향)
            birdImageView.transform = .init(scaleX: -1, y: 1)
        } 
        // 2-2. 왼쪽 경계 도달 확인
        else if birdLeadingConstraint.constant < 0 {
            // 왼쪽 끝에 도달하면 오른쪽으로 방향 전환
            direction = 1
            // 새의 이미지를 원래대로 복원 (오른쪽 방향)
            birdImageView.transform = .init(scaleX: 1, y: 1)
        }
    }
    
    // MARK: - 리소스 정리
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        // 화면이 사라질 때 타이머 중지
        if timer != nil {
            timer.invalidate()
            timer = nil
        }
    }
}

프로그래밍 Action

  1. Action이란?
  • 간단히 말해 '액션'은 "사용자가 화면의 무언가와 상호작용할 때 실행될 코드"를 말함. 이러한 연결 방식을 '타겟-액션 메커니즘'이라고 부름.

  • Interface Builder에서 Action에 대한 함수를 설정할 수 있듯이 프로그래밍에서도 Action에 대한 함수를 설정할 수 있음
    • GestureRecognizer와 유사

// 버튼 생성
let button = UIButton(type: .system)
button.setTitle("클릭", for: .normal)

// 액션 연결하기
button.addTarget(self, 
                action: #selector(buttonClicked), 
                for: .touchUpInside)

  • GestureRecognizer와의 비교
    1. UIControl: 버튼, 슬라이더 등 정해진 컨트롤의 상호작용에 사용
    2. GestureRecognizer: 복잡한 제스처(핀치, 스와이프, 롱프레스 등)를 모든 뷰에 추가할 때 사용
// 제스처 인식기 사용 예시
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
imageView.addGestureRecognizer(tapGesture) // 이미지뷰에 탭 제스처 추가

@objc func handleTap() {
    print("이미지가 탭되었습니다!")
}
  • Action 종류(UIControl.EVENT)
    UIControl은 버튼, 슬라이더, 스위치 등의 기본 클래스로, 다양한 이벤트를 감지할 수 있음.

@MainActor
func addTarget(
    _ target: Any?,
    action: Selector,
    for controlEvents: UIControl.Event
)
  • 특정 이벤트(UIControl.Event)가 발생하면 target에 있는 Selector 함수를
  • Selector 함수주소로써 #selector(함수이름)으로 주어야 함
    • 이때 함수는 @objc키워드를 주어야 함

왜 단순히 함수 이름만 쓰면 안 될까?
Swift와 Objective-C의 '호출 방식'이 다르기 때문

Swift: 컴파일 시점에 함수가 연결됨 (정적 디스패치)
UIKit(Objective-C 기반): 실행 중에 함수 이름으로 찾아서 호출함 (동적 디스패치)

@objc func 함수이름(){ }

UIButton에 Action달기

  • 버턴을 생성하여 view에 부착
    • 타이틀: Start // setTitle함수 사용
    • 배경색: 파란색 – 제약조건 설정
    • 수평으로 화면 중앙
    • Bottom이 화면의 바닥에 위치
    • 크기: 전체 화면의 ½
  • 액션 달기
    • 버턴을 클릭하면 Start/Stop가 토글링함
import UIKit

class ProgrammingViewController: UIViewController {
    
    // MARK: - 뷰 라이프사이클
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // UIButton 객체 생성 및 설정
        setupButton()
    }
    
    // MARK: - 버튼 설정
    
    func setupButton() {
        // 1. 버튼 객체 생성
        let button = UIButton()
        
        // 2. 버튼 기본 속성 설정
        button.setTitle("Start", for: .normal)  // 버튼의 텍스트 설정
        button.backgroundColor = .blue         // 배경색을 파란색으로 설정
        button.translatesAutoresizingMaskIntoConstraints = false  // 자동 크기 조정 비활성화
        
        // 3. 버튼을 메인 뷰에 추가
        view.addSubview(button)
        
        // 4. Auto Layout 제약조건 설정
        NSLayoutConstraint.activate([
            // 버튼을 화면 중앙에 위치시키기 위한 X축 가운데 정렬
            button.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
            
            // 버튼을 화면 하단에 붙이기
            button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            
            // 버튼의 너비를 화면 너비의 50%로 설정
            button.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5)
        ])
        
        // 5. 버튼에 액션 연결하기 (GestureRecognizer와 유사한 방식)
        // - 버튼이 눌렸다 떼어질 때(touchUpInside) buttonPressed 함수 호출
        button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
    }
    
    // MARK: - 버튼 액션 처리
    
    // @objc: Objective-C 런타임에서 이 함수를 찾을 수 있도록 하는 키워드
    // 파라미터로 sender(이벤트를 발생시킨 객체)를 받아 어떤 버튼이 눌렸는지 알 수 있음
    @objc func buttonPressed(sender: UIButton) {
        // 버튼의 현재 텍스트 확인
        if let text = sender.titleLabel?.text {
            // 삼항 연산자로 버튼 텍스트 토글: "Start"면 "Stop"으로, 아니면 "Start"로
            sender.setTitle((text == "Start") ? "Stop" : "Start", for: .normal)
        }
        
        // 여기에 버튼이 눌렸을 때 실행할 추가 코드를 작성할 수 있음
        // 예: 애니메이션 시작/중지, 데이터 처리 등
    }
}

예제: 날아가는 새 제어하기

  • 앞의 날으는 새 예제에 버턴 액션 추가
    – viewDidLoad()에서 타이머 생성을 주석처리
    – buttonPressed함수를 다음과 같이 수정
import UIKit

class ProgrammingViewController: UIViewController {

    // MARK: - 속성

    // 새 이미지를 표시할 이미지뷰
    @IBOutlet weak var birdImageView: UIImageView!

    // 새의 위치 제약조건 (왼쪽 여백)
    @IBOutlet weak var birdLeadingConstraint: NSLayoutConstraint!

    // 새의 위치 제약조건 (위쪽 여백)
    @IBOutlet weak var birdTopConstraint: NSLayoutConstraint!

    // 타이머 객체
    var timer: Timer!

    // 새의 이동 방향 (1: 오른쪽, -1: 왼쪽)
    var direction = 1

    // MARK: - 뷰 라이프사이클

    override func viewDidLoad() {
        super.viewDidLoad()

        // 새 이미지뷰 초기 설정
        setupBirdImageView()

        // 버튼 생성 및 설정
        setupButton()

        // viewDidLoad에서는 타이머를 생성하지 않음 (버튼 클릭 시 생성)
    }

    // MARK: - 새 이미지뷰 설정

    func setupBirdImageView() {
        // 이미지뷰에 bird.png 이미지 설정
        birdImageView.image = UIImage(named: "bird")

        // 초기 위치 및 크기 설정 (스토리보드에서 설정해도 됨)
        // 왼쪽 여백: 0, 위쪽 여백: 0, 크기: 40x20
        birdImageView.translatesAutoresizingMaskIntoConstraints = false
    }

    // MARK: - 버튼 설정

    func setupButton() {
        // 1. 버튼 객체 생성
        let button = UIButton()

        // 2. 버튼 기본 속성 설정
        button.setTitle("Start", for: .normal)  // 버튼의 텍스트 설정
        button.backgroundColor = .blue         // 배경색을 파란색으로 설정
        button.translatesAutoresizingMaskIntoConstraints = false  // 자동 크기 조정 비활성화

        // 3. 버튼을 메인 뷰에 추가
        view.addSubview(button)

        // 4. Auto Layout 제약조건 설정
        NSLayoutConstraint.activate([
            // 버튼을 화면 중앙에 위치시키기 위한 X축 가운데 정렬
            button.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),

            // 버튼을 화면 하단에 붙이기
            button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),

            // 버튼의 너비를 화면 너비의 50%로 설정
            button.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5)
        ])

        // 5. 버튼에 액션 연결하기
        button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
    }

    // MARK: - 애니메이션 관련 메소드

    // 타이머 액션 메소드: 0.1초마다 호출되어 새의 위치를 업데이트
    @objc func timerAction() {
        // 1. 새의 위치 업데이트 (왼쪽 여백 변경)
        birdLeadingConstraint.constant += CGFloat(direction * 10)

        // 2. 화면 경계 체크 및 방향 전환
        // 2-1. 오른쪽 경계 도달 확인
        if birdLeadingConstraint.constant + birdImageView.frame.size.width >= view.frame.size.width {
            // 오른쪽 끝에 도달하면 왼쪽으로 방향 전환
            direction = -1
            // 새의 이미지를 수평 방향으로 뒤집기 (왼쪽 방향)
            birdImageView.transform = .init(scaleX: -1, y: 1)
        }
        // 2-2. 왼쪽 경계 도달 확인
        else if birdLeadingConstraint.constant < 0 {
            // 왼쪽 끝에 도달하면 오른쪽으로 방향 전환
            direction = 1
            // 새의 이미지를 원래대로 복원 (오른쪽 방향)
            birdImageView.transform = .init(scaleX: 1, y: 1)
        }
    }

    // MARK: - 버튼 액션 처리

    // 버튼이 눌렸을 때 호출되는 메소드
    @objc func buttonPressed(_ sender: UIButton) {
        // 버튼의 현재 텍스트 확인
        if let text = sender.titleLabel?.text {
            if text == "Start" {
                // 1. "Start" 버튼이 눌렸을 때: 타이머 생성 및 애니메이션 시작
                timer = Timer.scheduledTimer(
                    timeInterval: 0.1,        // 호출 주기 (0.1초)
                    target: self,             // 호출 주체 (현재 클래스)
                    selector: #selector(timerAction), // 실행할 함수
                    userInfo: nil,            // 사용자 정보 (없음)
                    repeats: true             // 반복 실행 여부 (계속 반복)
                )
            } else {
                // 2. "Stop" 버튼이 눌렸을 때: 타이머 중지 및 애니메이션 정지
                timer.invalidate()
                // 참고: 타이머를 nil로 설정하지 않으면 메모리 누수 발생 가능
                timer = nil
            }

            // 3. 버튼 텍스트 토글 (Start ↔ Stop)
            sender.setTitle((text == "Start") ? "Stop" : "Start", for: .normal)
        }
    }

    // MARK: - 리소스 정리

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        // 화면이 사라질 때 타이머 중지 (메모리 누수 방지)
        if timer != nil {
            timer.invalidate()
            timer = nil
        }
    }
}

근데 진지하게 이거 알바에 SwiftUI 공부하는게 정신건강에 훨 좋을듯

profile
디지털 치매 예방

0개의 댓글