CharactersGrid 를 만들어보며

Dophi·2023년 1월 30일
0

코드 따라하기

목록 보기
4/5

소개글

Xcoding with Alfian 이라는 유튜브 채널을 보며 캐릭터들이 컬렉션뷰로 그리드 형태를 이루고 있는 CharactersGrid를 따라 만들었습니다.
만들면서 배울만하다고 생각이 든 요소들을 공유하고자 합니다.
해당 영상은 아래 링크를 통해 볼 수 있으며, UICollectionView에 대해 자세히 설명해주시니 관심이 있다면 보시는 것을 추천드립니다.

영상 링크
코드

프리뷰 세팅

UIKit에서 프리뷰 쓰기

UIKit 코드베이스로 구현한 뷰에서도 프리뷰가 나오게 할 수 있습니다.

// UIKit 코드를 SwiftUI처럼 쓸 수 있도록 하는 프로토콜
struct CharacterCellViewRepresentable: UIViewRepresentable {
    
    let character: Character
    
    func updateUIView(_ uiView: CharacterCell, context: Context) {
        
    }
    
    func makeUIView(context: Context) -> CharacterCell {
        let cell = CharacterCell()
        cell.setup(character: character)
        return cell
    }
}

struct CharacterCell_Previews: PreviewProvider {
    static var previews: some View {
        CharacterCellViewRepresentable(character: Universe.ff7r.stubs[0])
            .frame(width: 120, height: 150)
    }
}

UIViewRepresentable을 쓰면 되는데, 사실 이 프로토콜은 UIView 객체를 SwiftUI 인터페이스에서 사용 가능하도록 해줍니다.
프리뷰로 사용할 목적이 아니더라도 SwiftUI 환경에서 개발을 하다가 UIView 객체가 필요할 때 사용합니다.

두가지 함수를 필수적으로 구현해야하는데

  • makeUIView는 보여주고 싶은 UIView 객체를 초기화해서 반환해주기만 하면 됩니다.
  • updateUIView는 뷰가 변화해야한다면 Binding 변수를 통해 구현하는 등의 작업을 해주면 되지만, 정적인 뷰라면 굳이 내부를 구현할 필요는 없습니다.

이런 코드를 써줘도 프리뷰가 안나온다면 Xcode의 Canvas 메뉴를 껐다가 다시 키면 아래와 같이 정상적으로 나옵니다.

여러 프리뷰 설정하기

위 사진과 같이 프리뷰에 여러개의 화면을 설정할 수 있습니다.

struct CharacterCell_Previews: PreviewProvider {
    static var previews: some View {
        
        // 여러 프리뷰 화면 띄우기 가능
        Group {
            CharacterCellViewRepresentable(character: Universe.ff7r.stubs[0])
                .frame(width: 120, height: 150)
            
            ScrollView {
                LazyVGrid(columns: [GridItem(.flexible()),GridItem(.flexible())]) {
                    ForEach(Universe.ff7r.stubs) {
                        CharacterCellViewRepresentable(character: $0)
                            .frame(width: 120, height: 150)
                    }
                }
            }
        }
    }
}

Group 키워드만 써주고 그 안에 보여주고 싶은 뷰들을 원하는 만큼 써주기만 하면됩니다.

텍스트 크기 조정하기

단순히 컴포넌트 한개의 크기를 조정하는게 아니라 앱의 환경설정에서 설정하는 것처럼 전체 텍스트 크기를 조정할 수 있습니다.

struct MultipleSectionCharactersViewController_Previews: PreviewProvider {
    static var previews: some View {
        MultipleSectionCharactersViewControllerRepresentable()
            .edgesIgnoringSafeArea(.top)
            // 글자 크기를 조정함
            .environment(\.sizeCategory, ContentSizeCategory.extraExtraExtraLarge)
    }
}

위와 같이 .environment(\.sizeCategory, ... ) 를 사용해서 프리뷰 화면의 텍스트들을 원하는 크기로 조정 가능합니다.

컬렉션뷰

자동 reload

데이터가 변하면 컬렉션뷰도 자동으로 reload 되도록 할 수 있습니다.

var characters = Universe.ff7r.stubs {
    didSet {
        // 데이터 바뀌면 알아서 reload 되게 하기
        collectionView.reloadData()
    }
}

정말 별거 아니긴 하지만 didSet을 통해 변수가 변할 때마다 실행시키고 싶은 코드를 넣을 수 있습니다.
데이터가 변하면 원래는 reloadData를 따로 호출했어야 했는데 이렇게 하면 훨씬 깔끔하다고 느껴졌습니다.

BatchUpdate

위와 같이 오른쪽 위 버튼을 누를때마다 부드러운 애니메이션이 적용됩니다.
코드 상으로는 데이터가 셔플되고 아래의 코드가 실행됩니다.

// 업데이트할 때 애니메이션 나타나게 하기
private func updateCollectionView(oldItems: [Character], newItems: [Character]) {
    collectionView.performBatchUpdates {
        let diff = newItems.difference(from: oldItems)
        diff.forEach { change in
            switch change {
            case let .remove(offset, _, _):
                self.collectionView.deleteItems(at: [IndexPath(item: offset, section: 0)])
            case let .insert(offset, _, _):
                self.collectionView.insertItems(at: [IndexPath(item: offset, section: 0)])
            }
        }
    } completion: { _ in
        let headerIndexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionHeader)
        headerIndexPaths.forEach { indexPath in
            let headerView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionHeader, at: indexPath) as! HeaderView
            headerView.setup(text: "Characters \(self.characters.count)")
        }
        self.collectionView.collectionViewLayout.invalidateLayout()
    }
}

여기서 핵심은 performBatchUpdates인데, 이 함수는 여러 작업들에 애니메이션을 동시에 적용해줍니다.
코드의 로직을 간단하게 설명드리자면 아래와 같습니다.
1. 셔플되기 전의 데이터와 셔플된 후의 데이터를 비교한다 (diff)
2. 없어진 아이템에 대해서는 deleteItems를, 생겨난 아이템에 대해서는 insertItems를 한다.
3. 이 작업들이 performBatchUpdates 안에서 실행되기 때문에 애니메이션이 적용된다.

기타

Constraint 지정

vStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8).isActive = true
vStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8).isActive = true

보동 constraint를 지정할 때 위와 같이 쓰는데, 만약 isActive를 매번 쓰기 귀찮다면 아래와 같이 표현도 가능합니다.

NSLayoutConstraint.activate([
	vStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
	vStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8)
])
profile
개발을 하며 경험한 것들을 이것저것 작성해보고 있습니다!

0개의 댓글