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

[feat] 모임 상세 화면 약속 분기 처리 #343

Merged
merged 9 commits into from
Aug 27, 2024

Conversation

JinUng41
Copy link
Contributor

@JinUng41 JinUng41 commented Aug 26, 2024

🔗 연결된 이슈

📄 작업 내용

  • 모임 상세 화면에서 '내가 속한 약속'과 '모든 약속'을 분기 처리합니다.
구현 내용 IPhone 15 pro
아무 약속도 없는 경우
내가 속한 약속이 없는 경우
둘 다 있는 경우
약속 상세 화면으로 이동

💻 주요 코드 설명

📌 '내가 속한 약속'과 '모든 약속' 서버에 요청하기

  • '내가 속한 약속'은 쿼리 파라미터가 필요하지만, '모든 약속'은 쿼리 파라미터가 없어야 합니다.
  • 따라서 TargetType의 case는 2가지로 분리하되, Service에서는 옵셔널로 분기 처리를 하였습니다.
MeetingService.swift
// MeetingService 내 MeetingInfoServiceProtocol
func fetchMeetingPromiseList(
    with meetingID: Int,
    isParticipant: Bool?
) async throws -> ResponseBodyDTO<MeetingPromisesModel>? {
    guard let isParticipant else {
        // '모든 약속' 요청
        return try await request(with: .fetchMeetingPromiseList(meetingID: meetingID))
    }
    // '내가 속한 약속' 요청
    return try await request(with: .fetchParticipatedPromiseList(meetingID: meetingID, isParticipant: isParticipant))
}

📌 밑줄을 가진 세그먼트 컨트롤

  • 더 디테일한 부분은 밑줄이 각 세그먼트의 길이 만큼 길어지거나 줄어들어야 하는 점입니다.
  • 기존 SOPT 과제 티빙 클론 프로젝트에서 구현한 바있어, 이를 가져와 구현하였습니다.
  • CALayer로 시도해보고 싶었으나, 도저히 하지 못하고 포기하였습니다.
  • XCode가 1.9인 이유를 다시 한번 알게 되었습니다. (시뮬레이터로 동작 시, 잔상이 남는 문제 -> 이를 해결하려고 3시간 넘게 투자했으나, 아... 망할 시뮬레이터..)
UnderlineSegmentedControl.swift
final class UnderlineSegmentedControl: UISegmentedControl {
    private let underlineView = UIView(backgroundColor: .maincolor)
    
    override init(items: [Any]?) {
        super.init(items: items)
        setupUI()
        removeBackgroundAndDivider()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        let segmentFrames = segmentFrames()
        let labelFrames = segmentLabelFrames()
        
        guard selectedSegmentIndex < labelFrames.count,
              selectedSegmentIndex < segmentFrames.count
        else {
            return
        }
        
        let segmentFrame = segmentFrames[selectedSegmentIndex]
        let labelFrame = labelFrames[selectedSegmentIndex]
        
        UIView.animate(withDuration: 0.1) {
            self.underlineView.frame = CGRect(
                x: segmentFrame.minX + labelFrame.minX,
                y: self.bounds.height - 2.0,
                width: labelFrame.width,
                height: 4.0
            )
        }
    }
}

private extension UnderlineSegmentedControl {
    func setupUI() {
        let attributes = [
            NSAttributedString.Key.foregroundColor: UIColor.gray4,
            .font: UIFont.pretendard(.body06)
        ]
        
        let selectedAttributes = [
            NSAttributedString.Key.foregroundColor: UIColor.gray8,
            .font: UIFont.pretendard(.body05)
        ]
        
        setTitleTextAttributes(attributes as [NSAttributedString.Key : Any], for: .normal)
        setTitleTextAttributes(selectedAttributes as [NSAttributedString.Key : Any], for: .selected)
        apportionsSegmentWidthsByContent = true
        selectedSegmentIndex = 0
        
        addSubviews(underlineView)
    }
    
