Skip to content

seosieve/TopazIOS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

80 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TOPAZ - 모바일로 경험하는 세계여행

Trip On Smart Phone A-Z, TOPAZ







Download on the App Store


프로젝트 소개

🌎 여행을 가기엔 시간도, 상황도 안되는 요즘, 저희가 생생한 여행경험을 통해 여행의 즐거움을 다시 느끼게 해드릴게요!

나만의 커스텀 배경 음악과 함께 여행 경험을 공유할 수 있는 앱 서비스


프로젝트 주요 기능

  • 이메일 회원가입, 로그인 기능
  • 시간대에 따라 다른 테마의 인터렉티브 지구 홈화면
  • 대륙에 따른 여행지 추천, 검색 기능
  • 커뮤니티 게시글 작성 / 수정 / 삭제 / 검색
  • 게시글과 함께 작성할 수 있는 커스텀 배경음악 기능
  • 내가 쓴 글, 프로필, 차단유저, 여행등급, 수집품 관리 기능

프로젝트 개발 환경

  • 개발 인원
    • 디자이너 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 저장소 병용

트러블 슈팅

1. SceneView에서 사용자 Interaction이 작동하지 않는 문제

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
        }
    }
}

2. 사용자가 지정한 배경음악을 FireBase 저장소에 저장

사용자가 커스텀한 배경음악을 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로 감소

3. Unsplash API 메모리 & 디스크 캐싱

각 나라별로 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이상 증가하지 않아 메모리에 큰 부담을 주지 않음