[Project 일지] 여행 기록 앱 만들기 (1) - ScrollView , CollectionCustomView

드디어 최종 프로젝트가 시작되었다. 다들 의견을 다양하고 적극적으로 내주셔서 많은 내용을 정하고 진도를 빠르게 나갈 수 있었다.

 

이번에는 여행을 다녀오고 기록을 할 수 있는 앱을 만들어 보려고 한다. 이번 프로젝트에서도 역시 여러가지 다양한 시도를 할 예정이고 아무래도 마지막 팀 프로젝트이다 보니 후회 없이 마무리 하고 싶다.

 

상세 페이지 구현

내가 맡은 내용은 여행의 기록을 넣을 수 있는 상세 페이지 구현이다. 먼저 우리가 만들려고 하는 내용을 살펴 보면 여행지에 대한 정보 및 쓰고 싶은 내용과 사진, 그리고 지도가 들어가며 같이 다녀온 인물을 추가할 수 있도록 구현할 예정이다.

 

먼저 기본적인 UI를 구성해보자. 스크롤 뷰를 주로 사용하여 스크롤 시 입력할 수 있는 부분의 면적을 넓히고 밑에 다양한 내용을 넣을 수 있도록 구현할 예정이다.

 

ScrollView

let backgroundImageView = UIImageView().then {
    $0.contentMode = .scaleAspectFill
    $0.clipsToBounds = true
    $0.backgroundColor = .black
}

let topContentView = UIView().then {
    $0.backgroundColor = .clear
}

let weatherButton = UIButton(type: .system).then {
    let imageConfig = UIImage.SymbolConfiguration(pointSize: 37, weight: .light)
    let image = UIImage(systemName: "sun.min", withConfiguration: imageConfig)
    $0.setImage(image, for: .normal)
    $0.tintColor = .white
    $0.contentHorizontalAlignment = .center
    $0.contentVerticalAlignment = .center
}

let locationLabel = UILabel().then {
    $0.text = "----"
    $0.font = UIFont.systemFont(ofSize: 50)
    $0.textColor = .white
}

let selectDateButton = UIButton(type: .system).then {
    $0.setTitle("Select Dates", for: .normal)
    $0.tintColor = .white
}

이런식으로 이전과 동일하게 컴포넌트를 코드로 생성해 구현하였다. 달라진 점이 있다면 이번에는 새로운 라이브러리인 Then을 사용한다는 것이다.

 

Then을 사용하므로써 중복적으로 사용해야하는 코드 내용을 줄일 수 있고 쉽게 사용할 수 있는 것 같아 좋은 라이브러리인 것 같다.

 

많은 내용을 코드로 구현하였으나 이전 내용과 중복되는 내용이 많으므로 생략하고 오토레이아웃 설정을 살펴보자

backgroundImageView.snp.makeConstraints {
    $0.top.equalToSuperview()
    $0.leading.trailing.equalTo(view.safeAreaLayoutGuide)
    $0.height.equalTo(350)
}

topContentView.snp.makeConstraints {
    $0.top.leading.trailing.bottom.equalToSuperview()
}

weatherStackView.snp.makeConstraints {
    $0.top.equalTo(150)
    $0.leading.trailing.equalTo(topContentView).inset(37)
    
}

아직 이미지가 들어오기 전이기 때문에 이미지가 들어올 부분에 검은 바탕의 이미지 뷰를 넣어두고 그 위에 view를 생성한 뒤 날짜 및 지역명이 나올 수 있는 스택 뷰를 올려주었다.

scrollView.snp.makeConstraints {
    $0.top.equalTo(backgroundImageView.snp.bottom).offset(-40)
    $0.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide)
}

contentView.snp.makeConstraints {
    $0.edges.equalTo(scrollView.contentLayoutGuide)
    $0.width.equalTo(scrollView.frameLayoutGuide)
    $0.height.equalTo(1500)
}

그런 다음 그 밑에 부터 스크롤 뷰가 들어와 그 안에있는 콘텐츠뷰안에 다양한 콘텐츠들이 들어올 수 있도록 오토레이아웃을 짜주었다. 사진에서 보는 것과 같이 스크롤 뷰에 모서리가 둥글게 하기 위해 살짝 탑을 올린 뒤 구현하여 자연스럽게 연결될 수 있도록 하였다.

 

그런 다음 콘텐츠 뷰 안에 구성되어야 할 컴포넌트들을 넣어주면 어느정도 가닥이 잡히게 된다.

 

ScrollView Delegate 활용

스크롤뷰 델리게이트를 활용해 스크롤이 시작했을때 살짝 스크롤 뷰 부분이 위로 올라와 편집 할 수 있는 영역이 높아질 수 있도록 구현하였다.

scrollView.delegate = self

