Things take time

[SWIFT] 5일차 : 스위프트 기본 앱 본문

iOS (교육)

[SWIFT] 5일차 : 스위프트 기본 앱

겸손할 겸 2017. 4. 15. 10:40

1. Alert에서 문자열 가져오기 및 텍스트 필드 추가, 그리고 ActionSheet(안드로이드의 multiitem alert)



import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func alertClicked(_ sender: UIButton) {
        let alert = UIAlertController(title: "알림", message: "메시지", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: {
            (action) in
            if let text = alert.textFields?[0].text{
                print(text)
            }
        }))
        
        
        //텍스트 필드 추가
        alert.addTextField(configurationHandler: {
            (textField) in
            textField.textAlignment = NSTextAlignment.center
        })
        
        self.present(alert, animated: true, completion: nil)
    
    }
    @IBAction func actionSheetClicked(_ sender: UIButton) {
        
        // 이 후 활용 시, handler부분에서 매개변수를 받는 공통 함수를 만들 것
        let actionSheet = UIAlertController(title: "선택", message: "선택해주세요", preferredStyle: .actionSheet)
        actionSheet.addAction(UIAlertAction(title: "냉장고", style: .default, handler: {(Action) -> Void in
                print("냉장고 선택")
            }))
        
        /* 클로저 사용시 핸들러 분리 가능
        actionSheet.addAction(UIAlertAction(title: "냉장고", style: .default)
        {(Action) -> Void in
            print("냉장고 선택")
        })
        */
        actionSheet.addAction(UIAlertAction(title: "세탁기", style: .default, handler: nil))
        actionSheet.addAction(UIAlertAction(title: "텔레비전", style: .default, handler: nil))
        
        actionSheet.addAction(UIAlertAction(title: "삭제", style: .destructive, handler: nil))
        actionSheet.addAction(UIAlertAction(title: "취소", style: .cancel, handler: nil))
        
        self.present(actionSheet, animated: true, completion: nil)
        
    }

}

- 텍스트 필드 추가는 alert의 default만 가능하며 actionsheet에는 텍스트 필드 추가가 불가능함


* 디버깅

- 4번째 : 브레이크 포인트 다음 줄 실행
- 5번째 : 함수 안으로

- 6번째 : 함수 밖으로



2. try catch, throw

import UIKit

class ViewController: UIViewController {
    // 데이터 파싱 시 에러를 체크할 열거형, 에러 프로토콜을 상속받아야 함
    enum ParseError: Error {
        case OverSize
        case UnderSize
        case InvalidFormat(value: String)
        case InvalidData(value: String)
    }
    
    struct Time {
        var hour: Int = 0
        var min: Int = 0
        var sec: Int = 0
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // throw를 사용하는 함수 호출 시 : do try catch
        do {
            let time = try parseTime(param: "10:28:ㅁㄹ")
            print(time)
        }
        catch ParseError.OverSize {
            print("문자열 길이 초과")
        }
        catch ParseError.UnderSize {
            print("문자열 길이 미만")
        }
        catch ParseError.InvalidFormat(let message) {
            print("형식 오류 : \(message)")
        }
        catch ParseError.InvalidData(let message) {
            print("값 오류 : \(message)")
        }
        catch {
            print("알 수 없는 오류")
        }
    }

    // try catch를 사용하려면 throws 키워드를 사용해야 함
    // param format은 HH:MM:SS로 받을 것으로 결정
    func parseTime(param: NSString) throws -> Time{
        
        // return value
        var returnTime = Time()
        
        guard param.length == 8 else {
            // 길이가 8이 아니면 거짓으로 판명
            if param.length > 8 {
                throw ParseError.OverSize
            } else {
                throw ParseError.UnderSize
            }
        }
        
        // if let으로 unwrapping, 처음부터 2까지
        // else는 Int 형 변환이 안됐을 경우, 문자열이 들어온 경우
        if let hour = Int(param.substring(to: 2)) {
            guard hour >= 0 && hour < 24 else {
                throw ParseError.InvalidData(value: "Hour Invalid Data")
            }
            returnTime.hour = hour
        } else {
            throw ParseError.InvalidFormat(value: "Not Number")
        }
        
        // substring(with: NSRange(시작, 시작부터 몇 개))
        if let min = Int(param.substring(with: NSRange(location: 3, length: 2))) {
            guard min >= 0 && min < 60 else {
                throw ParseError.InvalidData(value: "Minutes Invalid Data")
            }
            returnTime.min = min
        } else {
            throw ParseError.InvalidFormat(value: "Not Number")
        }
        
        // 6번째 부터 끝까지
        if let sec = Int(param.substring(from: 6)){
            guard sec >= 0 && sec < 60 else {
                throw ParseError.InvalidData(value: "Second Invalid Data")
            }
            returnTime.sec = sec
        } else {
            throw ParseError.InvalidFormat(value: "Not Number")
        }
        
        return returnTime
    }
}


