Skip to content

Latest commit

 

History

History
592 lines (450 loc) · 24.8 KB

sprint-ios.md

File metadata and controls

592 lines (450 loc) · 24.8 KB

iOS Readme

Sprint #3 - Day4

최종결과화면

App Login IssueList-Open IssueList-Close NewIssue
Search-Filter FilterButton IssueDetail IssueCardView
LabelList Label Add&Edit MilestoneList Milestone Add&Edit

Sprint #3 - Day3

New Issue Function

  • 이슈 생성, 수정 기능 추가
    • Interactor, Presenter, Worker 생성
    • 수정은 생성 화면 재활용

CardView 화면 구성

  • Manager과 Label은 스크롤뷰로 구성하여 드래그하여 확인할 수 있도록 구성
  • 마일스톤은 이름 값과 open, close를 확인하여 progress 표기

Sprint #3 - Day2

결과화면

Issue Detail Dynamic Textview Filter(WIP) Milestone 진행율

Issue Detail Controller

  • Issue Content, Comment Randering
    • 해당 목록에서 마크다운으로 표시
    • MarkdownView로 표시하기에 랜더링 높이가 나중에 출력되는 현상
      • 마지막 IndexPath에서 한번 더 레이아웃 변경
      • 현재 링크를 갔다와 돌아올 때 다시 레이아웃이 변경되는 문제 해결 중..
    • 렌더링이 되는 순간에는 조작할 수 없도록 하기위해 로딩화면 추가
      • Indicater를 추가하여 렌더링이 끝날 때 Stop

Dynamic TextView

  • 텍스트 뷰를 클릭할 때 키보드에 가려지지 않도록 조정
    • Keyboard 관련 Notification을 이용하여 움직이는 시점에 post
      • View 자체의 frame y좌표를 위로 이동
      • 이후 TextView의 Bottom Constraint를 변경
        • (keyboard.height - tabbar.height - 움직인 y좌표 거리 + 기존 Bottom Constraint) 로 변경
        • 텍스트를 모두 작성할 경우 다시 기존 bottom Constraint로 변경

Filter(WIP)

  • 열린 이슈, 닫힌 이슈는 switch를 통해 load 함
  • Search bar 선택 시 추가적으로 filter 할 수 있는 segment control이 나타남 (실제 Github application 벤치마킹)
  • 추후에 Context Menu를 사용하여 상세 filter 구현 예정

Milestone

  • closedIssue / (openedIssue + closedIssue) * 100
  • 정수형으로 표기되므로 2번의 형변환 거쳐야함

Sprint #3 - Day1

결과화면

Issue List Search MarkDown View Date-Time Picker
Github OAuth Response Data Example

Github SignIn

  • OctoKit의 authorize를 통해 apiEndpoint와 accessToken 값을 구함
  • 두 값과 URLSession을 이용해 user의 정보를 response 로 받아 옴
  • 알고 있는 정보들을 server에게 전송하여 JWT Token을 발급 받을 예정

Search

  • search bar가 비었는지, 작성 중인지 판단하여 search 기능을 쓰고 있으면 검색 진행
  • search controller를 사용
  • UISearchResultUpdating 구현
    extension IssueListViewController: UISearchResultsUpdating {
    
        func updateSearchResults(for searchController: UISearchController) {
            let searchBar = searchController.searchBar
            filterContentForSearchText(searchBar.text!)
        }
    
        func filterContentForSearchText(_ searchText: String) {
            filteredIssues = displayedIssues.filter({ (issue: IssueViewModel) -> Bool in
                return issue.title.lowercased().contains(searchText.lowercased()) || issue.content.lowercased().contains(searchText.lowercased())
            })
            issueListCollectionView.reloadData()
        }
    
    }

