iOS 프로젝트/카멜레온

[iOS] 카멜레온 개발 일지 - 3 (UICollectionView와 UICollectionViewCell)

유정주 2022. 6. 15. 22:02
반응형

안녕하세요. 개발하는 정주입니다.

 

오늘은 카멜레온 개발 일지 - 3에 대해 포스팅하려고 합니다.

UICollectionView와 UICollectionViewCell를 사용하며 어떤 문제가 생겼고, 어떻게 해결했는지 알아봅시다.

 

* 해당 포스팅은 대략적인 개발 일지로 자세한 내용은 필요시에만 따로 포스팅합니다.

 

UICollectionView를 어디에 썼나?

카멜레온 앱은 여러 개의 얼굴 중 원하는 얼굴만 선택해서 변환할 수 있습니다.

여러 개의 얼굴을 표시해야 했기 때문에 UICollectionView를 사용했습니다.

 

위 사진처럼 기본은 선택이 된 것처럼 보이도록 해야 했고 바꾸지 않을 얼굴을 선택 해제하는 기획입니다.

 

UICollectionView 구현하기

코드를 작성하기 전 어떤 작업이 필요할지 생각해보았습니다.

  • 서버에서 얼굴 이미지 가져오기
  • Custom Cell 만들기
  • 행렬 맞춰 표시하기
  • 모두 선택한 상태로 만들기
  • 선택한 아이템 index 배열 만들기

이렇게 크게 4개 작업을 하면 원하는대로 만들어질 것 같았습니다.

하나씩 보도록 하죠.

 

서버에서 얼굴 이미지 가져오기

사진에서 Face Detection을 하고 crop 한 얼굴 이미지를 서버에서 받아와야 합니다.

이는 GET으로 해결했고 자세한 내용은 다음 포스팅인 서버 통신에서 다루도록 하겠습니다.

여기에서 다룰 것은 GET으로 이미지 URL을 받은 후의 내용이에요.

 

URL을 UIImage로 변환하기

얼굴 이미지가 1명, 2명일 때는 괜찮았지만 20~30명정도 대량일 때 로딩이 느렸습니다.

정확히는 URL에서 이미지를 불러와 UIImage로 변환하는 과정이 느렸습니다.

 

원인을 생각해보니 cell이 load 될 때 url에서 이미지를 load 하고 있었습니다.

그래서 스크롤을 올리거나 내릴 때 cell이 reload 되면서 새로운 URL로 이미지를 얻어오고,

이 과정에서 로딩이 느리구나 느끼는 거였어요.

 

그래서 저는 로딩 화면에서 UIImage 배열을 만들어 전달해주었습니다.

//얼굴 데이터를 UIImage로 변경하고 다음 화면으로 이동함
var faceImageList: [UIImage?] = []
for faceImage in faceImages {
    if let url = URL(string: faceImage.url) {
        if let data = try? Data(contentsOf: url) {
            faceImageList.append(UIImage(data: data))
        }
    } else {
        faceImageList.append(nil)
    }
}

...

let selectVC = SelectViewController()
selectVC.faceImages = faceImages //얼굴 이미지를 넘김

cell이 load될 때마다 URL에서 불러오는 게 아니라 UIImage 자체를 넣으면 버벅거림 없이 보일 수 있겠다고 생각했어요.

하지만 이렇게 하면 모든 url에서 이미지를 얻어오기 전까지 대기해야 한다는 단점이 생기는데요.

 

다행인지 불행인지 카멜레온 앱의 Face Detection은 100명 이상을 잡기 힘들고

이렇게 대량의 얼굴이 들어있을 때는 얼굴의 사이즈가 작았기 때문에 모든 UIImage를 load해도 괜찮겠다고 판단했습니다.

혹여나 메모리 문제가 생길 수 있을 것 같아 확인해봤지만 다행히 문제가 없었네요 ㅎㅎ

 

UICollectionView 설정

이 얼굴 이미지 배열을 UICollectionView로 보여주었습니다.

extension SelectViewController: UICollectionViewDelegate, UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return faceImages.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = faceCollectionView.dequeueReusableCell(withReuseIdentifier: "faceCellIdentifier", for: indexPath) as! SelectCell

        cell.setupImage(faceImages[indexPath.row])

        return cell
    }
     ...
}

오류 없이 깔끔하게 처리가 되어서 좋았네요.

SelectCell은 제가 만든 Custom Cell로 바로 아래에서 다룹니다!

 

Custom Cell 생성하기

얼굴 이미지를 보여줄 Custom Cell을 생성했습니다.

Custom Cell의 구현 내용은 아래와 같습니다.

  1. corner가 rounding 처리됨
  2. 정사각형 모양
  3. 선택/해제 상태를 알 수 있어야 함
  4. (추가) 선택이 되면 테두리 색이 변함

여기에 4번은 카카오톡의 사진 선택에서 아이디어를 얻어서 추가했습니다.

결과를 보면 이쁘게 잘 나와서 아주 뿌듯했어요.

 

아무튼 1, 2, 4번은 아주 쉽게 구현할 수 있었습니다.

1번은 비율이 아닌 가로세로 고정치를 주었고, 2번은 10을 주었습니다. 

고정 치로 준 이유는 비율로 줄 경우 cell이 과하게 커지는 것을 방지하기 위해서입니다.

왜냐하면 얼굴 이미지의 사이즈가 그리 크지 않아 깨져 보였기 때문입니다.

4번도 border 속성만 주면 끝이었어요.

 

Cell의 Reusable 문제

3번에서 생각보다 오래 걸렸습니다.

cell의 재사용을 위해 UICollectionViewCell의 dequeueReusableCell( )를 사용했는데요.

