Skip to content

비행기에 '탑승(Boarding)' 하는 순간의 설레임과 기대감

Notifications You must be signed in to change notification settings

seosieve/BoardingIOS

Repository files navigation

BOARDING - 비행기에 탑승하는 순간의 설레임

The Beginning of a New Journey







Download on the App Store


프로젝트 소개

🛩️ 처음 여행을 준비하고 비행기 탑승을 기다리던 그 때의 설레임을 기억하시나요?

사용자들이 기록한 게시물들을 스크랩해서 나만의 플랜과 일정을 만들 수 있는 커뮤니티 앱 서비스


프로젝트 주요 기능

  • 카카오톡, 애플 간편로그인 기능
  • 홈 탭 내 유저 게시물 좋아요, 댓글, 스크랩 기능
  • 동영상, 사진 형식의 게시물 업로드 가능, Google Map 장소 데이터와 연동
  • 플립 인터렉션이 가능한 카드 형태로도 살펴볼 수 있는 게시글
  • 스크랩한 게시글을 메모와 함께 내 여행플랜, 내 여행일정에 추가 가능
  • 내가 쓴 글, 마일리지, 여행 레벨, 차단 유저 관리 기능

프로젝트 개발 환경

  • 개발 인원
    • 디자이너 1명, iOS개발 1명
  • 개발 기간
    • 2023.09 - 2024.01 (4개월)
  • iOS 최소 버전
    • iOS 16.0+

프로젝트 기술 스택

  • 활용기술 및 키워드

    • iOS : swift 5.9.2, xcode 15.1, UIKit, Photos, Apple Authorization
    • Network : RxSwift, Firebase, Kakao API
    • UI : CodeBaseUI, Snapkit, Then
  • 라이브러리

라이브러리 사용 목적 Version
GoogleMap 장소 세부 정보 8.3.1
FSCalendar 플랜, 일정 날짜 선택 2.8.4
Kingfisher 이미지 처리 7.0
FirebaseStorageUI 저장소 이미지 처리 -

프로젝트 아키텍처


RxSwift + MVVM Architecture

  • SnapKit과 Then을 활용한 CodeBase UI로 생산성을 높이고, 반복되는 UI요소들에 대한 View의 재사용성을 높임
  • Photos를 통해 가져온 유저 사진 데이터의 부족한 정보들을 Google Map 장소데이터와 연동하여 Storage에 저장
  • Kakao Auth와 Apple Auth를 담당하는 모델들이 Firebase Auth 데이터와 연동되어 통합 Token으로 유저 로그인 상태 관리

트러블 슈팅

1. Firebase 기반 애플로그인의 '회원탈퇴'를 구현할 수 없는 문제

Firebase내에 구현된 revokeToken 메소드는 Firebase내에서의 token만 다룰 뿐이지, 애플로그인 토큰 자체의 연결 해제까지는 할 수 없었다.


  • apple에게 Rest API Call을 보내서 revoke에 필요한 refresh Token을 가져온다. 이 과정에서 JWT(JsonWebToken)생성이 필요
  • JWT를 클라이언트단에서 호출하는 것은 보안 문제가 발생할 수 있으므로, Firebase Cloud Function을 사용해서 서버리스 백엔드 기능을 사용
  • SignIn을 하는 과정에서 받아온 Refresh Token을 UserDefaults에 저장한 다음, 회원 탈퇴 revoke Token시점에서 이를 사용하는 형태로 문제 해결

Sign In

let credential = OAuthProvider.appleCredential(withIDToken: tokenString, rawNonce: nonce, fullName: appleIDCredential.fullName)

if let code = appleIDCredential.authorizationCode, let codeString = String(data: code, encoding: .utf8) {
    //Use FireBase Cloud Function
    let url = URL(string: "https://us-central1-boarding-ef2f1.cloudfunctions.net/getRefreshToken?code=\(codeString)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "https://apple.com")!
    let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
        if let data = data {
            //Get Refresh Token
            let refreshToken = String(data: data, encoding: .utf8) ?? ""
            //Save in UserDefaults
            UserDefaults.standard.set(refreshToken, forKey: "refreshToken")
        }
    }
    task.resume()
}

//Sign In with Credential
Auth.auth().signIn(with: credential) { ... }

Withdraw

let credential = OAuthProvider.credential(withProviderID: "apple.com", idToken: tokenString, rawNonce: nonce)

//Use Refresh Token Saved in UserDefaults
let token = UserDefaults.standard.string(forKey: "refreshToken")
if let token = token {
    //Revoke Token
    let url = URL(string: "https://us-central1-boarding-ef2f1.cloudfunctions.net/revokeToken?refresh_token=\(token)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "https://apple.com")!
    let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
        guard data != nil else { return }
    }
    task.resume()
}

//Sign Out in FireBase
Auth.auth().signOut() { ... }

2. 세 개의 User Interaction이 겹치는 Sticky Header View

Sticky Header View와 Page View Controller, 그리고 그 안에 Scroll View들이 병용되어야 하는 상황


  • Pan Gesture의 state에 따른 translation과 velocity로 Sticky Header View를 우선적으로 구현 (이 때 y값만을 사용하여 Page View Controller의 interaction과 겹치지 않도록 설정)
  • Page List마다 RxSwift의 BehaviorRelay를 정의하여 Sticky Header View의 UI 상태를 Observe 할 수 있도록 설정
  • BehaviorRelay를 View.isScrollEnabled의 값과 bind하여 Pan Gesture의 우선순위를 명시하여 인터렉션을 구현

Sticky Header View

@objc func addModalMotion(_ recognizer: UIPanGestureRecognizer) {
    let minY = window.safeAreaInsets.top
    let maxY = self.maxY
    
    switch recognizer.state {
        
    case .began, .changed:
        //Set Translation
        let translation = recognizer.translation(in: modalView)
        //Set max, min Bound
        let y = min(maxY, max(minY, modalView.frame.minY + translation.y))
        //Change Top Constraints
        modalView.snp.updateConstraints { make in
            make.top.equalToSuperview().offset(y)
        }
        recognizer.setTranslation(CGPoint.zero, in: self.view)
        
    case .ended:
        //Direction of Pin Gesture
        let velocity = recognizer.velocity(in: modalView).y
        let shouldClose = velocity > 0
        //Auto Animation
        UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut, animations: {
            self.modalView.snp.updateConstraints { make in
                make.top.equalToSuperview().offset(shouldClose ? maxY : minY)
            }
            self.view.layoutIfNeeded()
        }, completion: nil)
        
        //Set BehaviorRelay to Each PageList
        modalPageViewController.pageList.forEach { vc in
            switch vc {
            case is NFTViewController:
                if let vc = vc as? NFTViewController {
                    vc.modalClosed.accept(shouldClose)
                }
            case is MILEViewController:
                if let vc = vc as? MILEViewController {
                    vc.modalClosed.accept(shouldClose)
                }
            default:
                if let vc = vc as? ExpertLevelViewController {
                    vc.modalClosed.accept(shouldClose)
                }
            }
        }
    default:
        break
    }
}

Scroll View

//Set Default Value True
let modalClosed = BehaviorRelay<Bool>(value: true)

func setRx() {
    //Bind to ScrollView
    modalClosed
        .bind(with: self) { owner, isClosed in
            owner.ScrollView.isScrollEnabled = isClosed
        }
        .disposed(by: disposeBag)
}

3. FireStore Plan 문서 저장에서 순서 바꾸기, 삭제된 게시글 기능이 정상적으로 동작하지 않는 문제

Plan 문서에 게시글 정보를 복사해서 모두 저장하다보니, 빠르게 수행되어야 하는 플랜 순서 바꾸기와 유저의 게시글 삭제 유무 탐색의 속도 문제가 있었다.


  • 필요한 객체의 복사 기반 저장에서 Card 문서의 ID값을 저장하는 방식으로 변경
  • ID값이 맞는 Card를 찾기 위해 모든 Card문서를 순회해야 하는 문제가 있으므로, 이는 Firestore Custom 색인을 생성하여 최적화

Scrap 추가

func addScrap(planID: String, CardID: String) {
    db.collection("User").document(userUid).collection("Plan").document(planID).updateData(["scrap": FieldValue.arrayUnion([CardID])]) { error in
        if let error = error {
            print("scrap 추가 에러: \(error)")
        } else {
            print("scrap 추가 성공")
            self.addScrapSubject.onNext(())
        }
    }
}

Plan에 삭제된 Card 포함시 Index 정리

//삭제된 Card 포함되어있을 때 dayPlan에서 삭제
for (index, item) in items.enumerated() where item.CardID == "" {
    let memoArray = dayMemoArray.enumerated().filter { $0.offset != index }.map { $0.element }
    self.removeDayPlan(day: day, CardID: dayArray[index], memoArray: memoArray)
}
items.removeAll { $0.CardID == "" }

About

비행기에 '탑승(Boarding)' 하는 순간의 설레임과 기대감

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages