GithubHelp home page GithubHelp logo

mart-holiday-app's Introduction

마트쉬는날

  • 마트 휴무일을 매번 검색하기 귀찮은 사람들을 위한 알리미 애플리케이션 (앱 출시 후기 블로그 포스팅)
  • 빌드 관련 이슈: 혹시 이 프로젝트를 빌드해보시는 분들께 미리 알려드립니다. 앱스토어에 올라가 있는 앱이라 Private저장소에 올려야 하는게 맞지만, 동시에 저의 포트폴리오이기도 하여 코드를 공개하는게 맞다고 생각했습니다. 다만 라이브러리 SSH키까지 노출 할 수는 없어서 관련된 plist파일은 현재 repository에 올라와있지 않습니다. 양해 부탁드립니다! 🙇

앱스토어 링크

주요 기능

  • 브랜드별 마트 검색, 상세정보를 제공한다.
  • 즐겨찾는 마트를 저장할 수 있고, 즐겨찾는 마트의 휴무일을 앱 메인 화면과 위젯에서 확인할 수 있다.
  • 즐겨찾는 마트의 휴무일 하루 전, 유저에게 푸시로 휴무일 알림을 보낸다.
  • (v2.0 추가예정) 유저의 현재 위치를 기준으로 가까운 마트를 찾는다.

구현 기술

  • Network, Push Notification, Today Extension, UISearchController, Error handling, UserDefault, Codable, NSKeyedArchiver, Custom View(SlideMenu), Custom Delegate, Autolayout, xib 등

아키텍쳐

  • MVC

경험한 디자인 패턴

  • Delegate, Singleton, Protocol Oriented

사용한 라이브러리

  • Firebase(FCM, Database, Storage) : 푸시기능을 구현하기위해 사용자 정보를 저장하고 클라우드 메시지를 전송하는데에 사용
  • 네이버지도(NMap) : 마트 위치 표시를 위해 사용. 구글지도나 애플지도보다 국내 점포의 위치 파악이 훨씬 잘되어있어 네이버지도 사용함
  • Reachability.swift : 네트워크 상태 변화 감지를 위해 사용
  • NVActivityIndicatorView : 화면 로딩중임을 알리기위한 인디케이터를 표시하기 위해 사용

Troubleshooting

App Crash

  • AppStore 첫번째 심사 결과: Reject
    • 사유: We were unable to review your app as it crashed on launch.
  • Crash report 분석결과, AppDelegate내의 FCM푸시토큰을 받아오는 콜백함수에서 옵셔널 값에 접근하는 문제가 있었다.
  • Optional Forced-unwrapping으로Bool!타입으로 선언된 변수가 있었고, 해당 변수에 값이 할당되기 전에 콜백함수 내에서 해당 변수에 접근한 문제
  • 앱을 처음 설치하면 Push Notification 승인에 대한 alert가 화면에 나타나는데, 문제가 되는 변수에 값을 할당하기위해서는 해당 alert의 버튼을 선택해야했음.
  • 하지만 해당 alert의 allow / don't allow를 바로 선택하지 않고 기다리면 콜백함수가 먼저 실행되어, nil값에 접근하는 상황이 발생함.
  • 해결: 해당 변수를 Bool?타입으로 선언하고 바인딩 후 사용하는 방식으로 변경

짧은 회고

  • 실행 시점을 정확히 예측하기 힘든 콜백함수에서는 어떤 작업을 하든지 신중하게 해야한다는 점
  • 따라서 여러 상황을 가정해서 테스트해야한다는 점(사용자가 당연히 내가 예상하는 액션을 그대로 할 것이라고 예상하지 말것)
  • 옵셔널처리같은 사소한 문제에서 정만 critical한 문제가 발생한다는 점을 깨달음

주요 구현사항

(v2.0)