IssueEnroll Controller

  • 제약 조건 추가

    • 기존에 주어지지 않았던 제약조건 추가
    • 입력할 수 있는 TextView 크기는 협의 예정
  • MarkDownView Pod

    • 입력한 결과를 Preview Segment에서 MarkDown 확인 기능 추가
    • 링크를 들어갈 수 있도록 SafariServices import
    markdownView.onTouchLink = { [weak self] request in
        guard let url = request.url else { return false }
    
        if url.scheme == "file" {
            return true
        } else if url.scheme == "https" {
        let safari = SFSafariViewController(url: url)
        self?.present(safari, animated: true, completion: nil)
            return false
        } else {
            return false
        }
    }
                
    markdownView.load(markdown: textDescription.text, enableImage: true)
  • TextView Field

    • UITextViewDelegate를 채택하여 placeHolder 추가

PopUpView

  • Color Error 수정

    • 크기가 6이 아닐 경우 Alert
    • 000000~FFFFFF값이 아닐 경우 Alert
  • Date-Time Picker

    • 정확한 시간을 보내줄 수 있도록 Picker 이용
    • 배경색을 바꿔주기 위해 subview에 들어가 변경
    datePicker.subviews[0].subviews[0].backgroundColor = .white
    datePicker.subviews[0].subviews[1].backgroundColor = .white    

Sprint #2 - Day4

결과화면

Issue close 및 다중 선택 화면 Dragable Card View Label 수정

다중 선택 화면

  • 하나의 ViewController에 목록을 나열하는 것 뿐만 아니라 선택 화면 및 search 까지 구현해야 하니 ViewController가 너무 커짐
  • 다중 선택 화면으로 넘어갈 시 tab bar가 사라지는 것 처럼 구현
    • tab bar의 높이를 통해 조정 : view.frame.height + tabbarHeight로 y 좌표를 바꾸면 결국 view.frame 화면의 끝 쪽
// 
let tabbarHeight = self.tabBarController!.tabBar.frame.height
self.tabBarController!.tabBar.frame.origin.y = view.frame.height + tabbarHeight

