Code Monkey home page Code Monkey logo

ios-project-manager's Introduction

📅 프로젝트 매니저 (Project Manager)

  • 팀 구성원 : YesCoach(YesCoach), Jiss(hrjy6278), 산체스(sanches37)
  • 프로젝트 기간 : 2021.10.25 ~ 2021.11.19 (4주)

목차

  1. 기능구현
  2. 이를 위한 설계
  3. Trouble Shooting
  4. 아쉽거나 해결하지 못한 부분
  5. 관련 학습내용


I. 기능 구현

Simulator Screen Recording - iPad mini (6th generation) - 2021-11-22 at 21.09.28.gif

  • Swift UI 를 사용한 UI 구현
  • MVVM 아키텍처 사용
  • Core Data 를 활용한 로컬 캐시 구현
  • Firebase FireStore 를 이용한 서버 DB 구현
  • Repository 의 변경사항을 Delegate로 처리




II. 이를 위한 설계

UML

ProjectManager UML.drawio.png

로컬, 리모트 동기화 시퀀스 다이어그램

Untitled Diagram.drawio.png





1. SwiftUI 를 이용한 설계

  • 선언형 UI인 SwiftUI를 사용하여 UI를 구현하였다. UIKit과는 다른, 새로운 SwiftUI를 공부하고, 적용하였다.

    struct ProjectListView: View {
        @EnvironmentObject var projectListViewModel: ProjectListViewModel
        let type: ProjectStatus
        var body: some View {
            let projectList = projectListViewModel.filteredList(type: type)
            VStack {
                HStack {
                    Text(type.description)
                        .padding(.leading)
                    Text(projectList.count.description)
                        .foregroundColor(.white)
                        .padding(5)
                        .background(Circle())
                    Spacer()
                }
                .font(.title)
                .foregroundColor(.black)
                List {
                    ForEach(projectList) { todo in
                            ProjectRowView(viewModel: todo)
                        }
                        .onDelete { indexSet in projectListViewModel.action(
                            .delete(indexSet: indexSet))
                        }
                 }
                .listStyle(.plain)
            }
            .onAppear {
                projectListViewModel.onAppear()
            }
        }
    } 




2.MVVM 아키텍처를 사용하여 프로젝트 진행

  • SwiftUI가 MVVM과 잘 어울린다는 글을 보고 MVVM 패턴을 적용시켰다.
  • View는 Model을 모르고, ViewModel에 View에 보여질 Model 데이터를 가지게 된다.
  • View의 User Interaction(Input)은 열거형으로 정의하였다.
  • Output은 메서드로 정의하였다.
  • 각 List Row에 해당하는 ProjectRowViewModel을 구현하였다
//리스트의 뷰모델 
final class ProjectListViewModel: ObservableObject{
//Input
  enum Action  {
      case create(project: ProjectPlan)
      case delete(indexSet: IndexSet)
      case update(project: ProjectPlan)
  }
		
  @Published private var projectList: [ProjectRowViewModel] = []

  //Repository
  private let projectRepository = ProjectRepository()

  func onAppear() {
      projectRepository.setUp(delegate: self)
  }

  func action(_ action: Action) {
      switch action {
      case .create(let project):
          projectRepository.addProject(project)
      case .delete(let indexSet):
          projectRepository.removeProject(indexSet: indexSet)
      case .update(let project):
          projectRepository.updateProject(project)
      }
  }

//Output
  func selectedProject(from id: String?) -> ProjectRowViewModel? {
      if let id = id, let index = projectList.firstIndex(where: { $0.id == id }) {
          return projectList[index]
      }
      return nil
  }

  func filteredList(type: ProjectStatus) -> [ProjectRowViewModel] {
      return projectList.filter {
          $0.type == type
      }
  }
}
final class ProjectRowViewModel: Identifiable {
		//Model
    private var project: ProjectPlan
		//Repository
    private let repository = ProjectRepository()
    weak var delegate: ProjectRowViewModelDelegate?
		
    init(project: ProjectPlan) {
        self.project = project
    }

//Input
    enum Action {
        case changeType(type: ProjectStatus)
    }

