App Login | IssueList-Open | IssueList-Close | NewIssue |
---|---|---|---|
Search-Filter | FilterButton | IssueDetail | IssueCardView |
---|---|---|---|
LabelList | Label Add&Edit | MilestoneList | Milestone Add&Edit |
---|---|---|---|
- 이슈 생성, 수정 기능 추가
- Interactor, Presenter, Worker 생성
- 수정은 생성 화면 재활용
- Manager과 Label은 스크롤뷰로 구성하여 드래그하여 확인할 수 있도록 구성
- 마일스톤은 이름 값과 open, close를 확인하여 progress 표기
Issue Detail | Dynamic Textview | Filter(WIP) | Milestone 진행율 |
---|---|---|---|
- Issue Content, Comment Randering
- 해당 목록에서 마크다운으로 표시
- MarkdownView로 표시하기에 랜더링 높이가 나중에 출력되는 현상
- 마지막 IndexPath에서 한번 더 레이아웃 변경
- 현재 링크를 갔다와 돌아올 때 다시 레이아웃이 변경되는 문제 해결 중..
- 렌더링이 되는 순간에는 조작할 수 없도록 하기위해 로딩화면 추가
- Indicater를 추가하여 렌더링이 끝날 때 Stop
- 텍스트 뷰를 클릭할 때 키보드에 가려지지 않도록 조정
- Keyboard 관련 Notification을 이용하여 움직이는 시점에 post
- View 자체의 frame y좌표를 위로 이동
- 이후 TextView의 Bottom Constraint를 변경
- (keyboard.height - tabbar.height - 움직인 y좌표 거리 + 기존 Bottom Constraint) 로 변경
- 텍스트를 모두 작성할 경우 다시 기존 bottom Constraint로 변경
- Keyboard 관련 Notification을 이용하여 움직이는 시점에 post
- 열린 이슈, 닫힌 이슈는 switch를 통해 load 함
- Search bar 선택 시 추가적으로 filter 할 수 있는 segment control이 나타남 (실제 Github application 벤치마킹)
- 추후에 Context Menu를 사용하여 상세 filter 구현 예정
- closedIssue / (openedIssue + closedIssue) * 100
- 정수형으로 표기되므로 2번의 형변환 거쳐야함
Issue List Search | MarkDown View | Date-Time Picker |
---|---|---|
Github OAuth Response Data Example |
---|
- OctoKit의 authorize를 통해 apiEndpoint와 accessToken 값을 구함
- 두 값과 URLSession을 이용해 user의 정보를 response 로 받아 옴
- 알고 있는 정보들을 server에게 전송하여 JWT Token을 발급 받을 예정
- 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() } }
-
제약 조건 추가
- 기존에 주어지지 않았던 제약조건 추가
- 입력할 수 있는 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 추가
-
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
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
-
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 }
-
하단 메뉴를 잡아서 끌어올려 이용할 수 있는 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로 설정할 예정
Label Add, Color Picker | Label Delete |
---|---|
Milestone Add | Milestone Delete |
---|---|
- Label List, Milestone List 적용
- 기존 UICollectionView에서 UIControllerView로 변경
- 변경 후 Modern Collection Layout 적용
-
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
-
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()
}
- LabelListViewController에서 UISwipeActionsConfiguration를 통해 delete action 발생
- LabelListInteractor에서 labelWorker를 통해 delete request를 함
- LabelListWorker에서 dataManger에게 delete request를 함
- DataManager는 NetworkService의 deleteData를 통해 delte 요청을 한 후 response를 받아 옴
- 받아온 response는 completion handler를 통해 interactor에게 전해짐
- interactor가 받은 response를 presenter에게 전달.
- presenter는 받은 response에 맞게 alert를 띄울 때 쓸 ViewModel을 만들고 viewcontroller에게 전함
- LabelListViewController는 상황에 알맞은 alert를 띄워줌
- 추후 Label과 Milestone에서 작업한 NetworkService 내용을 조율할 예정
Text Input → Color | Random Color | Color Picker |
---|---|---|
- Reference : WWDC 2020 Advances in UICollectionView LINK
- UICollectionLayoutListConfiguration
- trailingSwipeAction 기능을 이용하여 액션 추가
- clese, delete Action 추가
- 이후 handler을 추가하여 기능 구현할 예정
- 기존에 있는 layout 변경
- CompositionalLayout.list에 위의 configure 추가
- 사용하고 있던 CollectionView의 collectionViewLayout를 변경
- delegate 채택
- trailingSwipeAction 기능을 이용하여 액션 추가
- UICellAccessory 중 multiselect를 통해 다중선택 화면 구현
- 기존의 cell을 옆으로 변경하기 위해서 cell의 separatorLayoutGuide에게 constraint를 줌
- isEditing property를 오버라이딩하여 현재 모드를 관찰하여(didSet) 적절하게 화면 구성을 바꿈
- Title, Navigation bar button item, Button
Swipe Cell | Multiselect Accessory |
---|---|
- 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이 재사용 될 때 문제가 생기지 않는지 확인해보아야 합니다.
- Issue Controller에서 Filter Controller로 Data 전달을 위해 사용
- 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) } } }
- 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! } }
- 받는 Controller에도 해당 Router 형식에 맞춰 제작
- 현재 어플은 API를 통해 계속해서 목록을 서버로부터 받아오는 일을 하므로 NetworkService class를 만들었습니다.
- dataTask를 통해 데이터를 받아옵니다.
- 추후에 사용자 프로필 사진처럼 용량이 큰 파일들이 오간다면 caching을 구현할 예정입니다.
- 각 Scene의 DataManager들이 NetworkService를 사용하여 필요한 데이터를 받아와 decoding을 진행합니다.
- Decoding 이후에는 알맞은 모델에 저장하여 collection view / table viewd에 표현되도록 했습니다.
이슈 목록 화면 | Interactive Bottom Card View |
---|---|
-
계층 구조화
-
흐름
- 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) } }
- IssueListInteractor는 worker에게 Issue목록을 request
- Worker는 DataManager를 통해 데이터를 받아온 후 decode한 후 interactor [Issue] 의 형태로 전달 (서버와 통신할 시 worker와 DataManager 로직은 변경 될 예정)
- 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) }) } }
- 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) } }
- IssueListViewController는 받은 ViewModel들로
dataReload()
-
VIP 참고 링크 Clean-Swift/CleanStore
- Cell 내의 UI 제약조건 및 레이아웃을 수정하였습니다.
- Issue 내 UI의 모든 Top Vertical를 증가시켰습니다.
- 셀 마다 아래 Border를 추가하였습니다.
- 설명 레이블 조건을 추가하였습니다.
- Linebreak를 이용해 2줄만 표시해주도록 구현
- milestone border를 추가하였고 문자 길이에 따라 Label Width를 조율하였습니다.
- 이전에 작업했던 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 |
---|---|
-
upstream/feat-issue branch를 merge 도중 conflict 발생
- 같은 파일을 건드리는 것 외에도 서로 다른 폴더 내의 다른 파일들을 넣으면서 생기게 되었습니다.
- xcodeproj 내의 pbxproj 충돌이 생겨 해당 프로젝트를 열 수 없었습니다.
-
'cannot be opened because the project file cannot be parsed' 메세지가 나타난다면 다음과 같이 해결하시면 됩니다.
- .xcodeproj 파일 우클릭 >
Show Packages Contents
> text editor로 파일 열기 - <<<<HEAD === 와 같이 conflict 난 부분을 해결해주시면 됩니다.
- .xcodeproj 파일 우클릭 >
문제 상황 | Conflict 발생 예시 |
---|---|
- 관련 ISSUE : -
- Day2 초기 작업 환경을 구축할 당시 gitignore 파일을 잘 확인하지 못해 xcuserstate 등의 바이너리 파일들이 계속해서 충돌을 발생시켰습니다.
- 이에 branch 들을 재구성하는 등 추후 협업 시 충돌을 최소화하기 위해 꼼꼼히 살피는 작업을 한번 더 진행했습니다.
- 관련 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 |
---|---|
- Web-server 팀과 데이터 구조를 논의하여 json Data 형식 제작
- milestone은 join을 통해 name 값 출력
- 현재 Label 데이터는 의견 조율중
Json Mock Data |
---|