SniffMEET. UITabBarController VIPER 컨테이너로 다루기

VIPER 모듈을 사용하더라도, 직접 탭 바를 구현하지 않는 이상 UITabBarController는 사용해야 한다. 일반적인 VIPER 화면처럼 View, Presenter, Interactor, Router를 모두 갖춘 독립 모듈로 보기엔 애매한 것 같다. 특히 Interactor는 여기서 아무 의미가 없어보이고, Router는 UITabBarController에 내장되어 있으니까… 모듈 빌더 router, interactor를 생성하고 이를 가지고 있는 presenter를 생성해서 뷰에 집어넣는 방식 HomeModuleBuilder는 Home 화면에 필요한 VIPER 구성 요소를 한 곳에서 조립한다. ViewController를 만들고, Router와 Interactor를 생성한 뒤 Presenter에 주입한다. 마지막으로 NavigationFactory를 통해 화면을 UINavigationController로 감싸서 반환한다. ...

November 11, 2024

DevOps. GitHub Actions 워크플로우

들어가며 GitHub Actions는 저장소에서 발생한 이벤트를 기준으로 빌드, 테스트, 배포 같은 작업을 자동화한다. 자동화 과정은 .github/workflows 디렉토리의 YAML 파일로 정의한다. 처음 workflow를 작성할 때는 name, on, jobs, steps가 각각 무엇을 나타내는지부터 이해할 필요가 있다. 전체 구조 GitHub Actions의 실행 흐름은 다음과 같다. Event -> Workflow -> Job -> Runner -> Step 이 흐름을 YAML로 표현하면 다음과 같다. name: Build and Test run-name: Build triggered by @${{ github.actor }} on: push: branches: - main pull_request: branches: - main workflow_dispatch: jobs: build: runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Build run: echo "Build" test: needs: build runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Test run: echo "Test" name과 run-name name은 workflow 자체의 이름이다. GitHub 저장소의 Actions 탭에서 workflow를 구분할 때 사용한다. ...

November 5, 2024

UIKit. TableViewCell 스와이프 동작 넣기

기본 메소드들 // 오른쪽에서 왼쪽으로 스와이프 tableView(_:trailingSwipeActionsConfigurationForRowAt:) // 왼쪽에서 오른쪽으로 스와이프 tableView(_:leadingSwipeActionsConfigurationForRowAt:) 구현 순서 1. UIContextualAction을 사용해서 액션 구현 let deleteAction = UIContextualAction(style: .destructive, title: "삭제") { (action, view, completionHandler) in // 삭제 로직 completionHandler(true) } 2. UISwipeActionsConfiguration에 액션 등록 // 액션 여러개 등록 가능 let configuration = UISwipeActionsConfiguration(actions: [deleteAction]) 3. 전체 코드 func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let deleteAction = UIContextualAction(style: .destructive, title: "삭제") { (action, view, completionHandler) in // 삭제 로직 completionHandler(true) } let configuration = UISwipeActionsConfiguration(actions: [deleteAction]) configuration.performsFirstActionWithFullSwipe = false return configuration } 기타 색깔, 이미지 지정 가능 deleteAction.backgroundColor = .systemRed deleteAction.image = UIImage(systemName: "trash") performsFirstActionWithFullSwipe true: 스와이프 하면 첫 번째 액션이 즉시 실행 false: 스와이프 된 상태에서 나타는 버튼을 눌러서 액션 실행 실제 적용 코드 (이슈 트래킹 앱) func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let deleteAction = makeDeleteAction() let closeAction = makeCloseAction() let configuration = UISwipeActionsConfiguration(actions: [deleteAction, closeAction]) configuration.performsFirstActionWithFullSwipe = false return configuration } private func makeDeleteAction() -> UIContextualAction { let deleteAction = UIContextualAction(style: .destructive, title: nil) { (action, view, completionHandler) in // TODO: 얼럿 띄우고 삭제 로직 completionHandler(true) } deleteAction.image = UIImage(systemName: "trash") return deleteAction } private func makeCloseAction() -> UIContextualAction { let closeAction = UIContextualAction(style: .normal, title: nil) { (action, view, completionHandler) in // TODO: 이슈 닫기 로직 completionHandler(true) } closeAction.image = UIImage(systemName: "archivebox.fill") closeAction.backgroundColor = .systemIndigo return closeAction }

September 20, 2024

UIKit. UIView와 CALayer