    func removeBackgroundAndDivider() {
        let image = UIImage()
        setBackgroundImage(image, for: .normal, barMetrics: .default)
        setBackgroundImage(image, for: .selected, barMetrics: .default)
        setBackgroundImage(image, for: .highlighted, barMetrics: .default)
        setDividerImage(image, forLeftSegmentState: .selected, rightSegmentState: .normal, barMetrics: .default)
    }
    
    func segmentLabelFrames() -> [CGRect] {
        subviews
            .sorted { $0.frame.minX < $1.frame.minX }
            .compactMap { $0.subviews.first(where: { $0 is UILabel }) as? UILabel }
            .map { $0.frame }
    }
    
    func segmentFrames() -> [CGRect] {
        let temp = subviews
            .sorted { $0.frame.minX < $1.frame.minX }
            .compactMap { $0 as? UIImageView }
            .map { $0.frame }
        
        var frames = [CGRect]()
        for i in 0..<temp.count {
            if i % 2 == 0 {
                frames.append(temp[i])
            }
        }
        return frames
    }
}

📌 '내가 속한 약속'과 '모든 약속' UI 분기처리

  • 약속 리스트를 보여줄 컬렉션 뷰를 단일로 사용합니다.
  • 세그먼트 컨트롤(0: '내가 속한 약속', 1: '모든 약속')의 값에 따라 데이터소스를 위한 배열을 결정합니다.
MeetingInfoViewModel.swift
// extension MeetingInfoViewModel: ViewModelType에서
struct Input {
    let viewWillAppear: PublishRelay<Void>
    let createPromiseButtonDidTap: Observable<Void>
    let actionButtonDidTapRelay: PublishRelay<Void>
    let selectedSegmentedIndex: Observable<Int> // 세그먼트 컨트롤의 선택된 인덱스 값
    let promiseCellDidSelect: Observable<Int>
}

// transform 메서드 내에서
let promises = input.selectedSegmentedIndex
    .flatMapLatest { [weak self] index -> Observable<[MeetingInfoPromiseModel]> in
        guard let self else {
            return Observable.just([])
        }
        
        // 삼항 연산자에 따라 데이터소스의 원본 배열을 결정함.
        let source = index == 0 ? self.partipatedPromisesModelRelay : self.meetingPromisesModelRelay
        return source
            .compactMap { $0?.promises }
            .map { self.convertToMeetingInfoPromiseModels(from: $0) }
            .asObservable()
    }
    .asDriver(onErrorJustReturn: [])

📌 셀 선택 시 화면 이동

  • 셀이 선택되었을 때에 대응하기 위해서는 일반적으로 CollectionViewDelegate의 didSelectItemAt을 이용합니다.
  • 하지만 RxCocoa를 이용하면, 이를 rx 코드로 작성할 수 있습니다.
MeetingInfoViewController.swift
// bindViewModel 메서드에서
let promiseCellDidSelect = rootView.promiseListView.rx.itemSelected
    .map { $0.item }
    .asObservable()

let input = MeetingInfoViewModel.Input(
    viewWillAppear: viewWillAppearRelay,
    createPromiseButtonDidTap: rootView.createPromiseButtonDidTap,
    actionButtonDidTapRelay: actionButtonDidTapRelay,
    selectedSegmentedIndex: rootView.selectedSegmentIndex,
    promiseCellDidSelect: promiseCellDidSelect // 선택된 아이템 (Int형)
)

output.navigateToPromiseInfo
    .drive(with: self) { owner, promiseID in // ViewModel로부터 `promiseID`를 전달받아, 약속 상세 화면으로 이동
        guard let promiseID else { return }
        
        let pagePromiseViewController = PromiseViewController(
            viewModel: PromiseViewModel(promiseID: promiseID, service: PromiseService())
        )
        
        owner.navigationController?.pushViewController(pagePromiseViewController, animated: true)
    }
    .disposed(by: disposeBag)

📚 참고자료

👀 기타 더 이야기해볼 점

  • PR 열심히 썼어용! 다들 화이팅하세요!!

