VesselWheel

러닝기록 타이머 만들기(2/3)(with thread, RunLoop) 본문

Xcode Study

러닝기록 타이머 만들기(2/3)(with thread, RunLoop)

JasonYang 2024. 2. 27. 14:37

이 글은 1/3에서 활용한 Timer 클래스의 커스텀 매소드를 호출하여 View에서 호출하는 로직을 구현하였다. 

MyTimer Class

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

import UIKit
import CoreLocation

class MyTimer {
    var timer: Timer?
    
    var time = 0
    var distance = 0.0
    var pace = 0.0
    
    var pausedTime = 0 // 일시정지된 시간을 저장할 변수
    var pausedDistance = 0.0 // 일시정지된 거리를 저장할 변수
    var pausedPace = 0.0 // 일시정지된 페이스를 저장할 변수
    
    var isPaused = false
    
    var updateUI: (() -> Void)?
    
//    var backgroundTask: UIBackgroundTaskIdentifier = .invalid
    //TODO: UIBackgroundTaskIdentifier는 앱이 백그라운드에 들어갔을 때 30초까지만 유지되기 때문에 종료시점과 재시작시점을 호출하는 방법으로 로직을 변경
    
    init(time: Int, distance: Double, pace: Double, updateUI: @escaping () -> Void) {
        self.time = time
        self.distance = distance
        self.pace = pace
        self.updateUI = updateUI
        startTimer()
    }
    
    @objc func timerFired() {
        // 이 메서드는 타이머가 동작할 때마다 호출됩니다.
        DispatchQueue.main.async {
            self.time += 1
            self.distance += 0.01 // RunningTimerManager과 연계해서 변경 필요
            // 거리가 0.05km, 즉 50m 이상일 때만 페이스를 계산
            self.pace = self.distance >= 0.05 ? Double(self.time) / self.distance : 0
            
            print("Timer fired with time: \(self.time), distance: \(self.distance), pace: \(self.pace)")
            
            self.updateUI?() // 상태가 변경될 때마다 UI 업데이트
        }
    }
    public func startTimer() {
        DispatchQueue.main.async {
            self.timer = Timer(timeInterval: 1.0, target: self, selector: #selector(self.timerFired), userInfo: nil, repeats: true)
            RunLoop.current.add(self.timer!, forMode: RunLoop.Mode.common)
            self.timer?.fire()
        }
    }
    
    public func pauseTimer() {
        timer?.invalidate()
        
        pausedTime = time // 일시정지 시점의 시간 저장
        pausedDistance = distance // 일시정지 시점의 거리 저장
        pausedPace = pace // 일시정지 시점의 페이스 저장
        
    }

    public func resumeTimer() {
        // 일시정지 했던 시점의 시간, 거리, 페이스를 복원
        time = pausedTime
        distance = pausedDistance
        pace = pausedPace
        
        print("Resuming timer with time: \(time), distance: \(distance), pace: \(pace)")

        startTimer()
    }
    
    public func stopTimer() {
        timer?.invalidate()
        timer = nil
//        endBackgroundUpdateTask()
    }
    
//    private func beginBackgroundUpdateTask() {
//        self.backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
//            self?.endBackgroundUpdateTask()
//        }
//    }
//    
//    private func endBackgroundUpdateTask() {
//        UIApplication.shared.endBackgroundTask(self.backgroundTask)
//        self.backgroundTask = .invalid
//    }

}

 

 

전체코드 

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

import UIKit

import SnapKit

class RunningTimerViewController: UIViewController{
    
    //MARK: - UI properties
    
    var distance: Double = 0
    var time: Int = 0
    var pace: Double = 0
    
    var myTimer: MyTimer?
    
    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 = .black
        let configuration = UIImage.SymbolConfiguration(pointSize: 50)
        if let image = UIImage(systemName: "pause.fill", withConfiguration: configuration) {
            button.setImage(image, for: .normal)
        }
        button.backgroundColor = .systemBlue
        button.layer.cornerRadius = 50
        button.clipsToBounds = true
        
        button.addTarget(self, action: #selector(pauseRunning), for: .touchUpInside)
        
        let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPresspauseRunning))
        longPressGesture.minimumPressDuration = 3
        button.addGestureRecognizer(longPressGesture)

