//
// AppDelegate.swift
// Todos
//
// Created by stayfoolish on 08/10/2018.
// Copyright © 2018 stayfoolish. All rights reserved.
//
import UIKit
import UserNotifications
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// User Notification Center를 통해서 노티피케이션 권한 획득
let center: UNUserNotificationCenter = UNUserNotificationCenter.current()
center.requestAuthorization(options: [UNAuthorizationOptions.alert, UNAuthorizationOptions.sound,
UNAuthorizationOptions.badge]) { (granted, error) in
print("허용여부 \(granted), 오류 : \(error?.localizedDescription ?? "없음")")
}
// 맨 처음 화면의 뷰 컨트롤러(TodosTableViewController)를 UserNotificationCenter의 delegate로 설정
if let navigationController: UINavigationController = self.window?.rootViewController as? UINavigationController,
let todosTableViewController: TodosTableViewController = navigationController.viewControllers.first as? TodosTableViewController {
UNUserNotificationCenter.current().delegate = todosTableViewController
}
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
//
// TodosTableViewController.swift
// Todos
//
// Created by stayfoolish on 08/10/2018.
// Copyright © 2018 stayfoolish. All rights reserved.
//
import UIKit
import UserNotifications
class TodosTableViewController: UITableViewController {
// todo 목록
private var todos: [Todo] = Todo.all
// 셀에 표시할 날짜를 포맷하기 위한 Date Formatter
private let dateFormatter: DateFormatter = {
let formatter: DateFormatter = DateFormatter()
formatter.dateStyle = DateFormatter.Style.medium
formatter.timeStyle = DateFormatter.Style.short
return formatter
}()
override func viewDidLoad() {
super.viewDidLoad()
// UIViewController에서 제공하는 기본 수정버튼
self.navigationItem.leftBarButtonItem = self.editButtonItem
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 화면이 보여질 때마다 todo 목록을 새로고침
self.todos = Todo.all
self.tableView.reloadSections(IndexSet(integer: 0), with: UITableViewRowAnimation.automatic)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
/// 테이블뷰의 섹션 수 (기본값 1)
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
/// 테이블뷰의 섹션 별 로우 수
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return self.todos.count
}
/// 인덱스에 해당하는 cell 반환
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 스토리보드에 구현해 둔 셀을 재사용 큐에서 꺼내옴
let cell = tableView.dequeueReusableCell(withIdentifier: "todoCell", for: indexPath)
guard indexPath.row < self.todos.count else { return cell }
let todo: Todo = self.todos[indexPath.row]
// 셀에 내용 설정
cell.textLabel?.text = todo.title
cell.detailTextLabel?.text = self.dateFormatter.string(from: todo.due)
return cell
}
/*
// Override to support conditional editing of the table view.
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
*/
/*
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Delete the row from the data source
tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
*/
/*
// Override to support rearranging the table view.
override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
}
*/
/*
// Override to support conditional rearranging of the table view.
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the item to be re-orderable.
return true
}
*/
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let todoViewController: TodoViewController = segue.destination as? TodoViewController else {
return
}
guard let cell: UITableViewCell = sender as? UITableViewCell else { return }
guard let index: IndexPath = self.tableView.indexPath(for: cell ) else { return }
guard index.row < todos.count else { return }
let todo: Todo = todos[index.row]
todoViewController.todo = todo
}
}
// User Notification의 delegate 메서드 구현
extension TodosTableViewController: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let idToshow: String = response.notification.request.identifier
guard let todoToShow: Todo = self.todos.filter({ (todo: Todo) -> Bool in
return todo.id == idToshow
}).first else {
return
}
guard let todoViewController: TodoViewController = self.storyboard?.instantiateViewController(withIdentifier: TodoViewController.storyboardID) as? TodoViewController else { return }
todoViewController.todo = todoToShow
self.navigationController?.pushViewController(todoViewController, animated: true)
UIApplication.shared.applicationIconBadgeNumber = 0
completionHandler()
}
}
//
// TodoViewController.swift
// Todos
//
// Created by stayfoolish on 08/10/2018.
// Copyright © 2018 stayfoolish. All rights reserved.
//
import UIKit
class TodoViewController: UIViewController {
/// 동일한 화면을 편집상태와 보기 모드로 변환
private enum Mode {
case edit, view
}
/// 스토리보드에 구현해 둔 인스턴스를 코드를 통해 더 생성하기 위하여 스토리보드 ID를 활용
static let storyboardID: String = "TodoViewController"
/// 화면에 보여줄 Todo 정보
var todo: Todo?
/// 현재 화면의 작업상태
private var mode: Mode = Mode.edit{
// mode 변경에 따라 적절한 처리
didSet {
self.titleField.isUserInteractionEnabled = (mode == .edit)
self.memoTextView.isEditable = (mode == .edit)
self.dueDatePicker.isUserInteractionEnabled = (mode == .edit)
self.shouldNotifySwitch.isEnabled = (mode == .edit)
if mode == Mode.edit {
if todo == nil {
self.navigationItem.leftBarButtonItems = [self.cancelButton]
}else {
self.navigationItem.rightBarButtonItems = [self.doneButton, self.cancelButton]
}
} else {
self.navigationItem.rightBarButtonItems = [self.editButton]
}
}
}
/// 수정 - 내비게이션 바 버튼
private var editButton: UIBarButtonItem {
let button: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.edit, target: self, action: #selector(touchUpEditButton(_:)))
return button
}
/// 취소 - 내비게이션 바 버튼
private var cancelButton: UIBarButtonItem {
let button: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.cancel, target: self, action: #selector(touchUpCancelButton(_:)))
return button
}
/// 완료 - 내비게이션 바 버튼
private var doneButton: UIBarButtonItem {
let button: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.done, target: self, action: #selector(touchUpDoneButton(_:)))
return button
}
@IBOutlet weak var titleField: UITextField!
@IBOutlet weak var memoTextView: UITextView!
@IBOutlet weak var dueDatePicker: UIDatePicker!
@IBOutlet weak var shouldNotifySwitch: UISwitch!
/// 화면초기화
private func initializeViews(){
// 이전화면에서 전달받은 todo가 있다면 그에 맞게 화면 초기화
if let todo: Todo = self.todo {
self.navigationItem.title = todo.title
self.titleField.text = todo.title
self.memoTextView.text = todo.memo
self.dueDatePicker.date = todo.due
self.mode = Mode.view
}
}
/// 간단한 얼럿을 보여줄 때 코드 중복을 줄이기위한 메서드
private func showSimpleAlert(message: String,
cancelTitle: String = "확인",
cancelHandler: ((UIAlertAction) -> Void)? = nil) {
let alert: UIAlertController = UIAlertController(title: "알림", message: message, preferredStyle: UIAlertControllerStyle.alert)
let action: UIAlertAction = UIAlertAction(title: cancelTitle, style: UIAlertActionStyle.cancel, handler: cancelHandler)
alert.addAction(action)
self.present(alert, animated: true, completion: nil)
}
/// 수정 버튼을 눌렀을 때
@objc private func touchUpEditButton(_ sender: UIBarButtonItem){
self.mode = Mode.edit
}
/// 취소 버튼을 눌렀을 때
@objc private func touchUpCancelButton(_ sender: UIBarButtonItem){
if self.todo == nil {
// 이전 화면에서 전달받은 todo가 없다면 새로 작성을 위한 상태이므로 모달을 내려주고
self.navigationController?.presentingViewController?.dismiss(animated: true, completion: nil )
} else {
// 그렇지 않으면 다시 원래 todo 상태로 화면을 초기화 해줌
self.initializeViews()
}
}
/// 완료 버튼을 눌렀을 때
@objc private func touchUpDoneButton(_ sender: UIBarButtonItem){
// todo 제목은 필수사항이므로 입력했는지 확인
guard let title: String = self.titleField.text, title.isEmpty == false else {
self.showSimpleAlert(message: "제목은 꼭 작성해야 합니다", cancelHandler: {(action: UIAlertAction) in
self.titleField.becomeFirstResponder()
})
return
}
// 새로운 todo 생성
let todo: Todo
todo = Todo(title: title, due: self.dueDatePicker.date, memo: self.memoTextView.text, shouldNotify: self.shouldNotifySwitch.isOn , id: self.todo?.id ?? String(Date().timeIntervalSince1970)) /// 유닉스 타임스템프를 할 일 고유 아이디로 활용
let isSuccess: Bool
if self.todo == nil {
// 새로 작성하기 위한 상태라면 저장을 완료하고 모달을 내려줌
isSuccess = todo.save {
self.navigationController?.presentingViewController?.dismiss(animated: true, completion: nil)
}
} else {
// 수정상태라면 저장을 완료하고 화면을 보기모드로 전환
isSuccess = todo.save(completion: {
self.todo = todo
self.mode = Mode.view
})
}
// 저장에 실패하면 알림
if isSuccess == false {
self.showSimpleAlert(message: "저장 실패")
}
}
override func viewDidLoad() {
super.viewDidLoad()
// 텍스트 필드 delegate 설정
self.titleField.delegate = self
// 이전 화면에서 전달받은 todo가 없다면 새로운 작성화면 설정
if self.todo == nil {
self.navigationItem.leftBarButtonItem = self.cancelButton
self.navigationItem.rightBarButtonItem = self.doneButton
} else {
self.navigationItem.rightBarButtonItem = self.editButton
}
// 화면 초기화
self.initializeViews()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 수정 모드라면 텍스트 필드에 바로 입력할 수 있도록 키보드 보여줌
if self.mode == Mode.edit {
self.titleField.becomeFirstResponder()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
/// 텍스트 필드 delegate 메서드 구현
extension TodoViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
self.navigationItem.title = textField.text
}
}
//
// Todo.swift
// Todos
//
// Created by stayfoolish on 12/10/2018.
// Copyright © 2018 stayfoolish. All rights reserved.
//
import Foundation
import UserNotifications
struct Todo: Codable {
var title: String // 작업이름
var due: Date // 작업기한
var memo: String? // 작업메모
var shouldNotify: Bool // 사용자가 기한에 맞춰 알림을 받기 원하는지
var id: String // 작업 고유 ID
}
// Todo 목록 저장/로드
extension Todo {
static var all: [Todo] = Todo.loadTodosFromJSONFile()
// Todo JSON 파일 위치
private static var todosPathURL: URL {
return try! FileManager.default.url(for: FileManager.SearchPathDirectory.applicationSupportDirectory, in: FileManager.SearchPathDomainMask.userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("todos.json")
}
// JSON 파일로부터 Todo 배열 읽어오기
private static func loadTodosFromJSONFile() -> [Todo] {
do {
let jsonData: Data = try Data(contentsOf: self.todosPathURL)
let todos: [Todo] = try JSONDecoder().decode([Todo].self, from: jsonData)
return todos
} catch {
print(error.localizedDescription)
}
return []
}
// 현재 Todo 배열 상태를 JSON 파일로 저장
@discardableResult private static func saveToJSONFile() -> Bool {
do {
let data: Data = try JSONEncoder().encode(self.all)
try data.write(to: self.todosPathURL, options: Data.WritingOptions.atomicWrite)
return true
} catch {
print(error.localizedDescription)
}
return false
}
}
// 현재 Todo 배열에 추가/삭제/수정
extension Todo {
@discardableResult static func remove(id: String) -> Bool {
guard let index: Int = self.all.index(where: { (todo: Todo) -> Bool in
todo.id == id
}) else { return false}
self.all.remove(at: index)
return self.saveToJSONFile()
}
@discardableResult func save(completion: () -> Void) -> Bool {
if let index = Todo.index(of: self) {
Todo.removeNotification(todo: self)
Todo.all.replaceSubrange(index...index, with: [self])
} else {
Todo.all.append(self)
}
let isSuccess: Bool = Todo.saveToJSONFile()
if isSuccess{
if self.shouldNotify {
Todo.addNotification(todo: self)
}else {
Todo.removeNotification(todo: self)
}
completion()
}
return isSuccess
}
private static func index(of target: Todo) -> Int? {
guard let index: Int = self.all.index(where: { (todo: Todo) -> Bool in
todo.id == target.id
}) else { return nil }
return index
}
}
/// Todo의 User Notification 관련 메서드
extension Todo {
private static func addNotification(todo: Todo) {
// 공용 UserNotification 객체
let center: UNUserNotificationCenter = UNUserNotificationCenter.current()
// 노티피케이션 콘텐츠 객체 생성
let content = UNMutableNotificationContent()
content.title = "할일 알림"
content.body = todo.title
content.sound = UNNotificationSound.default()
content.badge = 1
// 기한 날짜 생성
let dateInfo = Calendar.current.dateComponents([Calendar.Component.year, Calendar.Component.day, Calendar.Component.hour, Calendar.Component.minute], from: todo.due )
// 노티피케이션 트리거 생성
let trigger = UNCalendarNotificationTrigger(dateMatching: dateInfo, repeats: false)
// 노티피케이션 요청 객체 생성
let request = UNNotificationRequest(identifier: todo.id, content: content, trigger: trigger)
// 노티피케이션 스케줄 추가
center.add(request, withCompletionHandler: { (error : Error?) in
if let theError = error {
print(theError.localizedDescription)
}
})
}
private static func removeNotification(todo: Todo) {
let center: UNUserNotificationCenter = UNUserNotificationCenter.current()
center.removePendingNotificationRequests(withIdentifiers: [todo.id])
}
}
'Swift > 기초&문법' 카테고리의 다른 글
do it 스위프트 아이폰 앱 만들기 01~03장 swift imageview (0) | 2018.10.16 |
---|---|
swift struct class 스위프트 구조체 클래스 value type reference type (0) | 2018.10.13 |
스위프트 함수 스코프 swift function return scope (0) | 2018.10.11 |
스위프트 고차함수 swift higher-order function (0) | 2018.10.10 |
스위프트 오류처리 swift Error (0) | 2018.10.09 |