@JinUng41 JinUng41 added ✨ feat 기능 구현시 사용 💙 JinUng 걸스 토크에 미쳐보고 싶다면 labels Aug 26, 2024
@JinUng41 JinUng41 self-assigned this Aug 26, 2024
@JinUng41 JinUng41 linked an issue Aug 26, 2024 that may be closed by this pull request
1 task
Copy link
Member

@mmaybei mmaybei left a comment

Choose a reason for hiding this comment

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

꼼꼼한 pr까지 .. 수고하셨습니다!

Copy link
Member

@hooni0918 hooni0918 left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Member

@youz2me youz2me left a comment

Choose a reason for hiding this comment

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

고생하셨습니다! 1.8의 저주를 이겨내셨으니 더욱 강해지시겠군요.

Comment on lines +61 to +64
guard let isParticipant else {
return try await request(with: .fetchMeetingPromiseList(meetingID: meetingID))
}
return try await request(with: .fetchParticipatedPromiseList(meetingID: meetingID, isParticipant: isParticipant))
Copy link
Member

Choose a reason for hiding this comment

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

굿!

Comment on lines +193 to +207
MeetingPromise(promiseID: 1,name: "꾸물 리프레시 데이",dDay: 0,time: "PM 2:00",placeName: "DMC역"),
MeetingPromise(promiseID: 2,name: "꾸물 잼얘 나이트",dDay: 10,time: "PM 6:00",placeName: "홍대입구"),
MeetingPromise(promiseID: 3,name: "친구 생일 파티",dDay: 5,time: "PM 7:00",placeName: "강남역"),
MeetingPromise(promiseID: 4,name: "주말 산책",dDay: 3,time: "AM 10:00",placeName: "서울숲"),
MeetingPromise(promiseID: 5,name: "프로젝트 미팅",dDay: 1,time: "AM 9:00",placeName: "삼성역"),
MeetingPromise(promiseID: 6,name: "독서 모임",dDay: 7,time: "PM 3:00",placeName: "합정역"),
MeetingPromise(promiseID: 7,name: "헬스클럽 모임",dDay: 2,time: "AM 8:00",placeName: "신촌역"),
MeetingPromise(promiseID: 8,name: "영화 관람",dDay: 4,time: "PM 8:00",placeName: "잠실역"),
MeetingPromise(promiseID: 9,name: "저녁 식사",dDay: 6,time: "PM 7:30",placeName: "이태원역"),
MeetingPromise(promiseID: 10,name: "아침 조깅",dDay: 14,time: "AM 6:00",placeName: "한강공원"),
MeetingPromise(promiseID: 11,name: "커피 브레이크",dDay: 8,time: "PM 4:00",placeName: "을지로입구"),
MeetingPromise(promiseID: 12,name: "스터디 그룹",dDay: 12,time: "PM 5:00",placeName: "강남역"),
MeetingPromise(promiseID: 13,name: "뮤직 페스티벌",dDay: 9,time: "PM 2:00",placeName: "난지공원"),
MeetingPromise(promiseID: 14, name: "낚시 여행", dDay: 11, time: "AM 5:00", placeName: "속초항"),
MeetingPromise(promiseID: 15, name: "가족 모임", dDay: 13, time: "PM 1:00", placeName: "광화문역")
Copy link
Member

Choose a reason for hiding this comment

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

이거 혹시 한번에 개행 없애는 방법 아시나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

나중에 직접 알려드릴게요~

Comment on lines +37 to +43
UIView.animate(withDuration: 0.1) {
self.underlineView.frame = CGRect(
x: segmentFrame.minX + labelFrame.minX,
y: self.bounds.height - 2.0,
width: labelFrame.width,
height: 4.0
)
Copy link
Member

Choose a reason for hiding this comment

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

고생하셨습니다 ㅎㅎ...

@JinUng41 JinUng41 merged commit 7275120 into suyeon Aug 27, 2024
@JinUng41 JinUng41 deleted the feat/#334-meetingInfo-promise-logic branch August 27, 2024 10:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨ feat 기능 구현시 사용 💙 JinUng 걸스 토크에 미쳐보고 싶다면
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[feat] 모임 상세 화면 약속 분기 처리
4 participants