본문 바로가기

프로젝트/메모앱

022 ~ 024. Memo delete, share and Keyboard Notification (메모 삭제, 공유 그리고 Keyboard Notification)

Memo delete (메모 삭제)

storyboard 뷰어의 툴바에 버튼을 추가한다.

Bar Button은 위와 같이 설정하면,
모양은 쓰레기통으로, 색은 붉은 색으로 바뀐다.

Flexible Space Bar Button Item은 위와 같이 버튼과 버튼 사이의 공백을 유동적으로 바꿔준다.
이후 새로 추가한 버튼을 뷰어의 컨트롤러 파일과 Action으로 연결한다.

해당 버튼을 누르면 동작할 시나리오는

  1. 삭제 확인 팝업 표시.
  2. 확인 버튼과 취소 버튼 표시.
  3. 확인 버튼 클릭 시 해당 메모 삭제.
  4. 취소 버튼 클릭 시 팝업 닫기.

의 구성이다.
따라서 이전 강의들에서 사용했던 팝업 생성 코드를 그대로 사용한다.

//
//  ViewerViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/23.
//

@IBAction func deleteMemo(_ sender: Any) {
	let alert = UIAlertController(title: "Delete", message: "Delete this memo?", preferredStyle: .alert)
    let deleteBtn = UIAlertAction(title: "Delete", style: .destructive) { [weak self] (action) in
    	//code
    }
    let cacelBtn = UIAlertAction(title: "Cancel", style: .default, handler: nil)
    
    alert.addAction(deleteBtn)
    alert.addAction(cacelBtn)
    
    present(alert, animated: true, completion: nil)
}

다른 점이라면 'deleteBtn'의 style을 destructive로 설정하는 부분이다.
해당 스타일을 적용하면 버튼은 붉은 색으로 표시되게 된다.

이후 실질적인 삭제에 대한 기능을 구현하기 위해 DataManagement로 이동한다.

//
//  DataManagement.swift
//  test
//
//  Created by Martin.Q on 2021/07/27.
//

func deleteMemo(_ memo: Memo?) {
    if let memo = memo {
        DataManagement.shared.context.delete(memo)
        saveContext()
    }
}

삭제 기능을 구현할 함수를 만들고 이를 구성한다.
파라미터로 전달 받은 memo가 존재하는 경우,
context에서 해당 메모를 삭제하고,
saveContext 함수로 최종 저장한다.

//
//  ViewerViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/23.
//

@IBAction func deleteMemo(_ sender: Any) {
	let alert = UIAlertController(title: "Delete", message: "Delete this memo?", preferredStyle: .alert)
    let deleteBtn = UIAlertAction(title: "Delete", style: .destructive) { [weak self] (action) in
    	dataManagement.shared.deleteMemo(self?.data)
        self?.navigationController?.popViewController(animated: true)
    }
    let cacelBtn = UIAlertAction(title: "Cancel", style: .default, handler: nil)
    
    alert.addAction(deleteBtn)
    alert.addAction(cacelBtn)
    
    present(alert, animated: true, completion: nil)
}

이후 팝업 창의 deleteBtn에서 호출할 수 있도록 수정해 주고,
NavigationController에 접근해 현재 화면을 지우도록 한다. 이를 'pop'이라고 부른다.

이 상태로 DB의 삭제 자체는 진행 된다.

이렇게 뷰어에서의 삭제 기능이 구현되었다.

매번 뷰어를 통해 삭제하는 방법도 나쁘지 않지만,
리스트에서 스와이프로 바로 삭제하는 기능이 있으면 더 사용하기 편할 것이다.

//
//  MainListTableViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

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

우선 리스트의 컨트롤러 파일에서 아랫쪽에 주석처리 되어있는 tableView(canEditRowAt indexPath:) 메소드를 복구해 준다.

//
//  MainListTableViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

// 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 func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
    return.delete
}

이후엔 tableView(editingStyleForRowAt indexPath:) 메소드를 추가하고,
반환할 동작으로 delete 속성을 전달한다.

//
//  MainListTableViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

// 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 func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
    return.delete
}

override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, 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
	}    
}

이후 주석처리 되어있는 tableView(commit editingStyle:) 메소드를 복구한다.
해당 메소드 내에서 실질적인 삭제 코드를 작성하게 된다.

//
//  MainListTableViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

// 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 func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
    return.delete
}

override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
	if editingStyle == .delete {
		let target = DataManagement.shared.memoList[indexPath.row]
		DataManagement.shared.deleteMemo(target)
            
		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
	}    
}

해당 셀의 index를 target으로 받아온 뒤,
DB에서 작성했던 삭제 함수를 호출한다.

이 상태로 시뮬레이션 하면 delete 버튼을 누르는 순간 충돌이 발생한다.
또한 다시 실행하면 삭제 자체는 진행 된 것을 확인할 수 있다.

