VesselWheel

SceneDeleagte(for 러닝기록 타이머) 본문

Xcode Study

SceneDeleagte(for 러닝기록 타이머)

JasonYang 2024. 2. 29. 12:24

https://medium.com/@Ariobarxan/ios-application-scene-delegate-vs-app-delegate-a-talk-about-life-cycle-a2ecae9d507e

 

iOS Application Scene Delegate VS App Delegate(A talk about Life cycle)

Preface

medium.com


IOS 13부터는 AppDelegate와 SceneDelegate의 책임이 구분되었다.

-> 해석하자면,

AppDelegate는 

과거에 출시, 종료, 시스템 수준 이벤트 처리 등 앱의 전반적인 라이프사이클을 처리한다. 초기 앱 환경 설정, 앱 수준의 데이터 및 리소스 관리, 푸시 알림 처리 등을 담당한다.

반면에 SceneDelegate는 

여러 UI 인스턴스의 라이프사이클 및 구성을 관리하는 데 중점을 둔다. 활성화, 비활성화, 종료 등 장면과 관련된 이벤트를 처리한다. 

SceneDelegate는 각 장면에 대한 초기 UI를 설정하고 해당 장면 내의 변경 사항에 대응하는 역할을 한다.


러닝기록 기능을 구현하기 위해서 내가 하고자 하는 것은

화면이 포그라운드에서 백그라운드로 갈 때와 백그라운드에서 포그라운드로 갈 때, 

러닝기록 프로퍼티를 저장 및 호출하여, 앱의 activate 상태에 따라 백그라운드에서 별도로 Task를 할당하지 않아도 러닝프로퍼티를 활용하고자 한다. 


 

https://developer.apple.com/documentation/uikit/uiscenedelegate/3197919-scenewillresignactive/

 

sceneWillResignActive(_:) | Apple Developer Documentation

Tells the delegate that the scene is about to resign the active state and stop responding to user events.

developer.apple.com

https://developer.apple.com/documentation/uikit/uiscenedelegate/3197915-scenedidbecomeactive/

 

sceneDidBecomeActive(_:) | Apple Developer Documentation

Tells the delegate that the scene became active and is now responding to user events.

developer.apple.com

 

SceneDelegate의 내장 매소드를 보면 아래와 같다. 

더보기
//
//  SceneDelegate.swift
//  AlarmButler
//
//  Created by mirae on 2/5/24.
//

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = CustomTabBarController() // 첫 화면의 ViewController를 설정
        window?.makeKeyAndVisible()
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.

        // Save changes in the application's managed object context when the application transitions to the background.
        (UIApplication.shared.delegate as? AppDelegate)?.saveContext()
    }


}

 

앱이 활성화, 즉 사용자가 앱을 탭하고 나서,

(앱을 처음 시작할 때는 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {} 이 매소드에서 rootViewController를 설정해준다.)

SceneDelegate 에서 화면의 active 상태와 관련된 매소드가 있다. 

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
    }

sceneDidBecomeActive는 화면이 active 되었을 때이고, sceneWillResignActive은 deactive 되었을 때 상태를 구현할 수 있다. 

 

    func sceneDidBecomeActive(_ scene: UIScene) {//
        MyTimer.shared.loadRunningRecord()
    }
    
    func sceneWillResignActive(_ scene: UIScene) { //
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
        MyTimer.shared.saveRunningRecord()
    }

-> SceneDelegate에 화면 전환에 관련된 타입 매소드에 싱글톤패턴으로 화면전환에 쓰일 매소드를 호출한 코드

 

    public func saveRunningRecord() {
        let defaults = UserDefaults.standard
        defaults.set(time, forKey: "time")
        defaults.set(distance, forKey: "distance")
        defaults.set(pace, forKey: "pace")
        defaults.set(datePaused, forKey: "datePaused")
    }
    
    public func loadRunningRecord() {
        let defaults = UserDefaults.standard
        time = defaults.integer(forKey: "time")
        distance = defaults.double(forKey: "distance")
        pace = defaults.double(forKey: "pace")
        datePaused = defaults.object(forKey: "datePaused") as? Date
        dateResumed = Date()
        if let datePaused = datePaused, let dateResumed = dateResumed {
            let elapsedTime = dateResumed.timeIntervalSince(datePaused)
            let additionalDistance = elapsedTime * pace
            time += Int(elapsedTime)
            distance += additionalDistance
            pace = distance / Double(time)
        }
    }

