Skip to content

Commit

Permalink
Merge pull request #246 from OMZigak/feat/#245-LocalNoti
Browse files Browse the repository at this point in the history
[feat] Local 알람 구현 완료 + 스크롤뷰 오류 해결
  • Loading branch information
hooni0918 authored Jul 19, 2024
2 parents aede74c + f469ffd commit efcd731
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 27 deletions.
3 changes: 3 additions & 0 deletions KkuMulKum/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

UNUserNotificationCenter.current().delegate = LocalNotificationManager.shared

// KakaoSDK 초기화 과정에서 앱 키를 동적으로 불러오기
if let kakaoAppKey = fetchKakaoAppKeyFromPrivacyInfo() {
KakaoSDK.initSDK(appKey: kakaoAppKey)
Expand Down
55 changes: 46 additions & 9 deletions KkuMulKum/Resource/Notification/LocalNotificationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,70 @@
//

import Foundation

import UserNotifications

class LocalNotificationManager {

class LocalNotificationManager: NSObject, UNUserNotificationCenterDelegate {
static let shared = LocalNotificationManager()
private let notificationCenter = UNUserNotificationCenter.current()


private override init() {
super.init()
notificationCenter.delegate = self
}

func requestAuthorization(completion: @escaping (Bool) -> Void) {
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
completion(granted)
if let error = error {
print("Authorization request error: \(error)")
}
DispatchQueue.main.async {
completion(granted)
}
}
}

func scheduleNotification(title: String, body: String, triggerDate: Date) {
func scheduleNotification(title: String, body: String, triggerDate: Date, identifier: String,
completion: @escaping (
Error?
) -> Void
) {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default

let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: triggerDate)
let components = Calendar.current.dateComponents(
[.year, .month,.day, .hour, .minute, .second ],
from: triggerDate
)
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)

let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)

notificationCenter.add(request) { error in
if let error = error {
print("Error scheduling notification: \(error)")
DispatchQueue.main.async {
completion(error)
}
}
}

func getPendingNotifications(completion: @escaping ([UNNotificationRequest]) -> Void) {
notificationCenter.getPendingNotificationRequests { requests in
DispatchQueue.main.async {
completion(requests)
}
}
}

func removeAllPendingNotifications() {
notificationCenter.removeAllPendingNotificationRequests()
}

func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .sound, .badge])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ final class AddPromiseViewController: BaseViewController {
name: owner.viewModel.name,
place: place,
promiseDateString: owner.viewModel.combinedDateTime,
service: MockSelectMemberService()
service: MeetingService()
)
)
owner.navigationController?.pushViewController(viewController, animated: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ final class SelectMemberViewController: BaseViewController {
place: owner.viewModel.place,
dateString: owner.viewModel.promiseDateString,
members: owner.viewModel.members,
service: MockSelectPenaltyService()
service: PromiseService()
)
)
owner.navigationController?.pushViewController(viewController, animated: true)
Expand Down
15 changes: 10 additions & 5 deletions KkuMulKum/Source/Promise/ReadyStatus/View/ReadyStatusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class ReadyStatusView: BaseView {
scrollView.addSubview(contentView)

addSubviews(scrollView)

}

