Skip to content

Log-Of-Yagom-Career/ios-diary

Repository files navigation

Diary ReadME

  • Kyo와 Baem가 만든 Diary App입니다.
  • 텍스트 기능을 구현 할 때, 아이폰 메모 앱의 텍스트 입력 로직을 참고하였습니다.

목차

  1. 팀 소개
  2. GroundRule
  3. Code Convention
  4. 실행 화면
  5. 폴더 구조
  6. 타임라인
  7. 기술적 도전
  8. 트러블 슈팅 및 고민
  9. 참고 링크

팀 소개

Kyo Baem

Ground Rule

Ground Rule 바로가기

Code Convention

Code Convention 바로가기

실행 화면

▶️ Step-1 실행화면

펼쳐보기
실행화면 언어변경

▶️ Step-2 실행화면

펼쳐보기
미 입력 일기생성 Background 진입시 저장
ActivityController구현 스와이프삭제 일기내에서 삭제

▶️ Step-3 실행화면

펼쳐보기
CLLocation의 사용 Cell찌그러짐 개선
AttributeString커서이동 보완 DarkMode

폴더 구조

├── Diary
│   ├── AppDelegate.swift
│   ├── SceneDelegate.swift
│   ├── Info.plist
│   ├── Base.lproj
│   │   └── LaunchScreen.storyboard
│   ├── Constant
│   │   └── Error.swift
│   ├── Extensions
│   │   ├── Formatter+Extension.swift
│   │   ├── NSMutableAttributedString+Extension.swift
│   │   ├── UIComponent+Extension.swift
│   │   └── UIViewController+Extension.swift
│   ├── Models
│   │   ├── CoreData
│   │   │   └── CoreDataManager.swift
│   │   ├── CurrentDiary.swift
│   │   ├── CurrentWeather.swift
│   │   ├── DecoderManager.swift
│   │   └── Network
│   │       ├── NetworkManager.swift
│   │       ├── NetworkRequest.swift
│   │       └── WeatherAPIData.swift
│   ├── Views
│   |   ├── CustomListCell.swift
│   |   ├── EditDiaryView.swift
│   |   └── MainDiaryView.swift
│   ├── Controllers
│   │   ├── EditViewController.swift
│   │   └── MainViewController.swift
│   ├── MappingModelV2ToV3.xcmappingmodel
│   │   └── xcmapping.xml
│   └──Diary.xcdatamodeld
│       ├── Diary.xcdatamodel
│       │   └── contents
│       ├── Diary_v2.xcdatamodel
│       │   └── contents
│       ├── Diary_v3.xcdatamodel
│       │   └── contents
│       └── MappingModelV2ToV3.xcmappingmodel
│           └── xcmapping.xml
├── DiaryData+CoreDataClass.swift
├── DiaryData+CoreDataProperties.swift
├── WeatherData+CoreDataClass.swift
├── WeatherData+CoreDataProperties.swift
├── MappingModelV2ToV3.xcmappingmodel
│   └── xcmapping.xml
├── Podfile
├── Podfile.lock
└──README.md

타임라인

👟 Step 1

  • ✅ Date Formatter의 지역 및 길이별 표현의 활용
  • ✅ Text View의 활용
  • ✅ Notification을 활용한 키보드 동작에 따른 View 제어
  • ✅ Compositional Layout을 이용한 CollectionView 활용
  • ✅ SwiftLint 적용
펼쳐보기

1️⃣ MainViewController

  • 앱 동작 시 가장 먼저 보여주는 View에 대한 Controller입니다.
  • MainViewController에서 CollectionView의 DataSource로는 DiffableDataSource를 사용하였습니다.

2️⃣ AddViewController

  • Right Navigation Bar Button을 클릭했을 때 보여지는 AddDiaryView에 대한 Controller입니다.
  • 내부에서 title을 설정 언어에 맞는 날짜로 설정하였습니다.

3️⃣ DecodeManager

  • 임시데이터인 Json 데이터에 대한 Decoder와 decode관련 메서드를 정의한 구조체가 정의된 파일입니다.

4️⃣ Diary

  • 말 그대로 Diary에 대한 데이터이며, Hashable을 만족하기 위해 uuid를 추가하였습니다.