뷰어에서 사용하는 데이터는 DB에서 저장하고 있는 원본과,
리스트에서 사용하는 memoList 배열이 있다.

지금 상태의 코드는 DB의 데이터는 삭제하지만, memoList 배열을 갱신하지는 않는다.
따라서 DB와 tableView가 사용하고 있는 memoList 배열의 불일치가 생기게 되고,
충돌이 발생한다.
따라서 이 부분을 동기화 해 줄 필요가 있다.

//
//  MainListTableViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

// 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 func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
    return.delete
}

override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
	if editingStyle == .delete {
		let target = DataManagement.shared.memoList[indexPath.row]
		DataManagement.shared.deleteMemo(target)
		DataManagement.shared.memoList.remove(at: indexPath.row)
            
		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
	}    
}

위와 같이 삭제한 index에 해당하는 데이터를 memoList 배열에서 삭제해 준 뒤,
셀을 삭제하면 충돌은 사라진다.

 

Memo share (메모 공유)

공유기능을 추가하기 위해서 버튼을 하나 추가하고,
버튼 사이에 Flexible Space Bar Button Item을 추가한다.
이후 버튼의 Syatem Item을 'Action'으로 바꾸고 뷰어 컨트롤러 파일에 Action으로 연결한다.

//
//  ViewerViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/23.
//

@IBAction func shareMemo(_ sender: Any) {
	guard let memo = data?.content else {return}
	let temp = UIActivityViewController(activityItems: [memo], applicationActivities: nil)
	
	present(temp, animated: true, completion: nil)
}

공유기능은 guard문을 통헤 해당 메모에 전달 할 내용이 있을 때만 작동하도록 하고,
iOS에서 기본으로 제공하는 공유기능을 사용하기 위해 UIActivityViewController를 사용한다.
해당 메소드의 첫번째 파라미터로 공유 할 데이터를 전달하고,
present 함수로 공유 시트를 열도록 한다.

공유 시트는 전달되는 데이터의 종류에 따라 자동으로 구성된다.

 

Keyboard Notification

현재 에디터에는 작지만 큰(?) 문제가 하나 있다.

키보드가 내려가 있을 때는 큰 문제가 없어 보이지만,
오른쪽 사진과 같이 키보드가 올라 왔을 때는 키보드의 영역이 뷰어의 textView의 영역을 침범해,
메모의 끝 부분을 볼 수 없다.

UITextView

Keyboard Notifications
When the system shows or hides the keyboard, it posts several keyboard notifications. These notifications contain information about the keyboard, including its size, which you can use for calculations that involve repositioning or resizing views. Registering for these notifications is the only way to get some types of information about the keyboard. The system delivers the following notifications for keyboard-related events:

keyboardWillShowNotification
keyboardDidShowNotification
keyboardWillHideNotification
keyboardDidHideNotification

For more information about these notifications, see their descriptions in UIWindow.

https://developer.apple.com/documentation/uikit/uitextview

UITextView의 개발문서를 확인해 보면,
키보드가 생기고 사라지기 직전과 직후에 Notification을 제공 가능하다고 안내 되어있다.

이전에 Sheet를 끌어 내렸을 때의 동작을 구현했던 것과 비슷하게,
키보드의 유무에 따라 부가적인 작업이 가능하다는 의미이다.

//
//  EditorViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

var keyboardUp: NSObjectProtocol?
var keyboardDown: NSObjectProtocol?

deinit {
	if let token = keyboardUp {
		NotificationCenter.default.removeObserver(token)
	}

	if let token = keyboardDown {
		NotificationCenter.default.removeObserver(token)
	}
}

우선은 키보드가 올라 왔을 때, 내려갔을 때를 감지 할 token을 생성하고,
해당 옵저버의 소멸자를 작성한다.

//
//  EditorViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

override func viewDidLoad() {
	super.viewDidLoad()
	if let memo = editTarget {
		navigationItem.title = "Edit"
		textField.text = memo.content
		originalContent = memo.content
	} else {
		navigationItem.title = "New Memo"
		textField.text = ""
	}
	textField.delegate = self

	keyboardUp = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: OperationQueue.main, using: { [weak self] (noti) in
		//code
	})
}

해당 옵저버들은 에디터가 열린 다음 최초에 생성 되면 된다.
viewDidLoad 메소드 안에서 옵저버를 생성하는데,
키보드의 Notification은 'UIResponder'의 속성으로 존재한다.
또한 iOS의 키보드의 높이는 앱과 사용 환경에 따라 유동적이므로 고정적으로 설정할 수 없어,
키보드가 생성될 때에 직접 계산 해야 한다.

//
//  EditorViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

