/TouchSchool

Team8 - 8

Primary LanguageSwift

๐Ÿซ ํ•™๊ต ๋Œ€ํ•ญ์ „

๐Ÿ“– ๋ชฉ์ฐจ

  1. ์†Œ๊ฐœ
  2. ๊ฐœ๋ฐœํ™˜๊ฒฝ ๋ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  3. Tree
  4. ํ‚ค์›Œ๋“œ
  5. ํƒ€์ž„๋ผ์ธ
  6. ์‹คํ–‰ํ™”๋ฉด
  7. ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๐ŸŒฑ ์†Œ๊ฐœ

ํ•™๊ต๋ฅผ ์„ ํƒํ•˜๊ณ  ๊ฒŒ์ž„ํ™”๋ฉด์œผ๋กœ ๊ฐ€์„œ ํ™”๋ฉด์„ ํ„ฐ์น˜ํ•˜๋ฉฐ ์ ์ˆ˜๋ฅผ ์˜ฌ๋ฆฌ๋Š” ์•ฑ์ž…๋‹ˆ๋‹ค.

๐Ÿง‘๐Ÿปโ€๐Ÿ’ป ํŒ€์›

์ตœ๋™ํ˜ธ ํ™ฉ์„ฑ์ง„ ๊น€์„ฑ์—ฝ ๊น€ํ˜œ๋ž€ ์œค์ค€์„ฑ
Kakao-Talk-Photo-2023-09-19-15-30-11 1f

โš™๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๋ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

swift xcode Firebase Alamofire GoogleMobileAds

๐ŸŒฒ Tree

๐Ÿ“ฆ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 ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ ํ›„ ์žฌ์‚ฌ์šฉ ๋กœ์ง์œผ๋กœ ๋ณ€๊ฒฝ
    • ์˜ค๋””์˜ค ์žฌ์ƒ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์ฒ˜๋ฆฌ

๐Ÿ“ฑ ์‹คํ–‰ ํ™”๋ฉด

์•ฑ ์‹คํ–‰ ํ•™๊ต์„ ํƒ
๊ฒŒ์ž„ํ™”๋ฉด ๋žญํ‚นํ™”๋ฉด

โ“ ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

Step1

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ํด๋กœ์ €๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
  • ์ด ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ ํ„ฐ์น˜ ์ด๋ฒคํŠธ๋ฅผ ๋™์‹œ์— ๊ฐ์ง€ํ•˜๊ณ  ์‘๋‹ตํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.

Step 2

ํ„ฐ์น˜ ํšจ๊ณผ์Œ ๊ด€๋ จ ์—๋Ÿฌ
  • 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
            }
        }
    }
}

Step 3

๋ฐฐํฌ ํ›„ ์ด์šฉ์ž ํ›„๊ธฐ๋กœ ์•Œ๊ฒŒ๋œ ์˜ค๋ฅ˜
  • 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
        }
        
    }