UIView는 화면의 직사각형 영역을 나타내고, 그 안에 콘텐츠를 관리하는 객체이다. 대부분의 UI 요소가 UIView를 상속받으며, 화면에 콘텐츠를 보여주고 터치나 제스처를 감지하는 역할을 한다. UIView로 콘텐츠 표시하기 UIView는 UIKit에서 제공하는 메소드나 Core Graphics를 이용해 직사각형 영역에 콘텐츠를 그릴 수 있다. 배경색만 있는 간단한 UIView를 만들어보자. var magentaRectView: UIView = { let magentaRectView = UIView() magentaRectView.backgroundColor = .magenta return magentaRectView }() 오토레이아웃 제약은 임의로 정했다. UIView는 내부에 다른 UIView를 포함할 수 있다. 상위 뷰에 포함된 뷰를 서브 뷰라고 한다. UIViewController의 view 프로퍼티도 UIView 인스턴스이기 때문에, 위 마젠타 사각형도 UIView 내부의 UIView이다. 좀 더 직관적으로 확인하려면, 마젠타 사각형 안에 검은색 사각형을 추가해보자. ...

September 17, 2024

UIKit. TableViewCell에 UITextView 넣고 동적으로 높이 변경하기

UITableViewCell 안에 UITextView를 넣고, 입력되는 텍스트 길이에 따라 셀의 높이가 자동으로 늘어나도록 처리했다. 처음에는 그냥 UITextView를 셀에 넣으면 알아서 늘어날 줄 알았는데, UITextView 자체의 스크롤과 UITableView의 셀 높이 계산이 같이 걸려 있어서 생각보다 손이 조금 간다ㅏ. 핵심은 UITextView의 스크롤을 끄고, 텍스트가 변경될 때 UITableView에 높이를 다시 계산하라고 알려주는 것이다. 구현 순서 1. UITextView에서 다음 속성들 체크 해제 Show Horizontal Indicator: 텍스트가 가로 크기를 초과한 경우, 가로 스크롤 바 표시 Show Vertical Indicator: 텍스트가 세로 크기를 초과한 경우, 세로 스크롤 바 표시 Scrolling Enabled: 스크롤 가능 여부 설정 Bounce on Scroll: 스크롤이 끝에 도달했을 때, 가볍게 튕기는 애니메이션 설정 2. Nib 등록 tableView.register( UINib(nibName: "GrowingCell", bundle: nil), forCellReuseIdentifier: "GrowingCell" ) 3. 데이터 소스 추가 extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell( withIdentifier: "GrowingCell", for: indexPath ) as? GrowingCell else { return UITableViewCell() } cell.textView.text = items[indexPath.row] cell.delegate = self return cell } } 4. 델리게이트 작성 protocol GrowingCellProtocol: class { func updateHeightOfRow(_ cell: GrowingCell, _ textView: UITextView) } 5. 뷰 컨트롤러가 델리게이트 채택하고 구현 extension ViewController: GrowingCellProtocol { func updateHeightOfRow(_ cell: GrowingCell, _ textView: UITextView) { let size = textView.bounds.size let newSize = tableView.sizeThatFits( CGSize( width: size.width, height: CGFloat.greatestFiniteMagnitude ) ) if size.height != newSize.height { UIView.setAnimationsEnabled(false) tableView?.beginUpdates() tableView?.endUpdates() UIView.setAnimationsEnabled(true) if let thisIndexPath = tableView.indexPath(for: cell) { tableView.scrollToRow(at: thisIndexPath, at: .bottom, animated: false) } } } } 추가. 공식 문서에서는 beginUpdates() 보다 performBatchUpdates(_:completion:) 의 사용을 권장하고 있음. ...

September 15, 2024

UIKit. Responder Chain

앱에서 터치 입력과 같은 유저 인터랙션을 처리할 때, UIKit은 UIResponder 객체를 통해 이벤트를 감지하고 적절히 처리한다. 그렇다면 이벤트가 발생했을 때, 어떻게 적절한 UIResponder 객체를 찾아서 처리할까? UIKit에서 이벤트가 어떻게 전달되고 처리되는지, 특히 UIResponder와 Responder chain의 흐름을 정리했다. UIResponder와 UIEvent 개발자 문서에서 UIResponder는 ‘이벤트에 응답하고 처리하기 위한 추상 인터페이스’로 소개한다. 여기서 이벤트란, UIKit에서 UIEvent라는 클래스로 표현되는 단일 유저 인터랙션(예: 터치)을 의미한다. UIEvent 역시 개발자 문서에서 ‘앱에서 단일 유저 인터랙션을 설명하기 위한 객체’라고 정의되어 있다. ...

