Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Week2] 토스 앱 상세페이지 구현 #7

Open
wants to merge 72 commits into
base: main
Choose a base branch
from
Open

[Week2] 토스 앱 상세페이지 구현 #7

wants to merge 72 commits into from

Conversation

yurim830
Copy link
Collaborator

@yurim830 yurim830 commented Oct 25, 2024

🔍 What is the PR?

  • 2주차 과제 결과물입니다!

📝 구현 내용

✅ 필수 구현

  • 컴포넌트 생성 및 배치
    • 좌우 여백 20으로 통일
    • 스크롤로 리뷰 작성 버튼까지 진행
    • 에디터의 선택과 같은 SF Symbols에 나오지 않는 이미지는 person 이모지로 대체
    • 미리보기에는 이미지가 하나 나오도록 구현
  • 액션 설정
    • [버전 기록] 버튼 : 버전 기록 페이지로 이동 (상세뷰 구현 x)
    • [평가 및 리뷰 모두 보기] 버튼 : 모두 보기 페이지로 이동 (상세뷰 구현 x)

➕ 추가 구현

  • 1. 리뷰 작성 페이지 구현
    • 컴포넌트 생성 및 배치
    • 보내기를 누를 경우, 탭하여 평가하기의 하단에 있는 내용이 바뀌도록 구현
  • 2. 앱 설명의 더보기를 눌렀을 때, 추가 내용이 보여지도록 구현
  • 3. 평점 버튼 눌렀을 때 색깔이 채워지도록 구현
  • 4. 스크롤을 내릴 때 네비게이션바 중앙에 아이콘, 우측에 버튼 배치

➕추가의 추가

  • 다크모드 대응

📷 Screenshot

Mode 스크린샷
light
dark

영상

sopt.week2.mp4

😎 Points

1️⃣ Extensions

이번 과제에는 UI 컴포넌트들이 정말 많으므로,
자주 사용하는 속성을 쉽게 설정하기 위해 아래 사진처럼 UIButton, UIImage, UILabel, Date의 extension을 만들었습니다.
🔽 예시) UIImage에서 심볼의 두께를 조정하는 함수

코드 보기
extension UIImage {
    class func configureImage(systemName: String, pointSize: CGFloat? = nil, symbolWeight: UIImage.SymbolWeight) -> UIImage? {
        if let pointSize = pointSize {
            let symbolConfig = UIImage.SymbolConfiguration(pointSize: pointSize, weight: symbolWeight)
            return UIImage(systemName: systemName, withConfiguration: symbolConfig)
        }
        
        let symbolConfig = UIImage.SymbolConfiguration(weight: symbolWeight)
        return UIImage(systemName: systemName, withConfiguration: symbolConfig)
    }
}

2️⃣ StarStackView

  • 별 5개 컴포넌트가 4번이나 사용되므로, 코드를 줄이기 위해 StarStackView를 별도의 클래스로 생성했습니다.
  • 4개 중 2개는 사용자의 액션을 인식해야하므로 UIPanGestureRecognizer, UITapGestureRecognizer를 활용하여 핸들러를 생성했습니다.
코드 보기
enum StarColor {
    case tint
    case gray
    case orange
}

protocol StarStackViewDelegate: AnyObject {
    func starStackView(_ view: StarStackView, newCount: Int)
}

class StarStackView: UIStackView {
    
    weak var delegate: StarStackViewDelegate?

    (생략)

    private func updateStarImage() {
        for (index, view) in self.arrangedSubviews.enumerated() {
            guard let imageView = view as? UIImageView else { continue }
            
            if index < starCount {
                imageView.image = starFilledImage
            } else {
                imageView.image = starEmptyImage
            }
            
            switch starColor {
            case .tint:
                imageView.tintColor = .tintColor
            case .gray:
                imageView.tintColor = .secondaryLabel
            case .orange:
                imageView.tintColor = .systemOrange
            }
        }
    }
    // 드래그 제스처 핸들러
    @objc func handlePangesture(_ gesture: UIPanGestureRecognizer) {
        let location = gesture.location(in: self)
        let startWidth = bounds.width / 5
        let selectedStarIndex = Int(location.x / startWidth)
        let newCount = selectedStarIndex + 1
        
        if newCount != starCount {
            starCount = newCount
            updateStarImage()
            delegate?.starStackView(self, newCount: newCount)
        }
    }
    
    // 클릭 제스처 핸들러
    @objc func handleTapGesture(_ gesture: UITapGestureRecognizer) {
        let location = gesture.location(in: self)
        let startWidth = bounds.width / 5
        let selectedStarIndex = Int(location.x / startWidth)
        let newCount = selectedStarIndex + 1
        
        if newCount != starCount {
            starCount = newCount
            updateStarImage()
            delegate?.starStackView(self, newCount: newCount)
        }
    }
}

3️⃣ UITextViewDelegate

TextView는 TextField와 달리 placeHolder를 설정하는 메소드가 제공되지 않습니다. 그래서 TextViewDelegate에서 textViewDidBegionEditing, textViewDidEndEditing 함수를 활용하여 직접 placeHolder를 설정했습니다.

코드 보기
extension FeedbackWriteViewController: UITextViewDelegate {
    func textViewDidBeginEditing(_ textView: UITextView) {
        feedbackWriteView.setTextViewToWrite()
    }
    
    func textViewDidEndEditing(_ textView: UITextView) {
        feedbackWriteView.setTextViewToEnd()
    }
}

class FeedbackWriteView: UIView {
    func setTextViewPlaceholder() {
        feedbackTextView.text = textViewPlaceHolder
        feedbackTextView.textColor = .systemGray4
    }
    
    func setTextViewToWrite() {
        if feedbackTextView.text == textViewPlaceHolder {
            feedbackTextView.text = nil
            feedbackTextView.textColor = .label
        }
    }
    