👟 Step 2

  • ✅ 코어데이터 모델 생성
  • ✅ Swipe를 통한 삭제기능 구현
  • ✅ Swipe를 통한 공유기능 구현
  • ✅ ActivityController 구현
  • ✅ NSMutableAttributeString 활용
  • ✅ UICollectionLayoutListConfiguration 활용
  • ✅ Text View Delegate의 활용
펼쳐보기

1️⃣ CoreDataManager

  • CoreDataManager에서 CRUD를 구현하였습니다.
    • Create(Save)
    • Read(Fetch)
    • Update
    • Delete
  • 위 메서드들을 정의하여 CoreDataManager의 싱글톤 객체에서 호출할 수 있도록 구현하였습니다.

2️⃣ AddViewController ➡️ EditViewController

  • Add, Modify하는 기능의 Controller을 하나의 Controller로 통합하였습니다.

3️⃣ EditDiaryView

  • Add, Modify 화면을 하나의 View로 통합하였습니다.

👟 Step 3

  • ✅ Open API의 활용
  • ✅ Core Location의 활용
  • ✅ 코어데이터 모델 및 DB 마이그레이션
  • ✅ 코어데이터 모델 Relationship 사용
  • ✅ NSMutableAttributeString 사용
  • ✅ selectedTextRange 사용
  • ✅ NSCache 사용
  • ✅ DarkMode 적용
펼쳐보기

1️⃣ CurrentDiary

  • CoreData의 ManagedObject에 직접 접근하지 않기 위한 Type입니다.
  • 해당 인스턴스를 생성하여 사용자가 입력한 Diary 정보들을 넣고 CoreData 내부에 Save하도록 하였습니다.

2️⃣ CurrentWeather

  • CoreData의 ManagedObject에 직접 접근하지 않기 위한 Type입니다.
  • 사용자 Device에 대한 위도 경도를 바탕으로 해당 인스턴스를 생성하여 Open API에서 가져온 날씨에 대한 data와 icon에 대한 data를 넣고 CoreData 내부에 Save하도록 하였습니다.

3️⃣ NetworkManager

  • Server에 데이터를 요청하기 위한 fetchData()가 속해있는 class입니다.
  • 해당 클래스는 여러개 만들 필요가 없다고 생각되어 싱글톤 패턴을 사용하였습니다.

4️⃣ NetworkRequest

  • 위도, 경도를 바탕으로 날씨에 대한 data, 날씨 iconID에 대한 data를 받아올 수 있는 URL.
  • 날씨 iconID을 서버에 보내서 해당 ID에 맞는 IconImage를 받아올 수 있는 URL.
  • 위의 2개의 case에 맞는 URL을 얻기 위해 만든 별도의 enum 타입입니다.

5️⃣ WeatherAPIData

  • 서버에서 받아온 weather 데이터를 디코딩하기 위한 Type입니다.

기술적 도전

⚙️ ModernCollectionView - CompositionalLayout

펼쳐보기
  • 확장성을 위해 CollectionView를 사용하여 TableView를 구성하고자 하였습니다. 추후에 요구사항이 Grid 형으로 변경되어도 빠른 대응이 가능하다고 생각하였습니다
  • 하지만 개발을 모두 마친 후, 개발 속도를 고려한다면 비교적 쉬운 TableView를 사용하는 것이 빠른 프로젝트 진행에 도움이 될것이라는 생각도 들었습니다.
  • 향후 Step2에서 해당 부분은 UICollectionViewList 혹은 UITableView로 변경될 예정입니다.

⚙️ DiffableDataSource

펼쳐보기
  • 기존의 DataSource를 경험해보고 새롭게 Diffable Data Source를 사용해보고자 하였습니다.
  • DiffableDataSource의 장점은 데이터 동기화, 데이터 추가, 업데이트, 삭제시 Animate적용이 가능하다는 점 입니다.
  • 또한 기존의 DataSource보다 코드량도 감소시킬수 있다는 점이 있습니다.
  • 그리고 Section마다 다른 데이터들을 적용할 수도 있다는 점이 장점이라고 생각합니다.
  • DiffableDataSource를 적용해보면서 코드의 길이가 길어지는 부분은 typealias를 활용하였습니다.
  • 아직 Animation을 적용하는 부분이 많지 않아 기존 DataSource와 비교해서 장점에 대한 체감은 못하고 있습니다. 하지만 추후 Animation을 적용하는 부분이 많아진다면 Diffable DataSurce로 사용자 경험을 높힐 수 있다는 점은 큰 장점이라고 생각이 되어 도입해보았습니다.

