Perfect 미니 프로젝트 [6]

1. 게시물 삭제 API


CRUD 중 마지막! Delete에 해당하는 API를 만들어 봅니다!!


참고

2017년 8월 현재 Swift 3 / Perfect 최신버전 2.0.x 환경에서 진행함을 알려드립니다.

* Swift 최신 버전 확인(https://github.com/apple/swift/releases)

* Perfect 최신 버전 확인(https://github.com/PerfectlySoft/Perfect/releases)


지난 내용 돌아보기

2017/06/27 - [Swift/Perfect] - 사진 게시판 API 만들기 [1]

2017/07/11 - [Swift/Perfect] - 사진 게시판 API 만들기 [2]

2017/08/09 - [Swift/Perfect] - 사진 게시판 API 만들기 [3]

2017/08/30 - [Swift/Perfect] - 사진 게시판 API 만들기 [4]

2017/09/06 - [Swift/Perfect] - 사진 게시판 API 만들기 [5]



게시물 삭제 API


지난 번에 올려둔 게시물을 삭제하는 API를 구성해보려고 합니다. 일단 처음 라우팅 해 둔대로 게시물 내용을 수정하는 API의 핸들러는 deleteArticleHandler(request:response:) 함수입니다. 게시물을 삭제하는 API 데이터 모양을 살펴볼까요?

요청(Request)

  • HTTP Method : DELETE
  • Content-Type: application/json

매개변수 없음


응답(Response)

Key 

자료형 

비고 

필수여부 

article_id

string

삭제된 게시물 고유 식별자

Y




HTTP DELETE 메서드로 URI Path를 통해 게시물 ID를 전달하면 게시물을 삭제한 후 게시물 ID를 응답으로 주면 끝입니다! 어려울 것 없어보여요!  

한 번 함께해보시죠 :)

( 이전 API들과 중복 코드가 많지만 포스팅에서 이전 내용을 수정하는 것이 여긴 귀찮은 것이 아니므로... 일단 중복되는 코드가 많아도 꾸역꾸역 구현해 봤습니다. 차후에 여러분 스스로 중복 코드를 줄여보세요 :D )


API 핸들러의 코드를 작성하기 위해 Souces/handlers.swift 파일의 deleteArticleHandler(request:response:) 함수 내부에 코드를 작성해봅니다.


우선 요청정보를 통해 게시물 고유번호를 받아보겠습니다.

    // 응답 컨텐츠 형식은 JSON
    response.setHeader(.contentType, value: ContentsType.json)
    
    // 요청 경로에서 이미지 고유번호를 가져옵니다
    guard let articleId: String = request.pathComponents.last else {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"need article id"])
        return
    }

이제 가져온 게시물 고유번호를 통해 MongoDB에서 문서를 찾아야합니다. find 메서드를 사용하여 쿼리를 보내면 DB는 커서정보를 보내줍니다. 이 커서는 우리가 파일탐색기에서 커서를 옮겨가며 파일을 지정하는 것처럼 DB에 저장되어있는 문서의 위치를 지정하여 가리키는 정보입니다. 위의 코드 아래에 이어서 작성합니다.
    // MongoDB에서 문서를 찾으려면 쿼리를 위한 BSON 객체를 만들 필요가 있습니다.
    // 새로 생성한 BSON 객체에 고유번호를 넣어서 쿼리 객체를 생성합니다
    let query: BSON = BSON()
    let oid: BSON.OID = BSON.OID(articleId)
    query.append(oid: oid)

    // 사용한 BSON 문서는 나중에 닫아줍니다
    defer {
        query.close()
    }
    
    // DB 컬렉션에 쿼리를 전송하여 해당 문서를 가리키는 커서를 가져옵니다
    guard let cursor: MongoCursor = DB.collection?.find(query: query, limit: 1) else {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"wrong article id"])
        return
    }


커서가 가리키는 문서, 즉, 우리가 DB에서 찾은 문서의 정보를 토대로 이미지도 삭제하고 DB에서 문서도 삭제합니다.
    do {
        // 커서가 가리키는 문서를 쭉 돌면서 수행됩니다
        try cursor.enumerated().forEach { (pair: (offset: Int, element: BSON)) in
            
            // 사용한 BSON 문서는 나중에 닫아줍니다
            defer {
                pair.element.close()
            }
            
            // DB 문서를 JSON 문자열로 변환 한 후 디코드하여 딕셔너리 인스턴스로 변경해줍니다
            guard let article: [String : Any] = try pair.element.asString.jsonDecode() as? [String : Any] else {
                sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.message:"can not find article from DB"])
                return
            }
           
            // mongoDB 컬렉션 가져오기
            guard let collection = DB.collection else {
                sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:"data base initialize failed"])
                return
            }
            
            // 컬렉션에서 게시물 정보에 해당하는 문서를 삭제합니다
            let result: MongoCollection.Result = collection.remove(selector: pair.element)
          
            // 삭제 결과를 토대로 성공/실패여부를 판단합니다.
            switch result {
                
            // 게시물 정보 삭제 성공
            case .success:
                // DB에 저장되어있던 이미지경로
                guard let imagePath = article[JSONKey.imagePath] as? String else {
                    sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.message:"can not image path from DB"])
                    return
                }
                
                // 이미지경로에 존재하는 파일을 삭제합니다
                File(imagePath).delete()
                
            // 게시물 정보 삭제 실패
            case .error(let errorCode, let errorCode2, let message):
                sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:"\(errorCode), \(errorCode2): \(message)"])
                return
            
            // 그 외 다른 응답
            default:
                sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:"wrong transaction"])
                return
            }
            
            // 모든 작업을 완료하고 JSON 응답
            sendCompleteResponse(response, status: .ok, jsonBody: [JSONKey.articleId:articleId])

        }
    } catch {
        // 도중에 오류가 생겼다면 오류 전달
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:error.localizedDescription])
        return
    }

삭제는 사실 간단하면서도 고려해야 할 점이 많습니다. 이미지를 먼제 삭제하고 DB 문서를 삭제할 것인지, 문서를 먼저 삭제하고 이미지를 삭제할 것인지. 등등, 잘 고민해보세요 :)


동작 테스트를 해봅니다.

삭제 후 정상적으로 게시물 ID가 전송된 것을 확인할 수 있습니다!


이제 CRUD가 다 끝났군요! 

다음엔 무엇을 해볼까요? 아직은 계획이 없습니다 :)

필요한 정보가 있다면 언제든 요청주세요!



오늘은 여기까지~!

다음에 또 만나요~~ :D



참고문서





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom

p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 




저작자 표시 비영리 변경 금지
신고

'Swift > Perfect' 카테고리의 다른 글

사진 게시판 API 만들기 [6]  (0) 2017.09.13
사진 게시판 API 만들기 [5]  (0) 2017.09.06
사진 게시판 API 만들기 [4]  (0) 2017.08.30
사진 게시판 API 만들기 [3]  (0) 2017.08.09
사진 게시판 API 만들기 [2]  (0) 2017.07.11
사진 게시판 API 만들기 [1]  (6) 2017.06.27
Posted by yagom


Perfect 미니 프로젝트 [5]


1. 게시물 수정 API

CRUD 중 Read에 해당하는 API를 만들어 봅니다!!


참고

2017년 8월 현재 Swift 3 / Perfect 최신버전 2.0.x 환경에서 진행함을 알려드립니다.

* Swift 최신 버전 확인(https://github.com/apple/swift/releases)

* Perfect 최신 버전 확인(https://github.com/PerfectlySoft/Perfect/releases)


지난 내용 돌아보기

2017/06/27 - [Swift/Perfect] - 사진 게시판 API 만들기 [1]

2017/07/11 - [Swift/Perfect] - 사진 게시판 API 만들기 [2]

2017/08/09 - [Swift/Perfect] - 사진 게시판 API 만들기 [3]

2017/08/30 - [Swift/Perfect] - 사진 게시판 API 만들기 [4]

게시물 수정 API

지난 번에 올려둔 게시물의 내용을 수정하는 API를 구성해보려고 합니다. 일단 처음 라우팅 해 둔대로 게시물 내용을 수정하는 API의 핸들러는 modifyArticleHandler(request:response:) 함수입니다. 게시물 내용을 수정하는 API 데이터 모양을 살펴볼까요?

요청(Request)

  • HTTP Method : POST
  • Content-Type: multipart/form-data

 매개변수

자료형 

값의 범위/기본 값 

비고 

 필수여부

image 

binary data

이전 데이터

이미지 데이터 

N

user_name

string 


사용자 이름이 이전 사용자 이름과 일치하지 않으면 수정에 실패

Y

description

string 

이전 데이터

이미지 설명 

N

title

string 

이전 데이터

이미지 제목 

N


응답(Response)

Key 

자료형 

비고 

필수여부 

article_id 

string 

수정된 게시물 고유 식별자 

 image_url

string 

이미지 주소 

user_name 

string 

사용자 이름 

description 

 string

이미지 설명 

title 

string 

이미지 제목 


사진 게시물 등록 API와 크게 다른 모양은 아닙니다만, 필수 매개변수 여부가 다릅니다. 이전 게시물 내용이 이미 등록되어 있으므로, 변경을 원하는 매개변수만 담아서 요청하도록 되어있습니다. 다만, 사용자 이름은 필수항목이군요. 응답 데이터의 모양도 다르지 않습니다.   

로직이 크게 다르지 않을 것 같은데요? 한 번 함께해보시죠 :)


( 사실 게시물 작성 API, 게시물 정보 API와 로직이 비슷한 부분이 많아 공용으로 사용할 함수를 많이 만들 수도 있지만, 포스팅에서 이전 내용을 수정하는 것이 여긴 귀찮은 것이 아니므로... 일단 중복되는 코드가 많아도 해당 로직으로 꾸역꾸역 구현해 봤습니다. 차후에 여러분 스스로 중복 코드를 줄여보세요 :D )


API 핸들러의 코드를 작성하기 위해 Souces/handlers.swift 파일의 modifyArticleHandler(request:response:) 함수 내부에 코드를 작성해봅니다.


일단 부족한 매개변수가 없는지 먼저 확인합니다.

    // 응답 컨텐츠 형식은 JSON
    response.setHeader(.contentType, value: ContentsType.json)
   
    // 부족한 매개변수가 없는지 확인
    if let lakedParams = lakedParams(paramsNeeded: [JSONKey.userName], paramsReceived: request.postParams) {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"need more params \(lakedParams)"])
    }


그 후, 요청정보를 통해 게시물 고유번호를 받아보겠습니다.
    // 요청 경로에서 이미지 고유번호를 가져옵니다
    guard let articleId: String = request.pathComponents.last else {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"need article id"])
        return
    }

이제 가져온 게시물 고유번호를 통해 MongoDB에서 문서를 찾아서 게시물 정보를 가져옵니다. 지난 번에 만들어둔 articleDocumentDicrionaries(query:skip:limit:)함수를 사용합니다. 위의 코드 아래에 이어서 작성합니다.
    // MongoDB에서 문서를 찾으려면 쿼리를 위한 BSON 객체를 만들 필요가 있습니다.
    // 새로 생성한 BSON 객체에 고유번호를 넣어서 쿼리 객체를 생성합니다
    let query: BSON = BSON()
    let oid: BSON.OID = BSON.OID(articleId)
    query.append(oid: oid)
    
    // 게시물 정보를 담은 딕셔너리 배열을 가져온 후 첫 번째 요소를 꺼냅니다
    guard let articleDocumentDictionary = articleDocumentDictionaries(query: query, limit: 1)?.first else {
        sendCompleteResponse(response, status: .internalServerError)
        return
    }

DB에서 정보를 가져왔으니 request를 통해 전달받은 사용자 이름과 DB에 저장되어 있던 사용자 이름이 같은지 확인합니다.
    // 전달받은 사용자 이름과 DB에 작성된 게시물의 사용자 이름과 같은지 확인합니다
    guard let dbUserName = articleDocumentDictionary[JSONKey.userName] as? String,
        let paramUserName = request.param(name:JSONKey.userName) else {
            
            sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:"can not check user name"])
            return
    
    }
    
    guard dbUserName == paramUserName else {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"diffrent user name"])
        return
    }

매개변수로 전달되어온 데이터가 새로이 있다면 DB에 저장할 데이터를 업데이트 해줍니다.
    // DB에 저장할 데이터
    // 차후에 응답 데이터로도 사용합니다
    var jsonDictionary = articleDocumentDictionary
    
    // 매개변수로 사진 제목이 전달되었다면 수정
    if let title = request.param(name: JSONKey.title) {
        jsonDictionary[JSONKey.title] = title
    }
    
    // 매개변수로 사진 설명이 전달되었다면 수정
    if let description = request.param(name: JSONKey.description) {
        jsonDictionary[JSONKey.description] = description
    }

이제 이미지만 남았습니다. 이미지가 업로드 되었다면 새로이 서버에 저장하고, 지난 이미지를 삭제합니다. 업로드 되지 않았다면 저장할 데이터 형태만 살짝 바꿔줍니다.
    // 이미지가 저장될 디렉터리
    guard let imageDirectory = imageDir else {
        sendCompleteResponse(response, status: .internalServerError, jsonBody: nil)
        return
    }
    
    // 원본 이미지 파일 이름 가져오기
    guard let originImageUrl: String = articleDocumentDictionary[JSONKey.imageUrl] as? String, let originImageFileName: String = originImageUrl.filePathComponents.last else {
        
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error: "can not find origin image"])
        return
    }
    
    // 원본 이미지 서버 내부 경로
    let originImageFilePath: String = imageDirectory.path + originImageFileName
    
    // 새로운 이미지 서버 내부 경로
    let imageFilePath: String!
    
    // 최종 이미지 파일 이름
    let imageFileName: String!
    
    // DB 저장시에는 URL과 게시물 ID가 필요없으므로 미리 삭제
    jsonDictionary.removeValue(forKey: JSONKey.imageUrl)
    jsonDictionary.removeValue(forKey: JSONKey.articleId)
    
    // 이미지 파일이 업로드 되었는지 확인
    if let imageInformation: MimeReader.BodySpec = request.postFileUploads?.first,
        let imageFile = imageInformation.file {
    
        
        // 고유한 이미지 이름을 위해 타임스템프 값을 활용
        let timestamp: Int = icuDateToSeconds(getNow())
        
        // 사용자이름_타임스템프.jpg 형식으로 파일이름 지정
        imageFileName = paramUserName + "_" + String(timestamp) + ".jpg"
        
        // 이미지가 저장될 경로
        imageFilePath = imageDirectory.path + imageFileName
        
        // 이미지 저장에 실패할 경우 실패 응답 보내기
        do {
            try imageFile.copyTo(path: imageFilePath, overWrite: false)
        } catch {
            sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:error.localizedDescription])
            return
        }
        
        // 기존 파일 삭제해주기
        File(originImageFilePath).delete()
        
        jsonDictionary[JSONKey.imagePath] = imageFilePath
        
    } else {    // 이미지 파일을 변경하지 않는 경우
       
        // 이미지 서버 내부 경로
        imageFilePath = imageDirectory.path + originImageFileName
        
        imageFileName = originImageFileName
    }

이제 마지막으로 변경된 딕셔너리를 DB에 업데이트 해주기만 하면 됩니다. 새로운 게시물 작성 API에서 사용하였던 컬렉션의 save(document:) 메서드는 OID가 같다면 업데이트를 수행합니다.
    // mongoDB 컬렉션 가져오기
    guard let collection = DB.collection else {
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:"data base initialize failed"])
        return
    }
    
    // DB에 저장
    do {
        let jsonString = try jsonDictionary.jsonEncodedString()
        
        let document: BSON = try BSON(json: jsonString)
        
        // 문서 사용 후에는 닫아주는게 좋겠습니다
        defer {
            document.close()
        }
        
        // 문서 고유 아이디 부여
        document.append(oid: oid)
        
        // 컬렉션에 문서 저장 - save 메서드는 업데이트 겸용입니다
        let result = collection.save(document: document)
        
        if case .success = result { } else {
            sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:result])
            return
        }
        
    } catch {
        
        Log.error(message: error.localizedDescription)
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:error.localizedDescription])
        return
    }

이제 DB에 저장도 마쳤으니 응답 데이터를 전송하는 일만 남았습니다. 필요없는 정보 대신에 클라이언트에서 필요한 정보로 대체해 준 후 전달합니다.
    // 응답 데이터에는 이미지 경로 대신 이미지 URL 전송
    jsonDictionary[JSONKey.imagePath] = nil
    jsonDictionary[JSONKey.imageUrl] = server.serverAddress + ":\(server.serverPort)" + "/image/" + imageFileName
    
    // 모든 작업을 완료하고 JSON 응답
    sendCompleteResponse(response, status: .created, jsonBody: jsonDictionary)


기존 게시물 정보입니다.


수정 후 게시물 정보입니다. 게시물 ID는 동일하지만 이미지 URL도 달라져 있고, 제목과 내용도 변경되었네요!



Update까지 해봤군요! 다음엔 Delete로 마무리 해봅니다!

오늘은 여기까지~!

다음에 또 만나요~~ :D




참고문서





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom

p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 




저작자 표시 비영리 변경 금지
신고

'Swift > Perfect' 카테고리의 다른 글

사진 게시판 API 만들기 [6]  (0) 2017.09.13
사진 게시판 API 만들기 [5]  (0) 2017.09.06
사진 게시판 API 만들기 [4]  (0) 2017.08.30
사진 게시판 API 만들기 [3]  (0) 2017.08.09
사진 게시판 API 만들기 [2]  (0) 2017.07.11
사진 게시판 API 만들기 [1]  (6) 2017.06.27
Posted by yagom