        return button
    }()
    
    let bottomView = UIView()
    
    //MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        addSubview()
        setupUI()
        setLayout()
        
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        recordRunning()
        
    }
    
    // MARK: - @objc
    @objc private func pauseRunning() {
        print("TappedButton - pauseRunning()")
        myTimer?.pauseTimer()
        
        let pauseRunningViewController = PauseRunningHalfModalViewController()
        
        pauseRunningViewController.myTimer = self.myTimer
        
        // myTimer의 상태를 pauseRunningViewController에 전달
        pauseRunningViewController.pausedTime = self.myTimer?.time ?? 0
        pauseRunningViewController.pausedDistance = self.myTimer?.distance ?? 0.0
        pauseRunningViewController.pausedPace = self.myTimer?.pace ?? 0.0
        
        

        showMyViewControllerInACustomizedSheet(pauseRunningViewController)

    }
    
    @objc func longPresspauseRunning(_ sender: UILongPressGestureRecognizer) {
        if sender.state == .began {
            UIView.animate(withDuration: 0.3, animations: {
                self.pauseRunningButton.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
            }) { _ in
                self.pauseRunning()
            }
        } else if sender.state == .ended || sender.state == .cancelled {
            UIView.animate(withDuration: 0.3, animations: {
                self.pauseRunningButton.transform = CGAffineTransform.identity
            })
        }
    }
    
    
}

extension RunningTimerViewController{
    
    // MARK: - Running Timer Method
    private func recordRunning() {
        myTimer = MyTimer(time: time, distance: distance, pace: pace, updateUI: { [weak self] in
            self?.updateTimerUI()
        })
    }

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



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

    }
    

}

 

2. PauseRunningHalfModalViewController

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

import UIKit

class PauseRunningHalfModalViewController: UIViewController {
    //MARK: - UI properties
    
    var pausedTime: Int = 0
    var pausedPace: Double = 0.0
    var pausedDistance: Double = 0.0
    
    
    var myTimer: MyTimer?
    
    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 = .black
        let configuration = UIImage.SymbolConfiguration(pointSize: 50)
        if let image = UIImage(systemName: "restart", withConfiguration: configuration) {
            button.setImage(image, for: .normal)
        }
        button.backgroundColor = .systemBlue
        button.layer.cornerRadius = 50
        button.clipsToBounds = true
        
        button.addTarget(self, action: #selector(restartRunning), for: .touchUpInside)
        
        let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(modallongPressrestartRunning))
        longPressGesture.minimumPressDuration = 3
        button.addGestureRecognizer(longPressGesture)

        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 = .black
        let configuration = UIImage.SymbolConfiguration(pointSize: 50)
        if let image = UIImage(systemName: "stop.fill", withConfiguration: configuration) {
            button.setImage(image, for: .normal)
        }
        button.backgroundColor = .systemBlue
        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()")
        
        myTimer?.resumeTimer()
        
        let runningTimerViewController = RunningTimerViewController()
        runningTimerViewController.modalPresentationStyle = .fullScreen
        
        // myTimer 인스턴스를 전달
        runningTimerViewController.myTimer = self.myTimer
        
        // 일시정지되었던 시간, 거리, 페이스를 전달
        runningTimerViewController.time = self.myTimer?.pausedTime ?? 0
        runningTimerViewController.distance = self.myTimer?.pausedDistance ?? 0.0
        runningTimerViewController.pace = self.myTimer?.pausedPace ?? 0.0
        

