연애상담애플리케이션 개요 연애고민에 특화된 전문적인 상담을 보다 쉽게 서비스하기 위해 기획한 애플리케이션
유저들간 연애고민에 대해 이야기를 나누고 공감을 하는 간단한 커뮤니티를 뛰어넘어, 연애 상담 경험과 지식이 많은 전문가에게 상담을 받아 신뢰성 높은 연애고민상담이 이루어지는 연애 관련 애플리케이션이다.
프로젝트 기간 2017년 6월 24일 ~ 2017년 7월 8일 (3주)
프로젝트 등급 알파테스트 (iPhone7) - 현재 안드로이드 버전 구글플레이 서비스중
IOS.ver 10.2
GitHub Repository https://github.com/godpp/cyrano_ios
워크플로우
개발설명 스플래쉬 & 로그인 화면
스플래쉬 화면에서 이전에 로그인했을 때의 accountSequence를 회원가입때 UserDefault에 저장했던 넘버와 비교를 통해 자동로그인을 구현하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 override func viewDidLoad () { let accountSequence = self.ud.integer(forKey: "login_user_id" ) let diffAS = self.ud.integer(forKey: "join_user_id" ) print ("어카운트시퀀스" ) print (accountSequence) print (diffAS) //2초뒤에 화면 전환 시켜줌 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+delayInSeconds){ if accountSequence == diffAS { //스토리보드 객체 생성 let main_storyboard = UIStoryboard(name: "Main" , bundle: nil) //메인 뷰컨트롤러 접근 guard let main = main_storyboard.instantiateViewController(withIdentifier: "Main_Tab" ) as? Main_Tab else {return } self.present(main, animated: true ) } else { let login_storyboard = UIStoryboard(name: "Login" , bundle: nil) guard let login = login_storyboard.instantiateViewController(withIdentifier: "Login" ) as? Login else {return } self.present(login, animated: true ) } } }
회원가입
회원가입은 총 3개의 뷰로 구성되며, 각각 페이지의 회원가입 정보들을 CoreData에 저장해 마지막 페이지에서 서버와 통신하게끔 구현하였다.
나이 상승,감소는 0~9까지의 이미지를 이용해 나이를 표현했는데, if문 분기를 통해 9에서 10의 자리로 넘어갈 때의 문제점을 해결하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 var tensPlace : Int = 2 //십의자리 var unitDigit : Int = 0 //일의자리 //나이 상승버튼 @IBAction func ageIncreaseListener(_ sender: Any) { if unitDigit == 9 && tensPlace == 9{ } else if unitDigit == 9 { tensPlace += 1 unitDigit = 0 tensPlaceImg.image = UIImage(named: "age_" +String(tensPlace)+"0" )! unitPlaceImg.image = UIImage(named: "age_0" )! } else { unitDigit += 1 unitPlaceImg.image = UIImage(named: "age_" +String(unitDigit))! } age = tensPlace*10 + unitDigit } //나이 감소버튼 @IBAction func ageDecreaseListener(_ sender: Any) { if unitDigit == 0 && tensPlace == 0 { } else if unitDigit == 0 { tensPlace -= 1 unitDigit = 9 tensPlaceImg.image = UIImage(named: "age_" +String(tensPlace)+"0" )! unitPlaceImg.image = UIImage(named: "age_9" )! } else { unitDigit -= 1 unitPlaceImg.image = UIImage(named: "age_" +String(unitDigit))! } age = tensPlace*10 + unitDigit }
메인화면
로그인 후 보게 되는 메인화면이다. (Simulator 캡처 화면은 실제 안드로이드 버전에서 등록한 상담글들 이다.) NavigationController와 TabbarController를 root로 삼고, 뷰는 Tableview를 사용했다.
허나 많은 사용자들이 등록하는 상담글들을 Tableview에 모두 감당하기는 양이 너무 방대한 문제가 생겨, 뷰에 보이는 네개의 셀들만 뜨게 구현하였고, 초기에 Int.max 큰 값을 서버에 전송해 가장 최신글이 최상단에 오게 하였다. 또한 실시간으로 등록되는 앱의 특성상 아래로 뷰를 당겨 리로딩할 수 있게 구현하였다. 아래는 리로딩 코드이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 override func viewWillAppear(_ animated: Bool) { mainTableView.delegate = self mainTableView.dataSource = self let model = LoginModel(self) model.getMainArticleList(id : id) //테이블 뷰 아래로 리로딩 self.mainTableView.es_addInfiniteScrolling { [weak self] in let model = LoginModel(self!) model.getMainArticleList(id: (self?.gino(self?.id))!) if ((self?.articleList.last) == nil){ self?.mainTableView.es_noticeNoMoreData() } else { self?.mainTableView.es_stopLoadingMore() } } self.setTabBar() //셀이 비어있을때 테이블뷰 줄가있는거 없애기 mainTableView.tableFooterView = UIView.init(frame : CGRect.zero) } ``` 아래로 스크롤 할 때마다 articleList 배열에 연속적으로 추가 시켜 주어 다시 위로 스크롤 했을때, 최신글들이 보이게 서버 통신을 구현하였다. ```bash //통신 성공 func networkResult(resultData: Any, code: String) { if code == "1" { articleList += resultData as! [ArticleItemVO] mainTableView.reloadData() } }
상담글 버튼(+버튼)을 플로팅 버튼으로 만들어, 직관적으로 구현하였다.
메인화면에서 가장 어려웠던 부분은 하단 TabBar의 커스텀이였다. Xcode TabBar의 기본적인 tintcolor 때문에 이미지를 덮어씌워 디자인을 표현하는 것이 불가능 하였다, 그래서 UITabBarController을 상속받는 class를 만들어 barTintColor를 clear로 설정하고, withRenderingMode를 사용해 해결하였다. 아래에 코드이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Main_Tab : UITabBarController{ override func viewDidLoad () { UITabBar.appearance().barTintColor = UIColor.init(red: 27/255.0, green: 43/255.0, blue: 56/255.0, alpha: 0.0) var tabBar = self.tabBar var homeImage = UIImage(named:"tab_home_selection_last" )?.withRenderingMode(.alwaysOriginal) var mypageImg = UIImage(named: "tab_mypage_selection_last" )?.withRenderingMode(.alwaysOriginal) var notificationImg = UIImage(named: "tab_alram_selection_last" )?.withRenderingMode(.alwaysOriginal) var settingImg = UIImage(named: "tab_setting_selection_last" )?.withRenderingMode(.alwaysOriginal) (tabBar.items![0] as! UITabBarItem).selectedImage = homeImage (tabBar.items![1] as! UITabBarItem).selectedImage = mypageImg (tabBar.items![2] as! UITabBarItem).selectedImage = notificationImg (tabBar.items![3] as! UITabBarItem).selectedImage = settingImg } }
상세고민
메인화면에서 상담글을 클릭 시 볼 수 있는 상세고민 화면이다. 너무많은 객체가 들어가고 답변 작성자가 멘토일 때, 일반 사용자일 때등 다양한 경우의 수가 있어 객체들을 중첩해 만드는데 큰 어려움이 있었다. 초기에는 상단 상담글은 일반 뷰를 사용하고 하단 답변글은 Tableview를 사용했지만 Constraint가 제대로 잡히지 않는 치명적인 오류가 생겨, 전체뷰를 Tableview로 구성하고 상담글과 답변하기는 각각의 정적인 셀로 구성하고, 답변글은 동적인 셀로 구현했다.
Tableview에서 글들의 길이가 항상 상이하기 때문에 셀 높이를 아래와 같은 코드로 동적으로 구현하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 테이블 셀 높이 func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { if (indexPath.section == 0){ return UITableViewAutomaticDimension } else if (indexPath.section == 1){ return 57 } else if (indexPath.section == 2){ return UITableViewAutomaticDimension } else { return UITableViewAutomaticDimension } }
답변쓰기
답변글 작성 화면이다. Textview를 사용해 Placeholder 설정하는 부분에서 어려움을 겪었는데, KMPlaceholder 라이브러리를 사용하여 해결하였다.
답변하기 버튼을 누르면 서버와 통신하게 되고, 아래와 같은 Alert창으로 ok버튼에 클로저 이벤트를 부여함으로써, 작성화면을 빠져나오게 된다.
1 2 3 4 5 6 7 8 func WriteAlert(title: String, msg: String) { let alert = UIAlertController(title: title, message: msg, preferredStyle: .alert) let okAction = UIAlertAction(title: "확인" , style: .default) { (UIAlertAction) in self.navigationController?.popViewController(animated: true ) } alert.addAction(okAction) self.present(alert, animated: true ) }
공개 & 비밀 고민작성
메인화면의 + 플로팅 버튼을 누르면 쓸 수 있는 고민 작성 화면이다. 일반 사용자한테 문의 할 수 있는 공개 고민 작성(기본 잉크 10), 연애 전문 멘토에게 상담할 수 있는 비밀 고민 작성(기본 잉크 300)으로 나뉜다.
고민 작성 화면에서 내 정보 공개 스위치를 Off하면 닉네임, 나이 등 개인정보가 비공개로 작성된다. 사용 잉크량을 클릭하면 잉크를 걸 수 있는 팝업 창이 뜨는데, 기존의 팝업창이 아래에서 위로 오는 구조 였다면, 이 팝업은 오른쪽에서 왼쪽으로 나타나도록 커스텀 하였다. 아래의 코드는 NSObject, UIViewControllerAnimatedTransitioning를 상속받는 클래스에서 팝업창이 이동하는 코드이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) let fromView = fromVC?.view let toView = toVC?.view let containerView = transitionContext.containerView if isPresentation { containerView.addSubview(toView!) } let animatingVC = isPresentation ? toVC : fromVC let animatingView = animatingVC?.view let finalFrameForVC = transitionContext.finalFrame(for : animatingVC!) var initialFrameForVC = finalFrameForVC initialFrameForVC.origin.x += initialFrameForVC.size.width let initialFrame = isPresentation ? initialFrameForVC : finalFrameForVC let finalFrame = isPresentation ? finalFrameForVC : initialFrameForVC animatingView?.frame = initialFrame UIView.animate(withDuration: transitionDuration(using: transitionContext), delay:0, usingSpringWithDamping:300.0, initialSpringVelocity:5.0, options:UIViewAnimationOptions.allowUserInteraction, animations:{ animatingView?.frame = finalFrame }, completion:{ (value: Bool) in if !self.isPresentation { fromView?.removeFromSuperview() } transitionContext.completeTransition(true ) }) }
내가 쓴 상담
두 번째 탭바인 내가 쓴 상담 확인 화면이다. 전체 Tableview로 구성되어있고, Section 분할을 통해 진행중 상담과 완료된 상담을 구분하고, Tableview의 titleForHeaderInSection 메소드를 이용해 타이틀을 구현하였다.
아래는 Alamofire를 통해 서버로 부터 JSON을 Parsing 받는 네트워킹 메소드이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Alamofire.request(URL, method: .get, parameters: nil, encoding: JSONEncoding.default, headers: nil).responseObject{ (response : DataResponse<MypageVO>) in switch response.result{ case .success: guard let mypageCounselingInfo = response.result.value else { self.view.networkFailed() return } if let IngcounselingList = mypageCounselingInfo.result1 { self.view.networkResult(resultData: IngcounselingList , code: "3-1" ) } if let ComCounselingList = mypageCounselingInfo.result2 { self.view.networkResult(resultData: ComCounselingList , code: "3-2" ) } case .failure(let err): print (err) self.view.networkFailed() } }
설정
계정 설정 화면이다. 앱 내 결제(기능 미구현)과 프로필 사진, 닉네임, 소개 메시지등이 변경 가능하고, 최종적으로 로그아웃이 가능한 화면이다.
프로필 사진 변경은 ImagePicker를 기존 클래스에 extension 시켜, pickerview로 사진첩에 접근 가능하게 구현하였으며, 네트워킹 함수에서 if문으로 code 값을 분기 시켜, 닉네임 중복확인, 변경 성공등을 구현하였다. 아래는 네트워킹 함수 분기문이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 if code == "3" { let inputNickname = gsno(nickTxt.text) let inputIntroduce = gsno(introTxt.text) let modiymodel = ModifyInfoModel(self) modiymodel.setUserInfo(nickname: inputNickname, introduce: inputIntroduce) } else if code == "2" { simpleAlert(title: "오류" , msg: "닉네임 조회 오류" ) } else if code == "1" { simpleAlert(title: "오류" , msg: "서버 연결 오류" ) } else if code == "4" { simpleAlert(title: "사용불가" , msg: "이미 사용중인 닉네임 입니다." ) } //회원가입 if code == "3-1" { let inputNickname = gsno(nickTxt.text) rename = inputNickname simpleAlert(title: "완료" , msg: "변경되었습니다." ) }else if code == "1-1" { simpleAlert(title: "실패" , msg: "커넥팅 에러" ) }else if code == "2-1" { simpleAlert(title: "실패" , msg: "업데이트 에러" ) } if code == "7-1" { simpleAlert(title: "성공" , msg: "프로필이 적용되었습니다." ) }else if code == "7-2" { simpleAlert(title: "실패" , msg: "설정에러" ) }else if code == "7-5" { simpleAlert(title: "실패" , msg: "로그인상태 아님" ) }
마지막으로 로그아웃 시 최초 로그인할 때 부여 받았던 accountSequence를 초기화 시키고, 스플래쉬 화면으로 돌아갈 수 있도록 storyboard를 객체로 선언해 연결하였다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if code == "1" { let alert = UIAlertController(title: "로그아웃" , message: "로그아웃 하시겠습니까?" , preferredStyle: .alert) let OKAction = UIAlertAction(title: "OK" , style: UIAlertActionStyle.default, handler: { (_)in let splash_storyboard = UIStoryboard(name: "Splash" , bundle: nil) //메인 뷰컨트롤러 접근 guard let splash = splash_storyboard.instantiateViewController(withIdentifier: "Splash_Main" ) as? Splash_Main else {return } self.present(splash, animated: true , completion: nil) }) let cancleAction = UIAlertAction(title: "Cancel" , style: .cancel) alert.addAction(OKAction) alert.addAction(cancleAction) self.present(alert, animated: true , completion: nil) }