실행 결과



- throw사용 시, 열거형 사용할 경우 Error 프로토콜 상속받을 것, throw 데이터 타입이 Error타입

- throw함수 작성 시 return 타입이 throw이며, throw 구문 사용시 그 안에 return이 들어가 있어 따로 작성하지 않아도 됨

- thorw함수 호출 시 do try catch 문법을 사용할 것

- do try catch 문법 확인 (try부분에는 throw함수만 들어감)



3. 테이블 뷰 : 기본, 커스터마이징 없이 기본 내장된 테이블 뷰 컨트롤러 이용시


import UIKit // TableViewController는 딜리게이트, 데이터 소스 변수가 이미 선언되어있어 따로 쓸 필요 없음(기본 테이블 뷰를 사용할 경우만) class TableViewController: UITableViewController { var todoArray = ["영화보기", "여행가기", "게임하기"] var iconArray = ["cart.png", "clock.png", "pencil.png"] 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. // 편집 버튼(Edit) 만들기(위치는 right, left 중) self.navigationItem.leftBarButtonItem = self.editButtonItem } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } // 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 todoArray.count } // cell 크기 : 테이블 뷰 한 row 크기 // reuseIdentifier의 값은 스토리보드상의 테이블 셀 네임 : attribute의 reuseName override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ToDoCell", for: indexPath) // 기본 내장 셀에 내장된 imageView, textLabel, 셀의 attribute안의 style참고 cell.textLabel?.text = todoArray[indexPath.row] // cell.detailTextLabel?.text = "Detail" style : subtitle일 때 cell.imageView?.image = UIImage(named: iconArray[indexPath.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 } // 테이블 뷰 안의 cell을 삭제할 때 발생하는 함수 (Edit버튼 클릭시 좌측에 생성) override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { todoArray.remove(at: indexPath.row) iconArray.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 } } // 테이블 뷰 안의 cell을 위 아래로 이동할 때 발생하는 함수 (Edit버튼 클릭시 우측에 생성) override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) { let itemToMove = todoArray[fromIndexPath.row] let iconToMove = iconArray[fromIndexPath.row] todoArray.remove(at: fromIndexPath.row) iconArray.remove(at: fromIndexPath.row) todoArray.insert(itemToMove, at: to.row) iconArray.insert(iconToMove, at: to.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 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let detailVC = segue.destination as? DetailViewController detailVC?.toDoText = ((sender as! UITableViewCell).textLabel?.text)! } // edit버튼 클릭시 나오는 - 버튼 클릭시 기본적으로 delete버튼이 나오는데 이 부분의 문자열을 변경하기 위한 함수, override써야 함(테이블 뷰는 딜리게이트 데이터소스가 미리 선언되어있어 다 정의되어있는 함수이므로 @available(iOS 3.0, *) override public func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String?{ return "삭제하기" } }





4. 테이블 뷰 : 커스터마이징, TableViewController가 아닌 일반 뷰 안에 TableView 컨트롤을 넣고 할 경우


import UIKit

// 일반 뷰 컨트롤러에 상속을 받았기 때문에 데이터소스, 딜리게이트 선언 필수
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    var titleArray = ["알포인트", "셜록", "베를린"]
    var pointArray = [9.1, 9.2, 8.0]
    
    @IBOutlet weak var movieTableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        movieTableView.dataSource = self
        movieTableView.delegate = self
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    @available(iOS 2.0, *)
    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
        return titleArray.count
    }
    
    
    @available(iOS 2.0, *)
    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
     
        
        /*
        let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell")
         
        // tag값을 이용하여 cell내(하위)의 컨트롤들을 가져올 수 있음, 다른 방법(권장) : MovieCell의 클래스 연결된 MoviewTableViewCell.swift 참고
        let imageView = cell?.viewWithTag(1) as? UIImageView
        imageView?.image = UIImage(named: String(format: "%d.jpg", indexPath.row+1))
        
        let titleLabel = cell?.viewWithTag(2) as? UILabel
        titleLabel?.text = titleArray[indexPath.row]
        
        let pointLabel = cell?.viewWithTag(3) as? UILabel
        pointLabel?.text = String(pointArray[indexPath.row])
 
        return cell!
        */
        
        
        
        // tag없이 하위 컨트롤 가져오기
        let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell") as! MoviewTableViewCell
        cell.posterImageView.image = UIImage(named: String(format: "%d.jpg", indexPath.row+1))
        cell.titleLabel.text = titleArray[indexPath.row]
        cell.pointLabel.text = String(pointArray[indexPath.row])
        return cell
    }
}