Milestone Post, Put, Delete Response 변경

  • Completion 이 필요없다 생각하여 처음에 배제

    • 해당 네트워크 통신 성공, 실패 판단을 해야하므로 구조 변경
    • 성공할 때의 형식에 맞춰 response 결과 값 받기
    {
      "status": "success",
      "data": {
        "fieldCount": 0,
        "affectedRows": 1,
        "insertId": 0,
        "info": "Rows matched: 1  Changed: 1  Warnings: 0",
        "serverStatus": 2,
        "warningStatus": 0,
        "changedRows": 1
    }

Card View 제작

  • 하단 메뉴를 잡아서 끌어올려 이용할 수 있는 View 제작

    • CardView Xib 화면

    • 열렸을 때와 닫혔을 때를 비교하여 (expanded, collapsed) 기능 분리

      • handle Tap, Pan 기능으로 크게 나눔
      • Tap : 끝까지 열렸을 때와 닫혔을 때를 비교하여 정해준 height 만큼 이동
      @objc func handleCardTap(recognzier: UITapGestureRecognizer) {
          switch recognzier.state {
          case .ended:
              animateTransitionIfNeeded(state: nextState, duration: 0.9)
          default:
              break
          }
      }
      • Pan : 시작지점과 끝지점, 변경지점을 나눠 translation을 변경
      @objc func handleCardPan(recognzier: UIPanGestureRecognizer) {
          switch recognzier.state {
          case .began:
              startInteractiveTransition(state: nextState, duration: 0.9)
          case .changed:
              let translation = recognzier.translation(in: self.cardViewController.handleArea)
              var fractionComplete = translation.y / cardHeight
              fractionComplete = cardVisible ? fractionComplete : -fractionComplete
              updateInteractiveTransition(fractionCompleted: fractionComplete)
          case .ended:
              continueInteractiveTransition()
          default:
              break
          }
      }
  • 필요한 레이아웃 추가

    • 기능을 위한 버튼 및 Progress 추가
    • 현재 레이아웃만 설정
  • 해당 이슈에 대한 Data Get

    • Request에 tag값을 추가한 후, 해당 url에 Get 요청
    • 요청하여 받은 데이터 중, 처음 content 값은 indexpath[0, 0]에 넣어 출력
    • 현재 comment get을 요청하지 않아 cell 개수가 1개
      • API 수정 및 comment 요청을 추가로 할 경우 cell 갯수는 1 + comment.count로 설정할 예정

Sprint #2 - Day3

결과화면

Label Add, Color Picker Label Delete
Milestone Add Milestone Delete

Modern Collection List View

  • Label List, Milestone List 적용
    • 기존 UICollectionView에서 UIControllerView로 변경
    • 변경 후 Modern Collection Layout 적용

VIP 패턴을 적용한 Add, Delete

Add (ex. Milestone)

  • Encodable 채택된 Struct 구조체 제작

    struct MilestoneFormField: Encodable {
        var title: String
        var dueDate: String?
        var content: String?
    }
  • reponse 받는 Data 형식은 Success 관련 Status로 가정하여 Complection 제거

    • Interactor에서 바로 Worker한테 Request 전송
  • NetworkService 에서 Json Data 제작

    • request method = POST 지정

    • forHTTPHeaderField 지정

      • application/json을 Accept, Content-Type 지정
    • 데이터를 Json Encoder 통해 인코딩 후 Post 전송

    let responseData = request.milestone
    let jsonData = try? JSONEncoder().encode(responseData)
    
    guard let requestURL = URL(string: url) else {
        return // completion으로 경우 넘겨 주어야 함
    }
        
    var request = URLRequest(url: requestURL)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Accept")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
    request.httpBody = jsonData

Delete (ex. Label)

 func deleteData(url: String, completion: @escaping FetchResult) {
     guard let requestURL = URL(string: url) else {
         return // completion으로 경우 넘겨 주어야 함
     }
     
     var request = URLRequest(url: requestURL)
     request.httpMethod = "DELETE"
     
     defaultSession.dataTask(with: request) { (data, response, error) in
         guard let data = data,
               let response = response as? HTTPURLResponse,
               response.statusCode == 200 else {
             return // completion으로 경우 넘겨 주어야 함
         }
         DispatchQueue.main.async {
             completion(data)
         }
         
     }.resume()
 }
  1. LabelListViewController에서 UISwipeActionsConfiguration를 통해 delete action 발생
  2. LabelListInteractor에서 labelWorker를 통해 delete request를 함
  3. LabelListWorker에서 dataManger에게 delete request를 함
  4. DataManager는 NetworkService의 deleteData를 통해 delte 요청을 한 후 response를 받아 옴
  5. 받아온 response는 completion handler를 통해 interactor에게 전해짐
  6. interactor가 받은 response를 presenter에게 전달.
  7. presenter는 받은 response에 맞게 alert를 띄울 때 쓸 ViewModel을 만들고 viewcontroller에게 전함
  8. LabelListViewController는 상황에 알맞은 alert를 띄워줌
  • 추후 Label과 Milestone에서 작업한 NetworkService 내용을 조율할 예정

ColorPicker

Text Input → Color Random Color Color Picker

Sprint #2 - Day2

Modern Collection List View

  • Reference : WWDC 2020 Advances in UICollectionView LINK

Swipe

  • UICollectionLayoutListConfiguration
    • trailingSwipeAction 기능을 이용하여 액션 추가
      • clese, delete Action 추가
      • 이후 handler을 추가하여 기능 구현할 예정
    • 기존에 있는 layout 변경
      • CompositionalLayout.list에 위의 configure 추가
      • 사용하고 있던 CollectionView의 collectionViewLayout를 변경
      • delegate 채택

Accessory Items(Multiselect)

  • UICellAccessory 중 multiselect를 통해 다중선택 화면 구현
  • 기존의 cell을 옆으로 변경하기 위해서 cell의 separatorLayoutGuide에게 constraint를 줌
  • isEditing property를 오버라이딩하여 현재 모드를 관찰하여(didSet) 적절하게 화면 구성을 바꿈
    • Title, Navigation bar button item, Button

결과화면

Swipe Cell Multiselect Accessory

Sprint #2 - Day1

이슈 목록 화면 완성

  • UIColor를 extension하여 hex string 값으로 데이터를 받아오면 label의 배경 색이 바뀝니다.
    • 또한, (redValue * 0.299 + greenValue * 0.587 + blueValue * 0.114) / 255 값이 0.5보다 작을 시에는 label의 글자 색상이 흰색, 0.5보다 크면 검은색으로 표기 됩니다.
  • Floating button은 custom class를 통해 제작했습니다.
  • 추후에 cell이 재사용 될 때 문제가 생기지 않는지 확인해보아야 합니다.

Router를 통한 Data 전달

  • Issue Controller에서 Filter Controller로 Data 전달을 위해 사용
    1. ViewController에서 프로토콜 채택
    // IssueListViewController
    var router: (NSObjectProtocol & IssueListRoutingLogic & IssueListDataPassing)?
    
    ...
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let scene = segue.identifier {
            let selector = NSSelectorFromString("routeTo\(scene)WithSegue:")
            if let router = router, router.responds(to: selector) {
                router.perform(selector, with: segue)
            }
        }
    }
    1. DestinationVC를 설정하여 Data 전달
    // IssueListRouter
    // IssueListViewController
    @objc protocol IssueListRoutingLogic {
        func routeTo\(scene)(segue: UIStoryboardSegue?)
    }
    
    protocol IssueListDataPassing {
        var Data: Data? { get }
    }
    
    func routeTo\(scene)(segue: UIStoryboardSegue?) {
        if let segue = segue {
            let destinationVC = segue.destination as! ViewController
            destinationVC.router?.Data? = Data!
        } else {
            let destinationVC = viewController?.storyboard?.instantiateViewController(withIdentifier: "") as! ViewController
            destinationVC.router?.Data? = Data!
        }
    }
    1. 받는 Controller에도 해당 Router 형식에 맞춰 제작

