일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- RunningTimer
- MKMapViewDelegate
- 러닝타이머
- Required Reason API
- Xcode
- weak var
- font book
- 클로저의 캡슐화
- UICollectionViewFlowLayout
- Protocol
- UIAlertAction
- CoreLocation
- 러닝기록앱
- swift
- dispatchsource
- weatherKit
- CLLocationManagerDelegate
- Startign Assignments
- 서체관리자
- MKMapItem
- AnyObject
- Timer
- SwiftUI Boolean 값
- 영문 개인정보처리방침
- App Store Connect
- 단일 책임원칙
- 한국어 개인정보처리방침
- WeatherManager
- addannotation
- xcode로 날씨앱 만들기
- Today
- Total
VesselWheel
SceneDeleagte(for 러닝기록 타이머) 본문
IOS 13부터는 AppDelegate와 SceneDelegate의 책임이 구분되었다.
-> 해석하자면,
AppDelegate는
과거에 출시, 종료, 시스템 수준 이벤트 처리 등 앱의 전반적인 라이프사이클을 처리한다. 초기 앱 환경 설정, 앱 수준의 데이터 및 리소스 관리, 푸시 알림 처리 등을 담당한다.
반면에 SceneDelegate는
여러 UI 인스턴스의 라이프사이클 및 구성을 관리하는 데 중점을 둔다. 활성화, 비활성화, 종료 등 장면과 관련된 이벤트를 처리한다.
SceneDelegate는 각 장면에 대한 초기 UI를 설정하고 해당 장면 내의 변경 사항에 대응하는 역할을 한다.
러닝기록 기능을 구현하기 위해서 내가 하고자 하는 것은
화면이 포그라운드에서 백그라운드로 갈 때와 백그라운드에서 포그라운드로 갈 때,
러닝기록 프로퍼티를 저장 및 호출하여, 앱의 activate 상태에 따라 백그라운드에서 별도로 Task를 할당하지 않아도 러닝프로퍼티를 활용하고자 한다.
https://developer.apple.com/documentation/uikit/uiscenedelegate/3197919-scenewillresignactive/
https://developer.apple.com/documentation/uikit/uiscenedelegate/3197915-scenedidbecomeactive/
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)
}
}
}
타이머를 정지하며 코어데이터에 저장하기 (-> 다음글 참조)
'Xcode Study' 카테고리의 다른 글
Mapkit의 지도에 MKLocalSearch활용하여 검색된 annotation 노출하기 (0) | 2024.03.04 |
---|---|
러닝기록 타이머를 정지하며 코어데이터에 저장하기(작성중) (0) | 2024.03.04 |
러닝기록 타이머 만들기(3/3)(with dispatchsource ) (0) | 2024.02.27 |
Background Tasks (0) | 2024.02.27 |
러닝기록 타이머 만들기(2/3)(with thread, RunLoop) (0) | 2024.02.27 |