Perfect 미니 프로젝트 [4]


1. 게시물 정보 API 
2. 게시물 목록 API 


CRUD 중 Read에 해당하는 API를 만들어 봅니다!!


참고

2017년 8월 현재 Swift 3 / Perfect 최신버전 2.0.x 환경에서 진행함을 알려드립니다.

* Swift 최신 버전 확인(https://github.com/apple/swift/releases)

* Perfect 최신 버전 확인(https://github.com/PerfectlySoft/Perfect/releases)


지난 내용 돌아보기

2017/06/27 - [Swift/Perfect] - 사진 게시판 API 만들기 [1]

2017/07/11 - [Swift/Perfect] - 사진 게시판 API 만들기 [2]

2017/08/09 - [Swift/Perfect] - 사진 게시판 API 만들기 [3]


게시물 정보 API

지난 번에 POST로 등록해본 게시물의 정보를 가져오는 API를 구성해보려고 합니다. 일단 프로젝트를 처음 생성하면서 라우팅 해 둔대로 게시물 정보를 가져오는 API의 핸들러는 articleInfoHandler(request:response:) 함수입니다. 게시물 정보 API 데이터 모양을 살펴볼까요?


요청(Request)

  • HTTP Method : GET
  • Content-Type: application/json

매개변수 없음


응답(Response)

매개변수 

자료형 

비고 

필수여부 

image_url

string

이미지 URL

Y

user_name

string

업로드한 사용자 이름 

description

string 

이미지 설명 

title

string 

이미지 제목 

article_id

string 

게시물 고유 식별자



게시물 정보를 받기 위해서 GET 메서드로 요청하고, PATH의 마지막에 게시물 고유번호를 보내게 되어있습니다. 그럼 우리도 받을 때 게시물 고유번호를 받고, DB에서 고유번호를 통해 게시물을 찾아 보내주면 됩니다~!


일단 요청정보를 통해 게시물 고유번호를 받아보겠습니다.

Souces/handlers.swift 파일의 articleInfoHandler(request:response:) 함수 내부에 코드를 작성해봅니다.

    response.setHeader(.contentType, value: ContentsType.json)
    
    // 요청 경로에서 이미지 고유번호를 가져옵니다
    guard let articleId: String = request.pathComponents.last else {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"need article id"])
        return
    }

이제 가져온 게시물 고유번호를 통해 MongoDB에서 문서를 찾아야합니다. find 메서드를 사용하여 쿼리를 보내면 DB는 커서정보를 보내줍니다. 이 커서는 우리가 파일탐색기에서 커서를 옮겨가며 파일을 지정하는 것처럼 DB에 저장되어있는 문서의 위치를 지정하여 가리키는 정보입니다. 위의 코드 아래에 이어서 작성합니다.

    // MongoDB에서 문서를 찾으려면 쿼리를 위한 BSON 객체를 만들 필요가 있습니다.
    // 새로 생성한 BSON 객체에 고유번호를 넣어서 쿼리 객체를 생성합니다
    let query: BSON = BSON()
    let oid: BSON.OID = BSON.OID(articleId)
    query.append(oid: oid)
    
    // 사용한 BSON 문서는 닫아줍니다
    defer {
        query.close()
    }
        
    // DB 컬렉션에 쿼리를 전송하여 해당 문서를 가리키는 커서를 가져옵니다
    guard let cursor: MongoCursor = DB.collection?.find(query: query, limit: 1) else {
        sendCompleteResponse(response, status: .internalServerError)
        return
    }


커서정보를 받아왔으면 이제 문서를 JSON 형태로 변환하여 클라이언트에게 전송해주면 됩니다. 자세한 설명은 주석으로 달려있으니, 위 코드 아래에 이어서 작성해봅니다.

    do {
        // 커서가 가리키는 객체들을 모두 JSON 문자열로 변경한 후,
        // JSON 문자열을 딕셔너리 배열로 변환해 봅니다
        guard let articleDocumentDictionaries = try cursor.jsonString.jsonDecode() as? [[String : Any]] else {
            sendCompleteResponse(response, status: .internalServerError)
            return
        }
        
        // 우리는 하나의 게시물 정보만 필요하므로 배열에서 첫 번째 요소를 꺼내옵니다.
        // 만약 배열이 비어있다면 게시물을 찾지 못했을 확률이 큽니다
        guard let article = articleDocumentDictionaries.first else {
            sendCompleteResponse(response, status: .notFound)
            return
        }
        
        // 꺼내온 게시물 정보를 응답해줍니다
        sendCompleteResponse(response, status: .ok, jsonBody: article)
        
    } catch {
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.message : error.localizedDescription])
        return
    }



짠! 벌써 끝났어요!! 우하하 한 번 테스트해봅니다.

(주소의 마지막 경로에 위치한 게시물 고유번호는 여러분의 DB에서 부여한 번호에 따라 다릅니다)

헛... 그런데 내려온 JSON 형식을 보아하니, 문서에 정리되어 있는 것처럼 깔끔하지가 않군요. DB에서 관리하고 있는 _id 객체라던지... null 상태로 내려온 description이라던지... 마음에 들지 않는 부분이 너무 많습니다. 게다가 이미지의 URL이 아닌 서버 내부의 경로가 내려오고 있죠. 이것들을 다 바꿔주고 싶습니다.

그리고 생각해보니 우리는 나중에 게시물 목록을 내려주는 API도 구성해야 합니다. 아마 중복작업들이 일어날 것 같으니 그냥 미리 빼서 함수로 만들어 놓는 것이 좋을 것 같아요. 이런 사전처리들을 위해서 Souces/handlers.swift 파일의 맨 하단에 함수를 하나 추가해봅니다.

// 쿼리를 통해 데이터베이스에서 문서 정보를 딕셔너리 형태로 변환하여 반환합니다.
// 딕셔너리에 불필요한 정보를 제거하고, 필요한 정보를 형식에 맞게 다시 넣어줍니다
private func articleDocumentDictionaries(query: BSON = BSON(), skip: Int = 0, limit: Int = 0) -> [[String: Any]]? {
    
    // 사용한 BSON 문서는 나중에 닫아줍니다
    defer {
        query.close()
    }
    
    // DB 컬렉션에 쿼리를 전송하여 해당 문서를 가리키는 커서를 가져옵니다
    guard let cursor: MongoCursor = DB.collection?.find(query: query, skip: skip, limit: limit) else {
        return nil
    }
    
    // 반환할 딕셔너리 배열 생성
    var documentDictionaries: [[String : Any]] = []
    
    do {
        // 커서가 가리키는 문서를 쭉 돌면서 수행됩니다
        try cursor.enumerated().forEach { (pair: (offset: Int, element: BSON)) in
            
            // 사용한 BSON 문서는 나중에 닫아줍니다
            defer {
                pair.element.close()
            }
            
            var iterator = pair.element.iterator()
            
            // 문서 내부에서 고유번호를 가져오기 위해 key에 해당하는 값을 가져옵니다
            guard iterator?.find(key: "_id") == true,
                let oid = iterator?.currentValue?.oid?.description else {
                return
            }
            
            // DB 문서를 JSON 문자열로 변환 한 후 디코드하여 딕셔너리 인스턴스로 변경해줍니다
            guard var article: [String : Any] = try pair.element.asString.jsonDecode() as? [String : Any] else {
                return
            }
            
            // 이미지 파일 이름을 가져온 후
            guard let imageFileName: String = (article[JSONKey.imagePath] as? String)?.lastFilePathComponent else {
                return
            }
            
            // 응답 데이터에는 이미지 경로 대신 이미지 URL을 넣어줍니다
            article.removeValue(forKey: JSONKey.imagePath)
            article[JSONKey.imageUrl] = server.serverAddress + ":\(server.serverPort)" + "/image/" + imageFileName
            
            // _id 키대신 우리가 원하는 형식으로 게시물 고유번호를 넣어줍니다
            article.removeValue(forKey: "_id")
            article[JSONKey.articleId] = oid
            
            // 딕셔너리 내부에 Null 값이 있다면 해당 키를 삭제해줍니다
            article.forEach({ (pair: (key: String, value: Any)) in
                if type(of: pair.value) == JSONConvertibleNull.self {
                    article.removeValue(forKey: pair.key)
                }
            })
            
            // 반환할 배열에 넣어줍니다
            documentDictionaries.append(article)
        }
        
        // 배열 반환
        return documentDictionaries
    } catch {
        return nil
    }
}

그리고 다시 사진 게시물 정보 핸들러 함수를 깔끔하게 정리해봅니다.

// 사진 게시물 정보
func articleInfoHandler(request: HTTPRequest, response: HTTPResponse) {
    response.setHeader(.contentType, value: ContentsType.json)
    
    // 요청 경로에서 이미지 고유번호를 가져옵니다
    guard let articleId: String = request.pathComponents.last else {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"need article id"])
        return
    }
    
    // MongoDB에서 문서를 찾으려면 쿼리를 위한 BSON 객체를 만들 필요가 있습니다.
    // 새로 생성한 BSON 객체에 고유번호를 넣어서 쿼리 객체를 생성합니다
    let query: BSON = BSON()
    let oid: BSON.OID = BSON.OID(articleId)
    query.append(oid: oid)
        
    // 게시물 정보를 담은 딕셔너리 배열을 가져온 후 첫 번째 요소를 꺼냅니다
    guard let articleDocumentDictionary = articleDocumentDictionaries(query: query, limit: 1)?.first else {
        sendCompleteResponse(response, status: .internalServerError)
        return
    }
    
    // 모든 작업을 완료하고 JSON 응답
    sendCompleteResponse(response, status: .ok, jsonBody: articleDocumentDictionary)
}

그리고 동작확인!

크아~ 우리가 원하던 대로 동작합니다!!



게시물 목록 API


게시물 목록을 가져오는 것은 이제 일도 아니군요! 미리 함수를 만들어 뒀으니 전체 게시물 목록도 손쉽게 가져올 수 있습니다. 물론 우리가 원하는 깔끔한 형태로요~

DB 컬렉션의 find 메서드의 매개변수로 전달하는 query 객체가 아무 정보도 가지고 있지 않다면 컬렉션에 존재하는 모든 문서를 가져올 수 있습니다. skip은 찾은 문서 중 지나갈 문서의 수, limit은 커서가 가리키게 될 문서의 최대 개수입니다. 이것을 설명하는 이유는 게시물 목록 가져올 때 전송할 매개변수 값들과 관계가 있기 때문이겠죠? 헤헤


게시물 목록을 가져오기 위한 API 정보입니다.


사진 게시물 목록

요청(Request)

  • HTTP Method : GET
  • Content-Type: application/json

 매개변수

자료형 

값의 범위/기본 값 

비고 

 필수여부

page

integer

0

조회하고자 하는 페이지 번호

user_name

string 


특정 사용자의 게시물만 받아오고자 할 때

N

articles_per_page

integer

1~100 / 10 

각 페이지 당 게시물 수 

N


응답(Response)

Key 

자료형 

비고 

필수여부 

articles 

json object array (string) 

게시물 정보 배열 

Y

articles_per_page

integer

각 페이지 당 게시물 수

 Y

current_page

integer 

전송된 페이지 

total_page

integer 

전체 페이지 수 

Y

articles item object 형태

Key 

자료형 

비고 

필수여부 

image_url

string

이미지 URL

Y

user_name

string

업로드한 사용자 이름 

description

string 

이미지 설명 

title

string 

이미지 제목 

article_id

string 

게시물 고유 식별자



클라이언트에서 요청할 때 부가정보 전달을 위해서 page, user_name, articles_per_page 매개변수를 사용하는군요. 일단 이 매개변수를 가져오는 코드를 작성해봅니다.


사진 게시물 목록을 핸들링하는 articleListHandler(request:, response:) 함수에 기존의 내용을 지워주고 새로운 코드를 작성합니다.

    response.setHeader(.contentType, value: ContentsType.json)
    
    // 요청된 페이지 번호
    let page: Int = Int(request.param(name: JSONKey.page) ?? "0") ?? 0
    
    // 잘못된 범위라면 응답처리
    if page < 0 {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message : "wrong page number"])
        return
    }
    
    // 한 페이지에 담길 게시물 개수
    let articlesPerPage: Int = Int(request.param(name: JSONKey.articlesPerPage) ?? "10") ?? 10
    
    // 잘못된 범위라면 응답처리
    if articlesPerPage < 1 || articlesPerPage > 100 {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message : "wrong paging size"])
        return
    }
    
    // 특정 사용자의 게시물만 검색하고자 할 때 요청하는 사용자 이름
    let userName: String? = request.param(name: JSONKey.userName)

자 이렇게 요청 정보에서 필요한 정보들을 가져왔습니다. 이제 이 정보들을 기반으로 검색할 쿼리를 만들어줍니다. 위 코드 바로 아래에 이어서 코드를 작성합니다.

    // 쿼리에 사용할 BSON 객체
    let query: BSON = BSON()
    
    // 만약 사용자 이름에 대해 검색하고 한다면 BSON 객체에 값 추가
    if let name = userName {
        query.append(key: JSONKey.userName, string: name)
    }

쿼리는 역시나 간단합니다. 그러면 쿼리를 통해 검색된 문서의 총 개수를 알아봅니다. 검색결과는 MongoResult라는 열거형 타입으로 반환됩니다. 그리고 그 연관 값으로 결과 값을 알아낼 수 있지요.

    // 쿼리를 통해 조건에 해당하는 문서가 총 몇개인지 확인
    guard let countResult = DB.collection?.count(query: query) else {
        sendCompleteResponse(response, status: .internalServerError)
        return
    }
    
    // 총 문서의 개수를 저장할 상수
    let totalCount: Int
    
    // count 결과에 따른 처리
    if case .replyInt(let count) = countResult {
        totalCount = count
    } else if case .error(_, _, let message) = countResult {
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.message : message])
        return
    } else {
        sendCompleteResponse(response, status: .internalServerError)
        return
    }


마지막으로 쿼리를 통해 검색한 문서들을 우리가 원하는 범위에서 뽑아내어 딕셔너리 형태로 반환받습니다.

    // 반환할 데이터 딕셔너리
    var responseData : [String : Any] = [:]
    responseData[JSONKey.articles] = articleDictionaries
    responseData[JSONKey.articlesPerPage] = articlesPerPage
    responseData[JSONKey.currentPage] = page
    responseData[JSONKey.totalPage] = totalCount / articlesPerPage
    
    // 모든 작업을 완료하고 JSON 응답
    sendCompleteResponse(response, status: .ok, jsonBody: responseData)

빠밤! 참 쉽죠? 헤헿 






Read까지 해봤군요! 다음엔 Update를 할까요 Delete를 할까요?

오늘은 여기까지~!


다음에 또 만나요~~ :D



참고문서




by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom

p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 




저작자 표시 비영리 변경 금지
신고

'Swift > Perfect' 카테고리의 다른 글

사진 게시판 API 만들기 [6]  (0) 2017.09.13
사진 게시판 API 만들기 [5]  (0) 2017.09.06
사진 게시판 API 만들기 [4]  (0) 2017.08.30
사진 게시판 API 만들기 [3]  (0) 2017.08.09
사진 게시판 API 만들기 [2]  (0) 2017.07.11
사진 게시판 API 만들기 [1]  (6) 2017.06.27
Posted by yagom


Perfect 미니 프로젝트 [3]


1. 사진 게시물 등록하기 
2. URL을 통해 사진 GET 


API를 하나씩 하나씩 완성해 나가도록 합시다!!

참고

2017년 7월 현재 Swift 3 / Perfect 최신버전 2.0.x 환경에서 진행함을 알려드립니다.