1. 유저의 현재 위치 기준으로 마트검색

  • 뷰컨트롤러가 로드되었을때 바로 유저의 현재위치를 기준으로 반경 n 킬로미터의 마트를 검색하여 지도에 표시하는 기능

  • 초기 구현: 유저의 위치에 프로퍼티 옵저버 didSet을 걸고 주변 마트 위치를 fetch()하는 함수를 호출하는 방식으로 구현

  • 문제상황 1: 유저의 위치가 미세하게 변경될 때마다 새롭게 갱신되어 계속 didSet에서 fetch함수가 호출되는 불필요한 상황 발생

  • 문제상황 2: 유저의 위치가 0,0이나 난수로 받아지는 경우 발생

  • 네이버지도가 제공하는 NMGLatLng의 inValid에서 해당 난수 좌표가 걸러지지 않음

    • 이는 네이버지도SDK를 개발하신 분께 문의한 내용
      • 실내에서 GPS를 잡는 경우 난수가 나오는 경우가 있을 수 있다.
      • 네이버지도에서 유효한 위,경도 좌표의 범위가-90 ~ 90, -180 ~ 180이며 난수 또한 이 사이의 수이기때문에 걸러지지않을 수 있다고 하심
  • isValid 조건을 만들어 프로퍼티 옵저버의 메소드가 호출되는 조건을 만들어서 불필요한 서버 호출을 제한함

    • 1번 조건: 유저의 위,경도 값이 소숫점 0.0005 미만으로 차이나는 경우에 서버 호출 skip
    • 2번 조건: 난수 위, 경도를 걸러내기위해 국내 좌표만 유효한 좌표로 감지하도록 구현
    • 1,2가 and 조건으로 유저의 현재위치 검사

(v1.0)

1. 즐겨찾기 데이터 모델 설계

  • 한 유저당 하나의 즐겨찾기(FavoriteList객체)만 존재해야하므로, Singleton으로 구현
  • 즐겨찾기를 pop / push 한 후 성공여부를 전달하기위해 Bool값을 리턴하는 것으로 구현
  • 이와 관련하여, 즐겨찾기를 pop / push 이벤트를 받는 버튼을 StarButton객체로 추상화하고 해당 버튼을 사용하는 뷰컨트롤러를 FavoriteTogglable 프로토콜로 추상화
  • 유저가 앱을 실행할때마다 가장 마지막(최근)시점에 설정했던 즐겨찾기 데이터가 표시되어야하기에, 설정값 저장하는 기능이 필요함
  • NSCoding을 구현하고 UserDefaults와 NSKeyedArchiver를 사용해서 객체를 아카이빙하는 방식으로 저장

2. Push Notification

  • Firebase Cloud Message(FCM)를 이용하여 구현
  • 초반엔 마트 지점별로 주제구독방식을 선택하였는데, Firebase API에서도 해당 방식 자체가 푸시 전송이 누락될 가능성이 높다고 권고함
  • 또한 유저의 사용성마다의 여러 가능성에따라 푸시 구독 / 구독취소된 항목이 푸시서버와 싱크가 맞지 않는 다수의 케이스 발견
  • 단일기기전송방법으로 변경하고 Firebase DB에 사용자토큰으로 즐겨찾기 정보 저장
  • 토큰에 대한 즐겨찾기 정보를 조회하고 FCM API를 이용하여 단일기기에 푸시 전송하는 방식으로 구현

3. 슬라이드 메뉴 커스터마이징

  • SlideMenuView 델리게이트 커스터마이징
  • 앱 내의 메뉴들을 표시하는 SlideMenuView를 컬렉션뷰를 사용해서 만듦
  • 하지만 슬라이드메뉴를 가진 MainViewController의 코드양이 슬라이드메뉴의 Delegate와 Datasource역할까지 하기때문에 코드가 너무 길어진 문제
  • 델리게이트 객체를 따로 만들고, 뷰컨트롤러와 분리하여 SlideMenuView의 델리게이트 지정해줌
  • SlideMenuView를 구성하는 컬렉션뷰 셀과 일반 뷰(문구 표시용 헤더)는 xib로 구현

4. Custom Delegate 활용

Protocol - Oriented 시도

  • 애플리케이션을 구성하는 View마다 사용하는 기능이 여기저기서 재사용되는 일이 많았음
  • View에 대한 Delegate객체 구현시 Protocol을 활용하여 수평적 기능확장, 재사용성을 높임
  • MainViewController나 DetailViewController에서 사용하는 Expandable TableView구현시 뷰와 모델간 이벤트 처리를 위한 Custom Delegate 사용하여 재사용성 높임
  • NMapView Delegate객체를 담당하는 재사용성을 높여 ViewController의 중복 코드 제거