재사용되면서 선택/해제 표시가 어긋나는 문제가 발생했습니다.

 

제가 해결한 방법은 UICollectionViewCell의 isSelected를 override 하여 didSet 됐을 때 UI를 변경해주었습니다.

isSelected는 cell만의 속성이므로 이 속성에 따라 UI를 변경해주면

cell이 다시 reload 돼도 제대로 선택/해제 표시가 보일 것이라는 생각이었죠.

 

override var isSelected: Bool {
    didSet {
        setSelected(isSelected)
    }
}

isSelected를 override 하여 didSet 됐을 때 setSelected( )가 호출되도록 했습니다.

 

func setSelected(_ selected: Bool) {
    if selected {
        setSelectedStyle()
    } else {
        setDeselectedStyle()
    }
}

setSelected( )는 전달받은 Bool 인자로 UI를 적용해줍니다.

이렇게 하니 스크롤을 해도 cell이 정상적으로 보이고 메모리 문제도 없었습니다!

굿굿

 

행렬 맞춰 표시하기

UICollectionView에서 행렬을 맞춰 표시를 해줘야 했습니다.

cell 간의 적절한 여백을 찾아 UICollectionViewDelegateFlowLayout를 이용해 설정을 해줬습니다.

 

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let interval: CGFloat = 10
    let count = floor(UIScreen.main.bounds.width / 120)
    let size = floor((UIScreen.main.bounds.width - interval * 4) / count)
    return CGSize(width: size, height: size)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return 10
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
    return 10
}

cell의 사이즈를 120 x 120으로 하기로 했습니다.

여기에 맞춰 적절히 여백을 줘야 했는데요. 직접 조금씩 바꾸면서 해보니 10이 적절하더라고요.

interval * 4를 해줘서 상하좌우 여백을 모두 고려해주었습니다.

 

결과적으로 iPhone에서는 기종에 상관없이 3열로 나올 수 있었고 iPad에서는 더 많은 콘텐츠를 보여줄 수 있게 되었습니다.

그래도 화면비에 따라 여백이 조금 많이 되는 등의 문제가 있습니다.

종강하면 수정을 해볼 생각이고 고정값을 주는 것보다 더 좋은 방법이 있으면 댓글로 남겨주시면 감사하겠습니다 ㅠㅠ

 

모두 선택한 상태로 바꾸기

처음 셀들은 모두 선택한 상태로 변경되어야 합니다.

 

방법이 두 개로 나뉘는 것 같았습니다.

  1. 실제로 선택한 상태로 바꾸기
  2. 실제로는 미선택인데 선택 UI를 보여주기

저는 서버로 선택한 cell의 index도 전달해주어야 했기 때문에

2번을 선택하면 여러 곳에서 개념이 반대로 적용되어 추후 많이 헷갈릴 것 같았습니다.

 

코드의 유지보수도 중요하기에 좀 번거롭겠지만 1번을 선택해주었습니다.

 

selectedIndex = Array(repeating: true, count: faceImages.count)

for (i, _) in selectedIndex.enumerated() {
    faceCollectionView.selectItem(at: IndexPath(row: i, section: 0), animated: false, scrollPosition: .init())
}

실제로 코드를 작성해보니 그리 번거롭진 않았어요.

위에서 말했듯이 배열 size가 100개 이상 될 일은 거의 없기 때문에

for문을 이용해 하나하나 select 해주었습니다.

 

이 과정을 통해 selectItem( )이란 메서드에 대해 알게 되었습니다.

전체 선택, 전체 해제 같은 기능도 추가할 예정인데 이때 쓰면 딱이겠구나 했어요 ㅎㅎ

 

선택한 Item index 배열 만들기

서버에 선택한 얼굴을 전달해줘야 합니다.

저희 팀은 그 방법으로 index 배열을 서버로 전송해주기로 했습니다.

 

그렇다면 cell이 선택된 것을 어떻게 알 수 있을지 찾아봤습니다.

cell class에서 index 배열에 item을 넣는 것은 Cell 역할을 초과한 작업이기에 그런 짓은 하지 않았습니다 ㅎㅎ;;

 

찾아보니 didSelectItemAt, didDeselectItemAt이 존재했습니다.

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    selectedIndex[indexPath.row].toggle()
}

func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
    selectedIndex[indexPath.row].toggle()
}

여기에서 선택했는가에 대한 Bool 배열 값을 toggle( ) 해주었습니다.

 

Int 배열이 아닌 Bool 배열로 처리한 이유는 알고리즘 효율 때문입니다.

Bool 배열로 처리하면 toggle( )만 해주면 되는데

Int 배열로 하면 remove를 해줘야 하기 때문에 연산 오버헤드가 걱정되었거든요.

 

물론 100개 이하의 배열이기 때문에 티는 안 나겠지만

이런 사소한 처리가 모여 큰 차이를 만드는 것 아니겠습니까? ㅎㅎ

 

var indexArray: [String] = []
for (i, isSelected) in selectedIndex.enumerated() {
    if isSelected {
        indexArray.append(String(format: "%03d", i))
    }
}

이렇게 만든 Bool 배열은 선택 완료했을 때 String 배열로 변환되어 서버로 전송됩니다.

 

마무리

UICollectionView는 아직까지 많이 복잡한 레이아웃 같습니다.

정말 많이 쓰이는 것이므로 좀 더 익숙해지는 연습을 해야겠어요 ㅠㅠ

 

다음 포스팅은 서버 통신이 될 것 같네요.

UIKit의 기본기를 익히고자 URLSession으로만 처리했으니 기대 많이 해주세요.

 

감사합니다!


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

 

반응형