        self.present(runningTimerViewController, animated: true)
    }
    
    @objc func modallongPressrestartRunning(_ sender: UILongPressGestureRecognizer) {
        if sender.state == .began {
            UIView.animate(withDuration: 0.3, animations: {
                self.restartRunningButton.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
            }) { _ in
                self.restartRunning()
            }
        } else if sender.state == .ended || sender.state == .cancelled {
            UIView.animate(withDuration: 0.3, animations: {
                self.restartRunningButton.transform = CGAffineTransform.identity
            })
        }
    }
    
    @objc private func stopRunning() {
        print("TappedButton - stopRunning()")
        
        let alert = UIAlertController(title: "운동을 완료하시겠습니까?", message: "근처 편의점에서 물 한잔 어떻신가요?", preferredStyle: .alert)
        
        alert.addAction(UIAlertAction(title: "운동 완료하기", style: .default, handler: { _ in
            let startRunningViewController =  StartRunningViewController()
            startRunningViewController.modalPresentationStyle = .fullScreen
            self.present(startRunningViewController, animated: true)
        }))
        
        //        // Core Data context를 가져옵니다.
        //        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        //
        //        // RunningData Entity의 새 인스턴스를 생성합니다.
        //        let runningData = NSEntityDescription.insertNewObject(forEntityName: "RunningData", into: context) as! RunningData
        //
        //        // 러닝 데이터를 설정합니다.
        //        runningData.time = Int32(self.time)
        //        runningData.distance = self.distance
        //        runningData.pace = self.pace
        //
        //        // 변경 사항을 저장합니다.
        //        do {
        //            try context.save()
        //        } catch {
        //            // 저장에 실패한 경우 에러를 출력합니다.
        //            print("Failed to save running data: \(error)")
        //        }
        
//        self.dismiss(animated: true, completion: nil)
        
        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 = pausedTime / 3600
        let minutes = (pausedTime % 3600) / 60
        let seconds = (pausedTime % 3600) % 60
        let paceMinutes = Int(pausedPace) / 60
        let paceSeconds = Int(pausedPace) % 60
        
        // 레이블의 텍스트를 설정
        modaltimeNumberLabel.text = String(format: "%01d:%02d:%02d", hours, minutes, seconds)
        modaldistanceNumberLabel.text = String(format: "%.2f", pausedDistance)
        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)
        }
        

    }
    

}

 

 

 

1. MyTimer 클래스를 활용하여 RunningTimerViewController에서 호출하기

class RunningTimerViewController: UIViewController {


    //MARK: - UI properties
    
    var distance: Double = 0
    var time: Int = 0
    var pace: Double = 0
    
    var myTimer: MyTimer?
    
    ...
    
       lazy var pauseRunningButton: UIButton = {
        let button = UIButton()
        button.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        button.tintColor = .black
        let configuration = UIImage.SymbolConfiguration(pointSize: 50)
        if let image = UIImage(systemName: "pause.fill", withConfiguration: configuration) {
            button.setImage(image, for: .normal)
        }
        button.backgroundColor = .systemBlue
        button.layer.cornerRadius = 50
        button.clipsToBounds = true
        
        button.addTarget(self, action: #selector(pauseRunning), for: .touchUpInside)
        
        let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPresspauseRunning))
        longPressGesture.minimumPressDuration = 3
        button.addGestureRecognizer(longPressGesture)