⚙️ DataFormatter, Locale의 사용

펼쳐보기
  • 새로운 Diary를 추가 할 때 지역에 맞는 날짜, 날짜 표기법을 수동적으로 선택해주는 것이 아닌 자동적으로 반환해주기 위해 DataFormatter를 추가해 주었습니다.
  • 사용자의 기기 preferredLanguage에 따라 날짜의 표기방법이 자동으로 변경되도록 구현하였습니다.. (세계화, Internationalization)
  • 사용자의 위치에 따라 날짜가 변할 수 있도록, Locale을 활용하여 Localization(지역화)를 해주었습니다.
extension Formatter {
    static func changeCustomDate(_ date: Date) -> String {
        let locale = NSLocale.preferredLanguages.first
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: locale ?? Locale.current.identifier)
        formatter.timeZone = TimeZone(identifier: TimeZone.current.identifier)
        ...
        return formatter.string(from: date)
    }
}

⚙️ Swipe

펼쳐보기
  • 각 메인화면의 List의 Cell을 Swipe시 Share, Delete할 수 있는 기능이 필요하였습니다.
  • Diary App에서 UICollectionLayoutListConfiguration를 사용하였기 때문에 UISwipeActionsConfigurationconfiguration.trailingConfiguration에 추가해주었습니다.
    var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
        configuration.trailingSwipeActionsConfigurationProvider = .some({ indexPath in
            self.delegate?.makeSwipeActions(for: indexPath)
        })
  • 그리고 ShareAction, DeleteAction을 추가 구현해주었습니다.

⚙️ NSMutableAttributedString 사용

펼쳐보기
  • NSMutableAttributedString은 문자열의 특정 부분에 원하는 속성을 주고 싶을 때 사용하는 객체입니다. 특정 문자열만 다른 폰트, 다른 Color을 부여할 수 있습니다.
  • 하나의 TextView에서 첫줄(Title)의 Text과 그 이외의 Text를 다른 Font를 적용하고자 하였습니다.
  • 또한 AttributeString을 사용함에 따라 커서의 맨마지막으로 이동됨에 따라 selectedTextRange를 사용하여 올바른 커서의 위치로 이동시켜주었습니다.

⚙️ CoreData Migration, Relationship

펼쳐보기
  • CoreData에 WeatherData 추가가 필요했기 때문에 Data Migration을 사용하였습니다. CoreData Migration은 Managed Object와 원본 간의 차이점을 자동으로 유추합니다. 이때 WeatherData를 추가만 했기때문에 별도의 MappingModel은 만들지 않았습니다.
  • Migration을 진행을 한 후에, Weather Data와 기존의 Diary Data가 데이터의 성향이 다르기 때문에 하나의 Entity에 있는 것은 맞지 않다고 느껴졌습니다.
  • 때문에, Entity들 간의 관계를 정의해 줄 수 있는, Entity들 간의 영향을 설명해줄 수 있는 Relationship을 추가해주었습니다.
  • 추가를 한후에는 하나의 Entity에서 두 개의 Entity로 나눠졌기 때문에 별도의 MappingModel 또한 추가해 주었습니다.
DiaryData WeatherData

⚙️ Open API

펼쳐보기
  • Device의 위도 경도를 바탕으로 현재 날씨 및 날씨와 관련된 Icon을 받아오기 위해 Openweather API를 사용하였습니다.
  • NetworkManager라는 클래스를 만들고 내부에서 API와 통신을 할 수 있는 메서드를 구현해주었습니다.

⚙️ Core Location

