해당 프로젝트는 y0unghyun github에서 보실 수 있습니다.
import Foundation
struct Todo {
var id: Int
var title: String
var isCompleted: Bool
var category: String
}
struct Pet: Decodable {
let id: String
let url: String
let width: Int
let height: Int
let breeds: [String]?
let favourite: String?
public enum Keys: String, CodingKey {
case id
case url
case width
case height
case breeds
case favourite
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
url = try container.decode(String.self, forKey: .url)
id = try container.decode(String.self, forKey: .id)
width = try container.decode(Int.self, forKey: .width)
height = try container.decode(Int.self, forKey: .height)
breeds = try? container.decode([String].self, forKey: .breeds)
favourite = try? container.decode(String.self, forKey: .favourite)
}
}
import UIKit
class MainViewController: UIViewController {
@IBOutlet weak var mainImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let session = URLSession.shared
if let mainImage = URL(string: "https://spartacodingclub.kr/css/images/scc-og.jpg") {
let task = session.dataTask(with: mainImage) { (data, response, error) in
if let error = error {
print("Error: \(error)")
} else if let data = data {
DispatchQueue.main.async {
self.mainImageView.image = UIImage(data: data)
}
print("Received Data: \(data)")
}
}
task.resume()
}
}
}
URLSession
을 사용한 통신으로 하여금 보여줄 수 있는 UIImageView
와 "할 일 확인하기" 버튼, PetView로 넘어갈 수 있는 버튼이 존재합니다.import UIKit
class TodoViewController: UIViewController {
//MARK: Variables in TodoViewController
var todoList: [Todo] = [Todo(id: 0, title: "Swift 문법 살펴보기 📚", isCompleted: false, category: "Swift"),
Todo(id: 1, title: "Storyboard 살펴보기 🖥️", isCompleted: false, category: "Swift"),
Todo(id: 2, title: "장 보러 다녀오기 🥬", isCompleted: false, category: "Life"),
Todo(id: 3, title: "청소기 돌려두기 🧹", isCompleted: false, category: "Life"),
Todo(id: 4, title: "빨래하기 👕", isCompleted: false, category: "Life"),
Todo(id: 5, title: "우편함 정리하기 📮", isCompleted: false, category: "Life")]
var sections: [String: [Todo]] = [:]
var userDefault = UserDefaults.standard
@IBOutlet weak var TodoTableView: UITableView!
//MARK: Functions in TodoViewController
override func viewDidLoad() {
super.viewDidLoad()
setSection()
TodoTableView.delegate = self
TodoTableView.dataSource = self
print(userDefault.dictionaryRepresentation())
}
//MARK: Organize Sections
func setSection() {
sections = [:]
for todo in todoList {
if sections[todo.category] == nil {
sections[todo.category] = [todo]
userDefault.setValue(todo.title, forKey: "\(todo.id)")
} else {
sections[todo.category]?.append(todo)
userDefault.setValue(todo.title, forKey: "\(todo.id)")
}
}
}
@IBAction func addTodo(_ sender: Any) {
let alertForAddTodo = UIAlertController(title: "Todo 추가", message: nil, preferredStyle: .alert)
alertForAddTodo.addTextField{ textField in
textField.placeholder = "내용을 입력하세요."
}
alertForAddTodo.addTextField{ textField in
textField.placeholder = "카테고리를 입력하세요."
}
let confirmAction = UIAlertAction(title: "추가", style: .default){ [weak self] _ in
guard let self else { return }
if let title = alertForAddTodo.textFields?[0].text, !title.isEmpty, let cat = alertForAddTodo.textFields?[1].text, !cat.isEmpty {
let newItem = Todo(id: (todoList.last?.id ?? -1) + 1, title: title, isCompleted: false, category: cat)
todoList.append(newItem)
setSection()
userDefault.set(title, forKey: "\(newItem.id)")
TodoTableView.reloadData()
} else {
let missingTitleAlert = UIAlertController(title: "내용이 모두 입력되지 않았습니다.", message: "빈 칸이 있는지 확인하십시오.", preferredStyle: .alert)
let confirm = UIAlertAction(title: "확인", style: .default)
missingTitleAlert.addAction(confirm)
present(missingTitleAlert, animated: true)
}
}
let rejectAction = UIAlertAction(title: "취소", style: .cancel)
alertForAddTodo.addAction(confirmAction)
alertForAddTodo.addAction(rejectAction)
present(alertForAddTodo, animated: true)
}
}
//MARK: TableView Function Extension
extension TodoViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return sections.keys.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return Array(sections.keys)[section]
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let category = Array(sections.keys)[section]
return sections[category]?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = TodoTableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TodoTableViewCell
let category = Array(sections.keys)[indexPath.section]
if let todosInSection = sections[category] {
let todo = todosInSection[indexPath.row]
cell.todoLabel?.text = todo.title
cell.isCompletedSwitch?.isOn = todo.isCompleted
}
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let category = Array(sections.keys)[indexPath.section]
if var todosInSection = sections[category] {
let deletedTodo = todosInSection.remove(at: indexPath.row)
if let indexInTodoList = todoList.firstIndex(where: { $0.id == deletedTodo.id }) {
todoList.remove(at: indexInTodoList)
}
userDefault.removeObject(forKey: "\(deletedTodo.id)")
sections[category] = todosInSection
tableView.deleteRows(at: [indexPath], with: .fade)
if todosInSection.isEmpty {
sections.removeValue(forKey: category)
}
tableView.reloadData()
}
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let alertForModify = UIAlertController(title: "내용 수정", message: nil, preferredStyle: .alert)
let category = Array(sections.keys)[indexPath.section]
if let todoInSections = sections[category] {
let todo = todoInSections[indexPath.row]
alertForModify.addTextField() { textfield in
textfield.text = todo.title
}
alertForModify.addTextField() { textfield in
textfield.text = todo.category
}
}
let confirmModifyAction = UIAlertAction(title: "확인", style: .default) { [weak self] _ in
guard let self else { return }
let category = Array(self.sections.keys)[indexPath.section]
if var todoInSections = self.sections[category], let title = alertForModify.textFields?[0].text, let cat = alertForModify.textFields?[1].text, !title.isEmpty, !cat.isEmpty {
var todo = todoInSections[indexPath.row]
todo.title = title
todo.category = cat
todoInSections[indexPath.row] = todo
self.sections[category] = todoInSections
userDefault.set(title, forKey: "\(todo.id)")
setSection()
TodoTableView.reloadData()
} else {
let noticeCannotModify = UIAlertController(title: "수정할 수 없습니다.", message: "빈 칸이 있는지 확인하세요.", preferredStyle: .alert)
let action = UIAlertAction(title: "확인", style: .default)
noticeCannotModify.addAction(action)
present(noticeCannotModify, animated: true)
}
}
let rejectModifyAction = UIAlertAction(title: "취소", style: .cancel)
alertForModify.addAction(confirmModifyAction)
alertForModify.addAction(rejectModifyAction)
present(alertForModify, animated: true)
}
}
category
속성으로 TableView의 섹션을 구분할 것이기 때문에, [String: [Todo]] 자료형의 딕셔너리인 sections Dictionary를 선언해줬다.2todoList
배열에 변화가 가해질 때마다 sections
변수를 재설정하기 위해서 setSections
함수를 구성했다.n
개 이상 들어가면, AlertController.addTextField[n]
로 접근할 수 있다.TextField
가 하나라도 비었을 경우, 입력을 다시 요청할 수 있도록 Alert를 띄우도록 만들었다. tableView
메서드들을 모두 한 군데에 몰아서 볼 수 있도록, extension
을 만들어서 따로 구성했다.indexPath
를 받아 category
값을 캡처한다.sections[category]
에 해당하는 배열을 캡처해둔다.indexPath
를 인자로 삼아서, 삭제할 내용을 deletedTodo
상수에 담아둔다.deletedTodo.id
와 todoList
내부의 아이템들의 id
를 비교하면서, 일치하는 Todo
아이템을 찾아서 삭제해주고,todosInSection
으로 대체해준다.TheCatAPI, TheDogAPI 와 네트워크 통신을 통해서 JSON 데이터를 받아와서 랜덤한 이미지를 보여준다.
import UIKit
class PetViewController: UIViewController {
var whichPetDoYouWantToDisplay: Bool = true
var urlForPet: String = "https://api.thecatapi.com/v1/images/search"
var imageURL: String? {
didSet {
getPetImage()
}
}
@IBOutlet weak var petSelector: UISegmentedControl!
@IBOutlet weak var petImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
petImageView.image = UIImage(named: "CatLoadingImage")
}
override func viewWillAppear(_ animated: Bool) {
getJSONData()
}
@IBAction func refreshButtonTapped(_ sender: Any) {
getJSONData()
}
@IBAction func selectPetToDisplay(_ sender: Any) {
switch petSelector.selectedSegmentIndex {
case 0:
whichPetDoYouWantToDisplay = true
case 1:
whichPetDoYouWantToDisplay = false
default:
whichPetDoYouWantToDisplay = true
}
}
func getJSONData() {
let session = URLSession.shared
if whichPetDoYouWantToDisplay { urlForPet = "https://api.thecatapi.com/v1/images/search" }
else { urlForPet = "https://api.thedogapi.com/v1/images/search" }
if let petURL = URL(string: urlForPet) {
let task = session.dataTask(with: petURL) { (data, response, error) in
if let error = error {
print("Error: \(error)")
} else if let data = data {
do {
let JSONData = try JSONDecoder().decode([Pet].self, from: data)
print("Received: ", JSONData)
self.imageURL = JSONData.first?.url
} catch {
print("Error: \(error)")
}
}
}
task.resume()
}
}
func getPetImage() {
let session = URLSession.shared
guard let url = self.imageURL else {
print("Error: Empty imageURL")
return
}
if let imageURL = URL(string: url) {
let task = session.dataTask(with: imageURL) { (data, response, error) in
if let error = error {
print("Error: \(error)")
} else if let data = data {
DispatchQueue.main.async {
self.petImageView.image = UIImage(data: data)
}
}
}
task.resume()
}
}
}
UISegmentedControl
을 사용하여 새로고침을 눌렀을 때 나오는 사진의 종류를 고양이와 강아지 중 선택 가능하게 만들었다. (whichPetDoYouWantToDisplay
Bool 변수를 활용했다. String
자료형을 사용할 수도 있을 것 같다....)getJSONData()
함수를 만들었다.getJSONDate()
함수로 얻은 URL을 imageURL
변수에 대입하고, 이걸 사용해서 비동기적으로 petImageView
에 표시해줄 수 있는 함수 getPetImage()
를 구성했다.UIImageView
에 보여지기 전에 사용자에게 로딩중임을 알려줄 수 있는 Placeholder 이미지를 viewDidLoad()
단계에서 UIImageView
에 담아준다.viewWillAppear
함수와, Navigation Bar Left Button을 눌렀을 때 실행되는 refreshButtonTapped
함수가 실행될 때, getJSONData()
함수를 실행시켜 API와 통신을 하도록 한다.imageURL
의 값을 프로퍼티 옵저버로 관측하며, 새로운 값이 저장됐을 때, getPetImage()
함수를 실행시키기 때문에 새로고침 버튼을 누를 때 마다 임의의 동물 사진이 나오게 된다.AlertController
를 사용해서 해당 작업을 수행했다.Todo
구조체에 자세한 내용을 담당하는 content
프로퍼티를 만들고, 새로운 뷰에서 이 content
까지 확인할 수 있도록 만들면 사용자에게 조금 더 나은 UX를 제공할 수 있을 것 같다.