네이버 쇼핑 API를 기반으로 만든 쇼핑 앱입니다.
홈 | 검색 | 찜 | 최근 본 상품 | 소셜 로그인 |
---|---|---|---|---|
- 최소 버전 : iOS 15.0
- 개발 인원 : 1인
- 개발 기간 : 2023.05.12 ~ 2023.06.21 (6주)
- 이메일 회원인증과 구글, 애플 소셜로그인 기능 제공
- 네이버 쇼핑 API 기반 주제/스타일 추천/카테고리 상품 정보 제공
- 검색/최근 검색어/찜/최근 본 상품 기능 제공
- Swift
- UIKit, WebKit, CryptoKit, Authentication Services
- MVP, Singleton, Delegate Pattern
- Alamofire, SnapKit, Kingfisher, Tabman, TTGTagCollectionView, Toast
- CodeBase UI, AutoLayout, CompositionalLayout, DiffableDataSource, UserDefaults, Codable,
- Firebase RealtimeDatabase, Firebase FireStore, Firebase Auth, Unit Test
- Firebase Auth, Firebase FireStore를 활용해 OAuth 2.0 기반
이메일 회원인증/소셜 로그인(구글, 애플)
구현 - 애플 로그인 구현 시, Firebase 무결성 검사를 위해 CryptoKit 기반
nonce 생성 로직
추가 - Alamofire를 사용한
REST API 통신
구현 - offset 기반
페이지 네이션
을 통해 상품 정보 표현 - UserDefaults를 활용해
최근 검색어 CRUD
구현, Firebase RealtimeDatabase를 활용해찜/최근 본 상품 CRUD
구현 - CompositionalLayout을 활용해
그리드, 리스트
레이아웃 구현 - DiffableDataSource + Notification을 통한
Expandable Section
구현 - BDD 기반의
Unit Test
구현
-
도전 상황
복잡한 화면을 구현할 때, UITableView + UICollectionView 구조로 구현하면 코드도 복잡해지고 핸들링하는데 많은 리소스가 들어가기 때문에CompositionalLayout
을 도입 -
도전 결과
단일 collectionView만으로 복잡한 레이아웃을 직관적으로 구현. 각각의 레이아웃 요소들을 모듈화 할 수 있었기에 레이아웃이 복잡해지더라도 가독성 및 유지보수 용이성 향상
/// HomeViewController에 collectionView의 layout
enum HomeCollectionViewLayout {
case `default`
/// layout 생성
var createLayout: UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment -> NSCollectionLayoutSection? in
let section = HomeCollectionViewSectionKind(
rawValue: sectionIndex
)
switch section {
case .메인배너:
return self.createMainBannerSection()
case .카테고리:
return self.createCategorySection()
case .오늘의랭킹:
return self.createRankSection()
case .오늘구매해야할제품:
return self.createTwoColumGridSection()
case .이주의브랜드이슈:
return self.createBrandOfTheWeekSection()
case .지금눈에띄는후드티:
return self.createTwoColumGridSection()
case .서브배너:
return self.createSubBannerSection()
case .일초만에사로잡는나의취향:
return self.createTwoColumGridSection()
case .none:
fatalError(
"레이아웃을 설정할 수 없는 섹션입니다."
)
}
}
return layout
}
}
// Section
private extension HomeCollectionViewLayout {
/// 메인배너 섹션 layout.
/// 하나의 열에 여러 컬럼을 가진 형태
func createMainBannerSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
)
let item = NSCollectionLayoutItem(
layoutSize: itemSize
)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.65)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered
return section
}
...
}
func didTapOutLineButton(_ sender: UIButton) {
// 버튼의 tag값으로 현재 섹션을 찾고
guard let section = dataSource.sectionIdentifier(for: sender.tag) else {return}
// 전체 스냡삿이 아닌 섹션 스냅샷 생성
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<CategoryItem>()
// 버튼이 선택되어 있다면
if sender.isSelected {
// 1. 기존 스냅샷을 지운다.
sectionSnapshot.deleteAll()
var snapshot = NSDiffableDataSourceSnapshot<CategoryCollectionViewSectionKind, CategoryItem>()
snapshot.appendSections(CategoryCollectionViewSectionKind.allCases)
// 2. item을 제외하고 section들만 추가해서 스냅샷을 적용한다.
dataSource.apply(sectionSnapshot, to: section, animatingDifferences: false)
}
// 버튼이 선택되어 있지 않다면
else {
// 1. 기존에 선택되어 있는 섹션의 스냅샷을 지운다.
NotificationCenter.default.post(
name: Notification.Name.likeRadioButton,
object: sender.tag,
userInfo: nil
)
// 2. section들을 추가하고
var snapshot = NSDiffableDataSourceSnapshot<CategoryCollectionViewSectionKind, CategoryItem>()
snapshot.appendSections(CategoryCollectionViewSectionKind.allCases)
dataSource.apply(snapshot, animatingDifferences: false)
let items = section.categorys
// 3. 해당 섹션에 아이템을 추가한다.
sectionSnapshot.append(items)
// 4. 스냅샷 적용
dataSource.apply(sectionSnapshot, to: section, animatingDifferences: true)
}
sender.toggle()
}
-
문제 원인
shadow는 뷰의 layer 외부에 그려지는데 clipsToBounds를 true로 설정하여 layer 외부의 모든 항목 제거한 상황이 원인 -
해결 방법
shadow만 적용한 빈 뷰를 두고 바로 위에 clipsToBounds를 true를 설정한 ImageView를 덮어씌우는 형태로 해결
private lazy var thumnailImageViewShadowView: UIView = {
let view = UIView()
shadowOffset = CGSize(width: 1, height: 1)
shadowOpacity = 0.5
shadowColor = UIColor.gray.cgColor
return view
}()
private lazy var thumnailImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleToFill
imageView.layer.cornerRadius = 16.0
imageView.clipsToBounds = true
return imageView
}()
thumnailImageViewShadowView.addSubview(thumnailImageView)
thumnailImageViewShadowView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
}
thumnailImageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}