override func viewDidLoad() {
	super.viewDidLoad()
	if let memo = editTarget {
		navigationItem.title = "Edit"
		textField.text = memo.content
		originalContent = memo.content
	} else {
		navigationItem.title = "New Memo"
		textField.text = ""
	}
	textField.delegate = self

	keyboardUp = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: OperationQueue.main, using: { [weak self] (noti) in
		guard let strongSelf = self else {return}
		if let frame = noti.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
		}
	})
}

self가 nil이 아니라면 유저의 키보드 정보를 frame에 저장한다.

//
//  EditorViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

override func viewDidLoad() {
	super.viewDidLoad()
	if let memo = editTarget {
		navigationItem.title = "Edit"
		textField.text = memo.content
		originalContent = memo.content
	} else {
		navigationItem.title = "New Memo"
		textField.text = ""
	}
	textField.delegate = self

	keyboardUp = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: OperationQueue.main, using: { [weak self] (noti) in
		guard let strongSelf = self else {return}
        if let frame = noti.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
        	let height = frame.cgRectValue.height

			var inset = strongSelf.textField.contentInset
			inset.bottom = height
			strongSelf.textField.contentInset = inset
		}
	})
}

frame에서 키보드 높이 정보를 height 변수에 저장하고,
textView의 영역을 inset 변수에 저장한 뒤,
키보드 높이를 textView 영역의 바닥 높이로 치환해 다시 저장한다.

이렇게 되면 키보드가 올라오기 직전,
키보드의 높이를 textView 영역의 바닥으로 설정하게 된다.

//
//  EditorViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

override func viewDidLoad() {
	super.viewDidLoad()
	if let memo = editTarget {
		navigationItem.title = "Edit"
		textField.text = memo.content
		originalContent = memo.content
	} else {
		navigationItem.title = "New Memo"
		textField.text = ""
	}
	textField.delegate = self

	keyboardUp = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: OperationQueue.main, using: { [weak self] (noti) in
		guard let strongSelf = self else {return}
        if let frame = noti.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
        	let height = frame.cgRectValue.height

			var inset = strongSelf.textField.contentInset
			inset.bottom = height
			strongSelf.textField.contentInset = inset
            
			inset = strongSelf.textField.scrollIndicatorInsets
			inset.bottom = height
			strongSelf.textField.scrollIndicatorInsets = inset
		}
	})
}

이와 같은 메커니즘으로 textView의 스크롤 바 영역도 수정한다.

//
//  EditorViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

override func viewDidLoad() {
	super.viewDidLoad()
	if let memo = editTarget {
		navigationItem.title = "Edit"
		textField.text = memo.content
		originalContent = memo.content
	} else {
		navigationItem.title = "New Memo"
		textField.text = ""
	}
	textField.delegate = self

	keyboardUp = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: OperationQueue.main, using: { [weak self] (noti) in
		guard let strongSelf = self else {return}
        if let frame = noti.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
        	let height = frame.cgRectValue.height

			var inset = strongSelf.textField.contentInset
			inset.bottom = height
			strongSelf.textField.contentInset = inset
            
			inset = strongSelf.textField.scrollIndicatorInsets
			inset.bottom = height
			strongSelf.textField.scrollIndicatorInsets = inset
		}
	})
	
	keyboardDown = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: OperationQueue.main, using: { [weak self] (noti) in
		guard let strongSelf = self else {return}
		var inset = strongSelf.textField.contentInset
		inset.bottom = 0
		strongSelf.textField.contentInset = inset
		
		inset = strongSelf.textField.scrollIndicatorInsets
		inset.bottom = 0
		strongSelf.textField.scrollIndicatorInsets = inset
	})
}

같은 방식으로 keyboardDown도 구현한다.
단, 키보드가 사라졌을 때의 값은 어느 앱이던 사용 환경이던 상관 없이 항상 0이므로
따로 현재의 높이를 계산할 필요는 없다.

이젠 키보드가 있을 때 자동으로 컨텐츠 영역을 줄여,
마지막 까지 작성할 수 있다.

지금의 에디터는 컨텐츠 영역을 한 번 터치해야 키보드가 올라온다.

//
//  EditorViewController.swift
//  test
//
//  Created by Martin.Q on 2021/07/21.
//

override func viewWillAppear(_ animated: Bool) {
	super.viewWillAppear(animated)
	
	textField.becomeFirstResponder()
	
	navigationController?.presentationController?.delegate = self
}

override func viewWillDisappear(_ animated: Bool) {
	super.viewWillDisappear(animated)
	
	textField.resignFirstResponder()
	
	navigationController?.presentationController?.delegate = nil
}

이는 해당 뷰를 FirstResponder로 설정하면 간단히 해결 된다.
에디터의 컨트롤러 파일에서 화면이 열리기 직전, 사라지기 직전에 해당 설정을 적용했다가 풀어준다.
각각 becomeFirstResponder와 resignFirstResponder 이다.

이젠 에디터로 집입했을 때 키보드가 바로 올라오게 됐다.