-
Notifications
You must be signed in to change notification settings - Fork 0
[REFACTOR] 레이아웃 팩토리 개선 및 빌더 도입 #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d8f4256
7cd9a17
ef4d22c
c13e8d4
b6a1c04
e18f572
123ccf7
7ad04b9
269afce
8bf7e35
eada1e1
c68ef47
30c6167
5505d8e
8a210b7
4fde99d
98ae966
08e9972
47f5066
2580238
65d74c8
db403e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| language: ko-KR # 언어 설정 | ||
|
|
||
| early_access: true # 미리보기 기능 활성화 | ||
| enable_free_tier: true # 프리 티어 활성화 | ||
| auto_resolve_threads: false # 자동 해결 비활성화 | ||
|
|
||
| reviews: | ||
| profile: chill | ||
| request_changes_workflow: true | ||
| high_level_summary: true # 리뷰에 대해 요약(high-level summary)를 자동 작성 | ||
| high_level_summary_placeholder: '@coderabbitai 요약' | ||
| auto_title_placeholder: '@coderabbitai' | ||
| poem: true | ||
| review_status: true # PR 리뷰 상태를 리뷰 요약란에 표시 | ||
| collapse_walkthrough: false # 리뷰 단계 설명을 기본적으로 접지 않음 | ||
|
|
||
| abort_on_close: true # PR이 닫히면 리뷰 수행을 중단(abort) | ||
|
|
||
|
|
||
| auto_review: | ||
| enabled: true # 자동 리뷰 기능을 활성화 | ||
| auto_incremental_review: true # 커밋이 추가될 때마다 변경 사항에 대해서만 자동 수행 | ||
| ignore_title_keywords: [] # PR 제목에 포함되면 리뷰를 건너뛰는 키워드 목록 | ||
| labels: [] # 특정 라벨이 붙은 PR만 자동 리뷰 대상 | ||
| drafts: false # Draft 상태인 PR은 자동 리뷰 대상에서 제외(false면 제외) | ||
| base_branches: [] # 특정 브랜치만 리뷰하도록 | ||
|
|
||
| tools: | ||
| shellcheck: # 셸 스크립트 문법 및 보안 검사 | ||
| enabled: true | ||
| ruff: # Python 코드 스타일 검사기 | ||
| enabled: true | ||
| markdownlint: # 마크다운 문법 검사 | ||
| enabled: true | ||
| github-checks: # GitHub 체크 연동 + 타임아웃(ms 단위) | ||
| enabled: true | ||
| timeout_ms: 90000 | ||
| languagetool: # 맞춤법, 문법 검사 | ||
| enabled: true | ||
| disabled_rules: | ||
| - EN_UNPAIRED_BRACKETS | ||
| - EN_UNPAIRED_QUOTES | ||
| disabled_categories: | ||
| - TYPOS | ||
| - TYPOGRAPHY | ||
| - CASING | ||
| enabled_only: false | ||
| level: default | ||
| enabled_rules: [] | ||
| enabled_categories: [] | ||
| biome: # JavaScript/TypeScript 정적 분석 | ||
| enabled: true | ||
| hadolint: # Dockerfile 코드 스타일 검사 | ||
| enabled: true | ||
| swiftlint: # Swift 코드 스타일 검사 | ||
| enabled: true | ||
| phpstan: # PHP 정적 분석 | ||
| enabled: true | ||
| level: default | ||
| golangci-lint: # Go 코드 스타일 검사 | ||
| enabled: true | ||
| yamllint: # YAML 형식 검사 | ||
| enabled: true | ||
| gitleaks: # Git 시크릿 노출 탐지 | ||
| enabled: true | ||
| checkov: # 인프라 보안 검사 | ||
| enabled: true | ||
| ast-grep: # AST 기반 코드 패턴 검사 | ||
| packages: [] | ||
| rule_dirs: [] | ||
| util_dirs: [] | ||
| essential_rules: true | ||
|
|
||
| # CodeRabbit AI 챗 기능을 사용 가능하게 하고, | ||
| # 한 번에 처리 가능한 토큰 수를 최대 4096으로 제한 | ||
| chat: | ||
| enabled: true | ||
| max_token_length: 4096 | ||
|
|
||
|
|
||
| # 지식 기반에 사용할 학습 범위를 지정하십시오. | ||
| # 'Local' - Repository | ||
| # 'Global'- Organization | ||
| # 'Auto' - Repository(users public) + Organization(private) | ||
| knowledge_base: | ||
| web_search: # AI 웹 검색 허용 | ||
| enabled: true | ||
| learnings: # 학습 범위 설정 (local, global, auto) | ||
| scope: local | ||
| issues: # 이슈 자동 참조 범위 설정 (local, global, auto) | ||
| scope: auto | ||
| jira: | ||
| project_keys: [] |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 안전한 서브스크립트 접근..! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import Foundation | ||
|
|
||
| public extension Collection { | ||
| subscript(safe index: Index) -> Element? { | ||
| return indices.contains(index) ? self[index] : nil | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| import UIKit | ||
|
|
||
| public final class CollectionLayoutBuilder { | ||
| private var itemSize: NSCollectionLayoutSize? | ||
| private var groupSize: NSCollectionLayoutSize? | ||
| private var numberOfItemsPerGroup: Int = 1 | ||
| private var interItemSpacing: NSCollectionLayoutSpacing? | ||
| private var section: NSCollectionLayoutSection? | ||
| private var headerItem: NSCollectionLayoutBoundarySupplementaryItem? | ||
|
|
||
| public init() { } | ||
|
|
||
| public init(section existingSection: NSCollectionLayoutSection) { | ||
| self.section = existingSection | ||
| } | ||
|
|
||
| @discardableResult | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컴파일러에서 경고 메시지를 이렇게 제한을 할 수도 있군요..! |
||
| public func item( | ||
| width: NSCollectionLayoutDimension, | ||
| height: NSCollectionLayoutDimension | ||
| ) -> Self { | ||
| itemSize = NSCollectionLayoutSize( | ||
| widthDimension: width, | ||
| heightDimension: height | ||
| ) | ||
|
|
||
| return self | ||
| } | ||
|
|
||
| @discardableResult | ||
| public func group( | ||
| width: NSCollectionLayoutDimension, | ||
| height: NSCollectionLayoutDimension | ||
| ) -> Self { | ||
| groupSize = NSCollectionLayoutSize( | ||
| widthDimension: width, | ||
| heightDimension: height | ||
| ) | ||
|
|
||
| return self | ||
| } | ||
|
|
||
| @discardableResult | ||
| public func numberOfItemsPerGroup(_ count: Int) -> Self { | ||
| numberOfItemsPerGroup = count | ||
|
|
||
| return self | ||
| } | ||
|
|
||
| @discardableResult | ||
| public func itemSpacing(_ spacing: CGFloat) -> Self { | ||
| interItemSpacing = .fixed(spacing) | ||
|
|
||
| return self | ||
| } | ||
|
|
||
| @discardableResult | ||
| public func withContentInsets( | ||
| top: CGFloat = 0, | ||
| leading: CGFloat = 0, | ||
| bottom: CGFloat = 0, | ||
| trailing: CGFloat = 0 | ||
| ) -> Self { | ||
| section?.contentInsets = NSDirectionalEdgeInsets( | ||
| top: top, | ||
| leading: leading, | ||
| bottom: bottom, | ||
| trailing: trailing | ||
| ) | ||
|
|
||
| return self | ||
| } | ||
|
|
||
| @discardableResult | ||
| public func composeSection(_ axis: UIAxis) -> Self { | ||
| guard let itemSize, let groupSize else { | ||
| fatalError("Item and Group must be set before creating section") | ||
| } | ||
|
|
||
| let item = NSCollectionLayoutItem(layoutSize: itemSize) | ||
|
|
||
| var group: NSCollectionLayoutGroup! | ||
|
|
||
| switch axis { | ||
| case .vertical: | ||
| group = NSCollectionLayoutGroup.vertical( | ||
| layoutSize: groupSize, | ||
| subitems: Array(repeating: item, count: numberOfItemsPerGroup) | ||
| ) | ||
|
|
||
| case .horizontal: | ||
| group = NSCollectionLayoutGroup.horizontal( | ||
| layoutSize: groupSize, | ||
| subitems: Array(repeating: item, count: numberOfItemsPerGroup) | ||
| ) | ||
|
|
||
| default: fatalError("Can't compose section to selected axis") | ||
| } | ||
|
|
||
| if let interItemSpacing { | ||
| group.interItemSpacing = interItemSpacing | ||
| } | ||
|
|
||
| section = NSCollectionLayoutSection(group: group) | ||
|
|
||
| return self | ||
| } | ||
|
|
||
| @discardableResult | ||
| public func header( | ||
| elementKind: String, | ||
| width: NSCollectionLayoutDimension = .fractionalWidth(1.0), | ||
| height: NSCollectionLayoutDimension = .fractionalHeight(1.0), | ||
| alignment: NSRectAlignment = .top | ||
| ) -> Self { | ||
| let headerSize = NSCollectionLayoutSize( | ||
| widthDimension: width, | ||
| heightDimension: height | ||
| ) | ||
|
|
||
| headerItem = NSCollectionLayoutBoundarySupplementaryItem( | ||
| layoutSize: headerSize, | ||
| elementKind: elementKind, | ||
| alignment: alignment | ||
| ) | ||
|
|
||
| if let headerItem { | ||
| section?.boundarySupplementaryItems = [headerItem] | ||
| } | ||
|
|
||
| return self | ||
| } | ||
|
|
||
| @discardableResult | ||
| public func withScrollingBehavior(_ behavior: UICollectionLayoutSectionOrthogonalScrollingBehavior) -> Self { | ||
| section?.orthogonalScrollingBehavior = behavior | ||
|
|
||
| return self | ||
| } | ||
|
|
||
| @discardableResult | ||
| public func groupSpacing(_ spacing: CGFloat) -> Self { | ||
| section?.interGroupSpacing = spacing | ||
|
|
||
| return self | ||
| } | ||
|
|
||
| @discardableResult | ||
| public func modifySection(_ modifier: (NSCollectionLayoutSection) -> Void) -> Self { | ||
| if let section = self.section { | ||
| modifier(section) | ||
| } | ||
| return self | ||
| } | ||
|
|
||
| @discardableResult | ||
| public func withExistingHeader(_ headerItem: NSCollectionLayoutBoundarySupplementaryItem) -> Self { | ||
| self.headerItem = headerItem | ||
|
|
||
| if let section = self.section { | ||
| section.boundarySupplementaryItems = [headerItem] | ||
| } | ||
|
|
||
| return self | ||
| } | ||
|
|
||
| @discardableResult | ||
| public func header(_ headerItems: [NSCollectionLayoutBoundarySupplementaryItem]) -> Self { | ||
| if let section = self.section { | ||
| section.boundarySupplementaryItems = headerItems | ||
| } | ||
|
|
||
| return self | ||
| } | ||
|
|
||
| public func build() -> NSCollectionLayoutSection { | ||
| guard let section else { fatalError("Section must be created before building") } | ||
| return section | ||
| } | ||
|
|
||
| public func buildHeader() -> NSCollectionLayoutBoundarySupplementaryItem { | ||
| guard let headerItem else { fatalError("Header must be created before building") } | ||
| return headerItem | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import UIKit | ||
|
|
||
| public protocol CollectionLayoutProvidable { | ||
| func makeLayout() -> NSCollectionLayoutSection | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import UIKit | ||
|
|
||
| public struct GridCollectionLayoutProvider: CollectionLayoutProvidable { | ||
| public init() { } | ||
|
|
||
| public func makeLayout() -> NSCollectionLayoutSection { | ||
| return CollectionLayoutBuilder() | ||
| .item(width: .fractionalWidth(0.5), height: .absolute(249)) | ||
| .group(width: .fractionalWidth(1.0), height: .absolute(249)) | ||
| .numberOfItemsPerGroup(2) | ||
| .itemSpacing(16) | ||
| .composeSection(.horizontal) | ||
| .withContentInsets(top: 16, leading: 20, bottom: 0, trailing: 20) | ||
| .groupSpacing(24) | ||
| .build() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import UIKit | ||
|
|
||
| public protocol HeaderLayoutProvidable { | ||
| func makeHeaderLayout(_ elementKind: String) -> NSCollectionLayoutBoundarySupplementaryItem | ||
| } |
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. protocol 을 분리하여 header를 정의하는 부분도 따로 채택할 수 있는 구조군요..! |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import UIKit | ||
|
|
||
| public struct TagCollectionLayoutProvider: CollectionLayoutProvidable, HeaderLayoutProvidable { | ||
| public init() { } | ||
|
|
||
| public func makeLayout() -> NSCollectionLayoutSection { | ||
| return CollectionLayoutBuilder() | ||
| .item(width: .estimated(100), height: .absolute(31)) | ||
| .group(width: .estimated(100), height: .estimated(31)) | ||
| .composeSection(.vertical) | ||
| .withScrollingBehavior(.continuous) | ||
| .groupSpacing(6) | ||
| .build() | ||
| } | ||
|
|
||
| public func makeHeaderLayout(_ elementKind: String) -> NSCollectionLayoutBoundarySupplementaryItem { | ||
| return CollectionLayoutBuilder() | ||
| .header(elementKind: elementKind, height: .absolute(24)) | ||
| .buildHeader() | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
공유주신 코드리뷰의 역할을 하는 기능인가 보네요!!