From_Base

[iOS Programming] 3. 뷰와 뷰 계층 구조(View & View Hierarchy) 본문

IOS

[iOS Programming] 3. 뷰와 뷰 계층 구조(View & View Hierarchy)

base_coding 2025. 3. 27. 15:48

iOS 에플리케이션에서 화면 UI를 구성하는 기본적인 단위, 화면에 보이는 UI요소들을 View 라고한다.

이러한 View들은 뷰들 간에 계층적으로 구성되며, 이렇게 구성된 View 계층을 View Hierarchy라고 한다.

 

이번에는 IOS 에플리케이션에서 화면 UI를 구성하는 뷰와 그 뷰들 간의 계층 구조에 대해서 알아보자.

 


▶️ 뷰(View)

  • 사용자에게 보일 수 있는 객체
  • 뷰는 UIView의 인스턴스이거나 UIView클래스 하위 클래스의 인스턴스
  • 통상적으로 클래스 계층에서 단말 뷰를 컨트롤 또는 위젯이라 부름
  • 뷰는 자신을 어떻게 그리는지 알고있음
  • 뷰는 터치 등등의 이벤트 처리 가능
  • 뷰 인스턴스는 뷰 계층 구조상에 존재
  • 뷰 계층 구조상의 루트 클래스는 바로 앱의 원도우(UIWindow)

 

iOS 앱에서 모든 View들은 뷰 계층구조상 UIView 클래스를 상속한 클래스의 인스턴스이다. 즉 모든 UI요소들은 UIView의 서브 뷰.

즉, Label, Button 등등의 뷰, UI 요소는 모두 UIView를 상속받아 만들어진 UIView 클래스의 하위클래스인 것이다. 

 

이러한 View는 서로간에 부모-자식 관계를 형성 - 즉 계층적으로 구성된다.

부모 View는 자식 View를 포함하는 Container View로써의 역할을 하며, 자식 View는 해당 부모 View 위에 얹힌다.

 


 

▶️ UIView와 UIViewController

🔹UIView

  • iOS에서 화면에 표시되는 모든 요소는 UIView를 기반으로 한다.
  • UIView는 화면을 구성하는 기본 단위로, 버튼, 레이블, 이미지 뷰 등의 기본 UI 요소들은 이 UIView를 상속받아 구현된다.
  • UIView의 주요 역할:
    • 화면에 그래픽 요소를 그린다.
    • 사용자 입력(터치 이벤트)을 처리한다.
    • 다른 뷰와 계층을 형성한다.

🔹UIViewController

  • UIViewController는 하나의 화면을 관리하는 컨트롤러 역할을 한다.
  • UIView는 화면을 구성하는 기본 단위이며, UIViewController는 이 UIView를 관리하는 컨트롤러
  • 각 뷰 컨트롤러는 하나의 루트 UIView를 가지며, 그 내부에 여러 개의 서브뷰를 추가할 수 있다.
  • UIViewController의 주요 역할:
    • 뷰의 생명주기 관리
    • 화면 전환 및 데이터 전달
    • 사용자 인터랙션 처리

🔹UIWindow

  • 모든 앱은 하나의 UIWindow 인스턴스를 가진다.
  • 앱의 여러 뷰들은 이 UIWindow 인스턴스 내 포함된다.

 

🔹앱 내의 view 인스턴스 포함 관계

UIWindow 인스턴스가 아이폰 앱 화면 그 자체, 즉 최상위 뷰 인스턴스임을 확인해볼 수 있다.

 


▶️  View 계층 구조(View Hierarchy)

View 계층 구조란?

  • iOS의 뷰 시스템은 계층적 구조를 가지고 있으며, 하나의 UIView 안에 여러 개의 Subview를 포함한다.
  • 최상위 뷰를 루트 뷰(root view)라고 하며, 그 아래에 여러 개의 서브 뷰(subview)를 배치할 수 있다.
  • 각 뷰는 슈퍼 뷰(superview)서브 뷰(subview) 관계를 가질 수 있다.

아래는 뷰 계층구조를 간단하게 보여주는 swift코드와 View 클래스 계층 구조도이다.

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let parentView = UIView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
        parentView.backgroundColor = .red
        
        let childView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
        childView.backgroundColor = .blue
        
        parentView.addSubview(childView) // childView를 parentView의 subview로 추가
        view.addSubview(parentView)      // parentView를 최상위 view의 subview로 추가
    }
}

 

  • parentView(빨간색)는 childView(파란색)를 포함하는 슈퍼 뷰
  • childView는 parentView의 서브 뷰
  • parentView는 ViewController의 루트 뷰에 추가됨(즉, ViewController의 루트 뷰->parentView->childView 구조의 계층)

 