개선점

  • 마트 지점 데이터 갱신 주기
    • 사용자 유입이 많아지고 서버 비용이 올라가면 검색 화면을 띄울때마다 서버에 요청을 보낸다면 무리일 것으로 판단
    • 처음에 우려했던, 유저가 주기적으로 앱을 실행하지 않으면 최신 데이터가 업데이트되는 주기와 싱크가 맞지 않을 수 있다는 문제점은 위젯에서 항상 최신 데이터를 표시해주는 것으로 해결되었음(위젯은 표시할때마다 사용자의 즐겨찾기에 맞는 최신 정보만 가져옴)
    • 전체 마트 정보가 갱신되는 주기는 평균적으로 2주 ~ 한 달 사이로 짧지 않기때문에, 새로운 데이터를 받았을때 데이터 저장 후 사용
    • 사용자의 마지막 앱 실행시점이 얼마나 지났는지 판단해서 새로운 데이터를 요청할지 캐싱된 데이터를 사용할지 판단
  • 앱에 필요한 리소스를 Firebase Storage에서 로딩하는 것으로 변경
    • 현재 '앱정보'메뉴에 표시되는 라이선스정보는 Firebase Storage에 .txt파일로 저장 후 네트워크로 받아서 표시하는 방식을 사용중
    • 따라서 이런식으로 변경이 자주 될 리소스는 따로 앱 업데이트를 하지 않아도 최신 정보로 표시되도록 구현함
    • 마트 브랜드를 표시하는 마트 로고 이미지또한 Storage에 저장하여 받아오는 방식으로 구현하여 앱 업데이트가 필요없도록 구현
  • Mart객체 커스텀 방식 변경
    • 마트를 담당하는 Mart객체들을 지금처럼 enum으로 구현하는것이아닌, 서버에서 받아오는 방식으로 구현하고 마트가 추가되어도 앱 업데이트가 필요없도록 구현

스크린샷

mart-holiday-app's People

Contributors

jinios avatar

Stargazers

사이먼 avatar Daheen Lee avatar Haksun LEE avatar hongsii avatar Hwijun Jeong avatar Youngsun avatar

Watchers

James Cloos avatar

Forkers

appleceo

mart-holiday-app's Issues

지도 API에서 넘어오는 에러 처리

현상

  • 마트 주소를 보내서 지도에 표시할 수 있도록 좌표로 변환하는 api에서 error반환
  • 네이버지도 API 플레이그라운드에서 확인결과 몇몇 마트 주소를 요청으로 보냈더니 에러 응답. { "errorMessage": "검색 결과가 없습니다.", "errorCode": "MP03" }
  • 서울 송파구 신천동 올림픽로 300처럼 구주소 '동'과 신주소 '로'가 같이 있으면 주소를 찾을 수 없다는 에러 반환
  • '동'을 빼고 신주소로만 검색했더니 좌표 표시됨

해결방향

  • 주소가 아예 잘못 들어가있는 경우도 있기때문에(마트 홈페이지에서 가져오는 정보에) 일단 지도를 표시할 수 없으면 이를 나타내는 에러화면을 띄울것
  • 신주소와 구주소가 함께 있는 주소에서 에러발생하는 케이스는 밝혀졌으므로 에러상황시 한번더 주소를 수정해서 체크하는 로직 추가

메인메뉴 사이드바 아키텍쳐 변경

  • SlideTopView, SlideMenu, SlideBackgroundView를 MainViewController의 하위 뷰로 추가하고
  • 기존의 SlideLauncher의 CollectionView(SlideMenu)의 delegate 역할과 datasource를 MainViewController가 담당

Network Reachability 추가

  • 앱 실행 중 네트워크 상태를 파악하는 리스너를 구현한다.
  • Network가 unreachable상태면 alert를 띄운다.

위젯(Today Extension)기능 구현

  • 사용자가 지정한 즐겨찾는지점의 가장 가까운 휴무일을 표시
  • 테이블뷰로 화면 구성하기
  • 최대 3개
  • Today Extension을 추가하고 host app 간의 데이터를 공유할수있도록 설정하기(App Group)

라이선스 표시 텍스트 Firebase/Storage이용하여 다운로드하기

  • 작은 수정이 발생할 수 있는 리소스는 클라우드에서 관리하기로함
  • 많은 것들을 옮겨야하겠지만, 앱 버전 1.1이나 2.0에서 개발하는 것으로 계획
  • 라이선스를 표시하는 텍스트가 꽤 길고, 직접 스토리보드에 입력하는 것 보다는 언제든 변경하더라도 앱 업데이트와 재배포 없이 모든 앱에서 새로운 정보가 표시될 수 있도록 클라우드 사용
  • 간단하게 Firebase/Storage를 이용하여 메모리의 NSData로 다운로드 구현

Firebase 디바이스 백그라운드 클라우드 메시징 수신하지 못하는 문제