    func setTextViewToEnd() {
        let isPlaceHolder = feedbackTextView.text == textViewPlaceHolder
        let isEmpty = feedbackTextView.text.isEmpty
        
        if isPlaceHolder || isEmpty {
            setTextViewPlaceholder()
        }
    }

    func returnFeedback() -> Feedback {
        var content = feedbackTextView.text
        if content == textViewPlaceHolder {
            content = nil
        }
        
        let feedback = Feedback(title: feedbackTitleTextField.text,
                                author: "김유림",
                                starCount: starCount,
                                authorDate: Date(),
                                content: content,
                                developerContent: nil,
                                developerDate: nil)
        return feedback
    }
}

🙏 To Reviewers

  • 피드백 환영입니다. 감사합니다!

  • 겉보기에는 레이아웃에 문제가 없고, 저도 잘 설정했다고 생각하는데...디버그 영역에 레이아웃 관련 경고가 왕창 날아옵니다... 제 생각엔 다 스크롤뷰와 연관 있는 것 같은데, 뭐가 잘못되었는지 모르겠어요..
    image

  • 미리보기 이미지를 넣는 과정에서 NSBundle 오류가 발생합니다. (AppDetailView 233번 줄)
    앱 아이콘도 Asset에서 가져오지만 별 문제가 없는데, 왜 얘만 NSBUndle 오류가 나는지는 모르겠습니다.
    image

  • 이렇게 라벨이 버튼에 맞닿을 때 그라데이션으로 흐려지는 거 구현 방법이 궁금합니다

💭 Related Issues

previewImage 인스턴스를 생성할 때 NSBundle 오류 발생함. 그런데 시뮬레이터에는 잘 보임..
@LaonCoder
Copy link

더보기 버튼 왼쪽에 작게 UIView를 하나 붙이고, 그라디언트(.clear -> .white)를 줄 것 같습니다..!
https://zeddios.tistory.com/948

Copy link

@SungMinCho-Kor SungMinCho-Kor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 고생하셨습니다 ㅠㅠ
완전 상세하게 모든 코드를 읽지는 못했지만 궁금한 점과 리뷰 남겨봤습니다!
주관적인 생각도 포함되어 있어서 걸러서 들어주세요... ㅎ

객체지향의 특징인 캡슐화를 위해서는 객체 역할을 적절히 분리하면 좋을 것 같아요.
예를 들면 A 객체 내부에 B객체가 있으면 B객체 내부에서 처리할 수 있는 일은 A객체가 처리하는 것이 아닌 B객체가 처리해야 합니다.
A에서 B.프로퍼티 접근방식은 은닉이 깨져 캡슐화가 깨지게 됩니다. 최대한 내부에서 처리해서 캡슐화를 유지하면 더 좋을 것 같아요!

import Foundation

extension Date {
static func form(year: Int, month: Int, day: Int) -> Date? {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Date형식을 생성하는 함수라고 생각하는데
제가 이해한 게 맞다면 init으로 구현하는 것도 고려해보시면 좋을 것 같아요!

return Calendar.current.date(from: dateComponents)
}

static func formattedDate(date: Date?) -> String {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

매개변수로 받지 않고 static이 아닌 Date의 함수로 작동할 수 있도록 self를 이용하면 더 깔끔할 것 같아요 😄

}

extension UIButton {
func configureButton(configType: ConfigurationType = .plain,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Button의 속성을 모두 컨트롤 할 수 있는 함수인 것 같아요.
속성 값을 대입하는 것과 함수의 매개 변수를 하나씩 컨트롤 하는 것의 차이가 궁금해요!

self.numberOfLines = numberOfLines
}

func setLineSpacing(_ lineSpacing: CGFloat) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UILabel에 줄 간격을 설정하는 방법으로 좋은 것 같아요!

Comment on lines +11 to +15
var title: String?
var author: String?
var starCount: Int?
var authorDate: Date?
var content: String?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional로 설정하신 이유가 궁금해요💭

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

값이 없을 경우를 대비해서 옵셔널로 설정했습니다...! 굳이 필요 없으려나용...

func dataBind(feedback: Feedback)
}

class AppDetailView: UIView {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

button의 동작을 AppDetailView가 아닌 ViewController에서 작동시켜서 private를 설정하지 못한 것 같아요.
ViewController에서 addTarget하는 이유가 궁금합니다!

Comment on lines +136 to +142
setTitleViewUI()
setSummaryViewUI()
setVersionViewUI()
setPreviewViewUI()
setDescriptionViewUI()
setFeedbackSummaryViewUI()
setFeedbackViewUI()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

선언 부분을 then 라이브러리나 프로퍼티 선언부에서 처리해줘도 좋을 것 같아요!

private func setPreviewViewUI() {
previewTitleLabel.text = "미리 보기"

previewImageView.image = UIImage(named: "toss_preview") // NSBundle 오류 발생

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asset에 포함되어 있다면 UIImage.toss~ 로 하면 나올 것 같아요!

Comment on lines +16 to +18
override func loadView() {
view = appDetailView
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadView를 오버라이딩한 경우 super.loadView()를 호출해줘야 할 것 같아요.
그리고 addSubview 방법을 사용하지 않은 이유가 궁금합니다!

Comment on lines +96 to +98
func dataBind(feedback: Feedback) {
appDetailView.dataBind(feedback: feedback)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delegate로 구현하신 이유가 궁금해요~
init에서 feedback을 View에 주입해주는 방법과 차이점이 궁금합니다!

Copy link

@juri123123 juri123123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생많으셨습니다 !!!!! 최고최고

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컴포넌트들을 class로 모두 빼서 처리한 이유가 궁금해요!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Week2] 과제 Todo
4 participants