September 4, 2024

UIKit. UIStackView 레이아웃

UIStackView는 여러 뷰를 한 방향으로 묶어 배치할 수 있는 컨테이너 뷰다. 직접 서브뷰마다 제약 조건을 하나씩 걸지 않아도, 스택 뷰에 arranged subview로 추가하면 스택 뷰가 내부 뷰들의 배치를 관리한다. 스택 뷰는 기기의 방향, 화면 크기, 그리고 axis, alignment, distribution, spacing 같은 프로퍼티를 바탕으로 내부 뷰의 레이아웃을 결정한다. 이 글에서는 스택 뷰의 주요 프로퍼티가 레이아웃에 어떤 영향을 주는지 정리한다. intrinsicContentSize 스택 뷰의 레이아웃을 이해하려면 먼저 intrinsicContentSize를 알아야 한다. intrinsicContentSize는 뷰가 자신의 내용만을 기준으로 자연스럽게 가지는 크기다. 대표적인 예시는 UILabel이다. ...

August 27, 2024

UIKit. 화면 전환 방식

UIKit에서는 화면을 전환하는 방법이 여러 가지 있지만, 실제로는 상황에 따라 적절한 방식을 선택하면 된다. 화면을 계층적으로 이동한다 → Show Split View의 Detail 화면을 변경한다 → Show Detail 현재 화면 위에 독립적인 화면을 띄운다 → Present Modally 큰 화면에서는 팝오버, 작은 화면에서는 모달로 표시한다 → Present as Popover 이번 글에서는 각각의 방식이 어떤 상황에서 사용되는지 간단히 정리해 보려고 한다. 화면 전환 방식 Show show(_:sender:) 내비게이션 컨트롤러를 사용하는 경우 새로운 뷰 컨트롤러를 Push하여 화면을 전환한다. 사용자가 계층 구조를 따라 이동하는 대부분의 화면 전환이 여기에 해당한다. ...

August 21, 2024

UIView와 UIViewController의 차이 + 뷰 생명주기 메소드

공식문서 먼저 UIView An object that manages the content for a rectangular area on the screen. 화면의 사각형 영역을 나타내며, 내부에 다른 뷰를 포함할 수 있다. 대부분의 UI 요소가 UIView를 상속받는다. 콘텐츠를 화면에 나타내고, 사용자의 터치나 제스처를 감지한다. UIViewController An object that manages a view hierarchy for your UIKit app. 하나 이상의 뷰를 관리하고, 이 뷰들이 사용자와 상호작용하는 방법을 제어한다. 뷰를 생성하고, 화면에 표시하며, 뷰의 생명 주기를 관리한다. 두 줄 요약 UIView는 화면의 직사각형 영역에 표시되는 콘텐츠를 관리하고, 사용자의 동작을 감지하는 역할을 한다. UIViewController는 이 UIView를 관리하는 역할을 한다. (사용자의 동작을 감지하면 이 뷰가 어떻게 상호 작용을 해야 할지, UIView 라이프 사이클 관리 등…) 뷰 라이프 사이클 + 뷰 컨트롤러 관련 메소드 순서대로 작성하면 ...

August 20, 2024

DevOps. Git 기초

깃 파일 라이프 사이클 Tracked: 관리 대상 파일, 이미 스냅샷에 포함되어 있는 파일 - 깃이 이미 알고 있는 파일들… Unmodified: 수정하지 않은 파일 Modified: 수정한 파일 Staged: 커밋으로 저장소에 기록할 파일 Untracked: 관리 대상이 아닌 파일, 스냅샷 Staging Area에도 포함되지 않은 파일 위 캡처에서 test2: (Staged) 현재 Staging Area에 포함된 파일, 커밋하면 저장소에 기록된다. test: (Untracked) 트래킹 되지 않은 파일, 스냅샷에도 Staging Area에도 존재하지 않고 워킹 디렉토리에만 존재한다. README: (Modified) 수정된 파일, 커밋을 하기 위해서는 Staged 상태로 만들어야 함. 파일 새로 추적하기 (git add) git add 명령은 파일을 새로 추적할 때도 사용하고, 수정한 파일을 Staged 상태로 만들 때도 사용한다. ...

August 20, 2024