* Swift 최신 버전 확인(https://github.com/apple/swift/releases)

* Perfect 최신 버전 확인(https://github.com/PerfectlySoft/Perfect/releases)


지난 내용 돌아보기

2017/06/27 - [Swift/Perfect] - 사진 게시판 API 만들기 [1]

2017/07/11 - [Swift/Perfect] - 사진 게시판 API 만들기 [2]



사진 게시물 등록하기

먼저 CRUD 중, Create를 먼저 해보도록 할게요. 사진 게시물 등록을 위한 API를 구현해 봅니다. 사진 게시물 등록은 /article 경로에 POST 메서드를 통해 multipart/form-data를 전송합니다.

지난 번 정의한 API 스펙 중 사진 게시물 등록 API의 정보입니다.

요청(Request)

  • HTTP Method : POST
  • Content-Type: multipart/form-data

 매개변수

자료형 

값의 범위/기본 값 

비고 

 필수여부

image 

binary data

 

이미지 데이터 

user_name

string 

 

사용자 이름 

Y

description

string 

 

이미지 설명 

N

title

string 

 

이미지 제목 

Y


응답(Response)

Key 

자료형 

비고 

필수여부 

article_id 

string 

업로드된 게시물 고유 식별자 

 image_url

string 

이미지 주소 

user_name 

string 

사용자 이름 

description 

 string

이미지 설명 

title 

string 

이미지 제목 


우선 Sources/handlers.swift 파일로 이동하여 반복적으로 수행할 것 같은 기능이나, 상수를 정의해보려 합니다.

먼저 필수 매개변수가 모두 실려왔는지 확인하는 함수를 만듭니다. handlers.swift 파일 맨 아래쪽에 함수를 만들어봅니다.


// 부족한 매개변수가 없는지 확인하여 부족한 매개변수가 있다면 String Array로 반환
private func lakedParams(paramsNeeded: [String], paramsReceived: [(String, String)]) -> [String]? {
    
    var laked: [String] = []
    
    for param in paramsNeeded {
        if paramsReceived.filter({ (key, _) -> Bool in
            return key == param
        }).isEmpty {
            laked.append(param)
        }
    }
    
    return laked.count > 0 ? laked : nil
}

또, 나중에 해보시면 아시겠지만 JSONConvertible 타입의 데이터를 응답 객체의 body에 셋팅하려면 꼭 오류처리(try)를 해줘야 합니다. 매 번 하기 귀찮아서 그냥 함수를 하나 만들어줬습니다. lakedParams(paramsNeeded:paramsReceived:) 함수 아래에 작성해봅니다.

// 응답 보내기
private func sendCompleteResponse(_ response: HTTPResponse, status: HTTPResponseStatus = .ok, jsonBody: JSONConvertible? = nil) {
    do {
        if let json = jsonBody {
            try response.setBody(json: json)
        }
        response.completed(status: status)
    } catch {
        response.completed(status: .internalServerError)
    }
}


참, 그리고 요청에서 실려온 이미지를 저장할 디렉터리도 미리 지정해 두고 싶구요, 매개변수 이름이나 JSON 키값도 미리 지정해두고 싶습니다. ContentsType 구조체 선언 아래쪽에 아래 코드를 추가해봅니다. 사실 어디에 위치하든 크게 상관은 없지만 그래도 이게 예쁘잖아요 :D

// 이미지 저장 디렉터리
private let imageDir: Dir? = {
    let imageDir = Dir("./image")
    
    if imageDir.exists == false {
        do {
            try imageDir.create()
            print("Working Directory (\(imageDir.path)) for examples created.")
        } catch {
            print("Could not create Working Directory for examples.")
            return nil
        }
    }
    
    return imageDir
}()

// JSON 데이터의 Key 값 혹은 요청 매개변수 이름에 사용
private struct JSONKey {
    static let image = "image"
    static let userName = "user_name"
    static let description = "description"
    static let title = "title"
    static let articles = "articles"
    static let articleId = "article_id"
    static let page = "page"
    static let articlesPerPage = "articles_per_page"
    static let currentPage = "current_page"
    static let totalPage = "total_page"
    static let imageUrl = "image_url"
    static let imagePath = "image_path"
    static let message = "message"
    static let error = "error"
}


자, 밑준비를 마쳤습니다.

사용자가 게시물 작성을 요청하는 postArticleHandler(request: response:) 함수를 본격적으로 작성해봅시다. 기존에 함수 내부에 있던 코드는 지워주세요 :)


먼저, 응답은 JSON 형식이 될 것이므로 맨 윗줄에 아래 코드를 작성합니다. response 객체는 클라이언트에게 응답을 줄 때 사용할 객체입니다.

// 응답 컨텐츠 형식은 JSON response.setHeader(.contentType, value: ContentsType.json)


매개변수로 전달된 request 객체에 클라이언트의 요청 정보가 모두 실려있습니다. 먼저 모든 필수 매개변수가 전달되었는지 확인합니다.


POST 메서드로 전달된 매개변수는 request 객체의 postParams() 메서드를 통해 가져올 수 있습니다. postParams() 메서드의 반환 타입은 (String, String) 튜플의 Array 타입입니다.

필수 매개변수인 user_name, title이 모두 전달되었는지 확인하고 싶습니다(image 매개변수는 파일로 받아올 것이니 다음 차례에서 확인합니다). 

// 부족한 매개변수가 없는지 확인
    if let lakedParams = lakedParams(paramsNeeded: [JSONKey.userName, JSONKey.title], paramsReceived: request.postParams) {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"need more params \(lakedParams)"])
    }
    
    // 이미지 파일이 업로드 되었는지 확인
    guard let imageInformation: MimeReader.BodySpec = request.postFileUploads?.first,
        let imageFile = imageInformation.file else {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"need image file"])
            return
    }

자, 이렇게 모든 전달 데이터가 확인되었다면 이미지를 서버의 이미지 디렉터리에 저장할 차례입니다. 먼저 이미지 이름에 사용자 이름을 넣고 싶기 때문에 클라이언트에서 전달된 데이터를 빼냅니다.

// 사용자 이름, 제목 추출
    guard let userName = request.param(name: JSONKey.userName),
        let title = request.param(name: JSONKey.title) else {
            sendCompleteResponse(response, status: .internalServerError, jsonBody: nil)
            return
    }


그리고 이미지가 저장될 디렉터리에 접근할 수 있는지 확인합니다.

// 이미지가 저장될 디렉터리
    guard let imageDirectory = imageDir else {
        sendCompleteResponse(response, status: .internalServerError, jsonBody: nil)
        return
    }

고유한 이미지 파일 이름을 만들어주기 위해서 타임스탬프 값을 활용했습니다. 위에서 request 객체에서 꺼내온 이미지 파일을 서버의 이미지 디렉터리 내부의 경로로 복사해줍니다.

// 고유한 이미지 이름을 위해 타임스템프 값을 활용
    let timestamp: Int = icuDateToSeconds(getNow())
    
    // 사용자이름_타임스템프.jpg 형식으로 파일이름 지정
    let imageFileName: String = userName + "_" + String(timestamp) + ".jpg"
    
    // 이미지가 저장될 경로
    let imageFilePath: String = imageDirectory.path + imageFileName
    
    // 이미지 저장에 실패할 경우 실패 응답 보내기
    do {
        try imageFile.copyTo(path: imageFilePath, overWrite: false)
    } catch {
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:error.localizedDescription])
        return
    }

이미지 복사도 마쳤으니 이제 데이터베이스에 정보를 저장할 차례입니다. mongoDB의 컬렉션도 불러오고, DB에 저장할 데이터를 담은 딕셔너리를 생성해줍니다.
  // mongoDB 컬렉션 가져오기
    guard let collection = DB.collection else {
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:"data base initialize failed"])
        return
    }
    
    // DB에 저장할 데이터
    // 차후에 응답 데이터로도 사용합니다
    var jsonDictionary = [JSONKey.userName: userName,
                          JSONKey.description: request.param(name: JSONKey.description),
                          JSONKey.title: title,
                          JSONKey.imagePath:imageFilePath]

이제 실질적으로 DB에 저장합니다. 저장을 하면서 문서의 고유ID도 생성해줍니다. 만약 DB 저장에 실패한다면 아까 복사해온 이미지 파일은 서버 디렉터리에서 삭제해주어야 합니다.

    // DB에 저장
    do {
        let jsonString = try jsonDictionary.jsonEncodedString()
        
        let document: BSON = try BSON(json: jsonString)
        
        // 문서 사용 후에는 닫아주는게 좋겠습니다
        defer {
            document.close()
        }
        
        // 문서 고유 아이디 생성 및 아이디 부여
        let oid: BSON.OID = BSON.OID(imageFileName)
        document.append(oid: oid)
        
        // 컬렉션에 문서 저장
        let result = collection.save(document: document)
        
        // 고유 아이디를 응답 데이터에 추가
        jsonDictionary[JSONKey.articleId] = oid.description
        
        if case .success = result { } else {
            sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:result])
            return
        }
        
    } catch {
        
        // DB 저장에 실패했으므로 이미지 파일은 삭제
        File(imageFilePath).delete()
        
        Log.error(message: error.localizedDescription)
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:error.localizedDescription])
        return
    }

DB 저장을 완료하면 응답 데이터를 보내 게시물 등록 처리를 완료합니다.

    // 응답 데이터에는 이미지 경로 대신 이미지 URL 전송
    jsonDictionary[JSONKey.imagePath] = nil
    jsonDictionary[JSONKey.imageUrl] = server.serverAddress + ":\(server.serverPort)" + "/image/" + imageFileName
    
    // 모든 작업을 완료하고 JSON 응답
    sendCompleteResponse(response, status: .created, jsonBody: jsonDictionary)

자 이렇게 postArticleHandler(request:response:) 함수 작성을 마쳤습니다.

// 사진 게시물 등록
func postArticleHandler(request: HTTPRequest, response: HTTPResponse) {
    
    // 응답 컨텐츠 형식은 JSON
    response.setHeader(.contentType, value: ContentsType.json)
    
    // 부족한 매개변수가 없는지 확인
    if let lakedParams = lakedParams(paramsNeeded: [JSONKey.userName, JSONKey.title], paramsReceived: request.postParams) {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"need more params \(lakedParams)"])
    }
    
    // 이미지 파일이 업로드 되었는지 확인
    guard let imageInformation: MimeReader.BodySpec = request.postFileUploads?.first,
        let imageFile = imageInformation.file else {
        sendCompleteResponse(response, status: .badRequest, jsonBody: [JSONKey.message:"need image file"])
            return
    }
    
    // 사용자 이름, 제목 추출
    guard let userName = request.param(name: JSONKey.userName),
        let title = request.param(name: JSONKey.title) else {
            sendCompleteResponse(response, status: .internalServerError, jsonBody: nil)
            return
    }
    
    // 이미지가 저장될 디렉터리
    guard let imageDirectory = imageDir else {
        sendCompleteResponse(response, status: .internalServerError, jsonBody: nil)
        return
    }
    
    // 고유한 이미지 이름을 위해 타임스템프 값을 활용
    let timestamp: Int = icuDateToSeconds(getNow())
    
    // 사용자이름_타임스템프.jpg 형식으로 파일이름 지정
    let imageFileName: String = userName + "_" + String(timestamp) + ".jpg"
    
    // 이미지가 저장될 경로
    let imageFilePath: String = imageDirectory.path + imageFileName
    
    // 이미지 저장에 실패할 경우 실패 응답 보내기
    do {
        try imageFile.copyTo(path: imageFilePath, overWrite: false)
    } catch {
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:error.localizedDescription])
        return
    }
    
    // mongoDB 컬렉션 가져오기
    guard let collection = DB.collection else {
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:"data base initialize failed"])
        return
    }
    
    // DB에 저장할 데이터
    // 차후에 응답 데이터로도 사용합니다
    var jsonDictionary = [JSONKey.userName: userName,
                          JSONKey.description: request.param(name: JSONKey.description),
                          JSONKey.title: title,
                          JSONKey.imagePath:imageFilePath]
    
    // DB에 저장
    do {
        let jsonString = try jsonDictionary.jsonEncodedString()
        
        let document: BSON = try BSON(json: jsonString)
        
        // 문서 고유 아이디 생성 및 아이디 부여
        let oid: BSON.OID = BSON.OID(imageFileName)
        document.append(oid: oid)
        
        // 컬렉션에 문서 저장
        let result = collection.save(document: document)
        
        // 고유 아이디를 응답 데이터에 추가
        jsonDictionary[JSONKey.articleId] = oid.description
        
        if case .success = result { } else {
            sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:result])
            return
        }
        
    } catch {
        
        // DB 저장에 실패했으므로 이미지 파일은 삭제
        File(imageFilePath).delete()
        
        Log.error(message: error.localizedDescription)
        sendCompleteResponse(response, status: .internalServerError, jsonBody: [JSONKey.error:error.localizedDescription])
        return
    }

    // 응답 데이터에는 이미지 경로 대신 이미지 URL 전송
    jsonDictionary[JSONKey.imagePath] = nil
    jsonDictionary[JSONKey.imageUrl] = server.serverAddress + ":\(server.serverPort)" + "/image/" + imageFileName
    
    // 모든 작업을 완료하고 JSON 응답
    sendCompleteResponse(response, status: .created, jsonBody: jsonDictionary)
}

자 이제 mongoDB도 실행된 상태에서, Perfect 서버 애플리케이션을 실행해봅니다. 그리고는 /article 경로에 새로운 게시물 등록을 요청해봅니다.

 

 


 

짜잔!! 성공했습니다! 올바른 응답이 오고 있어요!



URL을 통해 사진 GET


자, 위 이미지의 응답 데이터를 보면 image_url이 오고 있습니다. 그 URL을 통해서 이미지를 한 번 받아와볼까요?

 


아니, 응답받은 URL인데...!! 왜 다운로드가 안되는거죠!? 흐음... 라우팅을 뭔가 해줘야 할 것 같습니다.


으음... 자 그럼 요청을 핸들링 할 수 있는 함수를 하나 또 만들어줍니다. handlers.swift 파일에 아래 함수를 하나 만들어줬습니다. 요청의 주소의 마지막에 위치한 이미지파일의 이름을 가지고 이미지 디렉터리의 파일이 있는지 확인하여 파일 핸들러를 사용하여 응답합니다.

// 이미지 파일
func imageHandler(request: HTTPRequest, response: HTTPResponse) {
    response.setHeader(.contentType, value: ContentsType.formData)
    
    guard let imageDirectory = imageDir else {
        sendCompleteResponse(response, status: .internalServerError)
        return
    }
    
    guard let imageFileName = request.pathComponents.last, imageFileName.contains(".jpg") else {
        sendCompleteResponse(response, status: .badRequest)
        return
    }
    
    request.path = imageFileName
    
    let handler = StaticFileHandler(documentRoot: imageDirectory.path)
    
    handler.handleRequest(request: request, response: response)
}

main.swift 파일로 이동하여 길을 하나 더 뚫어줍니다. 일단 articleURI 상수 아래에 imageURI도 하나 선언해줍니다.

let articleURI = "/article" let imageURI = "/image" let subArticleURI = articleURI + "/*"


그리고 서버 객체를 생성하는 코드 윗 줄에 아래 코드를 작성해줍니다.

routes.add(method: .get, uri: imageURI + "/*", handler: imageHandler(request:response:))

이렇게 작성하면 /image/xxx.jpg 형식으로 요청이 들어오면 imageHandler(request:response:) 함수가 핸들링 하게 될 것입니다.


자 이제 서버를 재시작하고, 다시 요청해볼까요?



 

으헿, 잘 나왔네요!


이번에는 CRUD 중, Create 먼저 해봤습니다. 다음 번에는 Read를 함께 해봐요~!

뿅!




참고문서



 



by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom

p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 




저작자 표시 비영리 변경 금지
신고

'Swift > Perfect' 카테고리의 다른 글

사진 게시판 API 만들기 [5]  (0) 2017.09.06
사진 게시판 API 만들기 [4]  (0) 2017.08.30
사진 게시판 API 만들기 [3]  (0) 2017.08.09
사진 게시판 API 만들기 [2]  (0) 2017.07.11
사진 게시판 API 만들기 [1]  (6) 2017.06.27
Perfect 라우팅  (0) 2017.05.24
Posted by yagom

더 알아보기

이제까지 포스팅한 내용 외에 추가적으로 알아가야 할 문법과 개념들을 모아봤습니다.

  • 제네릭(Generics)
  • 서브스크립트(Subscript)
  • 접근수준(Access Control)
  • ARC(Automatic Reference Counting)
  • 중첩타입(Nested Types)
  • 사용자정의 연산자(Custom Operators)

스위프트 기본문법 강좌는 여기서 마칩니다. 많은 도움이 되었길 소망합니다.


yagom  



관련저서


스위프트 프로그래밍 - 야곰 지음 (한빛미디어)

http://book.naver.com/bookdb/book_detail.nhn?bid=11445773





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 

저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

기본문법을 마치며...  (0) 2017.07.31
고차함수  (0) 2017.07.27
오류처리  (0) 2017.07.24
익스텐션  (0) 2017.07.20
프로토콜  (0) 2017.07.17
assert와 guard  (0) 2017.07.13
Posted by yagom

고차함수

Swift/기본문법 2017.07.27 11:00

고차함수

고차함수(Higher-order function)은 '다른 함수를 전달인자로 받거나 함수실행의 결과를 함수로 반환하는 함수'를 뜻합니다.

스위프트의 함수(클로저)는 일급시민(first-citizen)이기 때문에 함수의 전달인자로 전달할 수 있으며, 함수의 결과값으로 반환할 수 있습니다.


이번 파트에서는 스위프트 표준라이브러리에서 제공하는 유용한 고차함수에 대해 알아봅니다.

  • map
  • filter
  • reduce

map, filter, reduce 함수는 스위프트 표준 라이브러리의 컨테이너 타입(Array, Set, Dictionary 등)에 구현되어 있습니다.


소스코드



map

map함수(메서드)는 컨테이너 내부의 기존 데이터를 변형(transform)하여 새로운 컨테이너를 생성합니다.


> 변형하고자 하는 numbers와 변형 결과를 받을 doubledNumbers, strings

let numbers: [Int] = [0, 1, 2, 3, 4]
var doubledNumbers: [Int]
var strings: [String]


> for 구문 사용

doubledNumbers = [Int]()
strings = [String]()

for number in numbers {
    doubledNumbers.append(number * 2)
    strings.append("\(number)")
}

print(doubledNumbers) // [0, 2, 4, 6, 8]
print(strings) // ["0", "1", "2", "3", "4"]


> map 메서드 사용

// numbers의 각 요소를 2배하여 새로운 배열 반환
doubledNumbers = numbers.map({ (number: Int) -> Int in
    return number * 2
})

// numbers의 각 요소를 문자열로 변환하여 새로운 배열 반환
strings = numbers.map({ (number: Int) -> String in
    return "\(number)"
})

print(doubledNumbers) // [0, 2, 4, 6, 8]
print(strings) // ["0", "1", "2", "3", "4"]

// 매개변수, 반환 타입, 반환 키워드(return) 생략, 후행 클로저
doubledNumbers = numbers.map { $0 * 2 }
print(doubledNumbers) // [0, 2, 4, 6, 8]


filter

filter함수(메서드)는 컨테이너 내부의 값을 걸러서 새로운 컨테이너로 추출합니다.


> for 구문 사용

// 변수 사용에 주목하세요
var filtered: [Int] = [Int]()

for number in numbers {
    if number % 2 == 0 {
        filtered.append(number)
    }
}

print(filtered) // [0, 2, 4]


> filter 메서드 사용

