iOS 파트 : YB 류세화, YB 최인정
프로젝트 기간 : 2020.6.27 ~ 2020.7.18
| View | Git Branch | 담당자 |
|---|---|---|
| CalendarView | calendar_branch | 세화 |
| AlertView | alert_branch | 세화 |
| JournalView | journal_branch | 인정 |
| MyPageView | mypage_branch | 인정 |
| SplashView | splash_branch | 인정 |
| LoginSignUpView | login_branch | 세화 |
- 워크플로우 : master(최종본) - dev(통합관리) - 각 기능별 브랜치(담당자가 관리)
- readme 작성에 대해서
- 결론 : 틈틈히 최대한 자세히 적기 (나중에 정리)
- 미루지 말고 작업할 때마다 기록해놓자! (기록하는 습관 잊지말긩!)
- Git commit message 형식 통일
- Message는 3가지 라벨만 사용
- Add : 아예 새로운 파일(swift, storyboard, VC 파일 등) 추가
- Update : 기존 파일에 기능, UI요소 추가
- Fix : 기존 기능 수정이나 에러 해결 등
- Format : 라벨 + commit comment
- Message는 3가지 라벨만 사용
- 우리의 Git Workflow 최종 정리 노션 링크 🔥
- view controller : Upper Camel Case 탭 이름 + VC
- eg.
CalendarVC,NotesVC - UI 요소 네이밍 : Upper Camel Case UI요소 이름 + View Cell
- eg.
CalendarCollectionViewCell - Xib 파일은 ViewCell 파일이랑 똑같이 네이밍
- eg.
- 변수명, 상수명 : Lower Camel Case
// 변수명 vardropDownButton:UIButton! // 상수명 letheaderView=JournalDateHeaderView(frame:CGRect(x:0, y:0, width:375, height:16))
- 함수명: Lower Camel Case
- Action 함수 네이밍: '주어+동사+목적어'
func backButtonDidTap(){ // ... }
- Extension 이름 : Extensions+확장클래스
- eg.
Extensions+String
- eg.
- Optional은 gaurd let 으로 선언하기
전체 실행 화면 및 새로 알게된 것들과 어려웠던 기능들 소개
Lottie 라이브러리에 animationView()를 사용하여 frame 크기와 aniamteion JSON파일을 지정해줄 수 있다. 반복할 횟수를 한번으로 지정하여 재생하도록 setup함수를 작성하였다.
letanimationView=AnimationView()func setup(){ //lottie 버전
animationView.frame = view.bounds //animationView 크기가 view와 같게
animationView.animation =Animation.named("data2") //어떤 jsonv파일을 쓸지
animationView.contentMode =.scaleAspectFill //화면에 적합하게
animationView.loopMode =.playOnce //view안에 Subview로 넣어준다,
view.addSubview(animationView)
animationView.play() //재생
}스플래시화면에서 지정된 시간(초)이 지난 후 자동으로 화면이 전환되도록 구현하였다. asyncAfter 메소드에 seconds 파라미터에 시간 초를 지정해주고 함수 블록 안에 전환될 뷰를 정의하면 해당 시간이 지난 후 자동으로 화면전환이 이루어진다.
DispatchQueue.main.asyncAfter(deadline:.now()+.seconds(4), execute:{ //Code }오른쪽 / 왼쪽으로 스와이프 제스쳐에 따라 호출되는 함수에 이미지 배열에 있는 온보딩 이미지 4개의 index 값을 계산하여 각 index에 해당하는 이미지 이름을 이미지 뷰에 적용해주었고, 추가로 이미지 전환 효과를 주었다.
@objcfunc respondToSwipeGesture(_ gesture:UIGestureRecognizer){
if let swipeGesture = gesture as?UISwipeGestureRecognizer
if swipeGesture.direction ==UISwipeGestureRecognizer.Direction.left
//imege 배열 인덱스 조정하여 이미지 전환 & 알파값 조정 애니메이션 코드
else if swipeGesture.direction ==UISwipeGestureRecognizer.Direction.right
//imege 배열 인덱스 조정하여 이미지 전환 & 알파값 조정 애니메이션 코드 }아이폰 8 처럼 화면이 작거나 텍스트 필드가 뷰의 밑에 위치해있을 경우 키보드가 열렸을 때 텍스트 필드가 가려진다. 그럴때 필요한 기능 두가지인 1. 뷰의 아무곳이나 터치했을 때 키보드 닫히기 2. 텍스트필드 위로 밀리기 기능들을 로그인, 회원가입 화면에 추가했다!
// 탭했을 때 키보드 action
func initGestureRecognizer(){ //
lettextFieldTap=UITapGestureRecognizer(target:self, action: #selector(handleTapTextField(_:)))
textFieldTap.delegate =selfself.view.addGestureRecognizer(textFieldTap)}
// 다른 위치 탭했을 때 키보드 없어지기
@objcfunc handleTapTextField(_ sender:UITapGestureRecognizer){ //
self.emailTextField.resignFirstResponder()self.passWordTextField.resignFirstResponder()}
// 키보드가 생길 떄 텍스트 필드 위로 밀기
@objcfunc keyboardWillShow(_ notification:NSNotification){guardlet duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey]as?Doubleelse{return}guardlet curve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey]as?UIntelse{return}guardlet keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey]as?NSValueelse{return}letkeyboardHeight:CGFloat // 키보드의 높이
if #available(iOS 11.0,*){
keyboardHeight = keyboardFrame.cgRectValue.height -self.view.safeAreaInsets.bottom
}else{
keyboardHeight = keyboardFrame.cgRectValue.height
}UIView.animate(withDuration: duration, delay:0.0, options:.init(rawValue: curve), animations:{self.imageView.alpha =0 // 이미지뷰 숨기기
self.imageToTextHeightConstraint.constant =0 // constraint 조정
self.bottomViewConstraint.constant =+keyboardHeight/2+100 // constraint 조정
})self.view.layoutIfNeeded()}CalendarCollectionView (달력 뷰)와 TutorCollectionView (하단의 일정 뷰)에 동시에 반영되어야 하는 기능들이 많아 로직을 짜는 것이 매우 어려웠다..! 하단의 기능들이 캘린더 탭에 들어가야 하는 주요 기능들이다.
- 캘린더셋업: 이번 달 숫자는 검정색으로 표시, 이전 달, 다음달 숫자는 회색으로 표시
- 뷰 처음 로드시 캘린더의 날짜에 일정이 존재하면 색깔 점으로 표시
- 뷰 처음 로드시 캘린더에 디폴트로 오늘 날짜 선택
- 캘린더의 날짜 선택시 밑의 뷰에 해당 날짜의 일정 표시
- 상단의 토글(수업일정)에 따라 캘린더뷰와 상세 일정이 전환되어야 함
들어오는 전체 수업 데이터 classList2를 CalendarData라는 구조체에 저장해주었다. classList2[i]의 날짜 스트링 값을 각각 월, 일로 파싱해주고 캘린더 셀의 일자데이터와 전체 캘린더의 현재 월값과 일치할 때 classDateList라는 새 리스트에 저장해준 후 해당 리스트를 TutorCollectionView의 cellForItem에 사용해주었다.
// CalendarCollectionView
forindexin0..<classList2.count {print(classList2[index].classDate)letdateMonthInt= currentMonthIndex +1letdate2=Int(date)letdayMove=String(format:"%02d", arguments:[dateMonthInt])letdayMove2=String(format:"%02d", date2 as!CVarArg)ifclassList2[index].classDate =="\(currentYear)-\(dayMove)-\(dayMove2)"{
classDateList.append(classList2[index])
tutorCollectionView.reloadData()}}
//TutorCollectionView
tutorInfoCell.infoView.frame.size.width = tutorInfoCell.frame.size.width/2
tutorInfoCell.set(classDateList[indexPath.row])foriin0..<self.classDateList.count {lethourTimes="\(self.classDateList[i].times)회차, \(self.classDateList[i].hour)시간"
tutorInfoCell.classHourLabel.text = hourTimes}
return tutorInfoCell// CalendarCollectionView
if classDateMonthZeros == dayMove && classDateDay == todaysDate {letimageName=classList2[i].color
if calendarCell.image1.image ==UIImage(named:""){
calendarCell.image1.image =UIImage(named: imageName)}elseif calendarCell.image2.image ==UIImage(named:""){
calendarCell.image2.image =UIImage(named: imageName)}else{
calendarCell.image3.image =UIImage(named: imageName)}}서버에서 해당 사용자가 연결되어있는 수업 리스트를 받아와 dropDown.DataSource 리스트에 저장해주었다. 해당 스트링 값을 선택할 시 스트링값과 수업 이름이 일치하는 데이터를 classListToggle에 append해주고 classList2에 classListToggle를 복사해 CalendarCollectionView와 TutorCollectionView에 뿌려줬다.
// 드롭박스 목록 내역
dropDownButton.addTarget(self, action: #selector(dropDownToggleButton), for:.touchUpInside)self.dateCollectionView.reloadData()self.tutorCollectionView.reloadData()
// 드롭박스 수업 제목 선택할 때 캘린더 컬렉션뷰, 튜터 컬렉션뷰 데이터 바꿔주기
dropDown?.selectionAction ={[unowned self](index:Int, item:String)inself.dropDownLabelButton.setTitle(item, for:.normal)self.classListToggle.removeAll()self.tutorCollectionView.reloadData()
// 전체 선택시
ifself.dropDownLabelButton.title(for:.normal)=="전체"{self.classList2 =self.classList2Copy
print(self.classList2)self.dateCollectionView.reloadData()self.tutorCollectionView.reloadData()}else{
// 수업 리스트 선택시
self.classList2 =self.classList2Copy
foriin0..<self.classList2.count {ifself.dropDownLabelButton.title(for:.normal)==self.classList2[i].lectureName {self.classListToggle.append(self.classList2[i])}print("새 리스트",self.classListToggle)}self.classList2.removeAll()self.classList2 =self.classListToggle
self.dateCollectionView.reloadData()self.tutorCollectionView.reloadData()}}CollectionView, TableView를 사용할 때 해당 요소의 뷰에 내가 한 수정이 반영되지 않는 경우가 있다. 이런 경우에는 꼭 그 함수가 실행되는 곳에서 collectionView.reloadData()를 실행시켜줘야 수정사항이 반영된다!
밑의 코드에서도 classList2에 append를 해줘도 컬렉션뷰들에 리스트가 업데이트 되지 않았는데 for 문이 끝난 후 reloadData를 해주었더니 성공적으로 반영됐다!
// 서버 통신 : 캘린더 전체 데이터 가져오기
func getClassList(){ClassInfoService.classInfoServiceShared.getAllClassInfo(){ networkResult inswitch networkResult {case.success(let resultData):print("successssss")
//guard let data = resultData as? [CalendarData] else { return }
guardlet data = resultData as?[CalendarData]else{returnprint(Error.self)}print("try")forindexin0..<data.count {letitem=CalendarData(classId:data[index].classId, lectureName:data[index].lectureName, color:data[index].color, times:data[index].times, hour:data[index].hour, location:data[index].location, classDate:data[index].classDate, startTime:data[index].startTime, endTime:data[index].endTime)self.classList2.append(item)self.classList2Copy =self.classList2
}self.dateCollectionView.reloadData()self.tutorCollectionView.reloadData()self.nextDate =0case.pathErr :print("Patherr")case.serverErr :print("ServerErr")case.requestErr(let message):print(message)case.networkFail:print("networkFail")}}}BottomOffset로 아래쪽에 펼쳐지는 드롭박스의 위치를 직접 지정할 수 있다. (더 세밀하게 컨트롤 가능!) y축에 아래 코드를 쓰면 버튼 높이 만큼 offset이 지정되어 바로 아래쪽에서 드롭박스가 펼쳐지게 되는데 조금 여유를 두고 펼쳐질 수 있도록 +6(pt)을 해주었다.
dropDown?.bottomOffset =CGPoint(x:0, y:(dropDown?.anchorView?.plainView.bounds.height)!+6)수업일지 뷰에서 과외를 선택했을때만 Progress View가 나오고 선택하지 않으면 Progress View를 숨기는 기능을 구현이 필요했다. 스택뷰에 해당 view를 hidden 시키고 Constranints height를 0으로 조절하는 방법으로 View를 숨기고, 보일 수 있도록 하였다.
func classHeaderHidden(_ ishide:Bool){
progressViewWrap.subviews[0].isHidden = ishide
if ishide //true(안보일때)
tableViewTopMargin.constant =191-117
else //false (보일때)
tableViewTopMargin.constant =191}알림을 오른쪽으로 스와이프해서 새로운 알림을 확인체크할 수 있고, 삭제도 할 수 있다.
func tableView(_ tableView:UITableView, trailingSwipeActionsConfigurationForRowAt indexPath:IndexPath)->UISwipeActionsConfiguration?{letdelete=UIContextualAction(style:.destructive, title:"삭제"){(action, sourceView, completionHandler)inself.noticeList.remove(at: indexPath.row)self.noticeTableView.reloadData()print(self.noticeList.count)
//completionHandler(true)
}letconfirm=UIContextualAction(style:.normal, title:"확인"){(action, sourceView, completionHandler)inprint("index path of edit: \(indexPath)")self.noticeList[indexPath.row].newNotice =falseself.noticeTableView.reloadData()completionHandler(true)}letswipeActionConfig=UISwipeActionsConfiguration(actions:[delete, confirm])
swipeActionConfig.performsFirstActionWithFullSwipe =falsereturn swipeActionConfig
}마이페이지에서 정규 수업시간을 입력하는 부분은 사용자의 입력에 따라 동적으로 텍스트 필드가 생성되도록 하기 위해 tableView로 구현하였는데, 테이블 뷰 셀 내에서 작성된 데이터를 VC로 전달하는 부분에서 어려움이 있었다. 변수에 직접데이터를 할당해보고, 리스트에 append를 해봐도 VC내 다른 함수에서 데이터를 호출하려고 하면 nil 값이 출력되었다. 이 문제를 해결하기위해 cell에 protocol을 정의하고 delegate를 설정해주었다.
func tableView(_ tableView:UITableView, cellForRowAt indexPath:IndexPath)->UITableViewCell{
cell.delegate =self // 매우중요!! tableView cellForRowAt 함수 내부에 delegate 선언
}
// Cell 파일 내부에 protocol 선언
protocolAddRegularClassTimeCellDelegate:class{func setScheduler(_ date:String, _ start:String, _ end:String)}
// Cell 파일에 delegate 변수 선언 및 함수 호출
vardelegate:AddRegularClassTimeCellDelegate?iflet delegate = delegate {
delegate.setScheduler(days, startTime, endTime)}
// 해당 VC에 extention으로 셀파일 protocol에 정의된 함수부를 구현한다.
extensionMyClassAddVC:AddRegularClassTimeCellDelegate{func setScheduler(_ date:String, _ start:String, _ end:String){letnewSchedule=Schedules(day: date, orgStartTime: start, orgEndTime: end)
schedule.append(newSchedule)}}VC내에 빈 리스트 regularClassTime을 두고 버튼을 눌렀을 때 리스트에 append를 해서 개수를 늘려 준 다음 tableView.reloadData()를 해주면 (테이블 row 개수는 리스트 regularClassTime.count이다) cell이 동적으로 증가된다.
@IBActionfunc regularClassAddButton(_ sender:Any){
regularClassTime.append("셀 추가")
tableView.reloadData()}텍스트 필드에 키보드 대신 피커뷰로 입력을 받으며, toolbar의 bar button들과 피커뷰 목록을 커스텀 하여 정규 수업시간을 입력 받도록 했다. 또한 시작시간을 입력했을 때, 끝 시간이 자동으로 시작시간과 맞춰지도록 didSelectRow 함수 내에 아래 소스코드를 구현했다.
pickerView.delegate =self
pickerView.dataSource =self
// 이후 delegate 와 detaSource 함수를 extention으로 컴포먼트 개수 지정 및 목록 데이터 리스트 적용
ifstartHours[pickerView.selectedRow(inComponent:1)]!="00"{ //시작시간이 입력되었으면 끝나는 시간도 시작시간과 동일하도록 자동으로 해당 component의 wheel이 돌아가면서 설정됨
startH =startHours[pickerView.selectedRow(inComponent:1)]
startrow = row
pickerView.selectRow(startrow, inComponent:3, animated:true)
endH =endHours[pickerView.selectedRow(inComponent:3)]}| 기능 이름 | 우선순위 | 담당자 | 뷰 | 구현 여부 |
|---|---|---|---|---|
| 스플래시 | P1 | 인정 | 스플래시 | O |
| 스플래시 애니메이션 | P3 | 인정 | 스플래시 | O |
| 앱 설명 온보딩 | P2 | 인정 | 회원가입 & 로그인 | O |
| 회원가입 & 역할 선택 | P1 | 세화 | 회원가입 & 로그인 | O |
| 이용약관, 개인정보보호정책 | P3 | 세화 | 회원가입 & 로그인 | O |
| 로그인 | P1 | 세화 | 회원가입 & 로그인 | O |
| 자동 로그인 | P3 | 세화 | 회원가입 & 로그인 | O |
| 회원가입/로그인 서버 연동 | P3 | 세화 | 회원가입 & 로그인 | O |
| 캘린더토글 | P1 | 세화 | 캘린더 | O |
| 캘린더 월 변경 (좌우 화살표) | P1 | 세화 | 캘린더 | O |
| 캘린더에 수업 일정 표시 | P1 | 세화 | 캘린더 | O |
| 선택 날짜의 일정 표시 | P1 | 세화 | 캘린더 | O |
| 플로팅 버튼 + | P1 | 세화 | 캘린더 | O |
| 일정 정보 화면 | P1 | 세화 | 캘린더 | O |
| 일정 편집/삭제 | P2 | 세화 | 캘린더 | O |
| 일정 추가 | P1 | 세화 | 캘린더 | O |
| 캘린더 서버 연동 | P3 | 세화 | 캘린더 | O |
| 수업일지토글 | P1 | 인정 | 수업일지 | O |
| 수업 일지 (월 단위) | P1 | 인정 | 수업일지 | △ |
| 수업 일지 수정 (입력) | P1 | 인정 | 수업일지 | O |
| 수업 일지 월 변경 (좌우 화살표) | P3 | 인정 | 수업일지 | O |
| 과외 시간 달성률 (막대그래프) | P2 | 인정 | 수업일지 | O |
| 튜티 일지 편집 방지 | P3 | 인정 | 수업일지 | O |
| 수업 일지 서버 연동 | P3 | 인정 | 수업일지 | |
| 알림토글 | P1 | 세화 | 알림 | O |
| 알림 | P1 | 세화 | 알림 | O |
| 알림 삭제, 확인 기능 | P2 | 세화 | 알림 | O |
| 데이터에 따른 알림 메시지 | P2 | 세화 | 알림 | |
| 간편 프로필 | P1 | 인정 | 내정보 | O |
| 프로필 편집 | P2 | 인정 | 내정보 | O |
| 수업 버튼 | P1 | 인정 | 내정보 | O |
| 수업 버튼의 프로필 | P2 | 인정 | 내정보 | O |
| 수업 추가 | P1 | 인정 | 내정보 | O |
| 수업 초대 | P1 | 인정 | 내정보 | O |
| 초대 코드 | P1 | 인정 | 내정보 | △ |
| 수업 정보 | P1 | 인정 | 내정보 | O |
| 수업 정보 편집 | P2 | 인정 | 내정보 | O |
| 계좌 정보 복사 버튼 | P2 | 인정 | 내정보 | O |
| 내정보 서버 연동 | P3 | 인정 | 내정보 | |
| 버전 정보 | P3 | 인정 | 내정보 | O |
| 개발자 정보 | P3 | 인정 | 내정보 | O |
| 로그아웃 | P3 | 인정 | 내정보 | O |
🐰26기 iOS파트 YB 류세화 SehwaRyu | @soonsophu
🐱26기 iOS파트 YB 최인정 InjeongChoi | @leanjeong









