-> sceneWillResignActive으로 화면이 비활성화 되면, saveRunningRecord() 매소드에서 UserDefaults에 프로퍼티를 저장, 저장할 때 저장 시점의 시간을 저장

-> sceneDidBecomeActive으로 화면이 활성화되면, loadRunningRecord() 매소드에서  UserDefaults에 저장된 프로퍼티(time, distance, pace, dataPaused)를 호출, 호출할 때 저장 시점의 시간에 현재 시간의 차이 만큼을 더해서 타이머에 프로퍼티에 저장


현재시간 Date()를 적용한 타이머 클래스

더보기
//
//  RunningTimer.swift
//  Run-It
//
//  Created by Jason Yang on 3/1/24.
//
enum TimerState {
    case suspended //일시정지
    case resumed   //재개
    case canceled  //취소
    case finished  //종료
    case background
    case foreground
}

import Foundation
import UIKit
import CoreData

class RunningTimer {
    
    var state: TimerState = .suspended
    private var timer: DispatchSourceTimer?
    private var startTime = Date()

    private var pauseTime: Date?
    private var restartTime: Date?
    private var pauseDuration: Int = 0
    
    private var backgroundTime: Date?
    
    var time: Int = 0
    var distance: Double = 0.0
    var pace: Double = 0.0
    
    // UI 업데이트를 위한 클로져
    var updateUI: (() -> Void)?
    
    func start() {
        timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
        timer?.schedule(deadline: .now(), repeating: .seconds(1))
        timer?.setEventHandler { [weak self] in
            guard let self = self else { return }
            // Int(Date().timeIntervalSince(startTime))
            self.time = Int(Date().timeIntervalSince(startTime)) - self.pauseDuration
            self.distance += 0.01 // RunningTimerManager와 연계해서 변경 필요
            // 거리가 0.05km, 즉 50m 이상일 때만 페이스를 계산
            self.pace = self.distance >= 0.05 ? Double(self.time) / self.distance : 0
            print("running properties : \(self.time), \(self.distance), \(self.pace)")
            DispatchQueue.main.async {
                // updateUI 클로져를 호출
                self.updateUI?()
            }
        }
//        startTime = Date()
        timer?.resume()
        state = .resumed
    }
    
    func pause() {
        if state == .resumed {
            timer?.suspend()
            state = .suspended
            pauseTime = Date()
            print("pause properties : \(self.time), \(self.distance), \(self.pace)")
        }
    }
    
    func timerEnterBackground() {
        if state == .background {
            timer?.suspend()
            state = .suspended
            backgroundTime = Date()
        }
        print(backgroundTime ?? Date())
    }

    
    func restart() {
        if state == .suspended {
//            timer?.activate()
            timer?.resume()
            state = .resumed
            restartTime = Date()
            
            
            //현지시간 출력
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
            let restartTimeString = dateFormatter.string(from: restartTime ?? Date())
            print("Restart Time: \(restartTimeString)")
            
            if let pTime = pauseTime, let rTime = restartTime {
                pauseDuration += Int(rTime.timeIntervalSince(pTime))
            }
            print("restart properties : \(self.time), \(self.distance), \(self.pace)")
            DispatchQueue.main.async {
                self.updateUI?()
            }
        }
    }
    
    func timerWillEnterForeground() {
        if state == .foreground {
            let backgroundDuration = Date().timeIntervalSince(backgroundTime ?? Date())
            print("Background Duration: \(backgroundDuration)")
            time += Int(backgroundDuration)

            timer?.activate()
            timer?.resume()
            state = .resumed
            DispatchQueue.main.async {
                self.updateUI?()
            }
        }
        print("Forground properties : \(self.time), \(self.distance), \(self.pace)")
    }

    func stop() {
        timer?.cancel()
        state = .canceled
        timer = nil
    }
}

 

SceneDelegate에 화면 활성상태에 따른 타이머 클래스의 매소드 호출

