
π μ±μ€ν μ΄ λ°λ‘κ°κΈ°
- κ°μΈ νλ‘μ νΈ
- 2023.09.25 ~ 2323.10.25(4μ£Ό)
- μ΅μ μ§μ λ²μ iOS 15.0
- μνλ μ₯μλ₯Ό κ²μνμ¬ μ§λμ μμΉλ₯Ό 보μ¬μ£Όκ³ κΈ°λ‘μ λ±λ‘ν μ μμ΅λλ€.
- μνλ μμΉλ₯Ό μ§λμμ μ°Ύμ κΈΈκ² ννμ¬ ν΄λΉ μμΉμ κΈ°λ‘μ λ±λ‘ν μ μμ΅λλ€.
- μ¬μ©μκ° λ±λ‘ν μμΉ λ¦¬μ€νΈλ₯Ό λ³Ό μ μκ³ , ν΄λΉ μμΉμ λ±λ‘λ κΈ°λ‘μ λ³Ό μ μμ΅λλ€.
- λ¬λ ₯μ ν΅ν΄ λ μ§ λ³ κΈ°λ‘μ λ³Ό μ μμ΅λλ€.
MVVM
RxSwift
,RxGesture
UIKit
,MapKit
Alamofire
,Codable
Realm
SnapKit
,Autolayout
DiffableDataSource
,CompositionalLayout
Firebase
Google Crashlytics
Push Notification
FSCalendar
,FloatingPanel
,Toast
Google Place API
DiffableDataSource
μCompositionalLayout
μ μ΄μ©νμ¬ CollectionView ꡬν- API ν΅μ μ
Alamofire
μRouter ν¨ν΄
μ μ μ©νμ¬ μ½λμ κ°λ μ±μ λμ΄κ³ , μ¬μ¬μ©μ±μ λμ MVVM ν¨ν΄
μ ν΅ν΄ λΉμ¦λμ€ λ‘μ§μ λΆλ¦¬νμ¬ ViewControllerμ μν μ μ€μ- UI ꡬν μ λΉμ·ν ꡬ쑰μ Viewλ€μ λͺ¨λν νμ¬ μ¬μ¬μ©μ±μ λμ
Localizing
μ ν΅ν΄ νκ΅μ΄μ μμ΄ λ²μ λμ
- Google PlaceAPIλ₯Ό ν΅ν΄ μ§λμμ μμμ μ₯μ μ 보λ₯Ό μ ννμ¬ μ£Όμκ°μ μλ΅λ°λ κ³Όμ μμ μ£Όμ μ λ³΄κ° λͺ ννμ§ μμ κ³³μ μ ννμμ λ μλ΅ κ°μ νμμ΄ λ¬λΌμ Έ μΈλ±μ€ μλ¬κ° λ°μνμκ³ , μΆμλ μ±μμ λΉμ μ μ’ λ£κ° λλ λ¬Έμ κ° λ°μνμλ€.
Firebase Crahlytics
λ₯Ό ν΅ν΄ μ±μ΄ λΉμ μ μ’ λ£ μ§μ μ νμ νμκ³ , 쑰건문μ ν΅ν΄ μλ΅ κ°μΌλ‘ λ°λ μ£Όμ λ°°μ΄μ κΈΈμ΄μ λ°λΌ μ΄λ¦ μ€μ λ°©μμ μ ννμ¬ ν΄κ²°νμλ€.