펼쳐보기
  • Core Location은 iOS 기본 Framework인 Core Service에 속해있는 Framework입니다.
  • 기본적으로 iPhone Device의 위치를 얻어올 수 있는 Framework입니다.
  • Core Location을 사용하기 위해서는 CLLocationManger라는 인스턴스를 반드시 사용해야합니다.
  • 또한, Info.plist에서 위치 권한을 허용할 수 있는 정보 제공 Alert을 띄워줘야합니다.
  • 해당 프로젝트에서는 일기를 입력할 때, 위치를 불러와서 위치에 대한 날씨 정보(날씨 아이콘)을 Navigation Bar에 보여줘야하고, Main 화면에서 저장한 날씨 정보에 대한 날씨 아이콘을 보여줘야했기 때문에 위도, 경도 저장을 위해 사용을 하였습니다.
if let coordinate = locations.last?.coordinate {
    let url = NetworkRequest.fetchData(lat: String(coordinate.latitude), 
    lon: String(coordinate.longitude)).generateURL()
...
  • 위 코드처럼 coordinate에서 위도, 경도를 받아 올 수 있습니다.

트러블 슈팅 및 고민

🔥 Keyboard 사용에 따른 AutoLayout Constraint 변경에 대한 고민

펼쳐보기

문제 👀

  • 저희는 키보드 사용에 따라 TextView의 제약조건을 변경하여 TextView가 키보드를 제외하고 보여지도록 했습니다. 하지만 TextViewBottom제약을 변경해주는 방식으로 구현했습니다.
위의 사진처럼 Constarint의 충돌이 일어났습니다. **해결 🔥** - `func setupConstraints()` 내부에서 초기 Constraint를 잡아 줄 때, TextView의 Bottom Constraint까지 잡아주고, 키보드 나타남에 따라 다시 제약을 추가적으로 잡아주기 때문에 발생했습니다. - 따라서 기존 Constraint을 `false`로 변경하고 새로운 제약을 `true` 해야 충돌나지 않기 때문에 주의해서 Constraint를 잡아주어야 합니다.

🔥 UIComponent Object 생성시 중복코드 감소에 대한 고민

펼쳐보기

문제 👀

  • UIComponent를 View에서 생성할때 클로저를 이용하여 생성하였었습니다. 여러개의 Label, StackView가 필요한 상황에서 코드의 중복성이 느껴졌고 비효율적이라고 생각이 들었습니다.

해결 🔥

  • 2개 이상 사용되는 UIComponent들에 대해서 Extension으로 저희가 원하는 convenience initializer을 생성해주었습니다.
  • 이렇게 구현의 결과 1개의 UIComponent를 생성할때, 기존보다 코드량이 1/5 줄로 감소하였습니다.
// Befor 
private lazy var bottomStackView: UIStackView = {
    let stackView = UIStackView(arrangedSubviews: [dateLabel, previewLabel])
    stackView.spacing = 5
    stackView.axis = .horizontal
    stackView.alignment = .center
    stackView.distribution = .fill
    return stackView
}()

// After
private lazy var bottomStackView = UIStackView(subview: [dateLabel, previewLabel],
                                            spacing: 5,
                                            axis: .horizontal,
                                            alignment: .firstBaseline,
                                            distribution: .fill)

🔥 Add, Modify 기능을 하나의 View, 하나의 Controller로 구현

펼쳐보기
  • Diary를 ADD하고, Modify하는 Controller의 역할과 View가 매우 유사하다고 생각을 했습니다.
  • MainViewController에서 Modify를 할 때는 indexPath를 통해 데이터를 전달하고, + 버튼을 눌러 추가할때는 nil을 전달하여 Controller가 Add기능, Modify기능을 분기처리 할 수 있도록 구현하였습니다.
  • 두 가지의 기능을 하나로 하였을 때의 장점은 로직은 추가되지만 전체적인 코드량 감소, 관리할 Class가 적어진다는 점이라고 생각이 듭니다.
  • 하지만, 두 개의 컨트롤러를 사용하면 로직이 간결해진다는 점, 명확하다는 점에서 이점이 있다고 생각이 들었습니다.

🔥 UISwipeActionsConfiguration 추가

펼쳐보기
  • 리스트를 구현하기위해 UICollectionLayoutListConfiguration을 사용하였습니다.
  • CollectionViewConfiguration 구성은 View의 역할이라고 생각이 되어 View 내부에 CollectionView Configure을 하는 메서드를 구현하였습니다.
  • 후에, Swipe를 구현해야했을 때 View에서 구현한 Configure하는 메서드에서 SwipeActione들을 추가해주어야 했고, Swipe 기능을 만들기 위해서는 Controller의 Snapshot에 대한 접근, Delete Swipe 기능을 위한 CoreData에 대한 접근을 필요로 하였습니다.
  • ViewController에서 CollectionView 혹은, Configure을 View 생성시점에 주입하는 방법도 좋겟다고 생각했지만, Controller의 역할에서 벗어난 기능을 수행한다고 판단하여 Delegate 패턴을 사용하여 SwipeConfiguration을 전달하였습니다.

🔥 NSMutableAttributedString 사용에 따른 TextView내부 텍스트 커서 이동

펼쳐보기
  • 처음에는 textFieldtextView에 만들었습니다. 각각에 해당하는 폰트를 사용했습니다.
    • textField : title3
    • textView : body
  • 그렇기 때문에 Field, View 간의 간섭이 없기 때문에 폰트에 대해서 신경을 많이 써주지 않았습니다.
  • 많은 생각 후, 우리는 아이폰 메모앱을 참고하여 사용을 고려하여, 하나의 TextView로 보여주고자 했습니다.
  • 그렇기에 NSMutableAttributedString을 사용하여 첫번째 Title 부분만을 다른 폰트로 적용하였습니다.

문제 👀

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { ... }
    
func textViewDidChange(_ textView: UITextView) { ... } 
  • 텍스트 입력시 즉각적으로 Font 변경을 위해 위의 두 메서드를 사용하였는데,
  • NSMutableAttributedString 이 호출될 때마다 텍스트 커서가 텍스트의 마지막에 위치하게 되었습니다.

해결방법 🔥

  • AttributeString을 사용하면서 텍스트 입력시 매번 AttributeString이 호출 되기 때문에 텍스트 커서가 맨 마지막으로 움직이는 것을 확인했습니다.
  • 그래서 텍스트의 변화가 감지되는 Delegate인 textViewDidChange를 이용했습니다.
  • 텍스트가 감지되고 커서의 위치를 기억한 후 AttributeString을 호출합니다. 그다음 원래 커서로 돌아가도록 만들어 주었습니다.
  • textRange(from: to:) 및 UITextPosition()을 사용하여 커서의 위치를 기억하도록 했습니다.
func textViewDidChange(_ textView: UITextView) {
        guard let range = textView.selectedTextRange else { return }
        guard let position = textView.position(from: range.start, offset: 0) else { return }
        ...
        AttributeString()
        ...
        textView.selectedTextRange = textView.textRange(from: position, to: position)
    }
  • 위와같이 텍스트를 기억 후 selectedTextRange를 통해 커서를 이동시켜 해결했습니다.

🔥 prepareForReuse 사용 시 Super 호출

펼쳐보기
  • 셀 재사용시 초기화를 위해 prepareForReuse를 호출했습니다.
  • 하지만 아래의 사진과 같이 super를 호출하지 않고 사용하다보니 셀의 모양이 변형되는 현상이 발생했습니다.
super 호출 super 미호출
  • 그래서 override 할 때는 super 호출에 대해 한번더 생각해야 한다는 교훈을 얻었습니다....

🔥 Image load 속도 개선

펼쳐보기
  • Data(contentsOf: url)를 사용하여 이미지 데이터를 가져왔었습니다.
  • 하지만 매우 느리게 아이콘이 업로드가 되었고, Data(contentsOf: url) 메서드를 알아본 결과 해당 메서드는 내부적으로 동기로 동작하기 때문에, 동작이 매우 느리다는 것을 알게되었습니다.
  • 또한 해당 메서드는 URLSession과 달리 작업에 대한 진단을 수행할 수 없었습니다.
  • URLSession에서는 오류가 네트워크 오류인지, HTTP 오류인지, contents 오류 인지 등을 판할 수 있는 반면 Data(contentsOf:)에서는 이를 확인할 수 없었습니다.
  • 따라서 이미지들을 dataTask를 활용하는 dataTask를 활용한 fetchData() 메서드를 사용하여 속도를 개선할 수 있었습니다.

참고 링크

[공식문서]

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •