//

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

    }

}




+ Recent posts