- 메모일지도 앱은 Swift로 구현한 지도기반 메모 앱입니다.
일상 속, 스쳐 지나가는 수많은 장소들은 종종 생각보다 더 깊은 의미를 지닐 때가 있습니다. 이런 장소들에 담긴 우리의 추억과 경험은 시간이 지나면서 희미해지기 쉬운데, 만약 이러한 순간들을 보다 구체적으로 기록으로 남길 수 있다면 어떨까요?
지도를 기반으로 메모와 사진을 남길수 있는 앱입니다.
- 글과 사진을 기록
- 지도
- 장소 검색
- 커스텀 마커
- 클러스터링
- 날짜별 메모 찾기
3/4 ~ 3/24 ( 약 3주간 )
- UIKit / MapKit
- MVVM / Facade / Router / Repository / strategy / SingleTone
- URLSession / Decodable
- CodeBaseUI / SnapKit / Compositional
- Realm(Swift) / FireBase Analytics/Crashlytics
- FloatingPanel / IQKeyboard / Toast
- API : KAKAO REST API ( 키워드로 장소 검색하기, 좌표로 주소 변환하기 )
Custom Observable 클래스를 생성하여 MVVM Input-output 패턴을 통해 비즈니스 로직을 분리하여 재사용성을 높였습니다.
API 요청 시 각각의 에러들을 직접 핸들링 하기 위해 Router 패턴과 전략적 패턴을 섞어 각 API의 에러코드나 URLResponse 등의 에러들을 핸들링 하였습니다.
EmbeddedObject와 LinkingObject를 통해 1 : N 관계를 컨트롤 하였으며, 각각의 에러들을 Enum으로 정의해 에러를 컨트롤 하였습니다.
- 사용자들이 실사용 시 문제가 발생하면, 해당 부분들을 분석하고 보완하기 위해 Crashlytics를 적용하였습니다.
- 또한 어떠한 뷰에서 많은 이탈이 발생하였는지 분석하기 위해 Analytics를 적용하였습니다.
| 온보딩 화면 | 장소 검색 | 마커 장소 등록 | 마커 이미지 변경 |
|---|---|---|---|
| Detail메모 작성 | Detail메모 작성 (추가) | Detail메모 수정 | Detail메모 삭제 |
|---|---|---|---|
| 메모 리스트뷰 | 설정 |
|---|---|
네트워크에 연결되어 있지 않은 상태에서도 사용할 수 있도록,
네트워크 상태를 감지하는 클래스를 만들어 사용자가 메모를 남기거나 사진을 남길 수 있도록 하였습니다.
import Network
final class NetWorkServiceMonitor {
static let shared = NetWorkServiceMonitor()
private let queue = DispatchQueue.global(qos: .background)
private let monitor: NWPathMonitor
public private(set) var isConnected: Bool = false
public private(set) var connectionType: ConnectionType = .unknown
enum ConnectionType {
case cellular
case ethernet
case unknown
case wifi
}
// 네트워크 상태 확인
public func startMonitor(){}
이미 사용자가 저장하였던 이미지를 수정하거나 삭제하였을 때
기존의 방식이었던 지우고 다시 쓰기는 데이터 처리와 성능에 있어 비효율적인 방법이라는 판단이 들었습니다.
이에 따라 기존의 데이터와 수정되어야 할 데이터를 비교하여 최소한의 비용으로
추가 삭제가 될 수 있도록 하였습니다.
var removeImageObject: [ImageObject] = []
var originalImageObject: [ImageObject] = []
........
// 원본 | A | B | C | >= index + 1
if (indexPath.item + 1) <= originalCount, originalCount != 0 {
// 지워질 에정으로 옮기기
let willRemove = emptyModel.value.originalImageObject[indexPath.item]
emptyModel.value.removeImageObject.append(willRemove)
emptyModel.value.originalImageObject.remove(at: indexPath.item)
}
........현재 프로젝트에는 여러 뷰에서 카메라와 이미지 권한을 요구하고, 이미지 받아야 하였습니다.
그때마다 해당하는 ViewController에서 권한을 확인하고 요구하고,
이미지를 처리하는 작업을 하는 것이 비효율적인 작업이라고 판단하여
Image 권한과 데이터를 전달하여 주는 클래스를 만들어 적용하였습니다.
enum ImagePickMode{
case camera // 한장만 할 경우
case maximum(Int) // 갤러리, 여러장이지만 최대정하기
}
enum ImageServiceError: Error {
case cantGetImage
}
/// 이미지 관련된 기능을 제공하는 서비스 클래스 입니다.
final class ImageService: NSObject {
// 이미지의 결과 타입
typealias ImageResult = ( Result<[UIImage]?, ImageServiceError> ) -> Void
/// 이미지 피커를 띄울 ViewController
private weak var presentationViewController: UIViewController?
/// 이미지 결과 핸들러
private var completionhandler: ( ( Result<[UIImage]?, ImageServiceError> ) -> Void )?
func pickImage(completion: @escaping ImageResult){ ... }
func checkCameraPermission(completion: @escaping (Bool) -> Void)
..........
}현재 앱은 한국에서만 사용할 수 있는 앱이지만, 한국에 사는 외국인들을 위해
현지화 작업을 통해 영어로도 앱을 사용할 수 있도록 현지화 작업을 했습니다.
// Localizable.strings (ko)
"Error_alert_title" = "에러";
"Alert_check_title" = "확인";
"Cancel_check_title" = "취소";
"Kakao_error_message_type1" = "서비스에 문제가 발생했습니다. 다시 시작해주세요!";
......
// Localizable.strings (en)
"Error_alert_title" = "Error";
"Alert_check_title" = "check";
"Cancel_check_title" = "Cancel";
"Kakao_error_message_type1" = "There was a problem with the service, please restart it!";
.....Panel을 보고 있는 동안 다른 뷰 컨트롤러 Panel을 띄어야 할 때 보고 있던 Panel이 내려가는 도중 새로운 Panel이 나오게 됨으로써 원래의 Panel이 deinit은 되었으나 화면에서 사라지지 않는 Issue ( UI 비동기 Issue ) 가 있었습니다.
Panel이 완전히 내려갔음을 @escaping으로 감지하여 새로운 Panel을 UI에 그리게 함으로써 문제를 해결하였습니다.
// MARK: 판넬을 내리고 싶을때
private func removeExistingPanelIfNeeded(completion: @escaping () -> Void) {
if let existingPanel = floatPanel {
existingPanel.removePanelFromParent(animated: true) { [weak self] in
self?.floatPanel = nil
completion()
}
} else {
completion()
}
} private func updateFloatingPanel(with configuration: PanelConfiguration) {
removeExistingPanelIfNeeded { [weak self] in
self?.setupPanel(with: configuration)
}
버튼이나 마커에 이미지를 적용할 때, 이미지 크기가 너무 커서 영역을 벗어나는 이슈가 있었습니다. 이미지 크기를 줄이기 위해 이미지 리사이징을 적용하였는데, 메모리 효율을 비교하기 위해 이미지 리사이징을 하지 않았을 때와 이미지 리사이징을 하고나서 저장하고 적용 하였을 때를 비교하여 최대한의 효율을 위해 이미지 리사이징을 한후 저장한 이미지를 불러오는 방법을 채택하였습니다.
(이미지 저장할 때 리사이징 후 저장)
// MARK: 이미지 리사이징
func resizingImage(targetSize: CGSize) -> UIImage?{
let widthScale = targetSize.width / self.size.width
let heightScale = targetSize.height / self.size.height
let minAbout = min(widthScale, heightScale)
let scaledImageSize = CGSize(width: size.width * minAbout, height: size.height * minAbout)
let render = UIGraphicsImageRenderer(size: scaledImageSize)
let scaledImage = render.image { [weak self] _ in
guard let self else { return }
draw(in: CGRect(origin: .zero, size: scaledImageSize))
}
return scaledImage
}




