Compositional Layout을 이용하여 DetailView의 레이아웃을 설정해 준다. (Snapshot X)
지난번에 우리가 제작한 DetailView는 별다른 내용이 들어있지 않았다. 이를 디자인하고 내용을 채워 넣을 것이다.
DetailView에서는 그 캐릭터에 대한 내용을 볼 수 있게 해야한다. 릭 앤 모티 API 문서에는 single character에 대한 내용을 받아오는 API가 있다.
안에 있는 내용으로 구상해보면 큰 이미지와 다양한 캐릭터 정보를 밑에 표시할 것이다. 또한, 에피소드가 굉장히 많으므로 스와이프 해서 에피소드를 확인할 수 있는 기능을 넣자.
컬렉션 뷰를 사용하는데 구버전에서 주로 사용됐던 FlowLayout 방법이 아니라 Compositional Layout으로 구현할 것이다. 아프라즈의 말버릇은 Good Citizen이 되라다. 이 말을 할 때는 항상 추상화와 연결이 되어있는데 귀찮아도 대충 만들고 끝내지 말고 각각의 역할에 맡게 적절한 조치를 취하라는 뜻인 거 같다.
View -> VC
아래와 같이 DetailView를 만들어 VC와 연결할 것이다.
final class RMCharacterDetailView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .systemPurple
}
required init?(coder: NSCoder) {
fatalError("Unsupported")
}
}
이제 VC에서 출력이 가능하도록 레이아웃을 잡아보자
final class RMCharacterDetailViewController: UIViewController {
...
private let detailView = RMCharacterDetailView()
...
override func viewDidLoad() {
...
addConstraints()
}
private func addConstraints() {
view.addSubview(detailView)
NSLayoutConstraint.activate([
detailView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
detailView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
detailView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor),
detailView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
}
}
성공적으로 영역이 구분이 간다.
NavigationItem
다음으로 네비게이션 버튼을 설정하자. 우리는 내비게이션바 영역의 오른쪽 끝에서 공유를 할 것이다. 내비게이션의 틀을 잡아두자. 아래의 단 한 줄과 빈 함수를 넣어두는 것으로 공유 기능을 자연스럽게 표시할 수 있게 되었다.
override func viewDidLoad() {
...
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action,
target: self,
action: #selector(didTapShare))
}
@objc
private func didTapShare() {
}
이제 컬렉션 뷰를 구현하기 전에 우리가 구현한걸 생각해 보자. 일단 VM은 우리가 이전에 생성한 적이 있다. 여기서 Character 모델에 들어가서 보면 안에 url이 있다. 이것이 각 캐릭터의 디테일뷰에서 사용할 수 있는 singe character API이다. 이를 return 하는 프로퍼티를 작성해 보자.
final class RMCharacterDetailViewViewModel {
...
public var requestUrl: URL? {
return URL(string: character.url)
}
...
}
다음으로 VC에 viewModel.fetchCharacterData()를 넣자. 이제까지 함수와 같은 로직 처리를 항상 VM안에 넣었는데 여기서는 VC에서 할지 고민하다가 VM에 넣었다.
- 조금 있다가 다시 나온다. 일단은 만들어보자
VC에서 viewModel에서 메서드를 실행하도록 하고 VM에서는 정의하자
final class RMCharacterDetailViewController: UIViewController {
...
override func viewDidLoad() {
...
viewModel.fetchCharacterData()
}
...
}
final class RMCharacterDetailViewViewModel {
...
public func fetchCharacterData() {
print(character.url)
guard let url = requestUrl,
let request = RMRequest(url: url) else {
return
}
}
}
이제 url과 request를 출력해서 확인해 볼 수 있다.
그런데 문제가 있다. 우리가 초기화시켜놨던 request 생성과정에서는 /를 기반으로 엔드포인트를 받는다. 실제로 이 상태에서 request를 출력하면 /1과 같은 캐릭터 넘버를 생략하고 가져오는데 이를 적절하게 수정해 보자
현재 pathComponent인 캐릭터 번호가 삭제돼서 들어올 것이기 때문에, pathComponents라는 변수를 만들고 총컴포넌트의 수를 측정하여 2개 이상이면 그대로 pathComponents를 만들고 첫 번째를 제거한다. 첫 번째는 엔드포인트이므로 쓸모가 없다. 이를 이용하여 다시 넣어주어 생성하고 리턴하도록 한다.
final class RMRequest {
...
convenience init?(url: URL) {
...
if trimmed.contains("/") {
...
if !components.isEmpty {
let endPointString = components[0] // Endpoint
var pathComponents: [String] = []
if components.count > 1 {
pathComponents = components
pathComponents.removeFirst()
}
if let rmEndPoint = RMEndpoint(rawValue: endPointString) {
self.init(endpoint: rmEndPoint, pathComponents: pathComponents)
return
}
}
}
...
}
}
완성으로 데이터가 정상적으로 출력되는 걸 볼 수 있다. 그런데 한번 생각을 해봐야 할거 같다. 우리가 정말 character를 VM에서 만들어서 사용해야 할까? 캐릭터는 이미 있는데 우리가 VM에서 fetch를 하는 것은 불필요한 작업이다.
CollectionView
우린 일단 fetch를 지우고 컬렉션뷰를 만들어볼 것이다. DetailView로 가자
여기서는 spinner와 collectionView를 생성하고 이용할 것이다. 기본적인 세팅은 ListView에서 정의된 것을 그대로 가져와서 사용한다. 보면 컬렉션뷰를 create라는 함수를 이용해서 생성해서 보내주는 걸 볼 수 있다. 이 이유에 대해서는 나중에 설명한다.
final class RMCharacterDetailView: UIView {
private var collectionView: UICollectionView?
private let spinner: UIActivityIndicatorView = {
let spinner = UIActivityIndicatorView(style: .large)
spinner.hidesWhenStopped = true
spinner.translatesAutoresizingMaskIntoConstraints = false
return spinner
}()
override init(frame: CGRect) {
...
let collectionView = createCollectionView()
self.collectionView = collectionView
addSubviews(collectionView, spinner)
addConstraints()
}
...
private func addConstraints() {
guard let collectionView = collectionView else {
return
}
NSLayoutConstraint.activate([
spinner.widthAnchor.constraint(equalToConstant: 100),
spinner.heightAnchor.constraint(equalToConstant: 100),
spinner.centerXAnchor.constraint(equalTo: centerXAnchor),
spinner.centerYAnchor.constraint(equalTo: centerYAnchor),
collectionView.topAnchor.constraint(equalTo: topAnchor),
collectionView.leftAnchor.constraint(equalTo: leftAnchor),
collectionView.rightAnchor.constraint(equalTo: rightAnchor),
collectionView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
private func createCollectionView() -> UICollectionView {
}
}
Compositional Layout
개인적으로 나는 이제까지 컴포지셔널 레이아웃을 구성할 때는 하나의 함수 안에 모든 걸 적어놨다. 이것은 intense가 높아지는 문제가 있기 때문에 분리해서 처리를 해본다.
먼저 collectionView를 정의하고 layout에 따라 collectionViewLayout을 정한다고 정해두자. 그리고 바로 위에 layout을 생성한다.
만약 컴포지셔널레이아웃 객체를 만들게 된다면 sectionIndex와 환경요소가 필요하다. sectionIndex에 섹션을 지정하면 다시 섹션은 그룹을, 그룹은 또 다른 걸 요구하게 된다. 이렇게 안으로 들어가는 구조를 가지고 있기 때문에 위로 계속 코드를 업데이트해나가는 방식으로 개발을 하게 된다. 환경요소는 이번에 중요하지 않아 뺀다.
private func createCollectionView() -> UICollectionView {
let layout = UICollectionViewCompositionalLayout { sectionIndex, _ in
return self.createSection(for: sectionIndex)
}
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}
private func createSection(for sectionIndex: Int) -> NSCollecitonLayoutSection {
}
- 진행한 걸 돌이켜보면 VC에서는 View를 받아 처리하도록 하고 V 안에서는 Collectionview의 레이아웃을 설정해 준다.
끝
'Swift > Rick & Morty' 카테고리의 다른 글
[iOS] Rick&Morty - #15 CollectionView Layouts (0) | 2023.03.13 |
---|---|
[iOS] Rick&Morty - #14 Compositional Layout (1) | 2023.03.12 |
[iOS] Rick&Morty - #12 Image Loader (0) | 2023.03.09 |
[iOS] Rick&Morty - #11 Pagination (0) | 2023.03.08 |
[iOS] Rick&Morty - #10 Pagination Indicator (0) | 2023.03.07 |