더보기
//
//  SceneDelegate.swift
//  Run-It
//
//  Created by Jason Yang on 2/22/24.
//

import UIKit
import KakaoSDKAuth

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    let runningTimer = RunningTimer()

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let windowsScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: windowsScene)
        let tabBarController = UITabBarController()
        tabBarController.viewControllers = [RunningMapViewController(), BookmarkViewController(), ProfileViewController()]
        window?.rootViewController = MainTabBarViewController()  // 코드작업 간 자신의 ViewController로 변경하되, github commit 간에는 unstaged 처리
        window?.makeKeyAndVisible()
    }
    
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) 
    {
        if let url = URLContexts.first?.url
        {
            if (AuthApi.isKakaoTalkLoginUrl(url))
            {
                _ = AuthController.handleOpenUrl(url: url)
            }
        }
    }

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
    }

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
        runningTimer.state = .foreground
        runningTimer.timerWillEnterForeground()
    }

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).
    }

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.
    }

    func sceneDidEnterBackground(_ scene: UIScene) {
        runningTimer.state = .background
        runningTimer.timerEnterBackground()
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.

        // Save changes in the application's managed object context when the application transitions to the background.
        (UIApplication.shared.delegate as? AppDelegate)?.saveContext()
    }


}

 

 

RunningTimerViewController에서 RunningTimer 클래스 호출

더보기
//
//  RunningTimerViewController.swift
//  Running&Eat
//
//  Created by Jason Yang on 2/21/24.
//

import UIKit

import SnapKit

class RunningTimerViewController: UIViewController, PauseRunningHalfModalViewControllerDelegate {

    let runningTimer = RunningTimer()
    //MARK: - UI properties
    
    var distance: Double = 0
    var time: Int = 0
    var pace: Double = 0

    
    let statusBarView = UIView()
    
    let timeLabel: UILabel = {
        let label = UILabel()
        label.text = "시간"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 30)
        return label
    }()
    
    lazy var timeNumberLabel: UILabel = {
        let label = UILabel()
        label.text = "0:00:00"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 45)
        return label
    }()
    
    let topContainer : UIView = {
       let container = UIView()
        return container
    }()
    
    
    let distanceContainer = UIView()
    
    let topSplitLine: UIView = {
        let line = UIView()
        line.alpha = 0.5
        line.backgroundColor = .gray
        return line
    }()
    
    let middleSplitLine: UIView = {
        let line = UIView()
        line.alpha = 0.5
        line.backgroundColor = .gray
        return line
    }()
    
    let paceLabel: UILabel = {
        let label = UILabel()
        label.text = "페이스"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 30)
        return label
    }()
    
    lazy var paceNumberLabel: UILabel = {
        let label = UILabel()
        label.text = "0:00"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 45)
        return label
    }()
    
    let distanceLabel: UILabel = {
        let label = UILabel()
        label.text = "거리"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 30)
        return label
    }()
    
    lazy var distanceNumberLabel: UILabel = {
        let label = UILabel()
        label.text = "0.00"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 100)
        label.adjustsFontSizeToFitWidth = false
        return label
    }()
    
    let kilometerLabel: UILabel = {
        let label = UILabel()
        label.text = "킬로미터"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 30)
        return label
    }()
    
    lazy var pauseRunningButton: UIButton = {
        let button = UIButton()
        button.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        button.tintColor = .white
        let configuration = UIImage.SymbolConfiguration(pointSize: 50)
        if let image = UIImage(systemName: "pause.fill", withConfiguration: configuration) {
            button.setImage(image, for: .normal)
        }
        button.backgroundColor = .systemIndigo
        button.layer.cornerRadius = 50
        button.clipsToBounds = true
        
        button.addTarget(self, action: #selector(pauseRunning), for: .touchUpInside)

        return button
    }()
    
    let bottomView = UIView()
    
    //MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        addSubview()
        setupUI()
        setLayout()
        
        runningTimer.updateUI = { [weak self] in
            self?.time = self?.runningTimer.time ?? 0
            self?.distance = self?.runningTimer.distance ?? 0.0
            self?.pace = self?.runningTimer.pace ?? 0.0
            self?.updateTimerUI()
        }
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
//        self.runningTimer.start()
        if runningTimer.state == .suspended {
            runningTimer.start()
        } else if runningTimer.state == .background {
            runningTimer.timerEnterBackground()
        } else if runningTimer.state == .foreground {
            runningTimer.timerWillEnterForeground()
        }

    }
    
    // MARK: - @objc
    @objc private func pauseRunning() {
        print("TappedButton - pauseRunning()")
        self.runningTimer.pause()
        
        let pauseRunningHalfModalViewController = PauseRunningHalfModalViewController()
        pauseRunningHalfModalViewController.time = self.time
        pauseRunningHalfModalViewController.distance = self.distance
        pauseRunningHalfModalViewController.pace = self.pace
        pauseRunningHalfModalViewController.delegate = self

        
        showMyViewControllerInACustomizedSheet(pauseRunningHalfModalViewController)
    }
    
    func didDismissPauseRunningHalfModalViewController() {
        runningTimer.restart()
    }
    
}