// numbers의 요소 중 짝수를 걸러내어 새로운 배열로 반환
let evenNumbers: [Int] = numbers.filter { (number: Int) -> Bool in
    return number % 2 == 0
}
print(evenNumbers) // [0, 2, 4]

// 매개변수, 반환 타입, 반환 키워드(return) 생략, 후행 클로저
let oddNumbers: [Int] = numbers.filter {
    $0 % 2 != 0
}
print(oddNumbers) // [1, 3]



reduce

reduce함수(메서드)는 컨테이너 내부의 콘텐츠를 하나로 통합합니다.


> 통합하고자 하는 someNumbers

let someNumbers: [Int] = [2, 8, 15]


> for 구문 사용

// 변수 사용에 주목하세요
var result: Int = 0

// someNumbers의 모든 요소를 더합니다
for number in someNumbers {
    result += number
}

print(result) // 25


> reduce 메서드 사용

// 초깃값이 0이고 someNumbers 내부의 모든 값을 더합니다.
let sum: Int = someNumbers.reduce(0, { (first: Int, second: Int) -> Int in
    //print("\(first) + \(second)") //어떻게 동작하는지 확인해보세요
    return first + second
})

print(sum)  // 25

// 초깃값이 0이고 someNumbers 내부의 모든 값을 뺍니다.
var subtract: Int = someNumbers.reduce(0, { (first: Int, second: Int) -> Int in
    //print("\(first) - \(second)") //어떻게 동작하는지 확인해보세요
    return first - second
})

print(subtract) // -25

// 초깃값이 3이고 someNumbers 내부의 모든 값을 더합니다.
let sumFromThree = someNumbers.reduce(3) { $0 + $1 }

print(sumFromThree) // 28



더 알아보기

flatmap








by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 

저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

기본문법을 마치며...  (0) 2017.07.31
고차함수  (0) 2017.07.27
오류처리  (0) 2017.07.24
익스텐션  (0) 2017.07.20
프로토콜  (0) 2017.07.17
assert와 guard  (0) 2017.07.13
Posted by yagom

오류처리

Swift/기본문법 2017.07.24 11:00

오류처리

스위프트에서 오류(Error)Error라는 프로토콜을 준수하는 타입의 값을 통해 표현됩니다. Error 프로토콜은 사실상 요구사항이 없는 빈 프로토콜일 뿐이지만, 오류를 표현하기 위한 타입(주로 열거형)은 이 프로토콜을 채택합니다.


스위프트의 열거형은 오류의 종류를 나타내기에 아주 적합한 기능입니다. 연관 값을 통해 오류에 관한 부가 정보를 제공할 수도 있습니다. 


이번 예제에는 프로그램 내에서 자판기를 작동시키려고 할 때 발생하는 오류상황을 구현해 보았습니다.


소스코드


오류표현

Error 프로토콜과 (주로)열거형을 통해서 오류를 표현합니다

enum 오류종류이름: Error {
    case 종류1
    case 종류2
    case 종류3
    //...
}


> 자판기 동작 오류의 종류를 표현한 VendingMachineError 열거형

enum VendingMachineError: Error {
    case invalidInput
    case insufficientFunds(moneyNeeded: Int)
    case outOfStock
}



함수에서 발생한 오류 던지기

자판기 동작 도중 발생한 오류를 던지는 메서드를 구현해봅니다.

오류 발생의 여지가 있는 메서드는 throws를 사용하여 오류를 내포하는 함수임을 표시합니다.

class VendingMachine {
    let itemPrice: Int = 100
    var itemCount: Int = 5
    var deposited: Int = 0
    
    // 돈 받기 메서드
    func receiveMoney(_ money: Int) throws {
        
        // 입력한 돈이 0이하면 오류를 던집니다
        guard money > 0 else {
            throw VendingMachineError.invalidInput
        }
        
        // 오류가 없으면 정상처리를 합니다
        self.deposited += money
        print("\(money)원 받음")
    }
    
    // 물건 팔기 메서드
    func vend(numberOfItems numberOfItemsToVend: Int) throws -> String {
        
        // 원하는 아이템의 수량이 잘못 입력되었으면 오류를 던집니다
        guard numberOfItemsToVend > 0 else {
            throw VendingMachineError.invalidInput
        }
        
        // 구매하려는 수량보다 미리 넣어둔 돈이 적으면 오류를 던집니다
        guard numberOfItemsToVend * itemPrice <= deposited else {
            let moneyNeeded: Int
            moneyNeeded = numberOfItemsToVend * itemPrice - deposited
            
            throw VendingMachineError.insufficientFunds(moneyNeeded: moneyNeeded)
        }
        
        // 구매하려는 수량보다 요구하는 수량이 많으면 오류를 던집니다
        guard itemCount >= numberOfItemsToVend else {
            throw VendingMachineError.outOfStock
        }
        
        // 오류가 없으면 정상처리를 합니다
        let totalPrice = numberOfItemsToVend * itemPrice
        
        self.deposited -= totalPrice
        self.itemCount -= numberOfItemsToVend
        
        return "\(numberOfItemsToVend)개 제공함"
    }
}

// 자판기 인스턴스
let machine: VendingMachine = VendingMachine()

// 판매 결과를 전달받을 변수
var result: String?



오류처리

오류를 던질 수도 있지만 오류가 던져지는 것에 대비하여 던져진 오류를 처리하기 위한 코드도 작성해야 합니다. 예를 들어 던져진 오류가 무엇인지 판단하여 다시 문제를 해결한다든지, 다른 방법으로 시도해 본다든지, 사용자에게 오류를 알리고 사용자에게 선택 권한을 넘겨주어 다음에 어떤 동작을 하게 할 것인지 결정하도록 유도하는 등의 코드를 작성해야 합니다.


오류발생의 여지가 있는 throws 함수(메서드)는 try를 사용하여 호출해야합니다.  

trydo-catch, try?try! 등에 대해 알아봅니다.


do-catch

오류발생의 여지가 있는 throws 함수(메서드)는 do-catch 구문을 활용하여 오류발생에 대비합니다.

> 가장 정석적인 방법으로 모든 오류 케이스에 대응합니다

do {
    try machine.receiveMoney(0)
} catch VendingMachineError.invalidInput {
    print("입력이 잘못되었습니다")
} catch VendingMachineError.insufficientFunds(let moneyNeeded) {
    print("\(moneyNeeded)원이 부족합니다")
} catch VendingMachineError.outOfStock {
    print("수량이 부족합니다")
} // 입력이 잘못되었습니다


> 하나의 catch 블럭에서 switch 구문을 사용하여 오류를 분류해봅니다. 굳이 위의 것과 크게 다를 것이 없습니다.

do {
    try machine.receiveMoney(300)
} catch /*(let error)*/ {
    
    switch error {
    case VendingMachineError.invalidInput:
        print("입력이 잘못되었습니다")
    case VendingMachineError.insufficientFunds(let moneyNeeded):
        print("\(moneyNeeded)원이 부족합니다")
    case VendingMachineError.outOfStock:
        print("수량이 부족합니다")
    default:
        print("알수없는 오류 \(error)")
    }
} // 300원 받음


> 딱히 케이스별로 오류처리 할 필요가 없으면 catch 구문 내부를 간략화해도 무방합니다.

do {
    result = try machine.vend(numberOfItems: 4)
} catch {
    print(error)
} // insufficientFunds(100)


> 딱히 케이스별로 오류처리 할 필요가 없으면 do 구문만 써도 무방합니다

do {
    result = try machine.vend(numberOfItems: 4)
}


try? 와 try!

try?

별도의 오류처리 결과를 통보받지 않고 오류가 발생했으면 결과값을 nil로 돌려받을 수 있습니다. 정상동작 후에는 옵셔널 타입으로 정상 반환값을 돌려 받습니다.

result = try? machine.vend(numberOfItems: 2)
result // Optional("2개 제공함")

result = try? machine.vend(numberOfItems: 2)
result // nil


try!

오류가 발생하지 않을 것이라는 강력한 확신을 가질 때 try!를 사용하면 정상동작 후에 바로 결과값을 돌려받습니다. 오류가 발생하면 런타임 오류가 발생하여 애플리케이션 동작이 중지됩니다.

result = try! machine.vend(numberOfItems: 1)
result // 1개 제공함

//result = try! machine.vend(numberOfItems: 1)
// 런타임 오류 발생!


더 알아보기

추가적으로 더 알아보면 좋은 개념입니다.

  • rethrows
  • defer

관련문서

* The Swift Programming Language - Error Handling

* The Swift Programming Language - Enumerations





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 

저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

기본문법을 마치며...  (0) 2017.07.31
고차함수  (0) 2017.07.27
오류처리  (0) 2017.07.24
익스텐션  (0) 2017.07.20
프로토콜  (0) 2017.07.17
assert와 guard  (0) 2017.07.13
Posted by yagom

익스텐션

Swift/기본문법 2017.07.20 15:59

익스텐션

익스텐션(Extension) 은 스위프트의 강력한 기능 중 하나입니다. 익스텐션은 구조체, 클래스, 열거형, 프로토콜 타입에 새로운 기능을 추가 할 수 있는 기능입니다. 

기능을 추가하려는 타입의 구현된 소스 코드를 알지 못하거나 볼 수 없다 해도, 타입만 알고 있다면 그 타입의 기능을 확장할 수도 있습니다.


스위프트의 익스텐션이 타입에 추가할 수 있는 기능

  • 연산 타입 프로퍼티 / 연산 인스턴스 프로퍼티 
  • 타입 메서드 / 인스턴스 메서드
  • 이니셜라이저
  • 서브스크립트 
  • 중첩 타입
  • 특정 프로토콜을 준수할 수 있도록 기능 추가 


익스텐션은 타입에 새로운 기능을 추가할 수는 있지만, 기존에 존재하는 기능을 재정의 할 수는 없습니다.


클래스의 상속과 익스텐션을 비교해보겠습니다. 이 둘은 비슷해보이지만 실제 성격은 많이 다릅니다. 

클래스의 상속은 클래스 타입에서만 가능하지만 익스텐션은 구조체, 클래스, 프로토콜 등에 적용이 가능합니다. 또 클래스의 상속은 특정 타입을 물려받아 하나의 새로운 타입을 정의하고 추가 기능을 구현하는 수직 확장이지만, 익스텐션은 기존의 타입에 기능을 추가하는 수평 확장입니다. 또, 상속을 받으면 기존 기능을 재정의할 수 있지만, 익스텐션은 재정의할 수 없다는 것도 큰 차이 중 하나입니다. 상황과 용도에 맞게 상속과 익스텐션을 선택하여 사용하면 됩니다. 

 

 상속

익스텐션 

확장 

수직확장 

수평확장 

사용 

클래스 타입 

클래스, 구조체, 프로토콜, 제네릭 등 모든 타입 

재정의 

가능 

불가능 


익스텐션을 사용하는 대신 원래 타입을 정의한 소스에 기능을 추가하는 방법도 있겠지만, 외부 라이브러리나 프레임워크를 가져다 썼다면 원본 소스를 수정하지 못합니다. 이처럼 외부에서 가져온 타입에 내가 원하는 기능을 추가하고자 할 때 익스텐션을 사용합니다. 따로 상속을 받지 않아도 되며, 구조체와 열거형에도 기능을 추가할 수 있으므로 익스텐션은 매우 편리한 기능입니다. 

익스텐션은 모든 타입에 적용할 수 있습니다. 모든 타입이라 함은 구조체, 열거형, 클래스, 프로토콜, 제네릭 타입 등을 뜻합니다. 즉, 익스텐션을 통해 모든 타입에 연산 프로퍼티, 메서드, 이니셜라이저, 서브스크립트, 중첩 데이터 타입 등을 추가할 수 있습니다.

더불어 익스텐션은 프로토콜과 함께 사용하면 굉장히 강력한 기능을 선사합니다. 이 부분은 프로토콜 중심 프로그래밍(Protocol Oriented Programming)에 대해 더 알아보면 좋습니다.


소스코드


정의 문법

extension 키워드를 사용하여 정의합니다.

extension 확장할 타입 이름 {
    /* 타입에 추가될 새로운 기능 구현 */
}


익스텐션은 기존에 존재하는 타입이 추가적으로 다른 프로토콜을 채택할 수 있도록 확장할 수도 있습니다. 이런 경우에는 클래스나 구조체에서 사용하던 것과 똑같은 방법으로 프로토콜 이름을 나열해줍니다.

extension 확장할 타입 이름: 프로토콜1, 프로토콜2, 프로토콜3... {
    /* 프로토콜 요구사항 구현 */
}


스위프트 라이브러리를 살펴보면 실제로 익스텐션이 굉장히 많이 사용되고 있음을 알 수 있습니다. Double 타입에는 수많은 프로퍼티와 메서드, 이니셜라이저가 정의되어 있으며 수많은 프로토콜을 채택하고 있을 것이라고 예상되지만, 실제로 Double 타입의 정의를 살펴보면 그 모든것이 다 정의되어 있지는 않습니다. 

그러면 Double 타입이 채택하고 준수해야 하는 수많은 프로토콜은 어디로 갔을까요? 어디에서 채택하고 어디에서 준수하도록 정의되어 있을까요? 당연히 답은 익스텐션입니다. 이처럼 스위프트 표준 라이브러리 타입의 기능은 대부분 익스텐션으로 구현되어 있습니다. Double 외에도 다른 타입들의 정의와 익스텐션을 찾아보면 더 많은 예를 보실 수 있습니다. 꼭 한 번 찾아보세요! 


익스텐션 구현

연산 프로퍼티 추가

extension Int {
    var isEven: Bool {
        return self % 2 == 0
    }
    var isOdd: Bool {
        return self % 2 == 1
    }
}

print(1.isEven) // false
print(2.isEven) // true
print(1.isOdd)  // true
print(2.isOdd)  // false

var number: Int = 3
print(number.isEven) // false
print(number.isOdd) // true

number = 2
print(number.isEven) // true
print(number.isOdd) // false

위 코드의 익스텐션은 Int 타입에 두 개의 연산 프로퍼티를 추가한 것입니다. Int 타입의 인스턴스가 홀수인지 짝수인지 판별하여 Bool 타입으로 알려주는 연산 프로퍼티입니다. 익스텐션으로 Int 타입에 추가해준 연산 프로퍼티는 Int 타입의 어떤 인스턴스에도 사용이 가능합니다. 위의 코드처럼 인스턴스 연산 프로퍼티를 추가할 수도 있으며, static 키워드를 사용하여 타입 연산 프로퍼티도 추가할 수 있습니다. 


메서드 추가

extension Int {
    func multiply(by n: Int) -> Int {
        return self * n
    }
}
print(3.multiply(by: 2))  // 6
print(4.multiply(by: 5))  // 20

number = 3
print(number.multiply(by: 2))   // 6
print(number.multiply(by: 3))   // 9

위 코드의 익스텐션을 통해 Int 타입에 인스턴스 메서드multiply(by:) 메서드를 추가했습니다. 여러 기능을 여러 익스텐션 블록으로 나눠서 구현해도 전혀 문제가 없습니다. 관련된 기능별로 하나의 익스텐션 블록에 묶어주는 것도 좋습니다. 


이니셜라이저 추가

extension String {
    init(int: Int) {
        self = "\(int)"
    }
    
    init(double: Double) {
        self = "\(double)"
    }
}

let stringFromInt: String = String(int: 100) 
// "100"

let stringFromDouble: String = String(double: 100.0)    
// "100.0"

인스턴스를 초기화(이니셜라이즈)할 때 인스턴스 초기화에 필요한 다양한 데이터를 전달받을 수 있도록 여러 종류의 이니셜라이저를 만들 수 있습니다. 타입의 정의부에 이니셜라이저를 추가하지 않더라도 익스텐션을 통해 이니셜라이저를 추가할 수 있습니다. 

하지만 익스텐션으로 클래스 타입에 편의 이니셜라이저는 추가할 수 있지만, 지정 이니셜라이저는 추가할 수 없습니다. 지정 이니셜라이저와 디이니셜라이저는 반드시 클래스 타입의 구현부에 위치해야 합니다(값 타입은 상관없습니다).



관련문서

The Swift Programming Language - Extensions





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 

저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

고차함수  (0) 2017.07.27
오류처리  (0) 2017.07.24
익스텐션  (0) 2017.07.20
프로토콜  (0) 2017.07.17
assert와 guard  (0) 2017.07.13
타입캐스팅  (2) 2017.07.10
Posted by yagom

프로토콜

Swift/기본문법 2017.07.17 11:00

프로토콜

프로토콜(Protocol)은 특정 역할을 수행하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진을 정의합니다. 구조체, 클래스, 열거형은 프로토콜을 채택(Adopted)해서 특정 기능을 수행하기 위한 프로토콜의 요구사항을 실제로 구현할 수 있습니다. 어떤 프로토콜의 요구사항을 모두 따르는 타입은 그 프로토콜을 준수한다(Conform)고 표현합니다. 타입에서 프로토콜의 요구사항을 충족시키려면 프로토콜이 제시하는 청사진의 기능을 모두 구현해야 합니다. 

즉, 프로토콜은 기능을 정의하고 제시 할 뿐이지 스스로 기능을 구현하지는 않습니다.


소스코드


정의 문법

protocol 키워드를 사용하여 정의합니다.

protocol 프로토콜 이름 {
    /* 정의부 */
}


프로토콜 구현

protocol Talkable {
    
    // 프로퍼티 요구
    // 프로퍼티 요구는 항상 var 키워드를 사용합니다
    // get은 읽기만 가능해도 상관 없다는 뜻이며
    // get과 set을 모두 명시하면 
    // 읽기 쓰기 모두 가능한 프로퍼티여야 합니다
    var topic: String { get set }
    var language: String { get }
    