        return button
    }()
    
        //MARK: - Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        addSubview()
        setupUI()
        setLayout()
        
    }
    
        override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        recordRunning()
        
    }
    
       // MARK: - @objc
    @objc private func pauseRunning() {
        print("TappedButton - pauseRunning()")
        myTimer?.pauseTimer()
        
        let pauseRunningViewController = PauseRunningHalfModalViewController()
        
        pauseRunningViewController.myTimer = self.myTimer
        
        // myTimer의 상태를 pauseRunningViewController에 전달
        pauseRunningViewController.pausedTime = self.myTimer?.time ?? 0
        pauseRunningViewController.pausedDistance = self.myTimer?.distance ?? 0.0
        pauseRunningViewController.pausedPace = self.myTimer?.pace ?? 0.0
        
        

        showMyViewControllerInACustomizedSheet(pauseRunningViewController)

    }
    
    extension RunningTimerViewController {
    
    // MARK: - Running Timer Method
    private func recordRunning() {
        myTimer = MyTimer(time: time, distance: distance, pace: pace, updateUI: { [weak self] in
            self?.updateTimerUI()
        })
    }

    private func updateTimerUI() {
        DispatchQueue.main.async {
            let hours = (self.myTimer?.time ?? 0) / 3600
            let minutes = (self.myTimer?.time ?? 0 % 3600) / 60
            let seconds = (self.myTimer?.time ?? 0 % 3600) % 60
            self.timeNumberLabel.text = String(format: "%01d:%02d:%02d", hours, minutes, seconds)
            
            if self.myTimer?.distance ?? 0 >= 0.05 {
                let paceMinutes = Int(self.myTimer?.pace ?? 0) / 60
                let paceSeconds = Int(self.myTimer?.pace ?? 0) % 60
                self.paceNumberLabel.text = String(format: "%02d:%02d", paceMinutes, paceSeconds)
            } else {
                // 거리가 50m 미만일 때는 페이스를 표시하지 않음
                self.paceNumberLabel.text = "--:--"
            }
            
            self.distanceNumberLabel.text = String(format: "%.2f", self.myTimer?.distance ?? 0)
        }
    }
    
//    -> pauseRunningButton을 원하는 뷰에 addSubview()해주고, setLayout() 레이아웃을 설정
        private func addSubview() {}   
        private func setLayout() {}

 

 

 

2. MyTimer 클래스를 활용하여 PauseRunningHalfModalViewController에서 호출하기

import UIKit

class PauseRunningHalfModalViewController: UIViewController {
    //MARK: - UI properties
    
    var pausedTime: Int = 0
    var pausedPace: Double = 0.0
    var pausedDistance: Double = 0.0
    
    
    var myTimer: MyTimer?
    
    //...
    
    lazy var restartRunningButton: UIButton = {
        let button = UIButton()
        button.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        button.tintColor = .black
        let configuration = UIImage.SymbolConfiguration(pointSize: 50)
        if let image = UIImage(systemName: "restart", withConfiguration: configuration) {
            button.setImage(image, for: .normal)
        }
        button.backgroundColor = .systemBlue
        button.layer.cornerRadius = 50
        button.clipsToBounds = true
        
        button.addTarget(self, action: #selector(restartRunning), for: .touchUpInside)
        
        let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(modallongPressrestartRunning))
        longPressGesture.minimumPressDuration = 3
        button.addGestureRecognizer(longPressGesture)

        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 = .black
        let configuration = UIImage.SymbolConfiguration(pointSize: 50)
        if let image = UIImage(systemName: "stop.fill", withConfiguration: configuration) {
            button.setImage(image, for: .normal)
        }
        button.backgroundColor = .systemBlue
        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()")
        
        myTimer?.resumeTimer()
        
        let runningTimerViewController = RunningTimerViewController()
        runningTimerViewController.modalPresentationStyle = .fullScreen
        
        // myTimer 인스턴스를 전달
        runningTimerViewController.myTimer = self.myTimer
        
        // 일시정지되었던 시간, 거리, 페이스를 전달
        runningTimerViewController.time = self.myTimer?.pausedTime ?? 0
        runningTimerViewController.distance = self.myTimer?.pausedDistance ?? 0.0
        runningTimerViewController.pace = self.myTimer?.pausedPace ?? 0.0
        

        self.present(runningTimerViewController, animated: true)
    }
    
    @objc func modallongPressrestartRunning(_ sender: UILongPressGestureRecognizer) {
        if sender.state == .began {
            UIView.animate(withDuration: 0.3, animations: {
                self.restartRunningButton.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
            }) { _ in
                self.restartRunning()
            }
        } else if sender.state == .ended || sender.state == .cancelled {
            UIView.animate(withDuration: 0.3, animations: {
                self.restartRunningButton.transform = CGAffineTransform.identity
            })
        }
    }
    
    @objc private func stopRunning() {
        print("TappedButton - stopRunning()")
        
        let alert = UIAlertController(title: "운동을 완료하시겠습니까?", message: "근처 편의점에서 물 한잔 어떻신가요?", preferredStyle: .alert)
        
        alert.addAction(UIAlertAction(title: "운동 완료하기", style: .default, handler: { _ in
            let startRunningViewController =  StartRunningViewController()
            startRunningViewController.modalPresentationStyle = .fullScreen
            self.present(startRunningViewController, animated: true)
        }))
        
        //...
        // 원하는 뷰와 레이아웃 설정하기
        private func setupModalUI()
        private func updateModalUI()
        private func setModalLayout()

 

 


https://hyesunzzang.tistory.com/257

 

[iOS] Location Service - 백그라운드에 있을 때 위치 이벤트 처리하기

백그라운드에 있을 때 위치 이벤트 처리하기 원문 링크 handling_location_events_in_the_background Capability 추가하기 백그라운드에서 위치 이벤트를 처리하려면 프로젝트의 Background Modes를 설정해야 한다.

hyesunzzang.tistory.com