extension RunningTimerViewController {
    
    // MARK: - Running Timer UI Update

    func updateTimerUI() {
        let hours = time / 3600
        let minutes = (time % 3600) / 60
        let seconds = (time % 3600) % 60
        timeNumberLabel.text = String(format: "%01d:%02d:%02d", hours, minutes, seconds)
        
        if self.distance >= 0.05 {
            let paceMinutes = Int(pace) / 60
            let paceSeconds = Int(pace) % 60
            paceNumberLabel.text = String(format: "%02d:%02d", paceMinutes, paceSeconds)
        } else {
            // 거리가 50m 미만일 때는 페이스를 표시하지 않음
            paceNumberLabel.text = "--:--"
        }
        
        distanceNumberLabel.text = String(format: "%.2f", distance)
    }

    // MARK: - setupUI
    
    private func setupUI() {

        view.backgroundColor = .systemGreen
        statusBarView.backgroundColor = .systemGreen
        bottomView.backgroundColor = .systemGreen
        
        [statusBarView, bottomView].forEach { subView in view.addSubview(subView)
        }

    }

    // MARK: - addSubview
    private func addSubview() {
        
        view.addSubview(statusBarView)
        
        view.addSubview(topContainer)
        topContainer.addSubview(timeLabel)
        topContainer.addSubview(timeNumberLabel)
        topContainer.addSubview(paceLabel)
        topContainer.addSubview(paceNumberLabel)
        topContainer.addSubview(topSplitLine)
        
        view.addSubview(distanceContainer)
        distanceContainer.addSubview(middleSplitLine)
        distanceContainer.addSubview(distanceLabel)
        distanceContainer.addSubview(distanceNumberLabel)
        distanceContainer.addSubview(kilometerLabel)
        
        view.addSubview(pauseRunningButton)
        
        view.addSubview(bottomView)

    }

    // MARK: - Layout
    private func setLayout() {
        
        statusBarView.snp.makeConstraints { make in
            make.leading.top.trailing.equalToSuperview()
            make.bottom.equalTo(view.safeAreaLayoutGuide.snp.topMargin)
        }

        timeLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(25)
            make.leading.equalToSuperview().offset(2)
        }

        timeNumberLabel.snp.makeConstraints { make in
            make.top.equalTo(timeLabel.snp.bottom).offset(40)
            make.leading.equalTo(timeLabel)
        }

        paceLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(25)
            make.trailing.equalToSuperview().offset(-10)
        }

        paceNumberLabel.snp.makeConstraints { make in
            make.top.equalTo(paceLabel.snp.bottom).offset(40)
            make.trailing.equalTo(paceLabel)
        }

        // topContainer의 정중앙에 수직의 선
        topSplitLine.snp.makeConstraints { make in
            make.centerY.equalToSuperview()
            make.centerX.equalToSuperview()
            make.height.equalTo(150)
            make.width.equalTo(1) //
        }

        topContainer.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top)
            make.width.equalTo(360)
            make.height.equalTo(200)
        }

        distanceContainer.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.top.equalTo(topContainer.snp.bottom)
            make.width.equalTo(360)
            make.height.equalTo(330)
        }

        middleSplitLine.snp.makeConstraints { make in
            make.top.equalToSuperview()
            make.centerX.equalToSuperview()
            make.height.equalTo(1)
            make.width.equalTo(350) //
        }
        
        distanceLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(25)
            make.leading.equalToSuperview().offset(10)
        }
        distanceNumberLabel.snp.makeConstraints { make in
            make.top.equalTo(distanceLabel.snp.bottom).offset(40)
            make.centerX.equalToSuperview()
        }
        kilometerLabel.snp.makeConstraints { make in
            make.top.equalTo(distanceNumberLabel.snp.bottom).offset(5)
            make.centerX.equalToSuperview()
        }
        
        pauseRunningButton.snp.makeConstraints { make in
            make.top.equalTo(distanceContainer.snp.bottom).offset(50)
            make.centerX.equalToSuperview()
            make.width.equalTo(100)
            make.height.equalTo(100)
        }
        
        bottomView.snp.makeConstraints { make in
            make.leading.bottom.trailing.equalToSuperview()
            make.top.equalTo(view.safeAreaLayoutGuide.snp.bottomMargin)
        }

    }
    

}

 

RunningTimerViewController에서 PauseRunningHalfModalViewController을 노출시키는 하프모달 extenstion

더보기
//
//  HalfModal.swift
//  Run-It
//
//  Created by Jason Yang on 2/23/24.
//

import Foundation

// In a subclass of UIViewController, customize and present the sheet.
extension RunningTimerViewController {
    func showMyViewControllerInACustomizedSheet(_ viewControllerToPresent: PauseRunningHalfModalViewController) {
        if let sheet = viewControllerToPresent.sheetPresentationController {
            sheet.detents = [.medium()] // 모달의 높이를 중간.medium로 설정하고, .large()를 추가하면 크게.large로 설정합니다.
            sheet.prefersGrabberVisible = true
            sheet.largestUndimmedDetentIdentifier = .medium // 최대 확장 시 어둡게 표시되지 않도록 설정
            sheet.prefersScrollingExpandsWhenScrolledToEdge = false // 모달 내부 스크롤 시 확장되지 않도록 설정
            sheet.prefersEdgeAttachedInCompactHeight = true // 컴팩트 높이에서 모달이 화면 가장자리에 붙도록 설정
            sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true // 모달의 너비가 preferredContentSize를 따르도록 설정
        }
        present(viewControllerToPresent, animated: true, completion: nil)
    }
}

 

 

PauseRunningHalfModalViewController에서 RunningTimer 클래스 호출

더보기
//
//  PauseRunningHalfModalViewController.swift
//  Run-It
//
//  Created by Jason Yang on 2/23/24.
//
protocol PauseRunningHalfModalViewControllerDelegate: AnyObject {
    func didDismissPauseRunningHalfModalViewController()
}

import UIKit

class PauseRunningHalfModalViewController: UIViewController {
    
    let runningTimer = RunningTimer()
    
    weak var delegate: PauseRunningHalfModalViewControllerDelegate?
    
    //MARK: - UI properties
    
    var time: Int = 0
    var distance: Double = 0.0
    var pace: Double = 0.0
    
    let modaltopContainer : UIView = {
       let container = UIView()
        return container
    }()
    