    // 메서드 요구
    func talk()
    
    // 이니셜라이저 요구
    init(topic: String, language: String)
}


프로토콜 채택 및 준수

// Person 구조체는 Talkable 프로토콜을 채택했습니다
struct Person: Talkable {
    // 프로퍼티 요구 준수
    var topic: String
    let language: String
    
    // 읽기전용 프로퍼티 요구는 연산 프로퍼티로 대체가 가능합니다
//    var language: String { return "한국어" }
    
    // 물론 읽기, 쓰기 프로퍼티도 연산 프로퍼티로 대체할 수 있습니다
//    var subject: String = ""
//    var topic: String {
//        set {
//            self.subject = newValue
//        }
//        get {
//            return self.subject
//        }
//    }
    
    // 메서드 요구 준수    
    func talk() {
        print("\(topic)에 대해 \(language)로 말합니다")
    }

    // 이니셜라이저 요구 준수    
    init(topic: String, language: String) {
        self.topic = topic
        self.language = language
    }
}


> 프로퍼티 요구는 다양한 방법으로 해석, 구현할 수 있습니다.

struct Person: Talkable {
    var subject: String = ""

    // 프로퍼티 요구는 연산 프로퍼티로 대체가 가능합니다
    var topic: String {
        set {
            self.subject = newValue
        }
        get {
            return self.subject
        }
    }
    
    var language: String { return "한국어" }
    
    func talk() {
        print("\(topic)에 대해 \(language)로 말합니다")
    }
    
    init(topic: String, language: String) {
        self.topic = topic
    }
}



프로토콜 상속

프로토콜은 하나 이상의 프로토콜을 상속받아 기존 프로토콜의 요구사항보다 더 많은 요구사항을 추가할 수 있습니다. 프로토콜 상속 문법은 클래스의 상속 문법과 유사하지만, 프로토콜은 클래스와 다르게 다중상속이 가능합니다.

protocol 프로토콜 이름: 부모 프로토콜 이름 목록 {
 /* 정의부 */
 }


protocol Readable {
    func read()
}
protocol Writeable {
    func write()
}
protocol ReadSpeakable: Readable {
    func speak()
}
protocol ReadWriteSpeakable: Readable, Writeable {
    func speak()
}

struct SomeType: ReadWriteSpeakable {
    func read() {
        print("Read")
    }
    
    func write() {
        print("Write")
    }
    
    func speak() {
        print("Speak")
    }
}


클래스 상속과 프로토콜

클래스에서 상속과 프로토콜 채택을 동시에 하려면 상속받으려는 클래스를 먼저 명시하고 그 뒤에 채택할 프로토콜 목록을 작성합니다.

class SuperClass: Readable { func read() { } } class SubClass: SuperClass, Writeable, ReadSpeakable { func write() { } func speak() { } }


프로토콜 준수 확인

is, as 연산자를 사용해서 인스턴스가 특정 프로토콜을 준수하는지 확인할 수 있습니다.

let sup: SuperClass = SuperClass()
let sub: SubClass = SubClass()

var someAny: Any = sup
someAny is Readable // true
someAny is ReadSpeakable // false

someAny = sub
someAny is Readable // true
someAny is ReadSpeakable // true

someAny = sup

if let someReadable: Readable = someAny as? Readable {
    someReadable.read()
} // read

if let someReadSpeakable: ReadSpeakable = someAny as? ReadSpeakable {
    someReadSpeakable.speak()
} // 동작하지 않음

someAny = sub

if let someReadable: Readable = someAny as? Readable {
    someReadable.read()
} // read



관련문서

The Swift Programming Language - Protocols





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 

저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

오류처리  (0) 2017.07.24
익스텐션  (0) 2017.07.20
프로토콜  (0) 2017.07.17
assert와 guard  (0) 2017.07.13
타입캐스팅  (2) 2017.07.10
옵셔널 체이닝  (2) 2017.07.06
Posted by yagom

assertguard

애플리케이션이 동작 도중에 생성하는 다양한 연산 결과값을 동적으로 확인하고 안전하게 처리할 수 있도록 확인하고 빠르게 처리할 수 있습니다.


소스코드


Assertion

assert(_:_:file:line:) 함수를 사용합니다. assert 함수는 디버깅 모드에서만 동작합니다. 배포하는 애플리케이션에서는 제외됩니다. 

주로 디버깅 중 조건의 검증을 위하여 사용합니다.

var someInt: Int = 0

// 검증 조건에 부합하므로 지나갑니다
assert(someInt == 0, "someInt != 0")

someInt = 1
//assert(someInt == 0) // 동작 중지, 검증 실패
//assert(someInt == 0, "someInt != 0") // 동작 중지, 검증 실패
// assertion failed: someInt != 0: file guard_assert.swift, line 26


func functionWithAssert(age: Int?) {
    
    assert(age != nil, "age == nil")
    
    assert((age! >= 0) && (age! <= 130), "나이값 입력이 잘못되었습니다")
    print("당신의 나이는 \(age!)세입니다")
}

functionWithAssert(age: 50)
//functionWithAssert(age: -1) // 동작 중지, 검증 실패
//functionWithAssert(age: nil) // 동작 중지, 검증 실패


assert(_:_:file:line:)와 같은 역할을 하지만 실제 배포 환경에서도 동작하는 precondition(_:_:file:line:) 함수도 있습니다. 함께 살펴보세요.



빠른 종료 - Early Exit

guard를 사용하여 잘못된 값의 전달 시 특정 실행구문을 빠르게 종료합니다. 디버깅 모드 뿐만 아니라 어떤 조건에서도 동작합니다. guardelse 블럭 내부에는 특정 코드블럭을 종료하는 지시어(return, break 등)가 꼭 있어야 합니다. 타입 캐스팅, 옵셔널과도 자주 사용됩니다. 그 외에도 단순 조건 판단 후 빠르게 종료할 때도 용이합니다.

func functionWithGuard(age: Int?) {
    
    guard let unwrappedAge = age,
        unwrappedAge < 130,
        unwrappedAge >= 0 else {
        print("나이값 입력이 잘못되었습니다")
        return
    }
    
    print("당신의 나이는 \(unwrappedAge)세입니다")
}

var count = 1

while true {
    guard count < 3 else {
        break
    }
    print(count)
    count += 1
}
// 1
// 2


func someFunction(info: [String: Any]) {
    guard let name = info["name"] as? String else {
        return
    }
    
    guard let age = info["age"] as? Int, age >= 0 else {
        return
    }
    
    print("\(name): \(age)")
    
}

someFunction(info: ["name": "jenny", "age": "10"])
someFunction(info: ["name": "mike"])
someFunction(info: ["name": "yagom", "age": 10]) // yagom: 10


관련문서

* The Swift Programming Language - Control Flow - Early Exit

* The Swift Programming Language - The Basics - Assertion





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 


저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

익스텐션  (0) 2017.07.20
프로토콜  (0) 2017.07.17
assert와 guard  (0) 2017.07.13
타입캐스팅  (2) 2017.07.10
옵셔널 체이닝  (2) 2017.07.06
인스턴스의 생성과 소멸  (0) 2017.07.03
Posted by yagom


Perfect 미니 프로젝트 [2]


1. 서버 객체 생성 및 라우팅 
2. mongoDB 연결하기 




본격적으로 API 서버 만들기에 돌입합니다!!


참고

2017년 7월 현재 Swift 3 / Perfect 최신버전 2.0.x 환경에서 진행함을 알려드립니다.

* Swift 최신 버전 확인(https://github.com/apple/swift/releases)

* Perfect 최신 버전 확인(https://github.com/PerfectlySoft/Perfect/releases)


서버 객체 생성 및 라우팅

서버 객체와 라우팅에 대한 설명은 지난 글에서 했으므로, 작성한 코드만 설명합니다.

2017/05/24 - [Swift/Perfect] - Perfect 라우팅


우선, 프로젝트 폴더의 Sources폴더의 handlers.swift 파일을 생성합니다.

> vi Sources/handlers.swift


혹시 Xcode로 프로젝트를 진행하고 있다면 파일 생성시 타깃을 ImageBoard로 설정해주세요.


handlers.swift 파일에 아래 코드를 입력합니다.

import PerfectHTTP

struct ContentsType {
    static let json = "application/json; charset=utf-8"
    static let formData = "multipart/form-data"
}

// 사진 게시물 목록
func articleListHandler(request: HTTPRequest, response: HTTPResponse) {
    response.setHeader(.contentType, value: ContentsType.json)
    
    // 첫 번째 테스트를 위해 임시로 response body 생성
    do {
        try response.setBody(json: ["hello":"test"])
        response.completed()
    } catch {
        response.completed(status: .internalServerError)
    }
}

// 사진 게시물 등록
func postArticleHandler(request: HTTPRequest, response: HTTPResponse) {
    response.setHeader(.contentType, value: ContentsType.json)
    response.completed()
}

// 사진 게시물 수정
func modifyArticleHandler(request: HTTPRequest, response: HTTPResponse) {
    response.setHeader(.contentType, value: ContentsType.json)
    response.completed()
}

// 사진 게시물 삭제
func deleteArticleHandler(request: HTTPRequest, response: HTTPResponse) {
    response.setHeader(.contentType, value: ContentsType.json)
    response.completed()
}

// 사진 게시물 정보
func articleInfoHandler(request: HTTPRequest, response: HTTPResponse) {
    response.setHeader(.contentType, value: ContentsType.json)
    response.completed()
}


라우팅을 위한 핸들러 생성을 미리 해둔 것입니다.

핸들러 생성을 모두 마쳤으면 이제 서버 객체를 생성하고 구동할 차례입니다. main.swift 파일로 이동합니다.

> vi Sources/main.swift


이전 포스팅에서 모두 설명한 내용이므로 따로 코드설명은 하지 않아도 아실 것이라 생각합니다.

2017/05/17 - [Swift/Perfect] - Perfect 시작하기


import PerfectLib
import PerfectHTTP
import PerfectHTTPServer


let articleURI = "/article"
let subArticleURI = articleURI + "/*"

var routes = Routes()

routes.add(method: .post, uri: articleURI, handler: postArticleHandler(request:response:))

routes.add(method: .get, uri: articleURI, handler: articleListHandler(request:response:))

routes.add(method: .post, uri: subArticleURI, handler: modifyArticleHandler(request:response:))

routes.add(method: .delete, uri: subArticleURI, handler: deleteArticleHandler(request:response:))

routes.add(method: .get, uri: subArticleURI, handler: articleInfoHandler(request:response:))

let server = HTTPServer()

server.addRoutes(routes)
server.serverPort = 8080

do {
    try server.start()
} catch PerfectError.networkError(let error, let message) {
    Log.error(message: "Error: \(error), \(message)")
} catch {
    Log.error(message: error.localizedDescription)
}


자 이렇게 코드 작성을 마쳤으면, 동작 테스트를 해봐야죠! ImageBoard 애플리케이션을 실행 한 후 접속하여 제대로 동작하는지 확인합니다.


이렇게 응답이 온다면 제대로 동작하고 있다고 볼 수 있지요 :)

제대로 응답이 온다면 다음 스텝으로!


mongoDB 연결하기

이번엔 몽고디비를 연결해보려고 합니다. 사실 이 미니 프로젝트는 여러 데이터베이스를 사용할 필요도, 여러 콜렉션을 사용할 필요도 없습니다. 그래서 단일 데이터베이스의 단일 콜렉션을 사용하려고 합니다. 몽고  데이터베이스 이름은 "image_board", 콜렉션 이름은 "articles"라고 사용하겠습니다.


몽고디비 관련한 내용을 작성할 소스코드 파일 mongo.swift를 생성합니다.

> vi Sources/mongo.swift


mongo.swift 파일에 아래 코드를 작성합니다.

import Foundation
import MongoDB
import PerfectLib

struct DB {
    static var client: MongoClient?
    static var database: MongoDatabase?
    static var collection: MongoCollection?
    
    static func connect(uri: String, database: String, collection: String) throws {
        
        do {
            self.client = try MongoClient(uri: uri)
        } catch {
            throw error
        }
        
        self.database = DB.client?.getDatabase(name: database)
        self.collection = DB.database?.getCollection(name: collection)
    }
    
    static func disconnect() {
        self.collection?.close()
        self.database?.close()
        self.client?.close()
    }
}


그리고 main.swift 파일로 이동해서 아래 코드로 업데이트 해줍니다.

> vi Sources/main.swift


/// 더 위의 코드는 생략 do { try DB.connect(uri: "mongodb://localhost", database: "image_board", collection: "articles") try server.start() } catch PerfectError.networkError(let error, let message) { Log.error(message: "Error: \(error), \(message)") } catch { Log.error(message: "\(error)") } DB.disconnect()


이제 데이터베이스 연결까지 준비를 마쳤습니다. 


다음 번에는 실제로 데이터베이스에 쓰고 읽는 작업까지 해보려 합니다~

다음에 또 만나요! :D



참고문서



by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom

p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 




저작자 표시 비영리 변경 금지
신고

'Swift > Perfect' 카테고리의 다른 글

사진 게시판 API 만들기 [4]  (0) 2017.08.30
사진 게시판 API 만들기 [3]  (0) 2017.08.09
사진 게시판 API 만들기 [2]  (0) 2017.07.11
사진 게시판 API 만들기 [1]  (6) 2017.06.27
Perfect 라우팅  (0) 2017.05.24
Perfect 시작하기  (0) 2017.05.17
Posted by yagom

타입캐스팅

스위프트의 타입캐스팅은 인스턴스의 타입을 확인하는 용도 또는 클래스의 인스턴스를 부모 혹은 자식 클래스의 타입으로 사용할 수 있는지 확인하는 용도로 사용합니다. is, as를 사용합니다.


소스코드


타입 캐스팅 예제를 위한 클래스 정의

class Person {
    var name: String = ""
    func breath() {
        print("숨을 쉽니다")
    }
}

class Student: Person {
    var school: String = ""
    func goToSchool() {
        print("등교를 합니다")
    }
}

class UniversityStudent: Student {
    var major: String = ""
    func goToMT() {
        print("멤버쉽 트레이닝을 갑니다 신남!")
    }
}


예제를 위한 인스턴스 생성

var yagom: Person = Person()
var hana: Student = Student()
var jason: UniversityStudent = UniversityStudent()


타입 확인

is를 사용하여 타입을 확인합니다.

var result: Bool

result = yagom is Person // true
result = yagom is Student // false
result = yagom is UniversityStudent // false

result = hana is Person // true
result = hana is Student // true
result = hana is UniversityStudent // false

result = jason is Person // true
result = jason is Student // true
result = jason is UniversityStudent // true

if yagom is UniversityStudent {
    print("yagom은 대학생입니다")
} else if yagom is Student {
    print("yagom은 학생입니다")
} else if yagom is Person {
    print("yagom은 사람입니다")
} // yagom은 사람입니다

switch jason {
case is Person:
    print("jason은 사람입니다")
case is Student:
    print("jason은 학생입니다")
case is UniversityStudent:
    print("jason은 대학생입니다")
default:
    print("jason은 사람도, 학생도, 대학생도 아닙니다")
} // jason은 사람입니다

switch jason {
case is UniversityStudent:
    print("jason은 대학생입니다")
case is Student:
    print("jason은 학생입니다")
case is Person:
    print("jason은 사람입니다")
default:
    print("jason은 사람도, 학생도, 대학생도 아닙니다")
} // jason은 대학생입니다


업 캐스팅

as를 사용하여 부모클래스의 인스턴스로 사용할 수 있도록 컴파일러에게 타입정보를 전환해줍니다. Any 혹은 AnyObject로도 타입정보를 변환할 수 있습니다. 암시적으로 처리되므로 꼭 필요한 경우가 아니라면 생략해도 무방합니다.

// UniversityStudent 인스턴스를 생성하여 Person 행세를 할 수 있도록 업 캐스팅
var mike: Person = UniversityStudent() as Person

var jenny: Student = Student()
//var jina: UniversityStudent = Person() as UniversityStudent // 컴파일 오류

// UniversityStudent 인스턴스를 생성하여 Any 행세를 할 수 있도록 업 캐스팅
var jina: Any = Person() // as Any 생략가능


다운 캐스팅

as? 또는 as!를 사용하여 자식 클래스의 인스턴스로 사용할 수 있도록 컴파일러에게 인스턴스의 타입정보를 전환해줍니다.


조건부 다운 캐스팅

as?를 사용합니다. 캐스팅에 실패하면, 즉 캐스팅하려는 타입에 부합하지 않는 인스턴스라면 nil을 반환하기 때문에 결과의 타입은 옵셔널 타입입니다.

var optionalCasted: Student?

optionalCasted = mike as? UniversityStudent
optionalCasted = jenny as? UniversityStudent // nil
optionalCasted = jina as? UniversityStudent // nil
optionalCasted = jina as? Student // nil


강제 다운 캐스팅

as!를 사용합니다. 캐스팅에 실패하면, 즉 캐스팅하려는 타입에 부합하지 않는 인스턴스라면 런타임 오류가 발생합니다. 캐스팅에 성공하면 옵셔널이 아닌 일반 타입을 반환합니다.

var forcedCasted: Student

optionalCasted = mike as! UniversityStudent
//optionalCasted = jenny as! UniversityStudent // 런타임 오류
//optionalCasted = jina as! UniversityStudent // 런타임 오류
//optionalCasted = jina as! Student // 런타임 오류


활용

func doSomethingWithSwitch(someone: Person) {
    switch someone {
    case is UniversityStudent:
        (someone as! UniversityStudent).goToMT()
    case is Student:
        (someone as! Student).goToSchool()
    case is Person:
        (someone as! Person).breath()
    }
}

doSomethingWithSwitch(someone: mike as Person) // 멤버쉽 트레이닝을 갑니다 신남!
doSomethingWithSwitch(someone: mike) // 멤버쉽 트레이닝을 갑니다 신남!
doSomethingWithSwitch(someone: jenny) // 등교를 합니다
doSomethingWithSwitch(someone: yagom) // 숨을 쉽니다


func doSomething(someone: Person) {
    if let universityStudent = someone as? UniversityStudent {
        universityStudent.goToMT()
    } else if let student = someone as? Student {
        student.goToSchool()
    } else if let person = someone as? Person {
        person.breath()
    }
}

doSomething(someone: mike as Person) // 멤버쉽 트레이닝을 갑니다 신남!
doSomething(someone: mike) // 멤버쉽 트레이닝을 갑니다 신남!
doSomething(someone: jenny) // 등교를 합니다
doSomething(someone: yagom) // 숨을 쉽니다


관련문서

The Swift Programming Language - Type Casting





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 

저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

프로토콜  (0) 2017.07.17
assert와 guard  (0) 2017.07.13
타입캐스팅  (2) 2017.07.10
옵셔널 체이닝  (2) 2017.07.06
인스턴스의 생성과 소멸  (0) 2017.07.03
상속  (0) 2017.06.29
Posted by yagom

옵셔널 체이닝

옵셔널 체이닝은 옵셔널의 내부의 내부의 내부로 옵셔널이 연결되어 있을 때 유용하게 활용할 수 있습니다. 매 번 nil 확인을 하지 않고 최종적으로 원하는 값이 있는지 없는지 확인할 수 있습니다. 


소스코드


예제 클래스

class Person {
    var name: String
    var job: String?
    var home: Apartment?
    
    init(name: String) {
        self.name = name
    }
}

class Apartment {
    var buildingNumber: String
    var roomNumber: String
    var `guard`: Person?
    var owner: Person?
    
    init(dong: String, ho: String) {
        buildingNumber = dong
        roomNumber = ho
    }
}


옵셔널 체이닝 사용

let yagom: Person? = Person(name: "yagom")
let apart: Apartment? = Apartment(dong: "101", ho: "202")
let superman: Person? = Person(name: "superman")


// 옵셔널 체이닝이 실행 후 결과값이 nil일 수 있으므로
// 결과 타입도 옵셔널입니다

// 만약 우리집의 경비원의 직업이 궁금하다면..?

// 옵셔널 체이닝을 사용하지 않는다면...
func guardJob(owner: Person?) {
    if let owner = owner {
        if let home = owner.home {
            if let `guard` = home.guard {
                if let guardJob = `guard`.job {
                    print("우리집 경비원의 직업은 \(guardJob)입니다")
                } else {
                    print("우리집 경비원은 직업이 없어요")
                }
            }
        }
    }
}

guardJob(owner: yagom)

// 옵셔널 체이닝을 사용한다면
func guardJobWithOptionalChaining(owner: Person?) {
    if let guardJob = owner?.home?.guard?.job {
        print("우리집 경비원의 직업은 \(guardJob)입니다")
    } else {
        print("우리집 경비원은 직업이 없어요")
    }
}

guardJobWithOptionalChaining(owner: yagom)
// 우리집 경비원은 직업이 없어요



yagom?.home?.guard?.job // nil

yagom?.home = apart

yagom?.home // Optional(Apartment)
yagom?.home?.guard // nil

yagom?.home?.guard = superman

yagom?.home?.guard // Optional(Person)

yagom?.home?.guard?.name // superman
yagom?.home?.guard?.job // nil

yagom?.home?.guard?.job = "경비원"




nil 병합 연산자

중위 연산자입니다. ??

예) Optional ??  Value