이슈

  • 개발자계정을 코드스쿼드 팀계정에서 개인 개발자계정으로 변경했다.
  • 앱id를 코드스쿼드에서 지우고 개인에 추가하면서 apns인증서또한 다시 갱신하고 그에따른 프로비저닝 파일을 다시 만들어야했다.
  • 원래 하던 것과 같이 푸시용 인증서를 발급받고 맞는 app id의 프로비저닝 파일을 만들었는데.... 빌드도되고 파이어베이스 토큰이 받아지지만 메시지 수신이 안됨
  • 메시지 수신만 안되는게 apns인증서와 관련있을것이라 생각해서 계속 반복.
  • 그래도 안됨. 파이어베이스 프로젝트 새로 만들기까지함...이번엔 배포용 apns인증서까지 한번에!
  • 그래도 메시지 수신 불가. 다시 구글링 시작.

해결

  • 한 가지 간단한 시도로 해결 할 수 있었다.
  • 파이어베이스 클라우드메시징 튜토리얼에 나와있는 설정에 꼭 Info.plist에서아래의 설정을 꼭 false로 변경하라는 내용이있다. 예전에는 이 설정을 안해주니까 메시지가 안왔었는데.. 아래의 라인을 아예 지우는 것이 이번이슈의 해결책이었다.
<key>FirebaseAppDelegateProxyEnabled</key>
	<false/>

DetailVC에서도 HeaderDelegate를 사용하도록 수정

  • expande/collapse 기능이 있는 테이블뷰의 델리게이트를 담당하는 뷰컨트롤러는 HeaderDelegate(or FooterDelegate)를 사용하도록 변경
  • HeaderDelegate에는 [Branch]타입을 커스텀하여 expandable하게 데이터를 관리할 수 있는 커스텀 모델 객체 ExpandCollapseTogglable를 사용하게되어있음
  • 이 커스텀타입이나 프로토콜을 사용하지 않으면 뷰컨트롤러가 테이블뷰가 expand상태인지 아닌지를 판별하는 isExpanded속성을 직접 가지고 관리할 필요가 없어짐
  • 또한 Expand이거나 Collapse상태일때 표시해야하는 데이터도 뷰컨트롤러가 아닌 모델객체가 관리하게된다
  • 따라서 DetailVC가 테이블뷰의 데이터로 사용하고있는 [Branch]를 [ExpandCollapseTogglable]타입으로 변경하고, HeaderDelegate 프로토콜을 채택하여 추상화하여 뷰컨트롤러가 직접 데이터를 관리/사용하지 않는 구조로 개선한다.

지점 상세화면에서 지도뷰 크게 표시하기

  • 작은 지도 화면에서 zoom, pan 제스쳐 disable처리
  • 지도화면에 tap 제스쳐 감지되면 화면 전체를 지도 뷰로 채운 컨트롤러 push하기
  • DetailViewController에 NMapView와 Delegate객체 분리

서버 에러일때 슬랙 webhook 보내기

  • 현재 서버 에러나 정보를 일정시간 이내 받아올 수 없을때 alert창을 띄움
  • 이때 사용자에게 보여지는 뷰만 띄우는 것이 아니라 개발자가 직접 확인하지 않아도 알 수 있도록 슬랙 webhook을 전송하도록 기능 추가하기

상세화면의 휴무일 expandable 테이블뷰 헤더 xib로 구현

  • 헤더를 UITableViewCell클래스로 구현하고, cell을 deque하는 tableView.dequeueReusableCell(withIdentifier: , for: )로 헤더를 불러올 경우, 계속 셀을 재사용하겠다는 용도이기때문에 헤더뷰 구현이 제대로 되지않음.

  • 임시방편으로 헤더의 contentView프로퍼티를 리턴하여 사용하였으나, 좋은 방법이 아니어서 xib로 뷰를 만들고 UIView를 리턴하는 방식으로 사용하도록 변경

  • 이전 코드

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        guard let branchData = branchData else { return UIView() }
        let view = tableView.dequeueReusableCell(withIdentifier: "headerCell") as! HolidayHeaderCell

        let contentView = view.contentView
        return contentView
    }

Branch 클래스 NSCoding 제거

즐겨찾는 마트를 저장하기 위해 구현했는데,
Branch 객체 전체를 저장하는 것이 아니라 Branch의 id인 Int값만 저장하고있기때문에 제거함.

[hotfix / 1.0] Crash

