최동호 | 황성진 | 김성엽 | 김혜란 | 윤준성 |
---|---|---|---|---|
📦TouchSchool
┣ 📂iOS
┃ ┣ 📂AD
┃ ┃ ┣ 📜BannerView.swift
┃ ┃ ┗ 📜BannerViewController.swift
┃ ┣ 📂Game
┃ ┃ ┣ 📜GameVM.swift
┃ ┃ ┗ 📜GameView.swift
┃ ┣ 📂Helpers
┃ ┃ ┣ 📂Font
┃ ┃ ┃ ┣ 📜Giants-Bold.otf
┃ ┃ ┃ ┗ 📜Recipekorea.ttf
┃ ┃ ┣ 📂Sound
┃ ┃ ┃ ┣ 📜buttomBGM.mp3
┃ ┃ ┃ ┣ 📜buttonBGM.mp3
┃ ┃ ┃ ┣ 📜errorBGM.mp3
┃ ┃ ┃ ┗ 📜mainBGM.mp3
┃ ┃ ┣ 📜ActivityIndicator.swift
┃ ┃ ┣ 📜Audio.swift
┃ ┃ ┣ 📜Colors.swift
┃ ┃ ┣ 📜Helpers.swift
┃ ┃ ┣ 📜infoView.swift
┃ ┃ ┣ 📜MultitouchRepresentable.swift
┃ ┃ ┗ 📜MultitouchView.swift
┃ ┣ 📂Main
┃ ┃ ┣ 📜MainVM.swift
┃ ┃ ┗ 📜MainView.swift
┃ ┣ 📂Model
┃ ┃ ┣ 📜School.swift
┃ ┃ ┗ 📜Smoke.swift
┃ ┣ 📂Rank
┃ ┃ ┗ 📜RankView.swift
┃ ┗ 📂Search
┃ ┃ ┣ 📜FirebaseManager.swift
┃ ┃ ┣ 📜SearchBar.swift
┃ ┃ ┣ 📜SearchGuide.swift
┃ ┃ ┣ 📜SearchVM.swift
┃ ┃ ┗ 📜SearchView.swift
┣ 📜ContentView.swift
┣ 📜GoogleService-Info.plist
┣ 📜Info.plist
┗ 📜TouchSchoolApp.swift
MVVM
URLSession
Alamofire
Firebase
Step 1 타임라인
- 23.10.11 ~ 23.10.17
- 프로젝트 시작
- 학교검색화면 구현
- 메인화면 구현
- 23.10.19 ~ 23.10.26
- 초,중,고등학교 데이터 가져와서 저장
- URLSession -> Alamofire 라이브러리 적용
- 학교정보 검색 시 필터링 기능 구현
Step 2 타임라인
- 23.11.02 ~ 23.11.03
- Firebase와 데이터 주고 받는 함수들 구현
- 학교 선택 시 Firebase에 추가 및 데이터 연결
- 배경화면 수정
- 깃 컨벤션 템플릿 추가
- 23.11.06 ~ 23.11.15
- 랭킹 화면 추가
- 게임 기능 구현 완료
- 앱 실행 시 메인화면이 먼저나오도록 로직 수정
- 23.11.16
- 앱 종료 후 들어왔을 때 데이터 남게 수정
- 터치시 이벤트 추가
Step 3 타임라인
- 23.11.17
- 비정상적인 값 검출 및 초기화 기능 구현
- 23.11.19 ~ 23.11.21
- UI 수정 및 sound데이터 추가
- 게임 화면 터치 애니메이션 추가
- 23.11.22
- 메인BGM, 터치BGM, 오류BGM 추가
- 게임화면 멀티터치 기능 구현
- 앱 아이콘 생성
- 23.11.23
- 커스텀 폰트 적용
- Sound 인스턴스 생성 후 재사용 로직으로 변경
- 오디오 재생 백그라운드 스레드에서 처리
앱 실행 | 학교선택 |
---|---|
게임화면 | 랭킹화면 |
---|---|
GameView 멀티 터치가 안되던 이슈
-
GameView
에서 화면을 터치할 때 여러 손가락으로 화면을 터치하면 먹히는 현상이 있었습니다. -
SwiftUI
는 직접적인 멀티터치 처리를 위한 API를 제공하지 않기에 기본적인onTapGesture
대신에 더 낮은 수준의 이벤트 처리를 사용했습니다. -
멀티터치 기능을 활성화하기 위해
UIViewRepresentable
프로토콜을 준수하는MultitouchRepresentable
과UIView
의 하위 클래스인MultitouchView
를 추가했습니다. -
makeUIView(context:)
이 메소드는MultitouchView
생성을 담당합니다.MultitouchView
의touchBegan
클로저를 설정합니다. 이 클로저는MultitouchView
에서 터치가 시작될 때마다 호출됩니다.
struct MultitouchRepresentable: UIViewRepresentable {
var touchBegan: ((CGPoint) -> Void)
func makeUIView(context: Context) -> MultitouchView {
let view = MultitouchView()
view.touchBegan = touchBegan
return view
}
func updateUIView(_ uiView: MultitouchView, context: Context) {
}
}
isMultipleTouchEnabled = true
이 코드를 통해 뷰가 여러 개의 동시 터치 이벤트를 감지할 수 있었습니다.touchesBegan(_:with:)
이는 뷰에서 새로운 터치가 감지될 때마다 호출되는UIView
의 메서드를 재정의합니다. 이 메서드는 각 터치를 처리하고 터치 위치와 함께touchBegan
클로저를 호출합니다.
import UIKit
class MultitouchView: UIView {
var touchBegan: ((CGPoint) -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
isMultipleTouchEnabled = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
touches.forEach { touch in
let location = touch.location(in: self)
touchBegan?(location)
}
}
}
- 사용자가 화면을 터치할 때마다
MultitouchView
의touchesBegan(_:with:)
가 호출되고, 이는 차례로touchBegan
클로저를 호출하게 됩니다. - 이 코드를 통해 여러 터치 이벤트를 동시에 감지하고 응답할 수 있게 해결했습니다.
터치 효과음 관련 에러
SoundSetting
클래스에서playSound
메서드로 버튼을 클릭하면 효과음이 나오는 효과를 주려고했습니다.playSound
메서드에서는 매번 새로운 `AVAudioPlayer 인스턴스를 생성하고 있었고, 이는 비효율적이며 성능 저하를 일으키고 있었습니다.- 또,
playSound
메서드가 메인 스레드에서 오디오를 재생하여 화면이 뚝뚝 끊기는 문제가 있었습니다.
//변경 전
class SoundSetting: ObservableObject {
static let instance = SoundSetting()
var player: AVAudioPlayer?
enum SoundOption: String {
case mainBGM = "mainBGM"
case buttonBGM = "buttonBGM"
case errorBGM = "errorBGM"
}
func playSound(sound: SoundOption) {
guard let url = Bundle.main.url(forResource: sound.rawValue, withExtension: ".mp3") else { return }
do {
player = try AVAudioPlayer(contentsOf: url)
player?.play()
player?.volume = 1
} catch {
print("재생하는데 오류가 발생했습니다. \(error.localizedDescription)")
}
}
- 각 사운드별로
AVAudioPlayer
인스턴스를 사전에 생성하고 저장하는 방식으로SoundSetting
클래스를 수정했습니다. - 또한, 오디오 재생을 백그라운드 스레드에서 수행하도록 변경했습니다.
// 수정 후
class SoundSetting: ObservableObject {
static let instance = SoundSetting()
private var players: [SoundOption: AVAudioPlayer] = [:]
enum SoundOption: String, CaseIterable {
case mainBGM = "mainBGM"
case buttonBGM = "buttomBGM"
case errorBGM = "errorBGM"
}
init() {
for sound in SoundOption.allCases {
if let url = Bundle.main.url(forResource: sound.rawValue, withExtension: "mp3") {
do {
let player = try AVAudioPlayer(contentsOf: url)
player.prepareToPlay()
players[sound] = player
} catch {
print("오디오 플레이어 초기화 실패: \(error)")
}
} else {
print("사운드 파일 로드 실패: \(sound.rawValue).mp3")
}
}
}
func playSound(sound: SoundOption) {
DispatchQueue.global().async {
if let player = self.players[sound], !player.isPlaying {
player.play()
player.volume = 0.1
}
}
}
}
배포 후 이용자 후기로 알게된 오류
GameView
에서 화면을 아주 많이 터치하다보면 어느순간부터 화면이 버벅이고 멈추는 현상이 있었습니다.- 팀원분들과 제작하면서 테스트를 할 때는 기능만 동작하는것만 확인되면 뒤로 돌아가 다른 기능 테스트를 진행하였기에 몰랐었던 오류였습니다.
- 초기 상태:
smokes
배열에 화면 탭 이벤트마다 새로운Smoke
객체가 추가되어 사용자가 화면을 많이 탭할수록 배열의 크기가 계속 증가하는 상태였습니다. - 배열의 크기가 커질수록, 각 탭 이벤트에 대해 더 많은
SmokeEffectView
인스턴스를 렌더링해야 했고, 이로 인해 UI가 버벅이는 성능 문제가 발생했습니다.
// 수정 전
ForEach(smokes.indices, id: \.self) { index in
let smoke = smokes[index]
if smoke.showEffect {
SmokeEffectView()
.rotationEffect(.degrees(smoke.angle))
.opacity(smoke.opacity)
.offset(x: smoke.location.x - UIScreen.main.bounds.width / 2,
y: smoke.location.y - UIScreen.main.bounds.height / 2)
.onAppear {
withAnimation(.linear(duration: 1)) {
smokes[index].opacity = 0
smokes[index].angle += 30
}
}
}
}
private func handleTap(location: CGPoint) {
let angle = Double.random(in: -30...30)
// Smoke 객체를 계속하여 추가
smokes.append(Smoke(location: location,
showEffect: true,
angle: angle,
opacity: 1))
myTouchCount += 1
soundSetting.playSound(sound: .buttonBGM)
vm.newAdd()
withAnimation {
self.animationAmount += 360
}
- 어떻게 해결해야할지 계속 생각하다가 FPS 게임에서 벽에 총을 계속하여 쏘다보면 총자국이 처음 쐈던거부터 사라지는게 생각이 났습니다.
- 배열 크기 제한: 먼저
smokes
배열의 크기를 30으로 제한하였습니다. - 코드 변경:
handleTap
함수에서 새로운Smoke
객체를 배열에 추가하기 전에 배열의 크기가 이미 30이면, 가장 오래된 요소(0번 인덱스)를 제거합니다. 그런 다음 새로운 요소를 배열에 추가합니다. - 결과: 이 방식은
smokes
배열의 크기를 일정하게 유지하여 각 탭 이벤트에 대해 일정한 수의SmokeEffectView
인스턴스만 렌더링하도록 보장하였고, 화면이 버벅이는 문제를 해결할 수 있었습니다.
// 수정 후
ForEach(smokes) { smoke in
if smoke.showEffect {
SmokeEffectView(smoke: smoke)
.rotationEffect(.degrees(smoke.angle))
.opacity(smoke.opacity)
.offset(x: smoke.location.x - UIScreen.main.bounds.width / 2,
y: smoke.location.y - UIScreen.main.bounds.height / 2)
.onAppear {
withAnimation(.linear(duration: 1)) {
smokes[smokes.firstIndex(where: { $0.id == smoke.id })!].opacity = 0
smokes[smokes.firstIndex(where: { $0.id == smoke.id })!].angle += 30
}
}
}
}
private func handleTap(location: CGPoint) {
let angle = Double.random(in: -30...30)
let newSmoke = Smoke(location: location,
showEffect: true,
angle: angle,
opacity: 1)
// 배열의 크기가 이미 30이면, 가장 오래된 요소(0번 인덱스)를 제거
if smokes.count >= 30 {
smokes.removeFirst()
}
smokes.append(newSmoke)
myTouchCount += 1
soundSetting.playSound(sound: .buttonBGM)
vm.newAdd()
withAnimation {
self.animationAmount += 360
}
}