    let modaltimeLabel: UILabel = {
        let label = UILabel()
        label.text = "시간"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 25)
        return label
    }()
    
    lazy var modaltimeNumberLabel: UILabel = {
        let label = UILabel()
        label.text = "0:00:00"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 45)
        return label
    }()
    
    let modaltopSplitLine: UIView = {
        let line = UIView()
        line.alpha = 0.5
        line.backgroundColor = .gray
        return line
    }()
    
    let modaldistanceLabel: UILabel = {
        let label = UILabel()
        label.text = "거리"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 25)
        return label
    }()
    
    lazy var modaldistanceNumberLabel: UILabel = {
        let label = UILabel()
        label.text = "0:00:00"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 45)
        return label
    }()
    
    let modalkilometerLabel: UILabel = {
        let label = UILabel()
        label.text = "킬로미터"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 15)
        return label
    }()
    
    let modalpaceContainer = UIView()
    
    let modalmiddleSplitLine: UIView = {
        let line = UIView()
        line.alpha = 0.5
        line.backgroundColor = .gray
        return line
    }()
    
    let modalpaceLabel: UILabel = {
        let label = UILabel()
        label.text = "평균 페이스"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 25)
        return label
    }()
    
    lazy var modalpaceNumberLabel: UILabel = {
        let label = UILabel()
        label.text = "0:00"
        label.textColor = .black
        label.font = UIFont.systemFont(ofSize: 45)
        return label
    }()
    let bottombuttonContainer = UIView()
    let restartbuttonContainer = UIView()
    
    lazy var restartRunningButton: UIButton = {
        let button = UIButton()
        button.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        button.tintColor = .white
        let configuration = UIImage.SymbolConfiguration(pointSize: 50)
        if let image = UIImage(systemName: "restart", withConfiguration: configuration) {
            button.setImage(image, for: .normal)
        }
        button.backgroundColor = .systemIndigo
        button.layer.cornerRadius = 50
        button.clipsToBounds = true
        
        button.addTarget(self, action: #selector(restartRunning), for: .touchUpInside)

        return button
    }()
    
    let stopbuttonContainer = UIView()
    
    lazy var stopRunningButton: UIButton = {
        let button = UIButton()
        button.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        button.tintColor = .white
        let configuration = UIImage.SymbolConfiguration(pointSize: 50)
        if let image = UIImage(systemName: "stop.fill", withConfiguration: configuration) {
            button.setImage(image, for: .normal)
        }
        button.backgroundColor = .systemIndigo
        button.layer.cornerRadius = 50
        button.clipsToBounds = true
        
        button.addTarget(self, action: #selector(stopRunning), for: .touchUpInside)
        return button
    }()
    
    //MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()

        addModalSubview()
        setupModalUI()
        setModalLayout()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        updateModalUI()
    }

    // MARK: - @objc
    @objc private func restartRunning() {
        print("TappedButton - restartRunning()")
        self.runningTimer.restart()
        self.dismiss(animated: true) {
            self.delegate?.didDismissPauseRunningHalfModalViewController()
        }

        
    }

    
    @objc private func stopRunning() {
        print("TappedButton - stopRunning()")
        print("stop Time: \(self.time), Distance: \(self.distance), Pace: \(self.pace)")
        let alert = UIAlertController(title: "운동을 완료하시겠습니까?", message: "근처 편의점에서 물 한잔 어떻신가요?", preferredStyle: .alert)
        
        alert.addAction(UIAlertAction(title: "운동 완료하기", style: .default, handler: { _ in
            self.runningTimer.stop()
            CoreDataManager.shared.createRunningRecord(time: self.time, distance: self.distance, pace: self.pace)


//            let records = CoreDataManager.shared.fetchRunningRecords()
//            for record in records {
//                print("CoreData Time: \(record.time), Distance: \(record.distance), Pace: \(record.pace)")
//            }
            let mainTabBarViewController =  MainTabBarViewController()
            mainTabBarViewController.modalPresentationStyle = .fullScreen
            self.present(mainTabBarViewController, animated: true)
        }))
        
        alert.addAction(UIAlertAction(title: "취소하기", style: .destructive, handler: nil))

        self.present(alert, animated: true, completion: nil)
    }
    
    
}

extension PauseRunningHalfModalViewController {
    
    // MARK: - Running Timer Method
    private func updateModalUI() {
        // 시간, 거리, 페이스를 포맷에 맞게 변환
        let hours = time / 3600
        let minutes = (time % 3600) / 60
        let seconds = (time % 3600) % 60
        
        let paceMinutes = Int(pace) / 60
        let paceSeconds = Int(pace) % 60
        
        // 레이블의 텍스트를 설정
        modaltimeNumberLabel.text = String(format: "%01d:%02d:%02d", hours, minutes, seconds)
        modaldistanceNumberLabel.text = String(format: "%.2f", distance)
        modalpaceNumberLabel.text = String(format: "%02d:%02d", paceMinutes, paceSeconds)
    }
    

    // MARK: - setupUI
    private func setupModalUI() {
        view.backgroundColor = .white


    }