먼저 당연하게 델리게이트를 셀프로 지정해주고

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let offset = scrollView.contentOffset.y
    if offset > 0 {
        UIView.animate(withDuration: 0.3) {
            self.weatherStackView.axis = .horizontal
            self.weatherStackView.spacing = 10
            self.weatherStackView.alignment = .center
            self.weatherStackView.snp.remakeConstraints {
                $0.top.equalTo(self.view.safeAreaLayoutGuide).offset(20)
                $0.centerX.equalToSuperview()
            }
            self.scrollView.layer.cornerRadius = 40
            self.view.clipsToBounds = true
            self.scrollView.snp.remakeConstraints {
                $0.top.equalTo(self.weatherStackView.snp.bottom).offset(10)
                $0.leading.trailing.bottom.equalTo(self.view.safeAreaLayoutGuide)
            }
            self.contentView.snp.remakeConstraints {
                $0.edges.equalTo(self.scrollView.contentLayoutGuide)
                $0.width.equalTo(self.scrollView.frameLayoutGuide)
                $0.height.equalTo(1500)
            }
            self.backgroundImageView.snp.updateConstraints {
                $0.height.equalTo(200) // 줄어든 높이
            }
            
            self.view.layoutIfNeeded()
        }
    } else {
        UIView.animate(withDuration: 0.3) {
            self.weatherStackView.axis = .vertical
            self.weatherStackView.spacing = 10
            self.weatherStackView.alignment = .leading
            self.weatherStackView.snp.remakeConstraints {
                $0.top.equalTo(150)
                $0.leading.trailing.equalTo(self.topContentView).inset(37)
            }
            self.scrollView.snp.remakeConstraints {
                $0.top.equalTo(self.backgroundImageView.snp.bottom).offset(-40)
                $0.leading.trailing.bottom.equalTo(self.view.safeAreaLayoutGuide)
            }
            self.contentView.snp.remakeConstraints {
                
                $0.edges.equalTo(self.scrollView.contentLayoutGuide)
                $0.width.equalTo(self.scrollView.frameLayoutGuide)
                $0.height.equalTo(1500)
            }
            self.backgroundImageView.snp.updateConstraints {
                $0.height.equalTo(350) // 원래 높이로 복원
            }
            self.view.layoutIfNeeded()
        }
    }
}

스크롤이 시작된다면 상단의 배경 이미지의 높이가 줄어들고 스택뷰가 위로 올라가게 된다.

그 이후 다시 올라왔을때 원래대로 돌아와 원래 높이를 가질 수 있도록 구현한다.

 

애니메이션 효과를 넣어 자연스럽게 위치가 옮겨지도록 설정하면 아래와 같이 구현이 완료 된다 🙂

 

사진 넣기 CollectionView

이전에 버튼을 넣어 PHPicker를 활용해 사진첩을 가져오도록 구현하였으나, 사진이 들어온 이후에는 컬렉션 뷰로 변경되어 사진이 들어와야하기 때문에 이 내용을 적용하기 적합한 점은 컬렉션 뷰로 구현하는게 맞는 것 같아 수정하게 되었다.

class GalleryCollectionViewCell: UICollectionViewCell {
    static let identifier = "GalleryCollectionViewCell"
    
    let imageView = UIImageView()
    let addButton = UIButton(type: .system)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupUI() {
        contentView.addSubview(imageView)
        contentView.addSubview(addButton)
        
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
        
        addButton.setTitle("+", for: .normal)
        addButton.setTitleColor(.white, for: .normal)
        addButton.titleLabel?.font = UIFont.systemFont(ofSize: 32)
        addButton.backgroundColor = #colorLiteral(red: 0.7674039006, green: 0.7674039006, blue: 0.7674039006, alpha: 1)
        addButton.layer.cornerRadius = 16
        addButton.clipsToBounds = true
        addButton.isHidden = true
        addButton.isUserInteractionEnabled = false
        addButton.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
    }
    
    func configure(with image: UIImage?) {
        if let image = image {
            imageView.image = image
            imageView.isHidden = false
            addButton.isHidden = true
        } else {
            imageView.isHidden = true
            addButton.isHidden = false
        }
    }
}

사진이 들어오기 전에는 아래 사진과 같이 플러스 버튼만 보여지도록 구현한 뒤 PHPicker를 통해 사진이 들어오게 되면

이렇게 각각 다른 크기를 가진 셀을 가져오도록 구현하였다.

CollectionView Cell Custom

각각 셀별로 다른 크기를 가지기 위해 UICollectionViewDelegateFlowLayout 를 사용하여 각각 셀의 레이아웃을 설정한다.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    if collectionView == galleryCollectionView {
        if selectedImages.isEmpty {
            return CGSize(width: 173, height: 115)
        } else {
            switch indexPath.row {
            case 0, 3:
                return CGSize(width: 173, height: 115)
            case 1, 2:
                return CGSize(width: 173, height: 223)
            default:
                return CGSize(width: 173, height: 115)
            }
        }
    } else {
        return CGSize(width: 73, height: 73)
    }
}

각 셀의 인덱스 값에 따라 이렇게 구현하면 각각 셀의 크기가 달라지는 컬렉션 뷰 셀이 구현된다 🙂


ㅎㅎ 이걸 바라고 TIL을 써온 건 아니지만 막상 받아보니 뿌듯하고 요즘 살짝 지치는 감이 없지 않았는데 다시 힘내서 갈 수 있는 동력을 얻은 것 같다!

 

이제 진짜 얼마 안남았으니 조금만 더 힘내보자