Skip to content

maxhyunm/ios-project-manager

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

42 Commits
 
 
 
 
 
 

Repository files navigation

Project Manager🗂️

소개

ToDo 리스트를 입력하고 Doing, Done으로 이동하며 스케줄을 관리하는 앱입니다.
마감일이 지나면 날짜가 빨간색으로 표시되며, 내용 수정/삭제가 가능합니다.
네트워크가 연결되어 있을 경우 모든 데이터는 원격 저장소에 동기화됩니다.

프로젝트 기간
1차: 2023.09.19 ~ 2023.10.06
2차: 2023.10.24 ~

목차

  1. 👩‍💻 팀원 소개
  2. 📅 타임 라인
  3. 🛠️ 활용 기술
  4. 📊 시각화 구조
  5. 📱 실행 화면
  6. 📌 핵심 경험
  7. 🧨 트러블 슈팅
  8. 📚 참고 자료

👩‍💻 팀원 소개

maxhyunm
([email protected])

📅 타임 라인

날짜 내용
2023.09.19 Firebase 라이브러리 추가
2023.09.24 CoreDataManager 타입 생성
Observable 타입 생성
ViewModel 타입 생성
TableView 포함 기본적인 ViewController 구현
TableView Header, Cell 타입 생성
AlertBuilder 타입 생성
2023.09.25 Value 타입 생성
ViewModel에 handle error 메서드 생성
2023.09.26 의존성 주입 수정
2023.10.03 ViewController 분할 및 child로 추가
TableView 배치 업데이트 추가
2023.10.06 ViewModel Input/Output으로 분할
UseCase 분리
PopOverView 생성
2023.10.09 RxSwift 라이브러리 설치
Observable을 Rx로 리팩토링
Firebase Database 연결
2023.10.24 DetailViewController 생성
2023.10.27 ViewModel 구조 변경
2023.11.01 NetworkMonitor 타입 생성
Firebase CRUD 추가
2023.11.02 DataSyncManager 타입 생성
ViewModel 리팩토링
2023.11.03 History Entity 추가
History 목록 보기 구현
CoreData 복수 접근 오류 수정
2023.11.04 CompletionHandler를 Single 리턴 형식으로 리팩토링
2023.11.05 README 작성

🛠️ 활용 기술

Framework Architecture Concurrency DB Dependency Manager
UIKit MVVM RxSwift CoreData SPM

📊 시각화 구조

File Tree

.
├── ProjectManager
│   ├── ProjectManager
│   │   ├── App
│   │   │   ├── AppDelegate.swift
│   │   │   └── SceneDelegate.swift
│   │   ├── Domain
│   │   │   ├── Local
│   │   │   │   ├── Entity
│   │   │   │   │   ├── History+CoreDataClass.swift
│   │   │   │   │   ├── History+CoreDataProperties.swift
│   │   │   │   │   ├── ToDo+CoreDataClass.swift
│   │   │   │   │   └── ToDo+CoreDataProperties.swift
│   │   │   │   └── CoreDataManager.swift
│   │   │   ├── Remote
│   │   │   │   ├── Entity
│   │   │   │   │   ├── HistoryDTO.swift
│   │   │   │   │   └── ToDoDTO.swift
│   │   │   │   └── FirebaseManager.swift
│   │   │   ├── HistoryDataSyncManager.swift
│   │   │   ├── ToDoDataSyncManager.swift
│   │   │   ├── HistoryUseCase.swift
│   │   │   └── ToDoUseCase.swift
│   │   ├── Utility
│   │   │   ├── AlertBuilder.swift
│   │   │   ├── KeywordArgument.swift
│   │   │   ├── NetworkMonitor.swift
│   │   │   ├── Observable.swift
│   │   │   ├── Output.swift
│   │   │   ├── ProjectManagerError.swift
│   │   │   └── ToDoStatus.swift
│   │   ├── Presentation
│   │   │   ├── ViewModelProtocol
│   │   │   │   ├── ViewModelDelegate.swift
│   │   │   │   └── ViewModelType.swift
│   │   │   └── View
│   │   │       ├── DetailView
│   │   │       │   ├── DetailViewController.swift
│   │   │       │   └── DetailViewModel.swift
│   │   │       ├── ListView
│   │   │       │   ├── Cell
│   │   │       │   │   ├── ListHeaderView.swift
│   │   │       │   │   └── ListTableViewCell.swift
│   │   │       │   ├── BaseView
│   │   │       │   │   ├── BaseListViewController.swift
│   │   │       │   │   ├── BaseListViewModel.swift
│   │   │       │   │   └── NavigationTitleView.swift
│   │   │       │   └── ChildView
│   │   │       │       ├── ChildListViewController.swift
│   │   │       │       └── ChildListViewModel.swift
│   │   │       └── PopOverView
│   │   │           ├── ChangeStatusView
│   │   │           │   ├── ChangeStatusButton.swift
│   │   │           │   ├── ChangeStatusViewController.swift
│   │   │           │   └── ChangeStatusViewModel.swift
│   │   │           └── HistoryView
│   │   │               ├── Cell
│   │   │               │   └── HistoryTableViewCell.swift
│   │   │               ├── HistoryViewController.swift
│   │   │               └── HistoryViewModel.swift
│   │   ├── Resource
│   │   │   └── Assets.xcassets
│   │   ├── Info.plist
│   │   └── ProjectManager.xcdatamodeld
│   │           └── contents
│   └── ProjectManager.xcodeproj
└── README.md