옵셔널 값이 nil일 경우, 우측의 값을 반환합니다. 

띄어쓰기에 주의하여야 합니다.

var guardJob: String
    
guardJob = yagom?.home?.guard?.job ?? "슈퍼맨"
print(guardJob) // 경비원

yagom?.home?.guard?.job = nil

guardJob = yagom?.home?.guard?.job ?? "슈퍼맨"
print(guardJob) // 슈퍼맨



관련문서

* The Swift Programming Language - Optional Chaining

* The Swift Programming Language - Basic Operators - Nil-Coalescing Operator





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 

저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

assert와 guard  (0) 2017.07.13
타입캐스팅  (2) 2017.07.10
옵셔널 체이닝  (2) 2017.07.06
인스턴스의 생성과 소멸  (0) 2017.07.03
상속  (0) 2017.06.29
프로퍼티 - Property  (2) 2017.06.26
Posted by yagom

상속

Swift/기본문법 2017.06.29 11:00

상속

스위프트의 상속은 클래스, 프로토콜 등에서 가능합니다. 열거형, 구조체는 상속이 불가능합니다. 스위프트는 다중상속을 지원하지 않습니다.   

이번 파트에서는 클래스의 상속에 대해서 알아봅니다


소스코드



클래스의 상속과 재정의

상속 문법

class 이름: 상속받을 클래스 이름 {
    /* 구현부 */
}



// 기반 클래스 Person
class Person {
    var name: String = ""
    
    func selfIntroduce() {
        print("저는 \(name)입니다")
    }
    
    // final 키워드를 사용하여 재정의를 방지할 수 있습니다
    final func sayHello() {
        print("hello")
    }
    
    // 타입 메서드
    // 재정의 불가 타입 메서드 - static
    static func typeMethod() {
        print("type method - static")
    }
    
    // 재정의 가능 타입 메서드 - class
    class func classMethod() {
        print("type method - class")
    }
    
    // 재정의 가능한 class 메서드라도 
    // final 키워드를 사용하면 재정의 할 수 없습니다
    // 메서드 앞의 `static`과 `final class`는 똑같은 역할을 합니다
    final class func finalCalssMethod() {
        print("type method - final class")
    }
}

// Person을 상속받는 Student
class Student: Person {
    var major: String = ""
    
    override func selfIntroduce() {
        print("저는 \(name)이고, 전공은 \(major)입니다")
    }
    
    override class func classMethod() {
        print("overriden type method - class")
    }
    
    // static을 사용한 타입 메서드는 재정의 할 수 없습니다
//    override static func typeMethod() {    }
    
    // final 키워드를 사용한 메서드, 프로퍼티는 재정의 할 수 없습니다
//    override func sayHello() {    }
//    override class func finalClassMethod() {    }

}


동작 확인

let yagom: Person = Person()
let hana: Student = Student()

yagom.name = "yagom"
hana.name = "hana"
hana.major = "Swift"

yagom.selfIntroduce()
// 저는 yagom입니다

hana.selfIntroduce()
// 저는 hana이고, 전공은 Swift입니다

Person.classMethod()
// type method - class

Person.typeMethod()
// type method - static

Person.finalCalssMethod()
// type method - final class


Student.classMethod()
// overriden type method - class

Student.typeMethod()
// type method - static

Student.finalCalssMethod()
// type method - final class



관련문서

The Swift Programming Language - Inheritance





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 

저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

옵셔널 체이닝  (2) 2017.07.06
인스턴스의 생성과 소멸  (0) 2017.07.03
상속  (0) 2017.06.29
프로퍼티 - Property  (2) 2017.06.26
클로저 - Closure  (0) 2017.06.22
클래스, 구조체, 열거형 비교  (0) 2017.06.19
Posted by yagom


Perfect 미니 프로젝트 [1]

1. 프로젝트 개요
2. mongoDB 설치 

3. 패키지 설치 



우분투에 스위프트 설치를 시작으로 이후 두 포스팅에 걸쳐 퍼펙트에 대해 조금 알아보았습니다.



2017/04/03 - [Swift/Perfect] - 우분투(Ubuntu)에 스위프트 설치하기

2017/05/17 - [Swift/Perfect] - Perfect 시작하기

2017/05/24 - [Swift/Perfect] - Perfect 라우팅


이번 부터는 실질적으로 퍼펙트를 사용하여 작은 미니프로젝트를 해보려합니다.

아... 물론 이런 허접한 구성은 실제 서버에서 사용하면 안되지만 간단한 예제로 감만 잡아보는 겁니다. 인증도 없고, 뭐도 없고, 보안도 안되고... 뭐... 암것도 없어요. 그러나 한 번 작은 프로젝트 해보는데에 의의를 가지는 것이니 나중에 삘받으면 하나하나 덧붙여 가면 됩니다.

그저 DB 연결해보고, API로 클라이언트와 핑퐁 해보는 것이 전부이긴 하지만, 그래도 감잡기는 좋을 것 같아요 :)

이 감 말고요... 'ㅁ' 아하핳


프로젝트 개요

간단한 사진게시판을 위한 REST API 서버를 만들어보겠습니다.

API 

HTTP Method 

URI 

사진 게시물 등록 

POST 

/article 

사진 게시물 목록

GET 

사진 게시물 수정 

POST 

/article/{article_id}

사진 게시물 삭제 

DELETE

사진 게시물 정보 가져오기 

GET


사진 게시물 등록

요청(Request)

  • HTTP Method : POST
  • Content-Type: multipart/form-data

 매개변수

자료형 

값의 범위/기본 값 

비고 

 필수여부

image 

binary data

 

이미지 데이터 

user_name

string 

 

사용자 이름 

Y

description

string 

 

이미지 설명 

N

title

string 

 

이미지 제목 

Y


응답(Response)

Key 

자료형 

비고 

필수여부 

article_id 

string 

업로드된 게시물 고유 식별자 

 image_url

string 

이미지 주소 

user_name 

string 

사용자 이름 

description 

 string

이미지 설명 

title 

string 

이미지 제목 


사진 게시물 목록

요청(Request)

  • HTTP Method : GET
  • Content-Type: application/json

 매개변수

자료형 

값의 범위/기본 값 

비고 

 필수여부

page

integer

0

조회하고자 하는 페이지 번호

user_name

string 


특정 사용자의 게시물만 받아오고자 할 때

N

articles_per_page

integer

1~100 / 10 

각 페이지 당 게시물 수 

N


응답(Response)

Key 

자료형 

비고 

필수여부 

articles 

json object array (string) 

게시물 정보 배열 

Y

articles_per_page

integer

각 페이지 당 게시물 수

 Y

current_page

integer 

전송된 페이지 

total_page

integer 

전체 페이지 수 

Y

articles item object 형태

Key 

자료형 

비고 

필수여부 

image_url

string

이미지 URL

Y

user_name

string

업로드한 사용자 이름 

description

string 

이미지 설명 

title

string 

이미지 제목 

article_id

string 

게시물 고유 식별자


사진 게시물 수정

요청(Request)

  • HTTP Method : POST
  • Content-Type: multipart/form-data

 매개변수

자료형 

값의 범위/기본 값 

비고 

 필수여부

image 

binary data

이전 데이터

이미지 데이터 

N

user_name

string 


사용자 이름이 이전 사용자 이름과 일치하지 않으면 수정에 실패

Y

description

string 

이전 데이터

이미지 설명 

N

title

string 

이전 데이터

이미지 제목 

N


응답(Response)

Key 

자료형 

비고 

필수여부 

article_id 

string 

수정된 게시물 고유 식별자 

 image_url

string 

이미지 주소 

user_name 

string 

사용자 이름 

description 

 string

이미지 설명 

title 

string 

이미지 제목 


사진 게시물 삭제

요청(Request)

  • HTTP Method : DELETE
  • Content-Type: application/json

매개변수 없음


응답(Response)

Key 

자료형 

비고 

필수여부 

article_id

string

삭제된 게시물 고유 식별자

Y


사진 게시물 정보 가져오기 

요청(Request)

  • HTTP Method : GET
  • Content-Type: application/json

매개변수 없음


응답(Response)

매개변수 

자료형 

비고 

필수여부 

image_url

string

이미지 URL

Y

user_name

string

업로드한 사용자 이름 

description

string 

이미지 설명 

title

string 

이미지 제목 

article_id

string 

게시물 고유 식별자



모든 응답데이터는 JSON 형식을 사용합니다.

이 정도를 구현해 볼건데요, 데이터베이스가 필요할테니 쓸만한 데이터베이스가 무엇이 있을지 찾아봅니다.


현재(2017년 6월) 퍼펙트에서 공식적으로 제공하는 데이터베이스 커넥터 목록입니다.

  • SQLite
  • MySQL
  • MariaDB
  • PostgreSQL
  • FileMaker
  • MongoDB
  • Working with BSON
  • Apache CouchDB
  • LDAP
  • Kafka
  • Mosquitto
  • ZooKeeper
  • Hadoop

와우... 많네요!! 저는... 몽고디비를 써봐야겠습니다. 



mongoDB 설치

그러려면 먼저 mongoDB를 설치해봐야겠지요.

설치방법은 아래 링크에 친절히 나와있으니 참고하시면 되겠습니다 :)


퍼펙트에서 몽고디비를 사용하기 위해서는 mongo-c 라이브러리를 사용해야합니다. 

먼저, mongo-c 드라이버를 설치합니다. mongo-c와 관련된 설명은 Perfect의 문서를 참고하셔도 좋습니다.


macOS

HomeBrew를 먼저 설치(링크)하신 후, 터미널에서 아래 명령어를 입력합니다.

> brew install mongo-c-driver


ubuntu

아래 명령어를 입력합니다.

> sudo apt-get install libmongoc


패키지 설치

그리고 이제 새로운 프로젝트를 시작하기 위해서 프로젝트를 진행하길 원하는 위치에서 프로젝트 폴더를 생성합니다. 프로젝트 폴더 이름은 ImageBoard라고 하겠습니다.

> mkdir ImageBoard
> cd ImageBoard


그 다음 스위프트 패키지 매니저를 통해 패키지 초기화를 합니다. 스위프트 패키지 매니저는 지난 포스트[우분투(Ubuntu)에 스위프트 설치하기]에서 설치했었죠?

> swift package init --type executable 


swift package init 명령어를 실행 후 폴더에 생성된 Package.swift 파일을 열어서 Perfect 패키지 의존성을 추가합니다.

> vi Package.swift


Package.swift 파일에 아래 코드를 작성합니다.  (우리는 새로운 프로젝트를 만들면서 함께 mongo-c 패키지도 설치할거예요.)