    func action(_ action: Action) {
        switch action {
        case .changeType(let type):
            project.type = type
            repository.updateProject(project)
            delegate?.updateViewModel()
        }
    }

//Output
    var id: String {
        return project.id
    }

    var title: String {
        return project.title
    }

    var convertedDate: String {
        return DateFormatter.convertDate(date: project.date)
    }

    var date: Date {
        return project.date
    }

    var detail: String {
        return project.detail
    }

    var type: ProjectStatus {
        return project.type
    }

    var dateFontColor: Color {
        let calendar = Calendar.current
        switch project.type {
        case .todo, .doing:
            if calendar.compare(project.date, to: Date(), toGranularity: .day) == .orderedAscending {
                return .red
            } else {
                return .black
            }
        case .done:
            return .black
        }
    }

    var transitionType: [ProjectStatus] {
        return ProjectStatus.allCases.filter { $0 != project.type }
    }
}

3. 로컬, 리모트 DB 구현을 위해 Core Data, Firestore 적용

  • 로컬 DB를 캐싱하고 관리하기 위해 Core Data를 프로젝트에 적용하였다.
    • 코어 데이터로 Project 엔티티를 생성하고, 모델의 attribute들을 각각 추가하였다.
      • type 프로퍼티는 커스텀 타입이므로 이를 코어 데이터로 저장하기 위해 Integer16 타입으로 설정.
  • 리모트 DB를 관리할 수 있도록 Firestore를 프로젝트에 적용하였다.

4. 레포지토리 패턴

  • 로컬 DB인지 리모트 DB인지와 관계 없이 동일한 인터페이스로 데이터에 접속할 수 있도록 하기 위해 레포지토리 패턴 도입.
  • 어떤 데이터를 가져올지는 레포지토리의 몫이며 뷰 모델은 레포지토리가 보내주는 데이터만 받으면 된다.
protocol ProjectRepositoryDelegate: AnyObject {
    func changeRepository(project: [ProjectPlan])
}

//레포지토리 클래스 생성
final class ProjectRepository {
    weak var delegate: ProjectRepositoryDelegate?
    private let firestore = FirestoreStorage()
    private var projects: [ProjectPlan] = [] {
        didSet {
            delegate?.changeRepository(project: projects)
        }
    }
    
    func setUp(delegate: ProjectRepositoryDelegate) {
        self.delegate = delegate
        self.projects = CoreDataStack.shared.fetch().map { project in
            ProjectPlan(id: project.id, title: project.title, detail: project.detail, date: project.date, type: project.type)
        }
    }

    func addProject(_ project: ProjectPlan) {
        projects.append(project)
        firestore.upload(project: project)
    }
    
    func removeProject(indexSet: IndexSet) {
        let index = indexSet[indexSet.startIndex]
        firestore.delete(id: projects[index].id)
        projects.remove(atOffsets: indexSet)
    }
    
    func updateProject(_ project: ProjectPlan) {
        projects.firstIndex { $0.id == project.id }.flatMap { projects[$0] = project }
        firestore.upload(project: project)
    }
}


//ViewModel
//ViewModel에서는 레포지토리를 소유하고 있다. CRUD의 기능은 레포지토리에게 시킨다.
final class ProjectListViewModel: ObservableObject{
    @Published private(set) var projectList: [ProjectRowViewModel] = []
    private let projectRepository = ProjectRepository()
    
      func action(_ action: Action) {
      switch action {
      case .create(let project):
          projectRepository.addProject(project)
      case .delete(let indexSet):
          projectRepository.removeProject(indexSet: indexSet)
      case .update(let project):
          projectRepository.updateProject(project)
      }
  }
    




III. 트러블 슈팅

  1. Target Membership 관련 이슈

