📌 평점 선택 기능 (RatingViewController)
✅ 목표
- RatingViewController에서 슬라이더로 영화/TV 평점을 선택하고
- 선택된 평점을 DetailHeaderView의 myScroeLabel에 반영
🔹 구현 흐름
- 사용자가 myScroeLabel을 누르면 DetailViewController에서 RatingViewController를 띄움
- RatingViewController에서 슬라이더 조작 시 값이 변경됨
- "완료" 버튼을 클릭하면 선택한 평점을 DetailViewController로 전달
- DetailViewController에서 받은 값을 DetailHeaderView의 myScroeLabel에 표시
1️⃣ RatingViewController (평점 선택 화면)
평점을 슬라이더를 이용해 0.5 단위로 선택할 수 있음.
📌 기능
- 슬라이더 (0~10 범위, 0.5 단위로 조절)
- 라벨에 선택한 값 표시
- 완료 버튼 클릭 시, 델리게이트를 통해 값 전달
- Bottom Sheet 형태로 표시됨
✅ 슬라이더 값 설정
- isContinuous = true → 슬라이더 이동 시 즉시 반영
- round(sender.value / stepValue) * stepValue → 0.5 단위로 설정
- 슬라이더에 최소값(0)과 최대값(10) 아이콘 추가
- 슬라이더 트랙 색상 지정 (초록색~노랑색)
import UIKit
class RatingViewController: UIViewController {
// MARK: - Variable
weak var delegate: RatingViewControllerDelegate? // ✅ Delegate 선언
private var selectedRatingPoint: String = "" // ✅ 선택한 평점 저장
// MARK: - UI Component
private let ratingLabel: UILabel = {
let label = UILabel()
label.text = "평점을 선택해주세요 😁"
label.font = .systemFont(ofSize: 20, weight: .bold)
label.textColor = .white
label.textAlignment = .center
return label
}()
private let slider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 10
let largeConfig = UIImage.SymbolConfiguration(pointSize: 20, weight: .bold)
slider.minimumValueImage = UIImage(systemName: "0.square", withConfiguration: largeConfig)
slider.maximumValueImage = UIImage(systemName: "10.square", withConfiguration: largeConfig)
slider.backgroundColor = .black
slider.thumbTintColor = .systemGray
slider.value = 0
slider.minimumValue = 0
slider.maximumValue = 10
slider.maximumTrackTintColor = .systemYellow
slider.minimumTrackTintColor = .systemGreen
slider.layer.cornerRadius = 10
slider.layer.masksToBounds = true
slider.isContinuous = true // ✅ 슬라이더를 특정 값에서 멈추도록 설정
return slider
}()
private let selectedRatingButton: UIButton = {
let button = UIButton(type: .custom)
button.setTitle("완료", for: .normal)
button.layer.cornerRadius = 10
button.layer.masksToBounds = true
button.backgroundColor = .systemBlue
button.tintColor = .white
button.titleLabel?.font = .systemFont(ofSize: 20, weight: .bold)
return button
}()
// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
configureConstraints()
slider.addTarget(self, action: #selector(sliderValueChange(_:)), for: .valueChanged)
selectedRatingButton.addTarget(self, action: #selector(didTapDoneButton), for: .touchUpInside)
}
// MARK: - Action
@objc private func sliderValueChange(_ sender: UISlider) {
let stepValue: Float = 0.5 // ✅ 0.5 단위로 눈금 끊기
let roundedValue = round(sender.value / stepValue) * stepValue
sender.value = roundedValue
let showValue = String(format: "%.1f", roundedValue)
self.selectedRatingPoint = showValue
ratingLabel.text = "평점을 선택해주세요 😁" + "\(showValue)"
}
// ✅ 완료 버튼 클릭 시 델리게이트를 통해 값 전달
@objc private func didTapDoneButton() {
delegate?.didSlidedValue(with: selectedRatingPoint)
dismiss(animated: true)
}
// MARK: - Layout
private func configureConstraints() {
view.addSubview(ratingLabel)
view.addSubview(slider)
view.addSubview(selectedRatingButton)
ratingLabel.translatesAutoresizingMaskIntoConstraints = false
slider.translatesAutoresizingMaskIntoConstraints = false
selectedRatingButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
ratingLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
ratingLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 30),
ratingLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
ratingLabel.heightAnchor.constraint(equalToConstant: 30),
slider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30),
slider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30),
slider.topAnchor.constraint(equalTo: ratingLabel.bottomAnchor, constant: 30),
slider.heightAnchor.constraint(equalToConstant: 60),
selectedRatingButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 30),
selectedRatingButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30),
selectedRatingButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
selectedRatingButton.heightAnchor.constraint(equalToConstant: 50)
])
}
}
// MARK: - Protocol: RatingViewControllerDelegate
protocol RatingViewControllerDelegate: AnyObject {
func didSlidedValue(with value: String)
}
2️⃣ DetailViewController (평점 값 전달 및 UI 업데이트)
RatingViewController에서 선택한 값을 DetailHeaderView에 전달하여 myScroeLabel을 업데이트.
✅ Bottom Sheet 설정
- .custom(resolver: { context in return 300 }) → 300px 높이로 설정
- sheet.prefersGrabberVisible = true → 상단 Grabber 표시
- sheet.prefersScrollingExpandsWhenScrolledToEdge = false → 스크롤이 확장되지 않도록 설정
✅ 델리게이트를 통한 값 전달
- RatingViewControllerDelegate 채택하여 didSlidedValue(with:) 구현
- DetailHeaderView 내 myScroeLabel 업데이트
extension DetailViewController: DetailHeaderViewDelegate {
func didTapRating() {
let ratingVC = RatingViewController()
ratingVC.delegate = self // ✅ Delegate 연결
if let sheet = ratingVC.sheetPresentationController {
sheet.detents = [
.custom(resolver: { context in return 300 })
]
sheet.prefersGrabberVisible = true
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
}
present(ratingVC, animated: true, completion: nil)
}
}
// MARK: - Extension: RatingViewControllerDelegate
extension DetailViewController: RatingViewControllerDelegate {
func didSlidedValue(with value: String) {
detailHeaderView?.updateMyScoreLabel(value: value)
}
}
3️⃣ DetailHeaderView (UI 업데이트)
✅ myScroeLabel 업데이트하는 메서드 추가
extension DetailHeaderView {
func updateMyScoreLabel(value: String) {
myScroeLabel.text = "내 맘속 별점 ⭐ \(value)"
}
}
🚀 개선할 점
- 별점 값을 Firebase에 저장할 예정이라면 → Combine 활용 가능 (제일 중요)
- myScroeLabel 클릭 시 애니메이션 효과 추가 가능 (UIView.animate 활용)
- RatingViewController의 초기값을 DetailHeaderView에서 가져온 값으로 설정하는 기능 추가
🔥 Combine으로 구현하는 방법 (추후 Firebase 연동)
Combine을 사용하면 DetailHeaderView에서 값이 변경될 때 즉시 UI를 업데이트하고, Firebase로 데이터를 저장할 수 있음.
✅ Combine을 사용해야 하는 경우
- 실시간 데이터 저장이 필요할 때
- 여러 UI에서 동일한 값(별점)을 표시할 때
- 추후 ViewModel을 도입할 계획이라면
🔹 Combine을 활용한 개선된 코드 (예시)
import Combine
class RatingViewModel {
@Published var rating: Float = 5.0 // ⭐ 별점 값
}
class RatingViewController: UIViewController {
var viewModel: RatingViewModel
init(viewModel: RatingViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private let slider: UISlider = {
let slider = UISlider()
slider.minimumValue = 0
slider.maximumValue = 10
slider.value = 5
return slider
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
}
@objc private func sliderValueChanged(_ sender: UISlider) {
viewModel.rating = sender.value // ✅ ViewModel을 통해 값 전달
}
}
🔹 DetailViewController에서 Combine 구독 (예시)
class DetailViewController: UIViewController {
private var viewModel = RatingViewModel()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// ✅ Combine 구독: 값이 변경될 때 UI 업데이트
viewModel.$rating
.sink { [weak self] value in
self?.detailHeaderView?.updateMyScoreLabel(value: value)
print("별점이 변경됨: \(value)")
// ✅ Firebase에 저장하는 로직 추가 가능
}
.store(in: &cancellables)
}
func didTapRating() {
let ratingVC = RatingViewController(viewModel: viewModel)
present(ratingVC, animated: true, completion: nil)
}
}
'Project > MovieClip' 카테고리의 다른 글
배우의 소셜 미디어 계정을 가져오는 방법 🤔 (0) | 2025.02.14 |
---|---|
❌ 문제 해결: 상세페이지에서 유사 영화 정보의 상세페이지 이동할 때 장르가 보이지 않음 (0) | 2025.02.13 |
thumnailImageView에서 .video 와 .poster 분기 처리하기 (0) | 2025.02.12 |
📌 컬렉션 뷰에서 .video 아이템 클릭 시 YouTube 영상 재생하기 (0) | 2025.02.12 |
상세 페이지 내에서 영화, 티비 관련한 유사한 내용은 컬렉션뷰로 보여주기 (0) | 2025.02.11 |