import PackageDescription let package = Package( name: "ImageBoard", targets: [], dependencies: [ .Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", versions: Version(0,0,0)..<Version(10,0,0)), .Package(url:"https://github.com/PerfectlySoft/Perfect.git", versions: Version(0,0,0)..<Version(10,0,0)), .Package(url:"https://github.com/PerfectlySoft/Perfect-MongoDB.git", versions: Version(0,0,0)..<Version(10,0,0)) ] )

위 패키지 설명은 Perfect HTTP Sever, Perfect, Perfect MongoDB 커넥터 패키지를 버전 0부터 10까지 중 최신 버전을 가져오도록 합니다. (현재시점으로 모두 2.x.x 버전)


소스를 저장하고 나와서 패키지를 설치합니다.

> swift package update


만약에 맥에서 Xcode 프로젝트를 생성해주고 싶다면

> swift package generate-xcodeproj

해주시면 해당 폴더에 Xcode 프로젝트가 생성됩니다. 그러면 이후 코딩을 Xcode 프로젝트로 진행할 수 있습니다.



이제 프로젝트 준비가 완료되었습니다!

다음부터는 실질적으로 코딩을 해봅니다 :D

다음에 또 만나요~!




by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom

p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 




저작자 표시 비영리 변경 금지
신고

'Swift > Perfect' 카테고리의 다른 글

사진 게시판 API 만들기 [3]  (0) 2017.08.09
사진 게시판 API 만들기 [2]  (0) 2017.07.11
사진 게시판 API 만들기 [1]  (6) 2017.06.27
Perfect 라우팅  (0) 2017.05.24
Perfect 시작하기  (0) 2017.05.17
우분투(Ubuntu)에 스위프트 설치하기  (0) 2017.04.03
Posted by yagom

프로퍼티

프로퍼티는 클래스, 구조체, 열거형과 연관된 값입니다. 타입과 관련된 값을 저장할 수도, 연산할 수도 있습니다.


소스코드


프로퍼티의 종류

  • 인스턴스 저장 프로퍼티
  • 타입 저장 프로퍼티
  • 인스턴스 연산 프로퍼티
  • 타입 연산 프로퍼티
  • 지연 저장 프로퍼티

이번 파트에서는 지연 저장 프로퍼티를 제외한 저장 프로퍼티와 연산 프로퍼티에 대해 알아봅니다.


정의와 사용

프로퍼티는 구조체, 클래스, 열거형 내부에 구현할 수 있습니다. 다만 열거형 내부에는 연산 프로퍼티만 구현할 수 있습니다. 연산 프로퍼티는 var로만 선언할 수 있습니다.

연산프로퍼티를 읽기전용으로는 구현할 수 있지만, 쓰기 전용으로는 구현할 수 없습니다. 읽기전용으로 구현하려면 get 블럭만 작성해주면 됩니다. 읽기전용은 get블럭을 생략할 수 있습니다. 읽기, 쓰기 모두 가능하게 하려면 get 블럭과 set블럭을 모두 구현해주면 됩니다.  

set 블럭에서 암시적 매개변수 newValue를 사용할 수 있습니다.

struct Student {
    
    // 인스턴스 저장 프로퍼티
    var name: String = ""
    var `class`: String = "Swift"
    var koreanAge: Int = 0
    
    // 인스턴스 연산 프로퍼티
    var westernAge: Int {
        get {
            return koreanAge - 1
        }
        
        set(inputValue) {
            koreanAge = inputValue + 1
        }
    }
    
    // 타입 저장 프로퍼티
    static var typeDescription: String = "학생"
    
    /*
    // 인스턴스 메서드
    func selfIntroduce() {
        print("저는 \(self.class)반 \(name)입니다")
    }
     */
    
    // 읽기전용 인스턴스 연산 프로퍼티
    // 간단히 위의 selfIntroduce() 메서드를 대체할 수 있습니다
    var selfIntroduction: String {
        get {
            return "저는 \(self.class)반 \(name)입니다"
        }
    }
        
    /*
     // 타입 메서드
     static func selfIntroduce() {
     print("학생타입입니다")
     }
     */
    
    // 읽기전용 타입 연산 프로퍼티
    // 읽기전용에서는 get을 생략할 수 있습니다
    static var selfIntroduction: String {
        return "학생타입입니다"
    }
}

// 타입 연산 프로퍼티 사용
print(Student.selfIntroduction)
// 학생타입입니다

// 인스턴스 생성
var yagom: Student = Student()
yagom.koreanAge = 10

// 인스턴스 저장 프로퍼티 사용
yagom.name = "yagom"
print(yagom.name)
// yagom

// 인스턴스 연산 프로퍼티 사용
print(yagom.selfIntroduction)
// 저는 Swift반 yagom입니다

print("제 한국나이는 \(yagom.koreanAge)살이고, 미쿡나이는 \(yagom.westernAge)살입니다.")
// 제 한국나이는 10살이고, 미쿡나이는 9살입니다.


응용

struct Money {
    var currencyRate: Double = 1100
    var dollar: Double = 0
    var won: Double {
        get {
            return dollar * currencyRate
        }
        set {
            dollar = newValue / currencyRate
        }
    }
}

var moneyInMyPocket = Money()

moneyInMyPocket.won = 11000

print(moneyInMyPocket.won)
// 11000

moneyInMyPocket.dollar = 10

print(moneyInMyPocket.won)
// 11000


지역변수 및 전역변수

저장 프로퍼티와 연산 프로퍼티의 기능은 함수, 메서드, 클로저, 타입 등의 외부에 위치한 지역/전역 변수에도 모두 사용 가능합니다.

var a: Int = 100
var b: Int = 200
var sum: Int {
    return a + b
}

print(sum) // 300


프로퍼티 감시자


프로퍼티 감시자를 사용하면 프로퍼티 값이 변경될 때 원하는 동작을 수행할 수 있습니다. 값이 변경되기 직전에 willSet블럭이, 값이 변경된 직후에 didSet블럭이 호출됩니다. 둘 중 필요한 하나만 구현해 주어도 무관합니다. 변경되려는 값이 기존 값과 똑같더라도 프로퍼티 감시자는 항상 동작합니다. 

willSet 블럭에서 암시적 매개변수 newValue를 사용할 수 있고, didSet 블럭에서 암시적 매개변수 oldValue를 사용할 수 있습니다.

프로퍼티 감시자는 연산 프로퍼티에 사용할 수 없습니다.


소스코드



정의 및 사용

struct Money {
    // 프로퍼티 감시자 사용
    var currencyRate: Double = 1100 {
        willSet(newRate) {
            print("환율이 \(currencyRate)에서 \(newRate)으로 변경될 예정입니다")
        }
        
        didSet(oldRate) {
            print("환율이 \(oldRate)에서 \(currencyRate)으로 변경되었습니다")
        }
    }

    // 프로퍼티 감시자 사용
    var dollar: Double = 0 {
        // willSet의 암시적 매개변수 이름 newValue
        willSet {
            print("\(dollar)달러에서 \(newValue)달러로 변경될 예정입니다")
        }
        
        // didSet의 암시적 매개변수 이름 oldValue
        didSet {
            print("\(oldValue)달러에서 \(dollar)달러로 변경되었습니다")
        }
    }

    // 연산 프로퍼티
    var won: Double {
        get {
            return dollar * currencyRate
        }
        set {
            dollar = newValue / currencyRate
        }
        
        /* 프로퍼티 감시자와 연산 프로퍼티 기능을 동시에 사용할 수 없습니다
        willSet {
            
        }
         */
    }    
}

var moneyInMyPocket: Money = Money()

// 환율이 1100.0에서 1150.0으로 변경될 예정입니다
moneyInMyPocket.currencyRate = 1150
// 환율이 1100.0에서 1150.0으로 변경되었습니다

// 0.0달러에서 10.0달러로 변경될 예정입니다
moneyInMyPocket.dollar = 10
// 0.0달러에서 10.0달러로 변경되었습니다

print(moneyInMyPocket.won)
// 11500.0



관련문서

The Swift Programming Language - Properties





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 


저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

인스턴스의 생성과 소멸  (0) 2017.07.03
상속  (0) 2017.06.29
프로퍼티 - Property  (2) 2017.06.26
클로저 - Closure  (0) 2017.06.22
클래스, 구조체, 열거형 비교  (0) 2017.06.19
열거형 - Enumerations  (0) 2017.06.15
Posted by yagom

Swift 4 변경사항

Swift 2017.06.23 11:00


오늘의 주제

스위프트 4 추가/변경사항


아, 벌써 스위프트가 4번째 버전이 나오네요! 참으로 빠르게 변합니다. 이제 문법적으로 변경되는 부분이 크게 많지 않아서 서 3 버전과 크게 달라진 문법은 많지 않습니다. 내부적으로 수정되거나 추가된 문법이 많고, 삭제된 부분은 거의 없습니다.

긴 말 필요없이 한 번 무엇이 바뀌었는지 살펴볼까요?

스따뚜~~~~~~~~~~~~


단방향 범위 연산자

이제 범위연산자에서 양 쪽 끝을 모두 신경쓸 필요가 없습니다, 야호! [SE-0172]

var numbers = [1, 2, 3, 4, 5]

// Swift 3
numbers[2..<numbers.endIndex] // [3, 4, 5]
numbers[0...2] // [1, 2, 3]
numbers[0..<2] // [1, 2]

// Swift 4
numbers[2...] // [3, 4, 5]
numbers[...2] // [1, 2, 3]
numbers[..<2] // [1, 2]

numbers = []

// Swift 3
//numbers[0...numbers.endIndex] // out of range - error

// Swift 4
numbers[0...] // []


let number: Int = 100

// Swift 3
switch number {
case Int.min..<0:
    print("negative")
case 0:
    print("zero")
case 1...Int.max:
    print("positive")
default:
    print("unknwon")
}


// Swift 4
switch number {
case ..<0:
    print("negative")
case 0:
    print("zero")
case 1...:
    print("positive")
default:
    print("unknwon")
}


 

String의 많은 변화

Swift 4에서 String은 정말 많은 변화가 있는 부분 중 하나입니다. 그 중 눈에 띌만한 내용을 위주로 정리했습니다. 더 많은 사항은 [String Processing For Swift 4] 문서를 참고해보시기 바랍니다.


String 구조체가 다시 Collection 프로토콜을 따릅니다. [SE-0163]

var greeting: String = "Hello, Swift!"

// Swift 3
greeting.characters.forEach { print($0) }
print(greeting.characters.count) // 13

// Swift 4
greeting.forEach { print($0) }
print(greeting.count) // 13


Unicode 9이 적용되었습니다. [String Processing For Swift 4 - Unicode 9 Conformance]

이전에 문자열 길이 결과가 제각각이었던 유니코드 문자들이 사람이 인지할 수 있는 단위로 세어집니다.

// Swift 3
"😍".characters.count // 1
"🇰🇷".characters.count // 1
"🐻".characters.count // 1
"🙌🏻".characters.count // 2
"🙌🏾".characters.count // 2
"‍‍👨‍👨‍👦".characters.count // 3
"‍‍‍👨‍👨‍👦‍👦".characters.count // 4

// Swift 4
"😍".count // 1
"🇰🇷".count // 1
"🐻".count // 1
"🙌🏻".count // 1
"🙌🏾".count // 1
"‍‍👨‍👨‍👦".count // 1
"‍‍‍👨‍👨‍👦‍👦".count // 1


여러 줄 리터럴 문법이 생겼습니다. [SE-0168]

  • 큰따옴표 세 개를 연결하여 표현하면 여러 줄 문자열을 표현하는 리터럴 문법입니다.   
  • 반드시 """ 다음 줄부터 문자열을 입력해야하며, 문자열 마지막 줄 다음줄에 """로 닫아줘야 합니다.

// Swift 3
greeting = "Hello\nHow are you?\nI'm fine thanks, and you?"

// Swift 4
greeting = """
Hello
How are you?
I'm fine thanks, and you?
"""


Substring 타입과 StringProtocol 프로토콜이 생겼습니다.

기존에 String 타입에 구현되어있던 주요 기능을 StringProtocol에 기본구현 하였습니다.

let subIndex = greeting.index(greeting.startIndex, offsetBy: 4)

// Swift 3
let subGreeting = greeting[greeting.startIndex...subIndex]
print(type(of: subGreeting)) // String

// Swift 4
let subGreeting = greeting[...subIndex]
print(type(of: subGreeting)) // Substring
let stringFromSubstring: String = String(subGreeting)


Character 타입에 unicodeScalars 프로퍼티가 추가되었습니다. [SE-0178]

Swift 3에서는 Character 인스턴스의 유니코드 스칼라 값을 알아내려면 String 타입으로 변환하여야 했지만, 이제는 바로 Character 인스턴스의 unicodeScalars 프로퍼티를 이용해 알아낼 수 있습니다.

let character: Character = ""

// Swift 3
print(String(character).unicodeScalars)

// Swift 4
print(character.unicodeScalars)


DictionarySet

자주 사용하는 콜렉션 타입인 DictionarySet의 기능이 한층 강화되었습니다.  

[SE-0154], [SE-0165]


키와 값의 시퀀스를 통해 새로운 딕셔너리를 생성할 수 있습니다. 또, 키와 값을 기존의 딕셔너리에 병합할 수 있습니다.

let zipSequence = zip("abcdefghijklmnopqrstuvwxyz", 97...)
let asciiTable = Dictionary(uniqueKeysWithValues: zipSequence)
// ["w": 119, "n": 110, "u": 117, "v": 118, "x": 120, "q": 113, ...]

let vegetables = ["tomato", "carrot", "onion", "onion", "carrot", "onion"]
let vegetableZipSequence = zip(vegetables, repeatElement(1, count: Int.max))

var vegetableCounts = Dictionary(vegetableZipSequence, uniquingKeysWith: +)
vegetableCounts.merge([("tomato", 1)], uniquingKeysWith: +)
// ["tomato": 2, "carrot": 2, "onion": 3]


DictionarySetfilter 결과가 이제 Array대신 각각의 원래 타입으로 반환됩니다. 또, 딕셔너리는 mapValues(_:) 메서드를 통해 값이 변형된 새로운 딕셔너리를 만들 수 있습니다.

let vowels: Set<Character> = ["a", "e", "i", "o", "u"]
let asciiVowels = asciiTable.filter({ vowels.contains($0.key) })

// Swift 3
print(type(of: asciiVowels)) // Array<(key : Character, value : Int)>

// Swift 4
print(type(of: asciiVowels)) // Dictionary<Character, Int>
asciiVowels["a"]  // 97
asciiVowels["b"]  // nil

let filteredSet: Set<Character> = vowels.filter{ $0 < "h" }

// Swift 3
print(type(of: filteredSet)) // Array<Character>

// Swift 4
print(type(of: filteredSet)) // Set<Character>

// Swift 3
let strangeMap = asciiTable.map { [$0: "0x" + String($1, radix: 16)] }
print(type(of: strangeMap)) // Array<Dictionary<Character, String>>
// [["u": "0x75"], ["v": "0x76"], ["w": "0x77"], ["x": "0x78"], ["q": "0x71"], ["n": "0x6e"], ...]

// Swift 4
let asciiHexTable = asciiTable.mapValues({ "0x" + String($0, radix: 16) })
// ["w": "0x77", "n": "0x6e", "u": "0x75", "v": "0x76", "x": "0x78", ...]


딕셔너리 서브스크립트 문법이 추가되었습니다. 원하는 키에 해당하는 값이 없으면 nil 대신 기본값을 돌려줍니다. 

기본값이 있다는 것은 값이 항상 존재한다는 의미이므로, 해당 서브스크립트의 반환값은 옵셔널이 아닌 값이라는 뜻입니다. 딕셔너리 옵셔널 전쟁에서 해방될 수도!

let favoriteSubject: [String: String] = ["yagom": "swift", "hana": "communication", "jisu": "swift"]

// Swift 3
type(of: favoriteSubject["minji"]) // Optional<String>

// Swift 4
type(of: favoriteSubject["minji"]) // Optional<String>
type(of: favoriteSubject["minji", default: "unknown"]) // String

var favoriteCount = [String: Int]()

favoriteSubject.forEach { favoriteCount[$0.value, default: 0] += 1 }
print(favoriteCount) // ["swift": 2, "communication": 1]


init(grouping:by:) 이니셜라이저를 통해 특정 조건에 따라 Array 또는 다른 시퀀스를 딕셔너리 형태로 그룹지을 수 있습니다. 그룹 기준값은 딕셔너리의 키가 됩니다.

struct Person: CustomStringConvertible {
    enum Gender {
        case male, female, unknwon
    }
    
    let name: String
    var gender: Gender
    
    var description: String { return name }
}

let yagom = Person(name: "yagom", gender: .male)
let hana = Person(name: "hana", gender: .female)
let jisu = Person(name: "jisu", gender: .unknwon)
let eric = Person(name: "eric", gender: .male)
let mike = Person(name: "mike", gender: .male)

let friends = [yagom, hana, jisu, eric, mike]
let friendsByGender = Dictionary(grouping: friends, by: { $0.gender })
print(type(of: friendsByGender))    // Dictionary<Gender, Array<Person>>
print(friendsByGender)
// [Person.Gender.unknwon: [jisu], Person.Gender.male: [yagom, eric, mike], Person.Gender.female: [hana]]


reserveCapacity(_:) 메서드를 통해 Array 처럼 예약된 공간을 가질 수 있습니다.

var emptyDictionary = [String: String]()
print(emptyDictionary.capacity) // 0
emptyDictionary.reserveCapacity(100)
print(emptyDictionary.capacity) // 192
// 최소 100개의 요소가 들어갈 수 있도록 공간을 확보하기 때문에 꼭 100이지 않을 수 있습니다
emptyDictionary.reserveCapacity(10)
print(emptyDictionary.capacity) // 192
emptyDictionary.reserveCapacity(1000)
print(emptyDictionary.capacity) // 1536


Key path

Cocoa에서 단순한 문자열로 표현하던 key path를 스위프트에서는 정확한 타입(KeyPath 라는 제네릭 클래스) 방식으로 표현합니다. 조금 더 안전한 동적 프로그래밍을 할 수 있지 않을까 기대됩니다. [SE-0161]

  • key path를 통하여 프로퍼티에 접근할 수 있도록 모든 타입에 자동으로 [keyPath:] 서브스크립트 메서드가 추가됩니다.
  • key path는 백슬래시(\)를 통해 표현합니다.

struct Person {
    var name: String
}

struct Stuff {
    var name: String
    var owner: Person
}


var yagom = Person(name: "yagom")
let macbook = Stuff(name: "macbook pro", owner: yagom)

print(type(of: \Person.name)) // WritableKeyPath<Person, String>

var result: Any = macbook[keyPath: \Stuff.name]
print(result) // macbook pro

let keyPath = \Stuff.owner
let nameKeyPath = keyPath.appending(path: \.name)

result = macbook[keyPath: nameKeyPath]
print(result) // yagom

result = macbook[keyPath: \Stuff.owner.name]
print(result) // yagom


프로토콜 혼합 타입

Objective-C 에서 특정 클래스와 프로토콜을 동시에 따르고 있는 타입이라는 의미로 사용되었던 표현이 있습니다. 

SomeClass<SomeProtocol, AnotherProtocol> *name
// SomeProtocol과 AnotherProtocol을 준수하는 SomeClass 타입의 변수 name

이 표현이 이제 스위프트에서도 가능해졌습니다. [SE-0156]

class ClassA { }
class ClassB: ClassA { }
protocol ProtocolA { }
extension ClassB: ProtocolA { }

var someVariable: ClassA & ProtocolA
// someVariable = ClassA()
// 오류발생 : ProtocolA를 충족하지 않음

someVariable = ClassB() // 모든 조건 충족


where 확장

프로토콜과 그 연관 타입에 where 절을 사용하여 타입 제약을 줄 수 있습니다. [SE-0142]

protocol StringRepresentable: RawRepresentable
where RawValue == String { }

protocol RawStringWrapper {
  associatedtype Wrapped: RawRepresentable
    where Wrapper.RawValue == String
}



제네릭 서브스크립트

서브스크립트가 이제 제네릭 매개변수와 반환 타입을 사용할 수 있게 되었습니다. [SE-0148]

struct CustomModel<Key: Hashable, Value> {
    var dictionary: [Key: Value]
    
    subscript<T>(key: Key) -> T? {
        return dictionary[key] as? T
    }
}

let information = CustomModel(dictionary: ["name": "yagom", "age": 100, "height": 183.0])
let name: String? = information["name"] // yagom


Private 접근수준의 변경

기존의 private 접근수준은 같은 파일인 여부와 상관없이 private 요소를 해당 범위를 벗어나면 사용할 수 없었습니다. 대체제로 fileprivate를 사용하였는데, 이는 문제를 야기할 수 있는 가능성을 가지고 있습니다. 같은 파일이라도 접근을 원치 않는 요소가 있는데, extension 등으로 타입을 확장하여 요소를 사용려면 fileprivate로 지정해 주어야 하는 문제가 있었기 때문이죠. 아래 Swift 3의 예를 먼저 보겠습니다.

// Swift 3
struct Person {
    var name: String
    // private로 지정하면 extension에서 접근할 수 없기 때문에 fileprivate로 선언
    fileprivate var age: Int = 0
    init(name: String) {
        self.name = name
    }
}

extension Person {
    mutating func passedYear() {
        self.age += 1
    }
}

var yagom = Person(name: "yagom")
yagom.passedYear()
yagom.age // 접근가능! 원하던 시나리오가 아님!


나이라는 민감한 부분은 숨기고 싶었기 때문에 private로, 어디서든 접근을 막고 싶었지만, 같은 파일의 익스텐션에서 조차 접근할 수 없기 때문에 fileprivate로 접근수준을 지정했습니다. 그 결과, 엉뚱하게 같은 파일의 다른 소스에서도 접근할 수 있게 되었습니다.

// Swift 4

struct Person { var name: String private var age: Int = 0 init(name: String) { self.name = name } } extension Person { mutating func passedYear() { // private라도 같은 파일의 extension에서 접근 가능 self.age += 1 } } var yagom = Person(name: "yagom") yagom.passedYear() // yagom.age // 접근불가

Swift 4에서는 이 문제를 개선하여 같은 파일의 익스텐션에서는 접근할 수 있도록 변경되었습니다.


아카이브와 시리얼라이제이션 / JSON 인코딩

NSCodingNSObject를 손쉽게 사용할 수 있는 클래스 타입을 제외하고, 구조체나 열거타입에서는 아카이빙이 참으로 난해했습니다. 물론 NSObject를 상속받지 않는 스위프트 고유 클래스도 마찬가지였습니다. 그러나 이제 타입이 스스로 어떻게 아카이브하고 시리얼라이즈 할지 정의할 수 있게 되었습니다. 그저 타입과 그 타입의 하위 타입이 모두 Codable 프로토콜을 준수하면 아카이브하고 그것을 풀어낼 수 있도록 할 수 있습니다. 기존에 사용하던 NSKeyedArchiver 클래스도 Codable 프로토콜을 완벽히 지원합니다. Codable과 함께 JSONEncoder, JSONDecoder로 인해 JSON 인코딩과 디코딩이 엄청나게 편해졌습니다!! 으아아아아아아아~!! [SE-0166],  [SE-0167]

struct Person: Codable {
    enum Gender: String, Codable {
        case male, female, unknown
    }
    
    var name: String
    var age: Int
    var gender: Gender
    var friends: [Person]
}

let yagom = Person(name: "yagom", age: 20, gender: .male, friends: [])
let hana = Person(name: "hana", age: 22, gender: .female, friends: [yagom])
let eric = Person(name: "eric", age: 25, gender: .male, friends: [yagom, hana])

var encoder = JSONEncoder()

let jsonData = try encoder.encode(eric)
let jsonString = String(data: jsonData, encoding: .utf8)
print(jsonString) 
// "{\"age\":25,\"gender\":\"male\",\"friends\":[{\"age\":20,\"gender\":\"male\",\"friends\":[],\"name\":\"yagom\"},{\"age\":22,\"gender\":\"female\",\"friends\":[{\"age\":20,\"gender\":\"male\",\"friends\":[],\"name\":\"yagom\"}],\"name\":\"hana\"}],\"name\":\"eric\"}"

let decoder = JSONDecoder()
let decoded: Person = try decoder.decode(Person.self, from: jsonData)
print(decoded.name) // eric


NSNumber 브릿징

예상치 못한 결과를 낼 수 있는 NSNumber 브릿징 결과가 수정되었습니다.

// Swift 3

let numberOne = NSNumber(value: Int64.max)

if numberOne is Int16 {
    print("numberOne == Int16")
} else if numberOne is Int64 {
    print("numberOne == Int64")
} // numberOne == Int16

let numberTwo = NSNumber(value: UInt32(777))
if let value = numberTwo as? Int8 {
    print(value)
} // 9

// Swift 4
let numberOne = NSNumber(value: Int64.max)

if numberOne is Int16 {
    print("numberOne == Int16")
} else if numberOne is Int64 {
    print("numberOne == Int64")
} // numberOne == Int64

let numberTwo = NSNumber(value: UInt32(777))
if let value = numberTwo as? Int8 {
    print(value)
} // 아무 출력 없음


그 외 기타 변경사항

* Core Foundation 타입들은 HashableEquatable 프로토콜을 CFHashCFEqual 함수구현을 통해 준수합니다. Swift 3 모드(Swift 3.2 버전)에서도 적용되는 사항입니다. [SR-2388]

* 암시적인 @objc 선언을 이제 더욱 명확히 표현해 주어야 합니다. [SE-0160]

* raw buffer를 슬라이스 한 결과는 더이상 같은 타입으로 반환되지 않습니다. [SE-0138]

* 재정의(override)가 더 완벽하게 지원됩니다. [SR-1529]

* 제네릭 매개변수를 갖는 서브스크립트를 정의할 수 있습니다. [SE-0148]

* 스위프트 타입 시스템이 이제는 하나의 튜플 전달인자를 가지는 함수와 여러 매개변수를 가지는 함수를 구분합니다. [SE-0110]

* 여러 연산자를 수반한 정수형 상수를 정의하는 C 언어의 매크로의 형태를 더 다양하게 임포트 할 수 있습니다. 

* inout 매개변수를 사용하는 reduce 함수가 추가되었습니다. [SE-0171]

* 컬렉션 타입의 두 요소를 서로 바꿔주는 swapAt(_:_:) 메서드가 추가됐습니다. [SE-0173], [SE-0176]


참고문서

Swift CHANGELOG - Swift 4.0






by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 


저작자 표시 비영리 변경 금지
신고

'Swift' 카테고리의 다른 글

Swift 4 변경사항  (10) 2017.06.23
Posted by yagom

클로저

클로저는 코드의 블럭입니다. 일급시민(first-citizen)으로, 전달인자, 변수, 상수 등으로 저장, 전달이 가능합니다.  
함수는 클로저의 일종으로, `이름이 있는 클로저`라고 생각하면 됩니다.


소스코드


기본 클로저 문법

{ (매개변수 목록) -> 반환타입 in
    실행 코드
}


클로저의 사용

// sum이라는 상수에 클로저를 할당
let sum: (Int, Int) -> Int = { (a: Int, b: Int) in
    return a + b
}

let sumResult: Int = sum(1, 2)
print(sumResult) // 3


함수의 전달인자로서의 클로저

클로저는 주로 함수의 전달인자로 많이 사용됩니다. 함수 내부에서 원하는 코드블럭을 실행할 수 있습니다.

let add: (Int, Int) -> Int
add = { (a: Int, b: Int) in
    return a + b
}

let substract: (Int, Int) -> Int
substract = { (a: Int, b: Int) in
    return a - b
}

let divide: (Int, Int) -> Int
divide = { (a: Int, b: Int) in
    return a / b
}

func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
    return method(a, b)
}

var calculated: Int

calculated = calculate(a: 50, b: 10, method: add)

print(calculated) // 60

calculated = calculate(a: 50, b: 10, method: substract)

print(calculated) // 40

calculated = calculate(a: 50, b: 10, method: divide)

print(calculated) // 5

//따로 클로저를 상수/변수에 넣어 전달하지 않고, 
//함수를 호출할 때 클로저를 작성하여 전달할 수도 있습니다.

calculated = calculate(a: 50, b: 10, method: { (left: Int, right: Int) -> Int in
    return left * right
})

print(calculated) // 500



다양한 클로저표현

클로저는 다양한 모습으로 표현될 수 있습니다.

  • 함수의 매개변수 마지막으로 전달되는 클로저는 후행클로저(trailing closure)로 함수 밖에 구현할 수 있습니다. 
  • 컴파일러가 클로저의 타입을 유추할 수 있는 경우 매개변수, 반환 타입을 생략할 수 있습니다.
  • 반환 값이 있는 경우, 암시적으로 클로저의 맨 마지막 줄은 return 키워드를 생략하더라도 반환 값으로 취급합니다.
  • 전달인자의 이름이 굳이 필요없고, 컴파일러가 타입을 유추할 수 있는 경우 축약된 전달인자 이름($0, $1, $2...)을 사용 할 수 있습니다.


소스코드



클로저 매개변수를 갖는 함수 calculate(a:b:method:)와 결과값을 저장할 변수 result를 먼저 선언해둡니다.

func calculate(a: Int, b: Int, method: (Int, Int) -> Int) -> Int {
    return method(a, b)
}

var result: Int


후행 클로저

클로저가 함수의 마지막 전달인자라면 마지막 매개변수 이름을 생략한 후 함수 소괄호 외부에 클로저를 구현할 수 있습니다.

result = calculate(a: 10, b: 10) { (left: Int, right: Int) -> Int in
    return left + right
}

print(result) // 20


반환타입 생략

calculate(a:b:method:) 함수의 method 매개변수는 Int 타입을 반환할 것이라는 사실을 컴파일러도 알기 때문에 굳이 클로저에서 반환타입을 명시해 주지 않아도 됩니다. 

대신 in 키워드는 생략할 수 없습니다.

result = calculate(a: 10, b: 10, method: { (left: Int, right: Int) in
    return left + right
})

print(result) // 20

// 후행클로저와 함께 사용할 수도 있습니다
result = calculate(a: 10, b: 10) { (left: Int, right: Int) in
    return left + right
}

print(result) // 20


단축 인자이름

클로저의 매개변수 이름이 굳이 불필요하다면 단축 인자이름을 활용할 수 있습니다. 단축 인자이름은 클로저의 매개변수의 순서대로 $0, $1, $2... 처럼 표현합니다.

result = calculate(a: 10, b: 10, method: {
    return $0 + $1
})

print(result) // 20


// 당연히 후행 클로저와 함께 사용할 수 있습니다
result = calculate(a: 10, b: 10) {
    return $0 + $1
}

print(result) // 20


암시적 반환 표현

클로저가 반환하는 값이 있다면 클로저의 마지막 줄의 결과값은 암시적으로 반환값으로 취급합니다.

result = calculate(a: 10, b: 10) { $0 + $1 } print(result) // 20 // 간결하게 한 줄로 표현해 줄 수도 있습니다 result = calculate(a: 10, b: 10) { $0 + $1 } print(result) // 20


축약 전과 후 비교

//축약 전
result = calculate(a: 10, b: 10, method: { (left: Int, right: Int) -> Int in
    return left + right
})

//축약 후
result = calculate(a: 10, b: 10) { $0 + $1 }

print(result) // 20


관련문서

* The Swift Programming Language - Closures

* 일급시민(first-citizen)





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 


저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

상속  (0) 2017.06.29
프로퍼티 - Property  (2) 2017.06.26
클로저 - Closure  (0) 2017.06.22
클래스, 구조체, 열거형 비교  (0) 2017.06.19
열거형 - Enumerations  (0) 2017.06.15
클래스  (0) 2017.06.12
Posted by yagom

Class vs Struct/Enum

소스코드


열거형과 구조체는 값 타입이며, 클래스는 참조 타입이라는 것이 가장 큰 차이입니다. 또한, 클래스는 상속이 가능하지만 구조체와 열거형은 상속이 불가능합니다.

struct ValueType {
    var property = 1
}

class ReferenceType {
    var property = 1
}

// 첫 번째 구조체 인스턴스
let firstStructInstance = ValueType()
// 두 번째 구조체 인스턴스에 첫 번째 인스턴스 값 복사
var secondStructInstance = firstStructInstance
// 두 번째 구조체 인스턴스 프로퍼티 값 수정
secondStructInstance.property = 2

// 두 번째 구조체 인스턴스는 첫 번째 구조체를 똑같이 복사한 
// 별도의 인스턴스이기 때문에 
// 두 번째 구조체 인스턴스의 프로퍼티 값을 변경해도
// 첫 번째 구조체 인스턴스의 프로퍼티 값에는 영향이 없음
print("first struct instance property : \(firstStructInstance.property)")    // 1
print("second struct instance property : \(secondStructInstance.property)")  // 2


// 클래스 인스턴스 생성 후 첫 번째 참조 생성
let firstClassReference = ReferenceType()
// 두 번째 참조 변수에 첫 번째 참조 할당
let secondClassReference = firstClassReference
secondClassReference.property = 2

// 두 번째 클래스 참조는 첫 번째 클래스 인스턴스를 참조하기 때문에
// 두 번째 참조를 통해 인스턴스의 프로퍼티 값을 변경하면
// 첫 번째 클래스 인스턴스의 프로퍼티 값을 변경하게 됨
print("first class reference property : \(firstClassReference.property)")    // 2
print("second class reference property : \(secondClassReference.property)")  // 2


관련문서

The Swift Programming Language - Enumerations

The Swift Programming Language - Classes and Structures





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 


저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

프로퍼티 - Property  (2) 2017.06.26
클로저 - Closure  (0) 2017.06.22
클래스, 구조체, 열거형 비교  (0) 2017.06.19
열거형 - Enumerations  (0) 2017.06.15
클래스  (0) 2017.06.12
구조체  (2) 2017.06.08
Posted by yagom

열거형


소스코드



정의 문법

스위프트의 열거형은 다른 언어의 열거형과는 많이 다릅니다. 잘 살펴보아야 할 스위프트의 기능 중 하나입니다.

  • enum은 타입이므로 대문자 카멜케이스를 사용하여 이름을 정의합니다
  • 각 case는 소문자 카멜케이스로 정의합니다
  • 각 case는 그 자체가 고유의 값입니다
  • 각 케이스는 한 줄에 개별로도, 한 줄에 여러개도 정의할 수 있습니다

enum 이름 {
	case 이름1
	case 이름2
	case 이름3, 이름4, 이름5
	// ...
}


열거형 사용

enum Weekday {
    case mon
    case tue
    case wed
    case thu, fri, sat, sun
}

// 열거형 타입과 케이스를 모두 사용하여도 됩니다
var day: Weekday = Weekday.mon

// 타입이 명확하다면 .케이스 처럼 표현해도 무방합니다
day = .tue

print(day) // tue

// switch의 비교값에 열거형 타입이 위치할 때
// 모든 열거형 케이스를 포함한다면
// default를 작성할 필요가 없습니다
switch day {
case .mon, .tue, .wed, .thu:
    print("평일입니다")
case Weekday.fri:
    print("불금 파티!!")
case .sat, .sun:
    print("신나는 주말!!")
}


원시값(Raw value)

C 언어의 enum처럼 정수값을 가질 수도 있습니다. rawValue를 사용하면 됩니다. 

case별로 각각 다른 값을 가져야합니다.

enum Fruit: Int {
    case apple = 0
    case grape = 1
    case peach
    
    // mango와 apple의 원시값이 같으므로 
    // mango 케이스의 원시값을 0으로 정의할 수 없습니다
//    case mango = 0
}

print("Fruit.peach.rawValue == \(Fruit.peach.rawValue)")
// Fruit.peach.rawValue == 2


정수 타입 뿐만 아니라 Hashable 프로토콜을 따르는 모든 타입이 원시값의 타입으로 지정될 수 있습니다.

enum School: String {
    case elementary = "초등"
    case middle = "중등"
    case high = "고등"
    case university
}

print("School.middle.rawValue == \(School.middle.rawValue)")
// School.middle.rawValue == 중등

// 열거형의 원시값 타입이 String일 때, 원시값이 지정되지 않았다면
// case의 이름을 원시값으로 사용합니다
print("School.university.rawValue == \(School.university.rawValue)")
// School.middle.rawValue == university


원시값을 통한 초기화

rawValue를 통해 초기화 할 수 있습니다. rawValuecase에 해당하지 않을 수 있으므로 rawValue를 통해 초기화 한 인스턴스는 옵셔널 타입입니다.

// rawValue를 통해 초기화 한 열거형 값은 옵셔널 타입이므로 Fruit 타입이 아닙니다
//let apple: Fruit = Fruit(rawValue: 0)
let apple: Fruit? = Fruit(rawValue: 0)

// if let 구문을 사용하면 rawValue에 해당하는 케이스를 곧바로 사용할 수 있습니다
if let orange: Fruit = Fruit(rawValue: 5) {
    print("rawValue 5에 해당하는 케이스는 \(orange)입니다")
} else {
    print("rawValue 5에 해당하는 케이스가 없습니다")
} // rawValue 5에 해당하는 케이스가 없습니다


메서드

스위프트의 열거형에는 메서드도 추가할 수 있습니다.

enum Month {
    case dec, jan, feb
    case mar, apr, may
    case jun, jul, aug
    case sep, oct, nov
    
    func printMessage() {
        switch self {
        case .mar, .apr, .may:
            print("따스한 봄~")
        case .jun, .jul, .aug:
            print("여름 더워요~")
        case .sep, .oct, .nov:
            print("가을은 독서의 계절!")
        case .dec, .jan, .feb:
            print("추운 겨울입니다")
        }
    }
}

Month.mar.printMessage()


관련문서

The Swift Programming Language - Enumerations





by yagom

facebook : http://www.facebook.com/yagomSoft

facebook group : https://www.facebook.com/groups/yagom/


p.s 제 포스팅을 RSS 피드로 받아보실 수 있습니다.

RSS Feed 받기   


↓↓↓ 블로거에게 공감은 큰 힘이 됩니다 ↓↓↓ 

저작자 표시 비영리 변경 금지
신고

'Swift > 기본문법' 카테고리의 다른 글

클로저 - Closure  (0) 2017.06.22
클래스, 구조체, 열거형 비교  (0) 2017.06.19
열거형 - Enumerations  (0) 2017.06.15
클래스  (0) 2017.06.12
구조체  (2) 2017.06.08
옵셔널  (0) 2017.06.05
Posted by yagom


티스토리 툴바