Swift 13주차 - 테이블 뷰 컨트롤러 이용해 할 일 목록 만들기

2024. 6. 14. 16:59swift

Swift 13주차 - 테이블 뷰 컨트롤러 이용해 할 일 목록 만들기

 

알림 앱, 메모장 앱 등 아이폰 앱에서 자주 보고 익숙하게 사용하고 있는 '목록' 기능은 테이블 뷰 컨트롤러(Table View Controller)를 이용해서 구현할 수 있다.

 

이번 주차에서는 테이블 뷰 컨트롤러에 대해 살펴볼 예정이다.

 

목차

     

     

    기본 환경 구성하기

     

    먼저 테이블 뷰 컨트롤러를 사용해야 하므로 기존의 뷰 컨트롤러를 삭제해보겠다.

     

    아이폰 모양의 스토리보드의 상단을 드래그한 후 'delete'키를 눌러 삭제하자.

     

     

     

    스토리보드에서 뷰 컨트롤러를 삭제하더라도 연결되어 있는 스위프트 파일은 삭제되지 않는다. 따라서 왼쪽 네비게이터 영역에서 ViewController.swift 파일을 직접 삭제해주자.

     

    그러면 스위프트 파일을 어떻게 삭제할 것인지 묻는 경고 창이 나타나는데, 이번 실습에서는 파일이 필요 없기 때문에 'Move to Trash' 를 클릭하여 삭제하자.

     

     

    메인 스토리보드를 클릭하면 'ViewController.swift' 까지 사라지고 텅 빈 스토리보드만 보이게 된다.

     

     

    스토리보드 꾸미기

     

    Library 버튼을 클릭한 후 팝업 창에 'tab'을 입력하여 검색한 후 테이블 뷰 컨트롤러(Table View Controller)를 찾아 스토리보드에 추가하자.

     

     

     

    테이블에 들어갈 새로운 리스트를 추가하고 리스트를 편집하려면 두 개의 뷰 컨트롤러가 필요하다. 이 두 개의 화면을 전환하기 위해 내비게이션 컨트롤러를 추가해보자.

     

    먼저 아래처럼 셀을 선택한 후 메뉴에서 Editor -> Embed In -> Navigation Controller를 선택하자.

     

     

     

     

    테이블 뷰 컨트롤러 왼쪽에 내비게이션 컨트롤러가 추가된 것을 확인할 수 있다.

     

     

    네비게이션 컨트롤러를 선택하고 오른쪽 인스펙터 영역의 'Attributes inspector'에서 'is Initial View Controller' 항목을 체크하자.

     

    앱이 실행될 때 처음으로 가야할 뷰 컨트롤러를 내비게이션 컨트롤러로 선택한 것이다.

     

     

     

    이번에는 뷰 컨트롤러(View Controller)를 찾아 테이블 뷰 컨트롤러의 오른쪽 위, 아래로 두 개를 추가하자.

     

     

     

    바 버튼 아이템(Bar Button Item)을 찾아 테이블 뷰 컨트롤러의 오른쪽 윗부분에 끌어다 놓자.

     

     

     

    바 버튼 아이템을 선택하고 인스펙터 영역에서 'Attributes inspector' 버튼을 클릭한 후 System Item을 'Add'로 수정하자.

     

    그러면 바 버튼 아이템의 아이콘 모양을 + 로 수정할 수 있다.

     

     

     

    바 버튼 아이템을 마우스 오른쪽 버튼으로 클릭하여 테이블 뷰 컨트롤러의 오른쪽 윗부분에 있는 뷰 컨트롤러로 드래그하자.

     

    그리고 뷰 컨트롤러가 전체적으로 파랗게 되면 마우스 버튼에서 손을 떼면 검은색 설정창이 나타나게 된다.

     

     

    설정창에서 액션 세그웨이(Action Segue)에서 'Show'를 선택하자.

     

     

    같은 방법으로 테이블 뷰 컨트롤러 위쪽의 Prototype Cells 를 선택한 후 마우스 오른쪽 버튼으로 클릭한 채 아래쪽의 뷰 컨트롤러로 드래그 하여 연결하자.

     

    마찬가지로 셀렉션 세그웨이(Selection Segue)에서 'Show'를 선택하자.

     

     

     

    아래쪽 세그웨이를 선택한 후 오른쪽 인스펙터 영역에서 'Attributes inspector' 버튼을 클릭한 후 Identifier에 'sgDetail'을 입력하자.

     

    뷰가 전환 될 때 전달할 데이터가 있다면 여기서 지정한 세그웨이의 이름을 활용한다.

     

    잠시후에 prepare 함수에서 이 이름을 사용할 것이다.

     

     

     

     

    이제 뷰 컨트롤러마다 제목을 추가해보자.

     

    제목이 들어갈 부분을 클릭한 후 오른쪽 인스펙터 영역에서 'Attributes inspector' 버튼을 클릭한 후 Title에 'Main View'를 입력하자.

     

     

    아래처럼 타이틀이 지정된 모습을 볼 수 있다.

     

     

     

     

    이번에는 위쪽 뷰 컨트롤러의 타이틀을 'Add View'로 지정하자.

     

     

    같은 방법으로 아래쪽 뷰 컨트롤러의 타이틀에 'Detail View'를 추가하자.

     

     

     

    'Add View' 뷰 컨트롤러에 텍스트 필드와 버튼을 아래와 같이 추가하자.

     

    그리고 버튼 스타일은 Default로 변경하자.

     

     

     

    그리고 'Detail View' 뷰 컨트롤러에 레이블을 아래와 같이 추가하자.

     

     

    오른쪽 위쪽의 Attributes inspector 버튼을 클릭한 후 Alignment의 두번째 항목을 클릭하여 레이블을 가운데 정렬하자.

     

     

    마지막으로 'Main View' 뷰 컨트롤러에서 Prototype Cells 를 선택하고 오른쪽 인스펙터 영역에서 Attributes inspector 버튼을 클릭한 후 Identifier에 'myCell'을 입력하자.

     

    테이블 뷰 컨트롤러의 셀 이름을 'myCell'로 지정한 것이다.

     

     

     

     

    이제 스위프트 파일을 추가해보자.

     

    왼쪽의 내비게이터 영역에서 마우스 오른쪽 버튼을 클릭한 후 'New File...'을 선택하자.

     

     

     

    다음 화면에서 'Cocoa Touch Class' 를 선택하자.

     

     

    서브 클래스를 'UITableViewController'로 선택하면 클래스명이 자동으로 'TableViewController'로 바뀌게 된다.

     

    Next 버튼을 클릭하고 저장할 폴더를 클릭하면 파일이 생성된다.

     

     

    같은 방법으로 클래스명이 'AddViewController'이고 서브 클래스는 'UIViewController'인 파일을 생성하고,

    클래스 명은 'DetailViewController'이고 서브 클래스는 'UIViewController'인 파일도 생성하자.

     

    그러면 아래와 같이 3개의 스위프트 파일이 생성된 모습을 볼 수 있다.

     

     

     

     

    다시 스토리보드로 돌아가서 'Main View' 컨트롤러를 선택하고 'Identity inspector' 버튼을 클릭한 후 클래스를 'TableViewController'로 선택하자.

     

    그러면 테이블 뷰 컨트롤러와 'TableViewController.swift' 파일이 연결되게 된다.

     

     

    마찬가지로 'Add View' 컨트롤러를 선택하고 'Identity inspector' 버튼을 클릭한 후 클래스를 'AddViewController'로 선택하자.

     

    그러면 뷰 컨트롤러와 'AddViewController.swift' 파일이 연결된다.

     

     

    마지막으로 'Deatil View' 컨트롤러를 선택하고 'Identity inspector' 버튼을 클릭한 후 클래스를 'DetailViewController'로 선택하자.

     

    그러면 뷰 컨트롤러와 'DetailViewController.swift' 파일이 연결된다.

     

     

     

    아웃렛 변수와 액션 함수 추가하기

     

    먼저 Assistant 메뉴를 선택하여 보조 편집기 영역을 열자.

     

    왼쪽 창에서 'Main View' 뷰 컨트롤러를 선택한 후 오른쪽 보조 편집기 영역의 클래스명을 보고 'TableViewController.swift' 파일인지 확인하자.

     

    그리고 아래처럼 테이블 뷰(Table View)의 아웃렛 변수를 추가하자.

     

     

    이름은 'tvListView', 타입은 'UITableView'로 지정하자.

     

     

    같은 방법으로 'Add View' 뷰 컨트롤러를 선택한 후 텍스트 필드 아웃렛 변수를 추가하자.

     

     

    이름은 'tfAddItem', 타입은 'UITextField'로 지정하자.

     

     

    'Add' 버튼에 대한 액션 함수도 추가해보자.

     

     

    이름은 'btnAddItem', 타입은 'UIButton'으로 지정하자.

     

     

    이번에는 'Detail View' 뷰 컨트롤러를 선택한 후 레이블의 아웃렛 변수를 추가해보자.

     

     

    이름은 'lblItem', 타입은 'UILabel'로 지정하자.

     

     

     

    테이블 목록 보여 주기

     

    앱을 실행했을 때 기본적으로 아이콘이 포함된 3개의 목록이 나타나게 하려고 한다.

     

    목록과 함께 아이콘으로 사용될 이미지를 추가하고 목록을 보여주는 코드를 작성해보자.

     

     

    먼저 목록 앱을 만들 때 사용할 이미지 파일인 'cart.png', 'clock.png', 'pencil.png' 파일을 추가하자.

     

     

     

    'TableViewController.swift' 파일을 선택하고 아래처럼 코드를 입력하여 앞에서 추가한 이미지 파일을 외부 변수인 'items'와 'ItemsImageFile'로 선언하자.

     

    이렇게 하면 모든 클래스에서 이미지를 사용할 수 있다.

     

     

     

     

    그리고 소스의 아래쪽에 있는 두개의 함수를 조금 바꿔보자.

     

     

    보통은 테이블 안에 섹션이 한개이므로 numberOfSections의 리턴값을 1로 수정했다.

     

    섹션당 열의 개수는 Items의 개수이므로 tableView 함수의 리턴값을 items.count로 수정했다.

     

     

    이번엔 tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 함수를 수정해보자.

     

    이 함수는 앞에서 선언한 변수의 내용을 셀에 적용하는 함수이다.

     

    이 함수의 앞뒤에 있는 /* 과 */을 지워서 주석을 제거하자.

     

     

     

    그리고 셀의 레이블과 이미지에 앞에서 선언한 변수가 적용되도록 수정하자.

     

     

     

    시뮬레이터를 돌려서 결과를 확인해보면 변수로 초기화했던 내용들을 볼 수 있다.

     

    아직 'Detail View'와 'Add View'는 아무런 동작도 하지 않는다.

     

     

     

     

    목록 삭제하기

     

    계속해서 'TableViewController.swift' 파일에서 함수를 수정해보자.

     

    주석문으로 되어 있는 tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) 함수를 찾아보자.

     

     

    이 함수가 바로 셀의 내용을 삭제하는 함수이다.

     

     

    이 함수의 앞뒤에 있는 /* 과 */을 지워서 주석문을 활성화하자.

     

     

     

    이 함수 안에 다음과 같이 선택한 셀을 삭제하는 코드를 추가하자.

     

     

     

    시뮬레이터를 실행하고 한 셀을 왼쪽으로 밀면 'Delete' 버튼이 나타나고 이를 클릭하면 항목이 사라진다.

     

     

     

    이번엔 영문으로 사용된 'Delete'를 한글인 '삭제'로 바꾸어 보자.

     

    아래처럼 함수를 추가해주자.

     

     

     

    다시 실행하여 한 셀을 왼쪽으로 밀면 'Delete' 대신 '삭제' 버튼이 나타나고 이를 클릭하면 항목이 사라지는 것을 확인할 수 있다.

     

     

     

     

    바 버튼으로 목록 삭제하기

     

    이번에는 Edit 바 버튼을 이용해서 목록을 삭제하는 기능을 추가해보자.

     

    TableViewController.swift 파일에 viewDidLoad 함수에서 self.navigationItem.rightBarButtonItem = self.editButtonItem 줄의 주석을 지우자.

     

     

     

    오른쪽 화면에는 이미 'Add' 버튼이 있으니 'edit' 버튼은 왼쪽에 추가해주자.

     

    코드에서 right를 left로 수정하자.

     

     

     

    시뮬레이터를 실행하고 왼쪽에 생성된 Edit 버튼을 클릭하면 왼쪽에 붉은 원이 나타난다.

     

    그 붉은 원 모양의 버튼을 클릭하면 '삭제' 버튼이 나타나고 이를 클릭하면 삭제된다.

     

     

     

     

     

    목록 순서 바꾸기

     

    이번에는 앞에서 만든 Edit 버튼을 활용해 목록의 순서를 바꾸는 기능을 추가해보자.

     

    계속해서 TableViewController.swift 파일에서 작업하자.

     

     

    tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) 함수 앞뒤의 /*과 */를 지워서 주석을 제거하자.

     

    이 함수를 사용하면 목록을 옮길 수 있다.

     

     

     

    이 함수 안에 하나의 목록을 선택하여 다른 곳으로 이동하는 소스를 추가하자.

     

     

     

     

    이제 시뮬레이터를 실행해서 Edit 버튼을 클릭해보면 목록 오른쪽에 순서 바꾸기 버튼이 표시되는 것을 확인할 수 있다.

     

     

     

     

     

    새 목록 추가하기

     

    이제 처음에 만든 'Add View'와 + 버튼을 활용하여 새 목록을 추가하는 기능을 구현해보자.

     

    'AddViewController.swift' 파일에서 btnAddItem(_ sender: UIButton) 함수를 수정하자.

     

     

    1. items에 텍스트 필드의 텍스트 값을 추가한다.
    2. itemsImageFile에는 무조건 'clock.png' 파일을 추가한다.
    3. 텍스트 필드의 내용을 지운다.
    4. 루트 뷰 컨트롤러, 즉 테이블 뷰로 돌아간다.

     

     

    다음으로 Add 버튼을 누르면 목록이 추가되는 동작을 구현해보자.

     

    'TableViewController.swift' 파일을 선택한 후 viewDidLoad 함수 아래에 다음과 같이 함수를 추가해주자.

     

    이 함수는 뷰가 전환될 때 호출되는 함수로, 리스트가 추가되어 'Main View'로 돌아올 때 호출되며 추가된 내용을 리스트에 보여준다.

     

     

     

    다시 시뮬레이터를 실행하고 화면 오른쪽에 있는 + 버튼을 클릭하면 'Add View' 로 이동하고, 텍스트 필드에 내용을 입력한 후 Add 버튼을 클릭하면 'Main View'로 돌아오고 목록이 추가된 것을 확인할 수 있다.

     

     

     

     

     

    목록의 세부 내용 보기

     

    마지막으로 목록의 아이템을 선택하면 'Detail View'로 이동하고 그 내용을 보여주도록 코드를 작성하자.

     

    'DetailViewController.swift' 파일을 선택한 후 다음 소스를 추가하자.

     

     

    1. Main View에서 받을 텍스트를 위해 변수 receiveItem을 선언한다
    2. 뷰가 노출될 때마다 이 내용을 레이블의 텍스트로 표시한다.
    3. Main View에서 변수를 받기 위한 함수를 추가한다.

     

     

    'TableViewController.swift' 파일을 선택한 후 prepare(for segue: UIStoryboardSegue, sender: Any?) 함수 앞뒤의 /*와 */를 삭제하여 주석을 제거하자.

     

    이 함수는 세그웨이를 이용하여 뷰를 이동하는 함수이다.

     

     

     

    앞 장에서 배운  세그웨이를 이용하여 뷰를 전환하는 것과 같은 방법을 사용한다.

     

    다만 TableViewCell의 indexPath를 구하는 부분이 추가되었다.

     

     

     

     

    다시 시뮬레이터를 실행하고 목록 중 하나를 클릭하면 'Detail View'로 전환되며 내용이 출력되게 된다.

     

     

     

     

    할 일 목록 앱 전체 소스 코드

     

    TableViewController.swift

    import UIKit
    
    var items = ["책 구매", "철수와 약속", "스터디 준비하기"]
    var itemsImageFile = ["cart.png", "clock.png", "pencil.png"]
    
    class TableViewController: UITableViewController {
    
        @IBOutlet var tvListView: UITableView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Uncomment the following line to preserve selection between presentations
            // self.clearsSelectionOnViewWillAppear = false
    
            // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
            self.navigationItem.leftBarButtonItem = self.editButtonItem
        }
        
        override func viewWillAppear(_ animated: Bool) {
            tvListView.reloadData()
        }
    
        // MARK: - Table view data source
    
        override func numberOfSections(in tableView: UITableView) -> Int {
            // #warning Incomplete implementation, return the number of sections
            return 1
        }
    
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            // #warning Incomplete implementation, return the number of rows
            return items.count
        }
    
        
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath)
    
            cell.textLabel?.text = items[(indexPath as NSIndexPath).row]
            cell.imageView?.image = UIImage(named: itemsImageFile[(indexPath as NSIndexPath).row])
    
            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: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
            if editingStyle == .delete {
                // Delete the row from the data source
                items.remove(at: (indexPath as NSIndexPath).row)
                itemsImageFile.remove(at: (indexPath as NSIndexPath).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
            }    
        }
        
        override func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? {
            return "삭제"
        }
        
    
        
        // Override to support rearranging the table view.
        override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
            let itemToMove = items[(fromIndexPath as NSIndexPath).row]
            let itemImageToMove = itemsImageFile[(fromIndexPath as NSIndexPath).row]
            items.remove(at: (fromIndexPath as NSIndexPath).row)
            itemsImageFile.remove(at: (fromIndexPath as NSIndexPath).row)
            items.insert(itemToMove, at: (to as NSIndexPath).row)
            itemsImageFile.insert(itemImageToMove, at: (to as NSIndexPath).row)
        }
        
    
        /*
        // 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?) {
            // Get the new view controller using segue.destination.
            // Pass the selected object to the new view controller.
            if segue.identifier == "sgDetail" {
                let cell = sender as! UITableViewCell
                let indexPath = self.tvListView.indexPath(for: cell)
                let detailView = segue.destination as! DetailViewController
                detailView.receiveItem(items[((indexPath! as NSIndexPath).row)])
            }
        }
        
    
    }

     

     

     

    AddViewController.swift

    import UIKit
    
    class AddViewController: UIViewController {
    
        @IBOutlet var tfAddItem: UITextField!
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Do any additional setup after loading the view.
        }
        
        
        @IBAction func btnAddItem(_ sender: UIButton) {
            items.append(tfAddItem.text!)
            itemsImageFile.append("clock.png")
            tfAddItem.text = ""
            _ = navigationController?.popViewController(animated: 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?) {
            // Get the new view controller using segue.destination.
            // Pass the selected object to the new view controller.
        }
        */
    
    }

     

     

     

    DetailViewController.swift

    import UIKit
    
    class DetailViewController: UIViewController {
        
        var receiveItem = ""
        
        @IBOutlet var lblItem: UILabel!
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Do any additional setup after loading the view.
            lblItem.text = receiveItem
        }
        
        func receiveItem(_ item: String) {
            receiveItem = item
        }
    
        /*
        // 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?) {
            // Get the new view controller using segue.destination.
            // Pass the selected object to the new view controller.
        }
        */
    
    }