- λ΄λΆ 컨ν μΈ μ λμ΄μ λ°λΌ λμ μΌλ‘ λμ΄λ₯Ό μ‘°μ νλλ‘ estimatedλ₯Ό μ¬μ©νμλλ°, estimatedλ‘ μ νν λμ΄ κ³μ°μ΄ λμ§ μμ μ μ μ©μ΄ μλμλ€.
- Viewμ Drawing Cycleμ κ³ λ €νμ¬ λ°μ΄ν°κ° μ
μ μ½μ
λ ν
layoutIfNeeded()
λ₯Ό νΈμΆνμ¬ λ μ΄μμ μ λ°μ΄νΈλ₯Ό μμ²νμ¬ ν΄κ²°νμλ€.
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<InfoCollectionViewCell, Record> { cell, indexPath, itemIdentifier in
cell.titleLabel.text = itemIdentifier.title
cell.address.text = itemIdentifier.placeInfo[0].address
cell.dateLabel.text = DateFormatter.convertDate(date: itemIdentifier.date)
cell.layoutIfNeeded() // λ μ΄μμ μ
λ°μ΄νΈ μμ²
}
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
return cell
})
}
- MVVM ν¨ν΄μ μ μ©κ³Ό Repository ν¨ν΄μ μ΄μ©νμ¬ Realmμ CRUD μμ μ μννλ©΄μ νλμ κΈ°λ₯μ΄ μ¬λ¬ νμΌμ κ±°μ³ ViewControllerμ μ λ¬μ΄ λλ κ΅¬μ‘°κ° λμλ€. λ‘μ§μ λΆλ¦¬νκΈ° μν΄ μ¬λ¬ λ¨κ³λ₯Ό κ±°μ³€μ§λ§ μλ¬ νΈλ€λ§ μμ μ κ³μν΄μ μνν΄μΌ νκΈ° λλ¬Έμ κ°μ do-catchλ¬Έμ μ¬λ¬ λ² μμ±νλ λΉν¨μ¨ λ¬Έμ κ° λ°μνμλ€.
- Observable νμ μ ν΅ν΄ μλ¬ λ°μ μ κ°μ errorDescription κ°μ λ³κ²½νμκ³ ViewControllerμμ bindνμ¬ μλ¬ λ©μΈμ§λ₯Ό Alertλ₯Ό ν΅ν΄ μ¬μ©μμκ² λ³΄μ¬μ£Όμλ€.
// RecordViewModel.swift
var errorDescription: Observable<String?> = Observable(nil)
func savePlace(_ location: PlaceElement?) -> Place? {
guard let data = location else {
errorDescription.value = InvalidError.noExistData.errorDescription
return nil
}
let place = Place(placeId: data.id, address: data.formattedAddress, placeName: data.displayName.placeName, latitude: data.location.latitude, longitude: data.location.longitude)
do {
try placeRepository.createItem(place)
NotificationCenter.default.post(name: .databaseChange, object: nil, userInfo: ["changeType": "save"])
return place
} catch {
errorDescription.value = DataBaseError.createError.errorDescription
return nil
}
}
// RecordViewController.swift
private func bindData() {
viewModel.errorDescription.bind { data in
if let message = data {
self.showOKAlert(title: "", message: message) { }
}
}
}
- viewμ κ·Έλ¦Όμλ₯Ό λ£μμ λ λλ²κ·Έ μ°½μμ κ·Έλ¦Όμ λ λλ§μ λ§μ λΉμ©μ΄ λ€κΈ° λλ¬Έμ
shadowPath
λ₯Ό λ³κ²½νλΌλ λ΄μ©μ κ²½κ³ κ° λνλ¬λ€. UIBezierPath
λ‘ κ·Έλ¦Όμλ₯Ό viewμ ν¬κΈ°μ λ§κ² μμ± νlayoutSubView()
λ΄μμ shadowPath κ°μΌλ‘ μ§μ νμ¬ ν΄κ²°νμλ€.
private func shadow() {
backView.layer.cornerRadius = backView.frame.size.width / 2
backView.layer.shadowColor = UIColor.black.cgColor
backView.layer.shadowOpacity = 0.4
backView.layer.shadowOffset = CGSize(width: 0, height: 0)
backView.layer.shadowRadius = 1
backView.layer.shadowPath = UIBezierPath(arcCenter: CGPoint(x: backView.bounds.width/2, y: backView.bounds.height/2), radius: backView.bounds.width / 2, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
}
- Geocoding API μ°λ μ Router ν¨ν΄μ μ μ©νμ¬ κ΅¬ννλ μ€ http load failed μ€λ₯κ° λ°μνμλ€.
- HTTPHeadersμ Content-Typeμ λν μ€μ μ νμ§ μμ νλΌλ―Έν° κ°μ΄ μ μμ μΌλ‘ μ μ‘λμ§ μμκΈ° λλ¬Έμ
application/json
μ header κ°μ μΆκ°νμ¬ ν΄κ²°νμλ€.
- 첫 μ±μ μΆμλ₯Ό νκΈ° μν΄ κ°λ°νλ©΄μ μκ°λ³΄λ€ κ³ λ €ν΄μΌ ν μμκ° κ΅μ₯ν λ§λ€λ κ²μ μμΌ κΉ¨λ¬μλ€. μκ°λ³΄λ€ κ³ λ €ν΄μΌ ν μμΈ μ¬νλ€μ΄ λ§μλ€. λ€μν μμΈμ²λ¦¬λ₯Ό ꡬννκ³ , νλ©΄ μ ν μ κ° μ λ¬κ³Ό μ λ°μ΄νΈ λ± κΉκ² μκ°ν΄μΌ ν μμλ€μ΄ λ§μλ€.
- κΈ°ν λ¨κ³μμ λ΄κ° λ§€μ° λΆμ‘±νμμ κΉ¨λ¬μλ€. UI ꡬμ±λ μ λ μ€λ₯΄μ§ μμμ λλ΅μ μΈ μ€ν 리보λλ§ κ·Έλ Έλλ κ³μν΄μ μμ νλ μΌμ΄ λ°μνκ³ , νμν κΈ°λ₯μ΄μ§λ§ κΈ°ν λ¨κ³μμ λμ³λ²λ¦° κΈ°λ₯λ€λ λ§μλ€.
- λ©λͺ¨λ¦¬ λμμ κ΄λ ¨νμ¬ μ λμμ νμ§ λͺ»ν κ² κ°λ€. ν΄λ‘μ ꡬ문μ λ§μ΄ μ¬μ©νκΈ° λλ¬Έμ λ§μ λ©λͺ¨λ¦¬ λμκ° μμ κ² κ°μλ° ν΄λΉ λΆλΆμμλ λ 곡λΆνμ¬ μ λ°μ΄νΈλ₯Ό ν΄μΌκ² λ€λ μκ°μ νκ² λμλ€.