Skip to content

minnnidev/Hyanggi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

향기

향기 앱을 통해 시향지를 핸드폰 속에 저장할 수 있습니다.
금방 사라지는 향을 오랫동안 간직하고 싶다면, 기억하고 싶은 그날의 향을 바로 기록하세요. 💐

개발 인원: 1명
프로젝트 기간: 2024.04~2024.05

💐 app store link


기술 스택

  • UIKit
  • RxSwift
  • RxCocoa
    • input-output pattern
  • Realm
  • MVVM

기능

전체 시향지/찜한 시향지/향수 정보 조회
시향지 등록
시향지 수정/삭제
시향지 검색

회고

RxSwift와 input-output 패턴 사용기

ViewModel은 View에 보여줄 형태로 가공, View는 UI에 보여줄 용도로만.
최대한 의도를 명확하게 하기 위해 RxSwift의 input-output pattern을 활용했다.

  • input-output pattern 사용 전

    // ViewModel
    
    let dateRelay = BehaviorRelay<String>(value: "")
    let brandNameRelay = BehaviorRelay<String>(value: "")
    let perfumeNameRelay = BehaviorRelay<String>(value: "")
    let contentRelay = BehaviorRelay<String>(value: "")
    let sentenceRelay = BehaviorRelay<String>(value: "")
    
    let completeAction = PublishRelay<Void>()
    
    var initialPerfume: Driver<Perfume?> {
      return Observable.just(perfume)
        .asDriver(onErrorJustReturn: nil)
    }
    
    var formValid: Observable<Bool> {
      return Observable.combineLatest(brandNameRelay,
                                      perfumeNameRelay,
                                      sentenceRelay)
        .map { !$0.isEmpty && !$1.isEmpty && !$2.isEmpty }
    }
    

  • input-output pattern 사용 후

    // ViewModel
    
    struct Input {
      let dateText: Observable<String>
      let brandNameText: Observable<String>
      let perfumeNameText: Observable<String>
      let contentText: Observable<String>
      let sentenceText: Observable<String>
      let dismissButtonTap: ControlEvent<Void>
      let completeButtonTap: ControlEvent<Void>
      let selectImage: Observable<UIImage?>
      let deletePhotoButtonTap: ControlEvent<Void>
    }
    
    struct Output {
      let isFormValid: Observable<Bool>
      let dismissToPrevious: Observable<Void>
      let initialPerfume: Driver<Perfume?>
      let perfumeImage: BehaviorRelay<UIImage?>
    }
    
    func transform(input: Input) -> Output {
      // return Output(...)
    }
    
    // ViewController - bindViewModel()
    
    let output = viewModel.transform(input: input)
    
    output.initialPerfume
      .compactMap { $0 }
      .drive(with: self, onNext: { vc, perfume in
          vc.layoutView.dateTextField.textField.text = perfume.date
          vc.layoutView.brandTextField.textField.text = perfume.brandName
          vc.layoutView.nameTextField.textField.text = perfume.perfumeName
          vc.layoutView.contentTextView.text = perfume.content
          vc.layoutView.sentenceTextField.textField.text = perfume.sentence
      })
      .disposed(by: disposeBag)
    
    output.perfumeImage
      .withUnretained(self)
      .subscribe(onNext: { vc, image in
          vc.layoutView.photoView.image = image
          vc.layoutView.deletePhotoButton.isHidden = (image == nil)
      })
      .disposed(by: disposeBag)
    
    output.dismissToPrevious
      .withUnretained(self)
      .bind { vc, _ in
          vc.dismiss(animated: true)
      }
      .disposed(by: disposeBag)
    
    output.isFormValid
      .bind(to: layoutView.completeButton.rx.isEnabled)
      .disposed(by: disposeBag)
    

모든 이벤트나 입력 데이터를 input으로 넣어줘야 하기 때문에, 간단하게 적을 수 있는 코드도 길어질 수 있다는 단점이 존재하였다.
하지만 output 데이터를 통해 View에 나타낼 수 있다는 점은 데이터의 흐름을 명확하게 보여주고, ViewModel과 View의 역할을 분리할 수 있다고 느껴졌다.
input-ouput pattern을 사용해 보면서, ViewModel은 View에 보여줄 데이터를 가공하는 곳이라는 정의를 확실하게 내릴 수 있었다.

massive viewController 관리
  • ViewController에서 View를 분리하기 위해 loadView() 메서드를 사용하여 View를 분리
  • View에 보여줄 모델을 가공하는 로직은 ViewModel로 이동
realm database
  • realm database에 향수 정보 저장
  • Repository Pattern 활용
    • PerfumeStorageType 프로토콜을 정의하여 local database → realm database 이전을 쉽게 수정할 수 있었음.
  • 관련 파일: Service 폴더
FileManager
  • 향수 이미지 등록 시, 이미지는 file 내에 저장하고, file에 접근하는 경로를 realm database에 저장하도록 하였다.
  • 관련 파일: Service 폴더 내의 ImageFileManager