Trip On Smart Phone A-Z, TOPAZ
🌎 여행을 가기엔 시간도, 상황도 안되는 요즘, 저희가 생생한 여행경험을 통해 여행의 즐거움을 다시 느끼게 해드릴게요!
나만의 커스텀 배경 음악과 함께 여행 경험을 공유할 수 있는 앱 서비스
- 이메일 회원가입, 로그인 기능
- 시간대에 따라 다른 테마의 인터렉티브 지구 홈화면
- 대륙에 따른 여행지 추천, 검색 기능
- 커뮤니티 게시글 작성 / 수정 / 삭제 / 검색
- 게시글과 함께 작성할 수 있는 커스텀 배경음악 기능
- 내가 쓴 글, 프로필, 차단유저, 여행등급, 수집품 관리 기능
- 개발 인원
- 디자이너 2명, iOS개발 1명
- 개발 기간
- 2022.10 - 2023.01 (3개월)
- iOS 최소 버전
- iOS 14.0+
-
활용기술 및 키워드
- iOS : swift 5.7, xcode 14.0, UIKit, SceneKit
- Network : URLSession, Firebase
- UI : StoryBoard
-
라이브러리
라이브러리 | 사용 목적 | Version |
---|---|---|
SwiftySound | 배경음악 처리 | - |
Kingfisher | 이미지 처리 | 7.0 |
lottie-ios | 스플래시, 로딩 인디케이터 | - |
StoryBoard + MVVM Architecture
- 3D Base UI등 Custom UI가 많아 커뮤니케이션과 전체적인 UI Flow관찰을 위해 StoryBoard 활용
- Manager객체에서 API Fetching 및 FireBase CRUD 구현 로직을 담당, ViewModel에서 호출
- Auth Token 저장 및 UserID 등 불필요한 API Call을 줄이기 위해 UserDefaults 저장소 병용
3D재질 맵핑을 SCN객체 전체에 하다보니 나눠져있던 객체 모듈들을 인식하지 못하는 문제 발생
- Interaction을 담당하는 SCN파일과 UI를 담당하는 SCN파일을 나눠서 SCNScene을 각각 제작
- hitTest함수를 통해 SCNView의 Geometry Name에 접근, 각각의 Enum값과 결과 맵핑
class EarthNode: SCNNode {
override init() {
super.init()
//Interaction SCNScene
let earthBound = SCNScene(named: "Assets.scnassets/earth_isolate.scn")!
let earthBoundArr = earthBound.rootNode.childNodes
earthBoundArr.forEach { childNode in
self.addChildNode(childNode as SCNNode)
}
//UI SCNScene
let earth = SCNScene(named: "Assets.scnassets/Earth.scn")!
let earthArr = earth.rootNode.childNodes
earthArr.forEach { childNode in
self.addChildNode(childNode as SCNNode)
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
let point = gestureRecognize.location(in: sceneView)
let hitResults = sceneView.hitTest(point, options: [:])
if hitResults.count > 0 {
let result = hitResults[0]
guard let continentTitle = result.node.geometry?.name else { return }
if continent.continentName.contains(continentTitle) {
continentButton.setTitle(continentTitle, for: .normal)
if let count = continent.continentName.firstIndex(of: continentTitle) {
continentCount = count
}
}
}
사용자가 커스텀한 배경음악을 FireBase Storage에 저장할 때, 음악 파일 자체를 저장하면 같은 음악 파일이 중복되어 저장되는 상황이 발생
- mp3파일이 아닌 '선택된 음악 이름'과 '사운드 값'를 String과 Int 배열로 저장하는 방식으로 접근
- 현재 재생되고있는 음악의 사운드 값을 Volume이라는 인스턴스로 제공해주는 SwiftySound 프레임워크 발견, 사용
- 사용자의 PanGesture의 point값과 volume을 연동시키고, 이를 배열로 저장해서 FireBase에 저장
var soundEffectArr = [Sound?]()
var musicNameArr = Array(repeating: "", count: 4)
func playBackgroundMusic(fileName: String, isFirst: Bool = false) {
DispatchQueue.global().async {
let url = Bundle.main.url(forResource: fileName, withExtension: "mp3")!
self.backgroundMusic = Sound(url: url)
self.backgroundMusic!.play(numberOfLoops: -1)
let volume = isFirst ? self.musicVolumeArr[0] : 0.5
self.backgroundMusic?.volume = volume
}
}
@objc func drag(sender: UIPanGestureRecognizer) {
//드래그 했을 때의 위치
let translation = sender.translation(in: self.view)
let x = sender.view!.center.x
sender.setTranslation(.zero, in: self.view)
//부가요소들 또한 같이 움직이게 설정
let index = sender.view!.tag
progressBarProgress[index-1].constraints.forEach { constraint in
if constraint.firstAttribute == .height {
constraint.constant = 1.1 * (180 - sender.view!.center.y)
}
}
//소수점값에 따라 Sound volume 조절
let volumeFloat = Float(0.009 * (170 - sender.view!.center.y))
if index == 1 {
backgroundMusic?.volume = volumeFloat
} else {
if sender.view?.subviews.first?.alpha != 0 {
soundEffectArr[index-2]?.volume = volumeFloat
}
}
}
- 변경 후, 게시글 하나당 평균 용량 4.05MB에서 1.87MB로 감소
- Storage 전체 용량도 398MB에서 121MB로 감소
각 나라별로 API Call을 하다보니 API 한도가 너무 빨리 소모되는 상황이 발생
- NSCache를 이용한 메모리 캐싱과 FileManager을 이용한 디스크 캐싱을 함께 사용
- FileManager 저장 시, 1시간 단위의 만료 기간을 UserDefaults에 함께 저장, API Call 초기화 주기와 Sync
- UIImage 리사이징을 통해 파일시스템 용량의 과중화 또한 방지
func getImage(urlString: String, fileName: String, completion: @escaping (UIImage) -> Void) {
//Memory Caching
if let cachedImage = getMemoryImage(fileName: fileName) {
completion(cachedImage)
return
}
//Disk Caching
if let diskImage = getDiskImage(fileName: fileName) {
//Set Memory Cache
cache.setObject(diskImage, forKey: fileName as NSString)
completion(diskImage)
return
}
guard let url = URL(string: urlString) else { return }
let urlRequest = URLRequest(url: url)
URLSession.shared.dataTask(with: urlRequest) { [weak self] (data, response, error) in
if let error = error {
print(error)
} else if let response = response as? HTTPURLResponse, let data = data {
print("Status Code: \(response.statusCode)")
guard let image = UIImage(data: data) else { return }
//Set Memory Cache
self?.cache.setObject(image, forKey: fileName as NSString)
//Set Disk Cache
self?.repository.addImage(image: image, fileName: fileName)
DispatchQueue.main.async {
completion(image)
}
}
}.resume()
}
- 변경 후, View당 평균 Unsplash API Call 27회에서 4회로 감소
- 앱 내 Document파일도 최대 32.5MB이상 증가하지 않아 메모리에 큰 부담을 주지 않음