URLSession으로 데이터 받아오기

  • 현재 어플은 API를 통해 계속해서 목록을 서버로부터 받아오는 일을 하므로 NetworkService class를 만들었습니다.
    • dataTask를 통해 데이터를 받아옵니다.
    • 추후에 사용자 프로필 사진처럼 용량이 큰 파일들이 오간다면 caching을 구현할 예정입니다.
  • 각 Scene의 DataManager들이 NetworkService를 사용하여 필요한 데이터를 받아와 decoding을 진행합니다.
  • Decoding 이후에는 알맞은 모델에 저장하여 collection view / table viewd에 표현되도록 했습니다.

결과화면

이슈 목록 화면 Interactive Bottom Card View

Sprint #1 - Day4

VIP 패턴을 적용한 IssueListViewController

  • 계층 구조화

    • Scene 별로 계층을 구조화 했으며 Scene들을 추가하여 구현할 때마다 추가할 예정입니다.
    • DataManager 역할의 확장성을 고려해 보았을 때 Services 폴더는 Scenes 밖으로 뺄 수 있습니다.
  • 흐름

    1. IssueListViewController 는 interactor에게 Issue목록을 request
    // IssueListViewController
    class IssueListViewController: UIViewController {
       var interactor: IssueListBusinessLogic?
       ...
       override func viewDidAppear(_ animated: Bool) {
          super.viewDidAppear(animated)
          fetchIssues()
       }
       ...
       func fetchIssues() {
            let request = ListIssues.FetchLists.Request()
            interactor?.fetchIssues(request: request)
        }
    }
    1. IssueListInteractor는 worker에게 Issue목록을 request
    2. Worker는 DataManager를 통해 데이터를 받아온 후 decode한 후 interactor [Issue] 의 형태로 전달 (서버와 통신할 시 worker와 DataManager 로직은 변경 될 예정)
    3. Interactor는 presenter에게 fetch 해온 [Issue]를 전달
    // IssueListViewController
    class IssueListInteractor: IssueListBusinessLogic {
       var presenter: IssueListPresentationLogic?
       var issueWorker = IssueListWorker(dataManager: IssueDataManager())
       ...
       func fetchIssues(request: ListIssues.FetchLists.Request) {
          issueWorker.fetchIssues(completion: { (issues) -> Void in
             self.issues = issues
             let response = ListIssues.FetchLists.Response(issues: issues)
             self.presenter?.presentFetchedIssues(response: response)
          })
        }
    }
    1. Presenter는 interactor로 부터 받아온 데이터를 ViewModel로 변경하여 viewController에게 전달
    // IssueListViewController
    class IssueListPresenter: IssueListPresentationLogic {
      weak var viewController: IssueListDisplayLogic?
      ...
      func presentFetchedIssues(response: ListIssues.FetchLists.Response) {
         var displayedIssues: [ListIssues.FetchLists.ViewModel.DisplayedIssue] = []
         for issue in response.issues {
            // [ViewModel] 로 formatting 하는 작업
         }
         viewController?.displayFetchedOrders(viewModel: viewModel)
       }
    }
    1. IssueListViewController는 받은 ViewModel들로 dataReload()
  • VIP 참고 링크 Clean-Swift/CleanStore