뷰 클래스 계층도

 

 

 


▶️  화면이 그려지는 과정

윈도우를 포함한 각 계층 구조의 뷰는 자신을 레이어(CALayer)에 그린다.

그 후, 모든 뷰의 레이어들은 전체 화면에 합성되는 방식으로 ios앱의 화면이 그려진다. 

계산기 앱이 핸드폰에 그려지는 과정

 


 

▶️  UIViewController Lifecycle

  • init: 생성자 역할
  • loadView: 스토리보드 등을 이용하여 View Hierarchy를 완성하는 부분, 대체로 코딩할 내용은 없다
  • viewDidLoad: View가 화면에 나오기전에 초기화 등을 하는 부분, 여기서 많은 코딩이 일어난다
  • viewWillAppear: 화면에 보이기 직전에 호출되는 함수로 여기서 UI layout등을 조정한다
  • viewDidAppear: 화면이 보이는 상태, 여기서부터 사용자와 인터렉션이 가능하다
  • viewWillDisappear: 화면이 보이지 않게 되는 시점에서 호출, 특별한 코딩은 요구되지 않는다
  • viewDidDisappear: 화면이 사라진후에 호출, 통상적으로 저장되어야 하는 데이터가 있다면 저장한다

 

✅  화씨 -> 섭씨로 바꾸는 앱 만들기

 

아래와 같은 계층 구조의 화씨를 섭씨로 변환해주는 ios앱을 만들어보자.

화씨 변환 앱 계층구조도

 

 

 

Main스토리보드에서 뷰들의 레이아웃 등을 구성해주고, 해당 스토리보드를 ConversionViewController와 연결해준다.

현재 프로젝트 디렉터리안에 File->New->File->Cocoa Touch Class – ConversionViewController.swift 생성
Class 이름은 ConversionViewController,  Subclass of는 UIViewController로 입력

Main.storyboard에서 Identity Inspector클릭 후, Class에서 ConversionViewController 선택.
이제 Main.storyboard와 ConversionViewController가 연결되었다.

 

 

스토리보드는 아래와 같이 구성되어야한다. 

 

 

위처럼 스토리보드를 구성해준 다음, 스토리보드에서 placeholder로 Value가 적힌 텍스트필드와

??? 라벨을 ConversionViewController.swift에 컨트롤 + 드레그하여 outlet으로 추가. 두 파일에서 View객체와 변수간 연결을 한다. 

여기서 추가로, Value 텍스트필드는 action으로 하나더 드레그해준다. fahrenheitEditingChanged 메서드로 추가하여 텍스트필드에 텍스트 입력시 해당 값을 섭씨로 변환 후 ??? 라벨에 해당 섭씨 값을 출력시킬 목적이다.

 

 

 

 🌟 전체 코드  

import UIKit

class ConversionViewController: UIViewController {
    @IBOutlet weak var fahrenheitTextField: UITextField!
    @IBOutlet weak var celsiusLabel: UILabel!
}

extension ConversionViewController{
    override func viewDidLoad() {
        super.viewDidLoad()
        fahrenheitTextField.delegate = self
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dissmissKeyboard))
        view.addGestureRecognizer(tapGesture)
        
        // Do any additional setup after loading the view.
    }
}

extension ConversionViewController {
    @IBAction func fahrenheitEditingChanged(_ sender: UITextField) {
        if let text = sender.text {
            if let fahrenheitValue = Double(text) {
                let celsiusValue = 5.0/9.0*(fahrenheitValue-32.0)
                celsiusLabel.text = String.init(format: "%.2f", celsiusValue)
                
            }else {
                celsiusLabel.text = "???"
            }
        }
    }
}

extension ConversionViewController{
    @objc func dissmissKeyboard(sender: UITapGestureRecognizer) {
        fahrenheitTextField.resignFirstResponder()
    }
}

extension ConversionViewController: UITextFieldDelegate{
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        let existingTextHasDecimalSeperator = textField.text?.range(of: ".")
        let replacementTextHasDecimalSeperator = string.range(of: ".")
        if existingTextHasDecimalSeperator != nil && replacementTextHasDecimalSeperator != nil {
            return false
        } else{
            return true
        }
    }
}

 

 

 🌟 실행 화면