- 태그 값 없이 컨트롤을 가져오려면 테이블 뷰 내의 cell이 상속받는 UITableViewCell클래스를 하나 생성하여 그 클래스를 상속받도록 함, 아래의 예는 MoviewTableViewCell이란 이름으로 클래스를 생성한 것(Moview는 오타)

import UIKit

class MoviewTableViewCell: UITableViewCell {

    @IBOutlet weak var posterImageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var pointLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}

- 테이블 뷰 위 공백제거하려면 ViewController의 Attribute Inspector -> Adjust Scroll View insets 체크 해제

- 영화 제목이 길어 ...로 나오는 것을 방지하려면 label의 lines(Attribute Inspector)을 0으로



- 추가 페이지 등록 시엔? : ViewController의 배열(titleArray, pointArray)에 append하도록 함(프로토콜, 딜리게이트를 선언)



5. 테이블 뷰 : 영화 DB API(http://www.kmdb.or.kr/)이용하여 XML Parsing -> Table View (내장 테이블 뷰 컨트롤러 사용)


import UIKit

class MovieTableViewController: UITableViewController, XMLParserDelegate {
    
    var parser = XMLParser() // XML 파싱 객체
    var curElement = "" // 현재 XML Element
    var movieInfoDicArray = [[String:String]] () // Dictionary 배열 == [Dictionary], Array>
    var movieInfoDic = [String:String]() // Dictionary 객체
    
    
    var pubTitle = "" // 영화 제목
    var contents = "" // 영화 내용
    var imageURL = "" // 영화 이미지 URL
    

    override func viewDidLoad() {
        super.viewDidLoad()

        requestMoveInfoList()
    }
    
    func requestMoveInfoList(){
        
        guard let urlToSend = URL(string: "http://api.koreafilm.or.kr/openapi-data2/service/api105/getOpenDataList") else {
            return
        }
        
        parser = XMLParser(contentsOf: urlToSend)!
        parser.delegate = self
        parser.parse()
    }
    
    // parser가 시작 태그를 만나면 호출되는 함수
    public func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]){
        
        curElement = elementName
        
        if elementName == "item"{
            // 시작 태그 = item : 초기화
            movieInfoDic = [String:String]()
            pubTitle = ""
            contents = ""
            imageURL = ""
        }
        
    }
    
    // parser가 닫는 태그를 만나면 호출되는 함수
    public func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?){
        if elementName == "item" {
            if contents.isEmpty == false{
                movieInfoDic["contents"] = contents
            }
            if pubTitle.isEmpty == false{
                movieInfoDic["pubtitle"] = pubTitle
            }
            if imageURL.isEmpty == false{
                movieInfoDic["imageurl"] = imageURL
            }
            movieInfoDicArray.append(movieInfoDic)
            print(movieInfoDicArray.count)
        }
    }

    // 현재 파서가 가리키는 태그에 담겨 있는 문자열을 얻는 함수
    public func parser(_ parser: XMLParser, foundCharacters string: String){
        if curElement == "contents" {
            contents += string
        } else if curElement == "imageurl" {
            imageURL += string
        } else if curElement == "pubtitle" {
            pubTitle += string
        }
    }

    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // 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 movieInfoDicArray.count
    }

    
    // 테이블 뷰 셀 데이터 설정
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell", for: indexPath)
        
        // 기본 내장 셀에 내장된 imageView, textLabel, 셀의 attribute안의 style참고
        cell.textLabel?.text = movieInfoDicArray[indexPath.row]["pubtitle"]
        return cell
    }
}



6. JSON PARSING : 기본

import UIKit


class JSONParser{
    func parseJsonData(data: Data?) -> Dictionary?{
        // jsonObject란 함수가 throw로 되어있기에 do catch
        do {
            // Dict = String이 key 값, AnyObject가 value 값
            if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: AnyObject]{
                return json
            } else {
                print("JSON PARSING ERROR")
            }
            
        } catch {
            print("JSON PARSING ERROR")
        }
        
        return nil
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 쌍따옴표 앞에 \써줘야 "" 안에서 인식
        let jsonString = "{\"number\":20, \"name\":\"gyeom\"}"
        let parser = JSONParser()
        if let infoDic = parser.parseJsonData(data: jsonString.data(using: .utf8)), let number = infoDic["number"], let name = infoDic["name"]{
            print("number : \(number), name : \(name)")
        }
    
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}