override func setupAutoLayout() {
Expand All @@ -100,19 +101,23 @@ class ReadyStatusView: BaseView {
}

contentView.snp.makeConstraints {
$0.edges.width.equalToSuperview()
$0.height.greaterThanOrEqualToSuperview()
$0.edges.equalTo(scrollView.contentLayoutGuide)
$0.width.equalTo(scrollView.frameLayoutGuide)
}

baseStackView.snp.makeConstraints {
$0.top.equalToSuperview().offset(24)
$0.horizontalEdges.equalToSuperview().inset(20)
$0.leading.trailing.equalToSuperview().inset(20)
}

ourReadyStatusCollectionView.snp.makeConstraints {
$0.top.equalTo(baseStackView.snp.bottom).offset(22)
$0.horizontalEdges.equalToSuperview().inset(20)
$0.bottom.equalToSuperview().inset(20)
$0.leading.trailing.equalToSuperview().inset(20)
$0.height.equalTo(Screen.height(72) * 2)
}

contentView.snp.makeConstraints {
$0.bottom.equalTo(ourReadyStatusCollectionView.snp.bottom).offset(20)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ private extension ReadyStatusViewController {
readyStatusViewModel.participantInfos.bind(with: self) { owner, participants in
DispatchQueue.main.async {
owner.rootView.ourReadyStatusCollectionView.reloadData()

owner.rootView.ourReadyStatusCollectionView.snp.updateConstraints {
$0.height.equalTo(
CGFloat(participants.count) * Screen.height(72)
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ final class SetReadyInfoViewController: BaseViewController {
fatalError("init(coder:) has not been implemented")
}


// MARK: - LifeCycle

override func viewWillAppear(_ animated: Bool) {
Expand All @@ -47,6 +48,7 @@ final class SetReadyInfoViewController: BaseViewController {
setupNavigationBarTitle(with: "준비 정보 입력하기")

bindViewModel()
setupTapGesture()
}

override func setupDelegate() {
Expand Down Expand Up @@ -95,6 +97,18 @@ final class SetReadyInfoViewController: BaseViewController {
private func doneButtonDidTap(_ sender: UIButton) {
viewModel.updateReadyInfo()
}


// MARK: - Keyboard Dismissal

private func setupTapGesture() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
view.addGestureRecognizer(tapGesture)
}

@objc private func dismissKeyboard() {
view.endEditing(true)
}
}


Expand Down Expand Up @@ -148,10 +162,9 @@ private extension SetReadyInfoViewController {
Toast().show(message: message, view: view, position: .bottom, inset: bottomInset)
}


// MARK: - Data Bind

func bindViewModel() {
func bindViewModel() {
viewModel.readyHour.bind { [weak self] readyHour in
self?.rootView.readyHourTextField.text = readyHour
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import UserNotifications

final class SetReadyInfoViewModel {
let promiseID: Int
Expand All @@ -21,22 +22,24 @@ final class SetReadyInfoViewModel {
let moveMinute = ObservablePattern<String>("")
let isSucceedToSave = ObservablePattern<Bool>(false)

// TODO: 준비 및 이동 시간 분 단위로 계산
var readyTime: Int = 0
var moveTime: Int = 0

private let service: SetReadyStatusInfoServiceType
private let notificationManager: LocalNotificationManager

init(
promiseID: Int,
promiseTime: String,
promiseName: String,
service: SetReadyStatusInfoServiceType
service: SetReadyStatusInfoServiceType,
notificationManager: LocalNotificationManager = LocalNotificationManager.shared
) {
self.promiseID = promiseID
self.promiseName = promiseName
self.promiseTime = promiseTime
self.service = service
self.notificationManager = notificationManager
}

private func validTime(time: Int, range: ClosedRange<Int>, defaultValue: String) -> String {
Expand Down Expand Up @@ -77,8 +80,7 @@ final class SetReadyInfoViewModel {
calculateTimes()
}

func checkValid(
readyHourText: String,
func checkValid(readyHourText: String,
readyMinuteText: String,
moveHourText: String,
moveMinuteText: String
Expand All @@ -92,11 +94,10 @@ final class SetReadyInfoViewModel {
}

func updateReadyInfo() {
// 확인 버튼이 눌렸을 때
// 1. 로컬 알림 만들기
// 2. 서버에 입력받은 거 전송하기
// TODO: 지훈이가 만들어준 로컬 알림 만드는 객체에 요청하기 <- 객체가 필요 <- 생성자 주입
/// 생성자 또는 메서드 전달인자 - promiseID, promiseTime, readyTime, moveTime
calculateTimes()

// 로컬 알림 설정
scheduleLocalNotification()

Task {
let model = MyPromiseReadyInfoModel(
Expand All @@ -118,4 +119,82 @@ final class SetReadyInfoViewModel {
}
}
}

private func scheduleLocalNotification() {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.locale = Locale(identifier: "ko_KR")
dateFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")

guard let promiseDate = dateFormatter.date(from: self.promiseTime) else {
print("Invalid date format: \(self.promiseTime)")
return
}

let totalPrepTime = TimeInterval((self.readyTime + self.moveTime) * 60)

let timeFormatter = DateFormatter()
timeFormatter.dateFormat = "HH:mm:ss"
timeFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")

print("약속 시간: \(timeFormatter.string(from: promiseDate))")
print("준비 시간: \(self.readyTime)")
print("이동 시간: \(self.moveTime)")
print("총 준비 시간: \(totalPrepTime / 60)")

let readyStartTime = promiseDate.addingTimeInterval(-TimeInterval(self.readyTime + self.moveTime) * 60)
let moveStartTime = promiseDate.addingTimeInterval(-TimeInterval(self.moveTime) * 60)

print("준비 시작 시간: \(timeFormatter.string(from: readyStartTime))")
print("이동 시작 시간: \(timeFormatter.string(from: moveStartTime))")

self.notificationManager.requestAuthorization { [weak self] granted in
guard let self = self else { return }
if granted {
UNUserNotificationCenter.current().getNotificationSettings { settings in
print("현재 알림 설정: \(settings)")
}

self.notificationManager.removeAllPendingNotifications()

self.notificationManager.scheduleNotification(
title: "준비 시작",
body: "\(self.promiseName) 약속 준비를 시작할 시간입니다!",
triggerDate: readyStartTime,
identifier: "readyStart_\(self.promiseID)"
) { error in
if let error = error {
print("준비 시작 알림 설정 실패: \(error)")
} else {
print("준비 시작 알림이 \(timeFormatter.string(from: readyStartTime))에 설정되었습니다.")
}
}

self.notificationManager.scheduleNotification(
title: "이동 시작",
body: "\(self.promiseName) 약속 장소로 이동할 시간입니다!",
triggerDate: moveStartTime,
identifier: "moveStart_\(self.promiseID)"
) { error in
if let error = error {
print("이동 시작 알림 설정 실패: \(error)")
} else {
print("이동 시작 알림이 \(timeFormatter.string(from: moveStartTime))에 설정되었습니다.")
}
}

self.notificationManager.getPendingNotifications { requests in
print("예정된 알림 수: \(requests.count)")
for request in requests {
if let trigger = request.trigger as? UNCalendarNotificationTrigger,
let nextTriggerDate = trigger.nextTriggerDate() {
print("알림 ID: \(request.identifier), 예정 시간: \(timeFormatter.string(from: nextTriggerDate))")
}
}
}
} else {
print("알림 권한이 허용되지 않았습니다.")
}
}
}
}

0 comments on commit efcd731

Please sign in to comment.