[1.0 / Build 4] 버전 앱스토어 심사 결과 Reject
사유 : Crash

  • Crash Report 에서 확인결과 Firebase에서 push token을 받아오는 콜백함수에서 에러 발생
  • 해당 함수에서 옵셔널 변수에 접근하고있었는데 초기값이 없는 Bool!타입변수였음
  • 최초로 앱을 실행하는 유저에게 푸시권한 허용을 묻는 Alert에서 선택을 안하고 계속 놔두면 해당 콜백이 실행되고 옵셔널에 접근하여 크래시가 발생하는 현상
  • 콜백함수가 불리는 시점은 매번 다르고 개발자가 예측할 수 없어서 주의가 필요함
  • 게다가 옵셔널을 강제해제하는건 정말 주의가 필요함...

마트 지점 상세 페이지 구현(DetailViewController)

  • 마트상세페이지: 마트목록에서 검색해서 해당 지점을 선택했을때 표시할 페이지

  • 표시정보

    • 이름 / 지점
    • 해당 월의 휴무일
    • 전화번호
    • 위치정보(주소)
    • 영업시간?
    • 홈페이지링크
    • 즐겨찾기 추가 버튼
  • 핵심 기능

    • 해당 월의 휴무일
    • 전화번호: 대표전화번호 클릭했을때 바로 통화로 연결되는기능
    • 홈페이지링크: 선택시 웹뷰 띄우기
    • 즐겨찾기 추가 버튼

메인메뉴에서 콜렉션뷰 델리게이트 역할 분리(SlideMenu / FavoriteCollectionView)

문제상황

  • MainViewController가 SlideMenu와 Favorite 콜렉션뷰를 모두 가지고 있는 상황
  • 두 콜렉션뷰의 델리게이트도 모두 담당하고있다.
  • 콜렉션뷰가 두개까지 있을때는 collectionView함수내에서 각 뷰의 tag값을 가지고 분기문 처리함
  • 하지만 Favorite콜렉션 뷰 내부에 또 다른 콜렉션뷰가 위치해야 하는 필요가 생김.
    • (각 마트 지점의 휴일 날짜 라벨을 표시하는 부분)
  • 이렇게 되면 하나의 뷰 컨트롤러가 세개의 콜렉션뷰 델리게이트 역할을 해야하며, collectionView 함수내에서 세 번의 분기 처리가 필요하게됨. 😱

해결방안

  • 기능 종류에 따라 사이드바메뉴를 담당하는 콜렉션뷰와 즐겨찾는 지점을 담당하는 콜렉션뷰를 분리하기로 결정
  • SlideMenu를 담당하는 델리게이트 객체를 따로 분리해서 MainViewController가 거대해지는 상황을 방지

즐겨찾기 기능 구현

  • 즐겨찾기: 사용자가 자주가는 마트 지점을 선택. 북마크 기능
  • 즐겨찾기는 세 개까지만 허용
  • 마트 json파일이 변경되어도 즐겨찾기 지점은 그대로 적용되도록 관리하는 객체를 만든다.

v2.0 - 주소복사하기 기능 수정 요청

지도를 표시할 수 없을 때 주소복사하기 기능 수정 요청

• 주소복사하기가 잘 안눌림! 영역을 좀더 넓게할 필요가 있을듯..
• 주소복사하기를 하면 토스트 메시지로 “클립보드로 복사되었습니다” 라던지 문구를 띄우면 좋을듯

메인메뉴에서 사이드바 구현

  • 메인 뷰 컨트롤러에 사이드바 버튼추가한다. (왼쪽상단)
  • 사이드바 버튼을 탭하면 뷰의 왼쪽 (leading부분)에서 사이드바 슬라이딩 효과로 나타난다.
  • 사이드바 메뉴는 화면의 반을 차지하도록 만든다.
  • 사이드바 메뉴를 탭하면 해당하는 화면으로 전환되도록 만든다.

JSON데이터를 파싱해서 BranchList객체 만들기

  • 에셋카탈로그에서 JSON파일 가져오기
  • 가져온 JSON파일을 Branch객체로 만들기
  • MartSelection 뷰 컨트롤러에서 검색할 마트 버튼을 누르면 SearchViewController에 data를 프로퍼티로 넘기기

Favorite리스트를 서버에 요청해서 받아오는 방식으로 변경

  • 즐겨찾기에 추가한 당시의 지점 데이터로 Favorite Branch객체가 만들어지기때문에 SearchVC와 DetailVC에서 보여주는 새로운 객체 데이터가 아님
  • 즐겨찾기한 지점의 id로 서버에 요청을 보내면 Response로 받는 JSON파일로 Favorite을 표시하는것으로 수정하기

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.