ํ๊ต๋ฅผ ์ ํํ๊ณ ๊ฒ์ํ๋ฉด์ผ๋ก ๊ฐ์ ํ๋ฉด์ ํฐ์นํ๋ฉฐ ์ ์๋ฅผ ์ฌ๋ฆฌ๋ ์ฑ์ ๋๋ค.
์ต๋ํธ | ํฉ์ฑ์ง | ๊น์ฑ์ฝ | ๊นํ๋ | ์ค์ค์ฑ |
---|---|---|---|---|
๐ฆ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
}
}