📱 실행 화면

일정 추가
일정 이동
일정 삭제
일정 수정
이력 확인

📌 핵심 경험

🌟 MVVM 패턴 + UseCase 활용

input 타입과 output 타입을 분리한 View Model을 적용한 MVVM 패턴을 활용하였습니다. 상세 데이터 처리 로직과 관련된 부분은 UseCase로 분리하였습니다.

상세코드
final class ChildListViewModel: ChildViewModelType, ChildViewModelOutputsType {
    var inputs: ChildViewModelInputsType { return self }
    var outputs: ChildViewModelOutputsType { return self }

    func viewWillAppear() {
        delegate?.readData(for: status)
    }

    func swipeToDelete(_ entity: ToDo) {
        guard let index = entityList.firstIndex(of: entity) else { return }
        delegate?.deleteData(entity, index: index)
    }
    ...
}
struct ToDoUseCase {
    let dataSyncManager: ToDoDataSyncManager

    func fetchDataByStatus(for status: ToDoStatus) throws -> [ToDo] {
        ...
    }

    func createData(values: [KeywordArgument]) throws {
        ...
    }
    
    @discardableResult
    func updateData(_ entity: ToDo, values: [KeywordArgument]) throws -> ToDo {
        ...
    }
    
    func deleteData(_ entity: ToDo) throws {
        ...
    }
    ...
}

🌟 RxSwift를 활용한 데이터 바인딩 구현

ViewModelView를 바인딩하기 위하여 RxSwift를 활용하였습니다. 그 외에도 Firebase의 처리와 ViewModel을 잇는 부분에서도 Single을 활용하였습니다.

상세코드
extension ChildListViewController {
    private func setupBinding() {
        viewModel.outputs.action.subscribe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] action in
                ...
            }, onError: { [weak self] error in
                ...
            }).disposed(by: disposeBag)
    }
}
func syncLocalWithRemote() -> Single⟪Void⟫ {
    return mergeRemoteDataToLocal().map { _ in
        try self.mergeLocalDataToRemote(for: .create)
        try self.mergeLocalDataToRemote(for: .update)
        try self.deleteData()
    }
}

🌟 Builder 패턴 활용

Builder 패턴을 활용해 Alert 처리를 조금 더 깔끔히 할 수 있도록 하였습니다.

상세코드
struct AlertBuilder {
    let configuration: AlertConfiguration
    
    init(prefferedStyle: UIAlertController.Style) {
        ...
    }
    
    @discardableResult
    func setTitle(_ title: String) -> Self {
        ...
    }
    
    @discardableResult
    func setMessage(_ message: String) -> Self {
        ...
    }
    