IssueList View 작업

  • Cell 내의 UI 제약조건 및 레이아웃을 수정하였습니다.
    • Issue 내 UI의 모든 Top Vertical를 증가시켰습니다.
    • 셀 마다 아래 Border를 추가하였습니다.
    • 설명 레이블 조건을 추가하였습니다.
      • Linebreak를 이용해 2줄만 표시해주도록 구현
    • milestone border를 추가하였고 문자 길이에 따라 Label Width를 조율하였습니다.

Bottom Popup View 제작

  • 이전에 작업했던 Popup View를 바탕으로 아래에서 불러오는 Popup View 제작
    • Filter 내의 세부 메뉴 기능에 이용할 예정입니다.
    • 현재 임의의 값을 넣어 동일한 Controller를 불러오도록 제작
let popupController = STPopupController(rootViewController: pushVC!)
popupController.style = .bottomSheet // bottom Animation
popupController.present(in: self) // 현재 자신의 controller을 불러옴

결과화면

IssueList CollectionView Bottom Popup View

Git branch conflict

  • upstream/feat-issue branch를 merge 도중 conflict 발생

    • 같은 파일을 건드리는 것 외에도 서로 다른 폴더 내의 다른 파일들을 넣으면서 생기게 되었습니다.
    • xcodeproj 내의 pbxproj 충돌이 생겨 해당 프로젝트를 열 수 없었습니다.
  • 'cannot be opened because the project file cannot be parsed' 메세지가 나타난다면 다음과 같이 해결하시면 됩니다.

    1. .xcodeproj 파일 우클릭 > Show Packages Contents > text editor로 파일 열기
    2. <<<<HEAD === 와 같이 conflict 난 부분을 해결해주시면 됩니다.
문제 상황 Conflict 발생 예시

Sprint #1 - Day3

[iOS] Git branch 재설정

  • 관련 ISSUE : -
  • Day2 초기 작업 환경을 구축할 당시 gitignore 파일을 잘 확인하지 못해 xcuserstate 등의 바이너리 파일들이 계속해서 충돌을 발생시켰습니다.
  • 이에 branch 들을 재구성하는 등 추후 협업 시 충돌을 최소화하기 위해 꼼꼼히 살피는 작업을 한번 더 진행했습니다.

[iOS] 스토리보드 작업 (WIP)

  • 관련 ISSUE : #9 #14 #55 #61 #65 #67
  • Pair programming으로 scene 별로 번갈아가며 진행
  • 화면 구성을 우선적으로 하며 각 화면에 필요한 component들을 정리해보았습니다. (constraint 설정은 아직 안됨)
  • 정리하는 과정에서 일부 Scene을 추려내는 작업을 하였습니다. (Filter는 논의가 덜 된 상태)
    • Scene : IssueList, LabelList, MilestoneList, ShowIssue, CreateIssue, Createlabel, CreateMilestone
  • Label과 Milestone 생성 및 수정 시 사용 될 controller는 STPopupController를 사용합니다.
Sign In Issue
Label & Milestone STPopupController

[iOS] Mock Data 생성

  • Web-server 팀과 데이터 구조를 논의하여 json Data 형식 제작
    • milestone은 join을 통해 name 값 출력
    • 현재 Label 데이터는 의견 조율중
Json Mock Data