    // MARK: - addSubview
    private func addModalSubview() {

        
        view.addSubview(modaltopContainer)
        modaltopContainer.addSubview(modaltimeLabel)
        modaltopContainer.addSubview(modaltimeNumberLabel)
        modaltopContainer.addSubview(modaldistanceLabel)
        modaltopContainer.addSubview(modaldistanceNumberLabel)
        modaltopContainer.addSubview(modalkilometerLabel)
        modaltopContainer.addSubview(modaltopSplitLine)
        
        view.addSubview(modalpaceContainer)
        modalpaceContainer.addSubview(modalmiddleSplitLine)
        modalpaceContainer.addSubview(modalpaceLabel)
        modalpaceContainer.addSubview(modalpaceNumberLabel)

        view.addSubview(bottombuttonContainer)
        bottombuttonContainer.addSubview(restartbuttonContainer)
        bottombuttonContainer.addSubview(stopbuttonContainer)
        restartbuttonContainer.addSubview(restartRunningButton)
        stopbuttonContainer.addSubview(stopRunningButton)

    }

    // MARK: - Layout
    private func setModalLayout() {

        modaltimeLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(25)
            make.leading.equalToSuperview().offset(10)
        }

        modaltimeNumberLabel.snp.makeConstraints { make in
            make.top.equalTo(modaltimeLabel.snp.bottom).offset(20)
            make.leading.equalTo(modaltimeLabel)
        }
        // topContainer의 정중앙에 수직의 선
        modaltopSplitLine.snp.makeConstraints { make in
            make.centerY.equalToSuperview()
            make.centerX.equalToSuperview()
            make.height.equalTo(100)
            make.width.equalTo(1) //
        }
        
        modaldistanceLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(25)
            make.leading.equalTo(modaltopSplitLine.snp.trailing).offset(10)
        }
        
        modaldistanceNumberLabel.snp.makeConstraints { make in
            make.top.equalTo(modaldistanceLabel.snp.bottom).offset(20)
            make.leading.equalTo(modaldistanceLabel)
        }
        modalkilometerLabel.snp.makeConstraints { make in
            make.top.equalTo(modaldistanceNumberLabel.snp.bottom).offset(2)
            make.centerX.equalTo(modaldistanceNumberLabel.snp.centerX)
        }
        
        
        modalpaceLabel.snp.makeConstraints { make in
            make.top.equalToSuperview().offset(15)
            make.leading.equalToSuperview().offset(10)
        }

        modalpaceNumberLabel.snp.makeConstraints { make in
            make.top.equalTo(modalpaceLabel.snp.bottom).offset(10)
            make.leading.equalTo(modalpaceLabel.snp.leading)
        }
    

        modaltopContainer.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top)
            make.width.equalTo(360)
            make.height.equalTo(150)
        }

        modalpaceContainer.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.top.equalTo(modaltopContainer.snp.bottom)
            make.width.equalTo(360)
            make.height.equalTo(150)
        }

        modalmiddleSplitLine.snp.makeConstraints { make in
            make.top.equalToSuperview()
            make.centerX.equalToSuperview()
            make.height.equalTo(1)
            make.width.equalTo(350) //
        }
        
        bottombuttonContainer.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.top.equalTo(modalpaceContainer.snp.bottom)
            make.width.equalTo(360)
            make.height.equalTo(100)
        }
        
        restartbuttonContainer.snp.makeConstraints { make in
            make.leading.equalToSuperview()
            make.centerY.equalToSuperview()
            make.width.equalTo(180)
            make.height.equalTo(100)
        }
        
        stopbuttonContainer.snp.makeConstraints { make in
            make.trailing.equalToSuperview()
            make.centerY.equalToSuperview()
            make.width.equalTo(180)
            make.height.equalTo(100)
        }
        
        restartRunningButton.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.centerY.equalToSuperview()
            make.width.equalTo(100)
            make.height.equalTo(100)
        }
        
        stopRunningButton.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            make.centerY.equalToSuperview()
            make.width.equalTo(100)
            make.height.equalTo(100)
        }
        

    }
    

}

타이머를 정지하며 코어데이터에 저장하기 (-> 다음글 참조)