Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- clean architecture
- 스위프트
- rxswift
- swift documentation
- ribs
- Xcode
- MVVM
- uiscrollview
- collectionview
- 리펙터링
- Protocol
- SWIFT
- Clean Code
- tableView
- RxCocoa
- uitableview
- HIG
- 리팩토링
- 애니메이션
- Human interface guide
- 리펙토링
- Refactoring
- UITextView
- UICollectionView
- map
- 클린 코드
- combine
- swiftUI
- ios
- Observable
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 3. 갤러리 화면 만들기, 사진 첨부 - 갤러리 화면 UI 구현 방법 본문
1. 갤러리 화면 만들기, 사진 첨부 - 앨범 가져오기 (PHFetchResult, PHAsset)
2. 갤러리 화면 만들기, 사진 첨부 - 사진 가져오기 (PHCachingImageManager, PHImageRequestOptions)
3. 갤러리 화면 만들기, 사진 첨부 - 갤러리 화면 UI 구현 방법
앨범과 사진 가져오는 방법
(이전글에 있는 내용 복습)
- Photos 모듈에서 제공하는 API를 사용
- 디바이스의 앨범을 먼저 가져오기 (PHFetchResult가 앨범을 의미)
- 앨범에 담긴 이미지 정보 가져오기 (PHAsset이 이미지나 비디오 정보를 의미)
- PHAsset을 가지고 UIImage 이미지 가져오기 (PHCachingImageManager가 요청한 크기에 맞추어 PHAsset으로부터 이미지를 가져옴)
- 쿼리는 모두 PHImageRequestOptions를 작성
- 위에서 얻은 이미지(UIImage)를 UICollectionView와 UICollectionViewFlowLayout으로 쉽게 구현이 가능
이전글에서 살펴본 내용
- 구현한 내용
- 앨범을 가져오는 albumService
- 앨범 정보로부터 PHAssets와 UIImage를 가져오는 PhotoService
// PhotoViewController.swift
private let albumService: AlbumService = MyAlbumService()
private let photoService: PhotoService = MyPhotoService()
private var albums = [PHFetchResult<PHAsset>]()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
loadAlbums(completion: { [weak self] in
self?.loadImages()
})
}
private func loadAlbums(completion: @escaping () -> Void) {
albumService.getAlbums(mediaType: .image) { [weak self] albumInfos in
self?.albums = albumInfos.map(\.album)
completion()
}
}
private func loadPHAssetsFromAlbums() {
let album = albums[currentAlbumIndex]
photoService.convertAlbumToPHAssets(album: album) { [weak self] phAssets in
print(phAssets)
}
}
- 현재 PHAssets까지 얻어왔으므로 이 값을 토대로 UIImage를 얻어서 UICollectionView에 그려줄 것
UI 구현 예제에 사용한 라이브러리
- Cocoapod 사용
- Then: sugar 프로그래밍에 도움을 주는 라이브러리
- SnapKit: 코드베이스로 오토레이아웃을 쉽게 적용하기 위한 라이브러리
pod 'Then'
pod 'SnapKit'
갤러리 UI 구성
- 상단에 완료 버튼이 하나
- 하단에 UICollectionView 하나
(view hierarchy)
PhotoCell 구현
- UICollectionView에 들어갈 셀 구현
- SelectionOrder 모델: 셀이 선택되었을때와 선택되지 않았을때의 UI에 필요한 데이터
- PhotoCellInfo 모델: 셀을 그릴때 필요한 데이터
import UIKit
import Then
import Photos
enum SelectionOrder {
case none
case selected(Int)
}
struct PhotoCellInfo {
let phAsset: PHAsset
let image: UIImage?
let selectedOrder: SelectionOrder
}
final class PhotoCell: UICollectionViewCell {
}
- PhotoCell 내부를 구현
- imageView: 앨범에서 가져온 이미지 정보가 담긴 뷰
- highlightedView: 선택했을때 테두리에 초록색이 뜨고 내부는 dimmed되는 효과를 주기 위한 view
- orderLabel: 이미지 선택 순서를 왼쪽 상단에 띄워주기 위한 label
private let imageView = UIImageView().then {
$0.isUserInteractionEnabled = false
$0.contentMode = .scaleAspectFill
}
private let highlightedView = UIView().then {
$0.backgroundColor = .clear
$0.layer.borderWidth = 2.0
$0.backgroundColor = .black.withAlphaComponent(0.5)
$0.layer.borderColor = UIColor.green.cgColor
$0.isUserInteractionEnabled = false
}
private let orderLabel = UILabel().then {
$0.textColor = .green
}
- 뷰 레이아웃
override init(frame: CGRect) {
super.init(frame: frame)
layer.masksToBounds = true // 주의: 이값을 안주면 이미지가 셀의 다른 영역을 침범하는 영향을 주는것
contentView.addSubview(imageView)
imageView.addSubview(highlightedView)
highlightedView.addSubview(orderLabel)
imageView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
highlightedView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
orderLabel.snp.makeConstraints {
$0.leading.top.equalToSuperview().inset(4)
}
}
- UICollectionView의 데이터소스 중 cellForItemAt에서 접근할때 사용될 메소드 prepare() 구현
func prepare(info: PhotoCellInfo?) {
imageView.image = info?.image
if case let .selected(order) = info?.selectedOrder {
highlightedView.isHidden = false
orderLabel.text = String(order)
} else {
highlightedView.isHidden = true
}
}
갤러리 화면 PhotoViewController 구현
- PhotoViewController 선언
import UIKit
import Then
import SnapKit
import Photos
final class PhotoViewController: UIViewController {
}
- 뷰의 위치, 크기와 관련된 상수 선언
private enum Const {
static let numberOfColumns = 3.0
static let cellSpace = 1.0
static let length = (UIScreen.main.bounds.size.width - cellSpace * (numberOfColumns - 1)) / numberOfColumns
static let cellSize = CGSize(width: length, height: length)
static let scale = UIScreen.main.scale
}
- 완료 버튼, UICollectionView 선언
private let submitButton = UIButton(type: .system).then {
$0.setTitle("완료", for: .normal)
$0.setTitleColor(.blue, for: .normal)
$0.setTitleColor(.systemBlue, for: [.normal, .highlighted])
}
private let collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: UICollectionViewFlowLayout().then {
$0.scrollDirection = .vertical
$0.minimumLineSpacing = 1
$0.minimumInteritemSpacing = 0
$0.itemSize = Const.cellSize
}
).then {
$0.isScrollEnabled = true
$0.showsHorizontalScrollIndicator = false
$0.showsVerticalScrollIndicator = true
$0.contentInset = .zero
$0.backgroundColor = .clear
$0.clipsToBounds = true
$0.register(PhotoCell.self, forCellWithReuseIdentifier: PhotoCell.id)
}
- 레이아웃 설정
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
view.backgroundColor = .white
collectionView.dataSource = self
collectionView.delegate = self
view.addSubview(submitButton)
submitButton.snp.makeConstraints {
$0.top.equalTo(view.safeAreaLayoutGuide).offset(20)
$0.centerX.equalToSuperview()
}
view.addSubview(collectionView)
collectionView.snp.makeConstraints {
$0.top.equalTo(submitButton.snp.bottom)
$0.leading.trailing.bottom.equalToSuperview()
}
}
- 앨범(PHFetchResult<PHAsset>), 이미지 및 비디오 데이터(PHAsset) 정보 가져오기
// MARK: Property
private let albumService: AlbumService = MyAlbumService()
private let photoService: PhotoService = MyPhotoService()
private var selectedIndexArray = [Int]() // Index: count
// album 여러개에 대한 예시는 생략 (UIPickerView와 같은 것을 이용하여 currentAlbumIndex를 바꾸어주면 됨)
private var albums = [PHFetchResult<PHAsset>]()
private var dataSource = [PhotoCellInfo]()
private var currentAlbumIndex = 0 {
didSet { loadPHAssetsFromAlbums() }
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
loadAlbums(completion: { [weak self] in
self?.loadPHAssetsFromAlbums()
})
}
private func loadAlbums(completion: @escaping () -> Void) {
albumService.getAlbums(mediaType: .image) { [weak self] albumInfos in
self?.albums = albumInfos.map(\.album)
completion()
}
}
private func loadPHAssetsFromAlbums() {
guard currentAlbumIndex < albums.count else { return }
let album = albums[currentAlbumIndex]
photoService.convertAlbumToPHAssets(album: album) { [weak self] phAssets in
self?.dataSource = phAssets.map { .init(phAsset: $0, image: nil, selectedOrder: .none) }
self?.collectionView.reloadData()
}
}
- UICollectionView의 dataSource 구현
- 핵심은 PHAsset 정보를 가지고 UIImage를 얻어서 cell을 그리는 것
extension PhotoViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoCell.id, for: indexPath) as? PhotoCell
else { return UICollectionViewCell() }
let imageInfo = dataSource[indexPath.item]
let phAsset = imageInfo.phAsset
let imageSize = CGSize(width: Const.cellSize.width * Const.scale, height: Const.cellSize.height * Const.scale)
photoService.fetchImage(
phAsset: phAsset,
size: imageSize,
contentMode: .aspectFit,
completion: { [weak cell] image in
cell?.prepare(info: .init(phAsset: phAsset, image: image, selectedOrder: imageInfo.selectedOrder))
}
)
return cell
}
}
셀 선택 처리
- 이미지를 누르면 왼쪽 상단에 카운트가 보여져야함
- 만약 선택된 1, 2, 3 셀에서 2를 다시 누르면 왼쪽 카운트 값 1, 3이 1, 2로 변경되어야 하는 로직이 필요
- selectedIndexArray를 사용하여 이전에 선택한 셀의 indexPath.item을 기록하고 있다가 이 값을 업데이트 치는 방식으로 구현
extension PhotoViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let info = dataSource[indexPath.item]
let updatingIndexPaths: [IndexPath]
if case .selected = info.selectedOrder {
dataSource[indexPath.item] = .init(phAsset: info.phAsset, image: info.image, selectedOrder: .none)
selectedIndexArray
.removeAll(where: { $0 == indexPath.item })
selectedIndexArray
.enumerated()
.forEach { order, index in
let order = order + 1
let prev = dataSource[index]
dataSource[index] = .init(phAsset: prev.phAsset, image: prev.image, selectedOrder: .selected(order))
}
updatingIndexPaths = [indexPath] + selectedIndexArray
.map { IndexPath(row: $0, section: 0) }
} else {
selectedIndexArray
.append(indexPath.item)
selectedIndexArray
.enumerated()
.forEach { order, selectedIndex in
let order = order + 1
let prev = dataSource[selectedIndex]
dataSource[selectedIndex] = .init(phAsset: prev.phAsset, image: prev.image, selectedOrder: .selected(order))
}
updatingIndexPaths = selectedIndexArray
.map { IndexPath(row: $0, section: 0) }
}
update(indexPaths: updatingIndexPaths)
}
private func update(indexPaths: [IndexPath]) {
collectionView.performBatchUpdates {
collectionView.reloadItems(at: indexPaths)
}
}
}
(구현 완료)
'UI 컴포넌트 (swift)' 카테고리의 다른 글
Comments