    @discardableResult
    func addAction(_ actionType: AlertActionType, action: ((UIAlertAction) -> Void)? = nil) -> Self {
        ...
    }
    
    func build() -> UIAlertController {
        ...
    }
}
let alertBuilder = AlertBuilder(prefferedStyle: .alert)
    .setMessage(errorMessage)
    .addAction(.confirm) { action in
        self.dismiss(animated: true)
    }
    .build()

🌟 NWPathMonitor를 활용한 네트워크 상태 확인

NWPathMonitorstatus를 통해 네트워크 연결 상태를 확인할 수 있도록 하였습니다.

상세코드
final class NetworkMonitor {
    static let shared = NetworkMonitor()
    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue.global()
    private(set) var isConnected = BehaviorRelayBool(value: false)
    
    private init() {
        monitor.pathUpdateHandler = { path in
            self.isConnected.accept(path.status == .satisfied)
        }
    }
    
    public func start() {
        monitor.start(queue: queue)
    }
    
    public func stop() {
        monitor.cancel()
    }
}

🌟 Delegate 패턴을 활용한 ViewModel 연결

ToDo에서 Doing, Done 등으로 상태가 바뀔 때마다 ChildViewModel간의 연동이 일어나야 했으므로, 각 ChildViewModelBaseViewModelDelegate 패턴으로 연결하고 BaseViewModel에서는 ChildViewModel들을 children으로 갖고있도록 만들어 관련 처리가 이루어질 수 있도록 구현하였습니다.

상세코드
extension ChildListViewModel: ChildViewModelDelegate {
    func changeStatus(_ entity: ToDo, to newStatus: ToDoStatus) {
        guard let index = entityList.firstIndex(of: entity) else { return }
        delegate?.changeStatus(entity, to: newStatus, index: index)
    }
}

🧨 트러블 슈팅

1️⃣ 여러 개의 CoreData Entity 활용

🚨 문제점
처음에는 CoreDataManager 타입 자체에 Generic으로 타입을 설정하여 각 Entity에 맞는 매니저를 활용할 수 있도록 구현하였습니다.

struct CoreDataManagerT: NSManagedObject {
    let persistentContainer: NSPersistentContainer
    ...
    func fetchData(entityName: String, predicate: NSPredicate? = nil, sort: String? = nil, ascending: Bool = true) throws -> [T] {
        ...
    }
    ...
}

하지만 이렇게 하니 아래와 같은 오류가 발생하는 것을 확인했습니다.
해당 오류 내용과 관련된 사례를 확인한 결과 위의 오류는 여러 개의 NSPersistentContainer를 활용하게 되면서 발생하는 오류라는 것을 알 수 있었습니다.

💡 해결 방법
타입 자체가 아닌 메서드를 Generic 처리하여, 하나의 CoreDataManager와 하나의 NSPersistentContainer로 여러 가지 Entity에 함께 활용할 수 있도록 수정하였습니다.

struct CoreDataManager {
    let persistentContainer: NSPersistentContainer
    ...
    func fetchData⟪T: NSManagedObject⟫(entityName: String, predicate: NSPredicate? = nil, sort: String? = nil, ascending: Bool = true) throws -> [T] {
        ...
    }
    ...
}

2️⃣ GestureRecognizer 오류

🚨 문제점
TableView에서 특정 Cell을 오래 누르면 ToDo / Doing / Done로 상태를 변경할 수 있도록 LongPress 관련 액션을 구현하였습니다. 하지만 실제 LongPress 이벤트가 발생할 때마다 아래와 같은 경고 메시지가 발생하였습니다.

💡 해결 방법
LongPress이벤트가 진행중인 상태부터(아직 눌리고 있는 상태) 메서드 내용이 호출되는 것이 원인으로 보여, GestureRecognizer의 상태가 .ended일 때에만 해당 메서드를 실행할 수 있도록 아래와 같은 코드를 추가하였습니다.

guard sender.state == .ended else { return }

📚 참고 자료

🍎 : Apple Developer Documentations
⚪️ : 기타 자료

About

프로젝트 관리 앱 저장소입니다.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Swift 100.0%