    타겟 추가 후 SceneDelegate에서 ContentView를 접근해야되는데 인식하지 못했음. (Can't not found ContentView)

→ View의 파일에서 Target Membership 체크하고 나서야 접근할 수 있었다.

타켓멤버쉽 에러

  1. 네비게이션 좌측 Plus버튼을 눌러도 Sheet View 가 안뜨는 문제

    // 수정 코드
    .toolbar {
      ToolbarItem(placement: .navigationBarTrailing) {
          Button {
              isPresented.toggle()
          } label: {
              Image(systemName: "plus")
          }.sheet(isPresented: $isPresented) {
              NewTodoView()
          }
    }
     // isPresented 값이 true 로 바뀌고 적용을 하도록 수정함
    // 이전 코드
    
    .toolbar {
        ToolbarItem(placement: .navigationBarTrailing) {
            Button {
                isPresented.toggle()
    						sheet(isPresented: $isPresented) {
                NewTodoView()
                }
            } label: {
                Image(systemName: "plus")
             }
    
    // 버튼 내부에서 sheet를 호출해서 문제가 됨
    • Swift UI는 선언형 프로그래밍 방식이라, 모든 State에 따라 뷰가 그려져야한다. 우리의 사고방식은 기존 UIKit을 썼을때 처럼 Button 을 누르면 어떤 뷰를 띄워줘야한다는 생각에 Button의 Action안에 sheet View를 생성하였다. 이는 잘못된 방법이란걸 깨닫고 선언형 방법으로 코드를 수정하였다.

  2. Published 프로퍼티가 동시에 변경되면서 뷰를 순환적으로 다시 그리는 이슈(CPU 오버헤딩)

    final class ToDoListViewModel: ObservableObject{
        @Published private(set) var toDoList: [Todo] = []
        @Published private(set) var count: [SortType: Int] = [.toDo: 0, .doing: 0, .done: 0]
    
        func action(_ action: Action) {
            switch action {
            case .create(let todo):
                toDoList.append(todo)
            case .delete(let indexSet):
                toDoList.remove(atOffsets: indexSet)
            case .update(let todo):
                toDoList.firstIndex { $0.id == todo.id }.flatMap { toDoList[$0] = todo }
            case .changeType(let id, let type):
                toDoList.firstIndex { $0.id == id }.flatMap { toDoList[$0].type = type }
            }
        }
    
        func fetchList(type: SortType) -> [Todo] {
            let list = toDoList.filter {
                $0.type == type
            }
            **count[type] = list.count**
            return list
        }
    
    1. fetchList를 호출

    2. count[type]에 새로운 변수 할당 → @Published var count 값 변경 → 뷰 다시 그리세여

    3. 다시 fetchList가 호출

    4. 반복...

      스크린샷 2021-11-05 21.09.01.png

    → 앱 실행시 화면이 계속 그려지는 (한무반복) 상황 발생 → CPU 100% 오버헤드 발생

    Published 프로퍼티 사용에 유념. 한 메소드에서 복수의 Published 프로퍼티가 변경되면, 뷰는 계속 순환하여 그려진다.





  1. 모달뷰를 키보드가 가리는 문제

    Simulator Screen Recording - iPad mini (6th generation) - 2021-11-05 at 16.36.04.gif

    var body: some View {
                NavigationView {
                    **GeometryReader { geo in**
                    VStack {
                        ScrollView {
                            TextField("Title", text: $title)
                                .textFieldStyle(.roundedBorder)
                            DatePicker("Title",
                                       selection: $date,
                                       displayedComponents: .date)
                                .datePickerStyle(.wheel)
                                .labelsHidden()
                            
                            **TextEditor(text: $description)**
                                .frame(width: geo.size.width,
    																	 height: geo.size.height * 0.65,
    																	 alignment: .center)
                        }
                        
                    }
                }
    1. 모달뷰에 키보드 자판이 생겼을 때 TextEditor 를 가리는 문제가 발생했다.
    2. TextField, DatePicker, TextEditor 를 ScrollView 로 감싸서 스크롤이 되도록하려고 했는데, TextEditor 가 화면에 보이지 않았다.
    3. TextEditor 의 사이즈를 지정해주기 위해 GeometryReader 를 사용해서 상위뷰와 관련한 높이를 지정해줘서 스크롤시 문제가 되지 않게 하였다.
    4. 해결하지 못한 문제는 GeometryReader 를 ScrollView 에 넣었을 때는 높이값이 엄청 작게 나오는 문제가 발생했다.
  2. Foreachforeach의 차이

    Foreach는 식별된 데이터의 기본 컬렉션에서 요구에 따른 뷰를 계산하는 Structure.

    foreach는 시퀀스의 각 요소에 대해 지정된 클로저를 for-in 반복문의 순서로 호출한다.

    수정 전

    스크린샷 2021-11-05 오후 8.33.18.png

    수정 후

    스크린샷 2021-11-05 오후 8.42.07.png

    LongPressed 로 popover 를 띄울 때, enum ProjectStatus 타입의 todo, doing, done 중 filter 를 이용하여 LongPressed 되지 않은 타입들을 뽑아낸 뒤 forEach를 사용하여 moveButton을 호출하려고 하였는데 오류가 발생했다.

    뷰를 그려주는 행위라 뷰를 그릴 때 사용하는 Foreach 를 사용하여 해결하였다.





  3. Cell 클릭 후 내용 수정을 했을 때 수정이 되지 않은 이슈

    struct ModalView: View {
        @EnvironmentObject var todoListViewModel: ProjectListViewModel
        @Binding var isDone: Bool
    		 ...
        let modalViewType: ModalType
        let currentProject: Project? // **#1**
    
    	.....
    }
    
    struct ProjectRowView: View {
        @EnvironmentObject var projectListViewModel: ProjectListViewModel
        @State private var isModalViewPresented: Bool = false
        @State private var isLongPressed: Bool = false
        **var project: Project**
    
        var body: some View {
    
    			....
    		 }.sheet(isPresented: $isModalViewPresented) {
                    ModalView(isDone: $isModalViewPresented,
                              modalViewType: .edit,
                              **currentProject: project) // #2**
                }
    }
    
    --------------------------------------------------------------------------
    // 수정 전 customTrailingButton
    extension ModalView {
    		private var customTrailingButton: some View {
            Button {
                if modalViewType == .add {
                    todoListViewModel.action(
                        .create(project: Project(title: title,
                                                 description: description,
                                                 date: date,
                                                 type: .todo)))
                } else if isEdit && modalViewType == .edit,
                          let currentProject = currentProject {
                    **todoListViewModel.action(
                        .update(project: Project(project: currentProject))) 
    													// #3**
                }
                isDone = false
            } label: {
                Text("Done")
            }
        }
    }
    -----------------------------------------------------------------------
    
    //수정 후 customTrailingButton
    extension ModalView {
    		private var customTrailingButton: some View {
            Button {
                if modalViewType == .add {
                    todoListViewModel.action(
                        .create(project: Project(title: title,
                                                 description: description,
                                                 date: date,
                                                 type: .todo)))
                } else if isEdit && modalViewType == .edit,
                          let currentProject = currentProject {
                    todoListViewModel.action(
                        .update(project: Project(id: currentProject.id,
                                                 title: title,
                                                 description: description,
                                                 date: date,
                                                 type: currentProject.type)))
    															// #4**
                }
                isDone = false
            } label: {
                Text("Done")
            }
        }
    }
    ---------------------------------------------------------------------------
    
    final class ProjectListViewModel: ObservableObject{
        @Published private(set) var projectList: [Project] = []
    
    			func action(_ action: Action) {
            switch action {
    					......
                  case .update(let project):
                projectList.firstIndex { $0.id == project.id }
    							.flatMap { projectList[$0] = project } // #5
            }
  4. ModalView 에서 Model 를 프로퍼티로 가지고 있어서 ModalView 를 호출 할 때 currentProject 를 초기화해야 된다.

  5. ProjectRowView 에서 ModalView 를 호출할 때 var project: Project 를 주입해주고 있기 때문에 클릭한 Cell 의 Project 를 ModalView가 알고 있다.

  6. 수정 전 customTrailingButton 에서는 currentProject 를 viewModel 로 전달해서 값을 수정하려고 했는데 currentProject 값이 모달 화면이 뜰 때의 값이라 입력한 값을 전달해주기 위해 코드를 수정했다.

  7. 수정 후 title, description, date 의 값을 우리가 입력한 값으로 viewModel 에 넘겨주었다. 이때 우리가 원하는 배열의 index 를 찾기 위해 currentProject.id 를 viewModel 에 넘겨주었다.

  8. currentProject.id 로 index 를 찾아서 배열의 원하는 index 의 내용을 수정했다.


  1. CoreData ManagedObject 을 프로젝트의 모델타입으로 사용했었는데 리모트 저장소의 데이터를 디코딩해 올 때마다 로컬 저장소에 여러번 저장되는 문제가 발생하여, 별개의 모델 타입을 생성하여 리모트저장소로부터 데이터를 받아오도록 수정하였다.

    이전 CoreData ManagedObject 을 사용한 코드

    import Foundation
    import CoreData
    
    @objc(Project)
    class Project: NSManagedObject, Codable {
    
        convenience init(id: String = UUID().uuidString, title: String, detail: String, date: Date, type: ProjectStatus) {
            self.init(context: CoreDataStack.shared.context)
            self.id = id
            self.title = title
            self.detail = detail
            self.date = date
            self.type = type
        }
        
        required convenience init(from decoder: Decoder) throws {
            self.init(context: CoreDataStack.shared.context)
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.id = try container.decode(String.self, forKey: .id)
            self.title = try container.decode(String.self, forKey: .title)
            self.detail = try container.decode(String.self, forKey: .detail)
            self.date = try container.decode(Date.self, forKey: .date)
            let status = try container.decode(Int16.self, forKey: .type)
            self.type = ProjectStatus(rawValue: status)!
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(self.id, forKey: .id)
            try container.encode(self.title, forKey: .title)
            try container.encode(self.detail, forKey: .detail)
            try container.encode(self.date, forKey: .date)
            try container.encode(self.type.rawValue, forKey: .type)
        }
        enum CodingKeys: CodingKey {
            case id, title, detail, date, type
        }
    }

    별도의 모델타입을 만든 코드

    import Foundation
    
    struct ProjectPlan: Identifiable, Codable {
        let id: String
        var title: String
        var detail: String
        var date: Date
        var type: ProjectStatus
    
        init(id: String = UUID().uuidString,
             title: String,
             detail: String,
             date: Date,
             type: ProjectStatus) {
            self.id = id
            self.title = title
            self.detail = detail
            self.date = date
            self.type = type
        }
    
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.id = try container.decode(String.self, forKey: .id)
            self.title = try container.decode(String.self, forKey: .title)
            self.detail = try container.decode(String.self, forKey: .detail)
            self.date = try container.decode(Date.self, forKey: .date)
            let status = try container.decode(Int16.self, forKey: .type)
            self.type = ProjectStatus(rawValue: status)!
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(self.id, forKey: .id)
            try container.encode(self.title, forKey: .title)
            try container.encode(self.detail, forKey: .detail)
            try container.encode(self.date, forKey: .date)
            try container.encode(self.type.rawValue, forKey: .type)
        }
    
        enum CodingKeys: CodingKey {
            case id, title, detail, date, type
        }
    }
    
    @objc
    public enum ProjectStatus: Int16, CustomStringConvertible, CaseIterable {
        case todo
        case doing
        case done
        
        public var description: String {
            switch self {
            case .todo:
                return "TODO"
            case .doing:
                return "DOING"
            case .done:
                return "DONE"
            }
        }
    }



IV. 해결하지 못한 문제

  1. 로컬 과 리모트의 동기화 시점를 적용하는것에 대해 많은 시간이 있질 않아, 해보다가 포기하였다.

    시퀀스 다이어 그램을 그려놓았으나, 실제로 해보니깐 이것저것 신경써야 될 게 많아 시간적 여유가 부족하였다.

    Untitled Diagram.drawio.png


  1. List Cell 간격

    Swift UIList View 를 사용했을때 Cell의 간격을 줄 수가 없었다. 이 부분을 해결하지 못했다.

    팀원들과 함께한 프로젝트

    팀원들과 함께한 프로젝트

    • 다른 캠퍼들의 얘기를 들어보면 List는 Cell간의 거리를 조절할 수 없다는 얘기가 많이 나왔다. 이에따라 Cell간의 간격을 주려면 LazyVStack을 써볼 수 있을 것 같다. Stack이기때문에 Spacing을 줄수 있다.

  2. 할일 리스트의 갯수를 세는 부분에 연산과정이 많은 문제.

  • 현재 View에서 ViewModel에게 그릴 Model을 요청하게 된다.

    할일, 하는중, 완료 이 세가지 case를 가진 Enum을 만들어 각각의 List를 뽑아 올 수 있게끔하였다.

    func filteredList(type: ProjectStatus) -> [Project] {
            return projectList.filter { $0.type == type }
        }

    이때 filter로 연산과정을 거쳐 List를 주게 된다.

    또 각각의 case마다 List의 갯수를 알아야 해서 리스트의 갯수를 뽑아올 수 있도록 메서드를 만들었다.

    func todoCount(type: ProjectStatus) -> String {
            return projectList.filter { $0.type == type }.count.description }

    이럴 경우 한 View를 만드는데 filter 과정이 두번 진행된다. 현재 case가 3개니 총 filter 과정이 6번 진행된다. 연산과정이 불필요하게 많아진 느낌인데, 이를 더 줄일 수 있는 방법은 없을까하고 팀원들끼리 고민을 하였다.

    1. 각각의 Case마다 배열을 만들어 List마다 따로 관리한다.

      //각각 할일 리스트에 대한 배열을 생성함.
      var todoList = [Todo]()
      var doingList = [Todo]()
      var doneList = [Todo]()
      
      // CRUD를 진행할때마다 case를 비교를 해줘야함.
      // 효율적이라 생각하지 않음.
      private func createProject(_ project: Todo) {
          switch project.type {
           case .toDo:
             toDoList.append(project)
          case .doing:
             doingList.append(project)
          case .done:
              doneList.append(project)
        }
      }
      • 이럴경우 현재 리스트의 CRUD의 과정이 전부 case마다 비교를 해줘야 하기 때문에 CRUD 코드를 전부 분기처리해야 된다는 점에서 비효율적이라 판단하여 진행하지 않았다.
    2. filteredList(type:) 메서드를 사용할때 Count를 계산한다.

      // ViewModel
      final class ProjectListViewModel: ObservableObject{
          @Published private(set) var projectList: [Project] = []
      		private(set) var count: [ProjectStatus: Int] = [.todo: 0, .doing: 0, .done: 0]
          
      		...
      
          func filteredList(type: ProjectStatus) -> [Project] {
              let list = projectList.filter {
                  $0.type == type
              }
              **count[type] = list.count**
              return list
          }
      
          func todoCount(type: ProjectStatus) -> String {
              guard let count = count[type] else { return "0" }
              **return String(count)**
          }
      
      // View
      // 먼저 Count를 계산
      Text(String(todoListViewModel.todoCount(type)))
        List {
      	//현재 리스트의 Count를 계산하는 로직이 한템포 뒤에 있음.
        ForEach(todoListViewModel.fetchList(type: type)) { todo in
                TodoRowView(todo: todo)
            }
      1. 해당 방법이 좋은 걸로 판단되었으나 뷰에서 해당 메서드를 사용하기 전에 Count를 계산해버리기 때문에, 갯수가 계속 한템포 느리게 반영됨.
        1. @Published 랩퍼를 이용하여 View 를 바로 그리게끔 하였으나, 트러블 슈팅중 Published 프로퍼티가 동시에 변경되면서 뷰를 순환적으로 다시 그리는 이슈(CPU 오버헤딩) 해당문제가 생김.

V. 관련학습 내용

1. Swift UI

Lecture 1: Getting started with SwiftUI

SwiftUI: ToolBar Item & Toolbar Group (2021, Xcode 12, SwiftUI) - iOS Development for Beginners

[Mastering SwiftUI] Managing Selection

SwiftUI : ForEach

Getting Started with Cloud Firestore and SwiftUI

2. Property Wrapper

All SwiftUI property wrappers explained and compared

SwiftUI : @Environment 프로퍼티 래퍼

3. CoreData

iOS) [번역] 코어데이터와 멀티스레딩

Apple Developer Documentation

SQL, NOSQL 개념 및 장단점

Core Data Tutorial - Lesson 2: Set up Core Data in Your Xcode Project (New or Existing)

4. FireBase

Apple 프로젝트에 Firebase 추가 | Firebase Documentation

iOS ) 왕초보를 위한 Firebase사용법!/오류 해결

https://github.com/firebase/firebase-ios-sdk.git

5. MVVM
  • 스위프트 UI를 사용하면 MVVM 디자인 패턴으로 앱을 제작하기 쉽다.

    Model

    • 모델은 UI에 들어갈 데이터들을 담고 있다.
    • 데이터를 구성하기 위해 필요한 Logic도 담고 있다.

    View

    • 단지 화면을 나타내는 View만 구성이 된다. import SwiftUI or UIKit 을 해서 쓰게된다.
    • 어떠한 경우에도 Data에 대한 로직이 존재하면 안된다.
    • StateView에 상태변화에 따라 View를 다르게 그려줄 뿐이지 이게 Data를 건드는 행위가 아니다.
      • 예시로 다양한 View들의 테마가 있을때, State를 활용한다. 해당 State에 따라 View가 바뀌게 된다.
      • View가 어떻게 보여야 되는지는 해당body var 만 알고 있다. 그리고 그렇게 해야된다.
    • 모델에 변경사항이 있을때마다 해당 변경사항에 의존하는 모든 ViewBody var를 요청해야된다. 매우 효율적인 시스템이 있어야한다.그렇지 않으면 UI전체를 지속적으로 그리게 된다.

    ViewModel

    • ViewModel에 바인딩(엮어서) 하여 Model의 변경 사항으로 인해 View가 반응하고 다시 그리도록 하는 것이다.
    • ViewModel 사이에 인터프리터? 역활을 할 수 도 있다.
    • Model에서 RestFul 을 요청하거나 데이터를 SQL 저장하거나 하는 로직은 Model이 복잡해지게 된다. 따라서 ViewModel은 모든 작업을 수행하고 View에게 가공된 데이터를 준다.
    • ViewModelModel의 게이트키퍼 (문지기) 역활도 하며 특히 모델을 변경할 때 모델에 대한 엑세스가 제대로 작동하는지 확인한다.
  • 기본적인 데이터 흐름은 다음과 같다.

    데이터흐름 이미지1

    • ViewModel은 Model의 변경사항을 지속적으로 알아 차린다. 변경 사항을 감지하면 즉시 변경된 사항 을 알리게 된다. (subscriber에게) 특정한 View를 알고 있는게 아니기 때문에 불특정 다수에게 알린다고 볼 수 있겠다. Model의 변경된 사항을 알고 싶은건 결국 subscriber(View) 가 해야 될 일이다. ViewModelModel의 변경사항을 Publishes(게시) 하는걸 구독하는 건 결국 View다. 이렇게 되면 Model에서 View로 가는 데이터의 흐름은 얼추 알게 된다.

    • 자 그러면 View에서 발생하는 유저의 이벤트에 대해 Model이 바뀌어야 된다면 어떻게 해야 될까?

      • 이벤트란 탭, 스와이프, UI탐색등등

      해당 이벤트로 모델이 변경 될 것이다. 이때 ViewModel에 사용자의 이벤트를 처리하는 또 다른 책임이 추가되어 이를 처리하도록 지시한다.

      데이터흐름 이미지2

      • 위의 그림과 같이 흐름의 화살표를 보면 좀 더 이해가 쉽다.

      도대체 어떻게 하는 것이 MVVM 인것이냐? 오늘 결론 내립니다.

6. Repository Pattern

[Design Pattern] Repository패턴이란

ios-project-manager's People

Contributors

hrjy6278 avatar yagom avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.