32YB SOPT/iOS 세미나

[SOPT] 합동세미나 review (부제 : 눈물의 첫 얼렁뚱땅 프로젝트)

신_이나 2023. 6. 5. 04:03

 

 

2023.05.13 ~. 5.27 (2주)
SOPT iOS 32YB 신지원의 첫 합동 세미나
토스 iOS APP 리디자인 구현

 

 

 

 

 

 사실 시작할 때만 해도 세미나랑 과제 이해안된다고 집에서 혼자 찡찡대고 울고 난리를 폈었다. 합세 과제를 받았던 첫 날에도 '네비게이션 바'가 뭔지도 몰라서 희재(우리 조장) 몰래 화장실 가서 네비게이션 바가 뭔지 쳐보고 온 다음 아는 척하고 그랬다. 그리고 카페에서 아요끼리 다같이 공부할 때만 해도 답답한 나머지 화장실 가서 몰래 울고 왔는데 ,,, 합세 끝나고 나니 정말 많이 성장한 것 같다. 이게 SOPT 의 힘일까? 이제는 울지 않는다는 것이, 그리고 자신있게 모르는 것을 물어볼 수 있다는 것이, 물어볼 든든한 친구들이 생겼다는 것이 나에게 큰 변화로 다가왔다. 코드 자체는 우당탕탕 일 수 있어도 완성했다는 것에 의의를 두면서 합동세미나 리뷰 시작해보자 ,, (서론이 길었다)

 

 

 

 

 

 

 

 

내가 맡은 파트의 전체 뷰다. 두 가지의 View 를 구현해야 했다.

첫 번째 view는 상품 상세 페이지다. 이 페이지에 도전 과제는 2가지가 있었다.

 

 

- 첫 번째 View

1. 하트 버튼의 클릭 유무를 서버에서 받아오는 미션

 

하트 버튼을 누르고 다른 화면으로 전화하였다면 다시 이 페이지로 돌아왔을 때 하트 버튼을 누른 것이 남아있어야 한다. 

 

 

heartButton.do {
                $0.setImage(Image.heart, for: .normal)
                $0.setImage(Image.heartFilled, for: .selected)
                
                $0.addTarget(self, action: #selector(heartBtnTap), for: .touchUpInside)
            }

우선 heartButton의 setstyle 을 짤 때, addTarget 을 사용하여 하트버튼을 누른다면 heartBtnTap 함수가 시행되도록 하였다.

 

 

func heartBtnTap() {
        heartButton.isSelected.toggle()
        GiftAPI.shared.patchHeart { result in
            print("data response를 받았습니다")
        }
    }

heartBtnTap() 함수가 시행되게 되면 현재 버튼 select 상태의 true, false 상태를 바꿔줄 수 있게 된다. 또한 서버 API에서 heart 버튼의 상태 data 를 받아오게 된다. 그렇다면 서버에서 어떻게 받아오고 있는지 확인해보자

 

 

import Alamofire

class GiftAPI: BaseAPI {
    static let shared = GiftAPI()

    private override init() {}
}

extension GiftAPI{
    
    public func patchHeart(completion: @escaping (NetworkResult<Any>) -> Void) {
        AFManager.request(GiftService.patchHeart).responseData { response in
            self.disposeNetwork(response,
                                dataModel: VoidResult.self,
                                completion: completion)
        }
    }
    public func getProduct(completion: @escaping (NetworkResult<Any>) -> Void) {
        AFManager.request(GiftService.getProduct).responseData { response in
            self.disposeNetwork(response,
                                dataModel: GiftproductModel.self,
                                completion: completion)
        }
    }
}

사실 처음에 이 부분의 코드를 구현할 때는 잘 이해하지 못하고 구현하였다. 희재가 짜놓은 코드 + 세미나 코드를 거의 복붙하다싶이 붙여 작동하도록 하였기 때문에 다시 공부할 필요가 있다. 우선 alamofire 을 사용하여 서버통신을 하였으며 heartButton 에서 서버를 받아오는 GiftAPI 는 희재가 짜놓은 BaseAPI 의 class 를 상속받는다.

 

 

//BaseAPI 내부 코드

let AFManager: Session = {
        var session = AF
        let configuration = URLSessionConfiguration.af.default
        let eventLogger = AlamofireLogger()
        session = Session(configuration: configuration, eventMonitors: [eventLogger])
        return session
    }()

AF 의 session 을 사용하면 alamofire 을 사용하여 AF.request(주소) 로 요청하는 것 대신 session.request(주소) 로 사용할 수 있다. 그렇다면 왜 이렇게 사용하는 것일까? 위 코드에서 session 선언 아래부터 구현한 코드를 커스텀 session 이라고 하는데, 이렇게 session 을 선언하면 커스텀 session 을 사용할 수 있기 때문이다. let configuration = URLSessionConfiguration.af.default 으로 커스텀을 시작하는 게 좋다.  URLSessionConfiguration.af.default 에는 Accept-Encoding, Accept-Language, User-Agent 의 헤더값들이 설정되어 있다. 서버와 관련되어 있는 헤더값들이다!

AlamofireLogger() 는 희재가 또 작성한 함수인데, request 를 진행하는 함수다. 보통 이와 같은 작업을 EventMonitor 로 API 로그 찍기 라고 한다고 한다.

DispatchQueue, requestDidFinish, toPrettyPrintedString 위 세 개에 대하여 알아보자, DispatchQueue 는 모든 이벤트를 저장하는 큐로 별도 큐를 만드는 것이다. requestDidFinish 는 요청이 끝났을 때 호출되는 메소드로 요청 내용을 출력한다. toPrettyPrintedString 는 data extension 으로 정의하여 사용할 수 있다.

 

 

extension GiftAPI{
    
    public func patchHeart(completion: @escaping (NetworkResult<Any>) -> Void) {
        AFManager.request(GiftService.patchHeart).responseData { response in
            self.disposeNetwork(response,
                                dataModel: VoidResult.self,
                                completion: completion)
        }
    }
    public func getProduct(completion: @escaping (NetworkResult<Any>) -> Void) {
        AFManager.request(GiftService.getProduct).responseData { response in
            self.disposeNetwork(response,
                                dataModel: GiftproductModel.self,
                                completion: completion)
        }
    }
}

 그렇다면 위에서 언급한 GiftAPI 내 extension 함수를 살펴보자! 첫 번째 뷰를 구현하기 위해서는 두 가지의 API 를 받아와야 했다. 앞서 언급한 하트 버튼과 상품 정보에 대한 API 이다. 따라서 두 함수를 선언하였다. @escaping 이란 escaping 클로저를 의미한다. escaping 클로저는 함수의 인자로 전달됐을 때, 함수의 실행이 종료된 후 실행되는 클로저다. 따라서 non-escaping closure 는 반대로 함수의 실행이 종료되기 전에 실행되는 클로저라고 할 수 있다. 따라서 모두 동기식 파라미터다. 둘을 나눠서 사용하는 이유는 효율적으로 객체의 라이프사이클을 관리하기 위해서다. @escaping 은 비동기식으로 escaping closure 과 non-escaping closure 를 사용할 수 있다. 위에서 @escaping 을 사용한 이유는 함수의 실행 흐름에 상관 없이 실행되는 클로저를 선언해야 하기 때문이다. @escaping 앞에 있는 completion 이라는 매개상수의 파라미터 타입은 클로저 타입이다. 하지만 이 곳에 옵셔널을 붙이게 된다면 옵셔널 타입으로 전환되기 때문에 클로저 조건에서 벗어난다. 조심~!

NetworkResult<Any> 는 타입 파라미터로 선언하여 당장 타입을 정해놓지 않겠다는 의미다. 

disposeNetwork 와 VoidResult, GiftproductModel 모두 따로 작성한 함수다.

 

 

func dataBind(_ productData: GiftproductModel?) {
        guard let productData = self.productData else { return }
        productbrandLabel.text = productData.brandTitle
        productnameLabel.text = productData.productTitle
        productpriceLabel.text = String(productData.price ?? 0) + "원"
        cashbackpointLabel.text = String(productData.point ?? 0) + "원"
        expirydateLabel.text = String(productData.expiration ?? 0) + "일"
        itemInfotextLabel.text = productData.productInfo
        itemInfoText = productData.productInfo
        
        let heart = productData.like ? Image.heartFilled : Image.heart
    }

지금까지 API 서버로 정보를 받아왔다면, 그 정보들을 view 에 가져와 뿌려주는 작업을 해야 한다. 따라서 dataBind 로 정보를 image, text 등의 형식으로 바꿔주는 작업이다. 마지막 let heart 로 선언한 곳이 heartButton 과 관련있는 data 다. 하지만 이대로만 코드를 작성하면 문제가 생겼다.

 

 

 

🚫 TrobleShooting

 원래는 하트 버튼을 누를 때마다 색깔이 채워졌다, 비었다를 반복해야 한다. 하지만 서버 연결을 해주었는데도 화면을 나갔다오면 그 상태가 저장되어있지 않았다. 그 이유는 dataBind 내부에 아래 코드를 작성해주지 않았기 때문이다. 따라서 "직접 Tap 하였을 때 HeartBtnTap() 애서 heartButton.isSelectend.toggle() + dataBind 내부 heart 상수에 의하여 받아온 heart 값으로 변화" 가 이루어져 버튼을 눌렸을 때 그르 저장하지 못하는 것처럼 보였던 것이다. 따라서 아래 코드를 작성하여 case 를 구분해주어야 한다.

 

 

if(heart == Image.heart){
            heartButton.isSelected = false
}
else {
            heartButton.isSelected = true
}

 

 

 

 

2. 상품정보에서 후기를 이동하는 애니메이션 구현

 

지금보니까 레이아웃이 안맞는 점이 있네,, 아래 리팩토리 부분에서 다시 구현하겠습니다 ㅎㅎ

상품정보와 후기를 오가는 bar 를 하나 만들어서 클릭에 맞게 왔다갔다 하게 했어야 한다. 이에 중요한 점은 글자 폭에 맞게 크기도 조정이 된다는 것이다. 

 

 

itemInfoView.do {
            $0.backgroundColor = .tossWhite
            
            //상품정보 버튼
            infoButton.do {
                $0.backgroundColor = .tossWhite
                $0.setTitle("상품정보", for: .normal)
                $0.titleLabel?.font = .tossTitle2
                $0.titleLabel?.textAlignment = .center
                if(checkInfo){
                    $0.setTitleColor(UIColor(hex: 0x191919), for: .normal)
                }
                else {
                    $0.setTitleColor(UIColor(hex: 0x999999), for: .normal)
                }
                $0.addTarget(self, action: #selector(infoBtnTap), for: .touchUpInside)
            }
            
            //후기 버튼
            reviewButton.do {
                $0.backgroundColor = .tossWhite
                $0.setTitle("후기", for: .normal)
                $0.titleLabel?.font = .tossTitle2
                $0.titleLabel?.textAlignment = .center
                if(checkInfo){
                    $0.setTitleColor(UIColor(hex: 0x999999), for: .normal)
                }
                else {
                    $0.setTitleColor(UIColor(hex: 0x191919), for: .normal)
                }
                $0.addTarget(self, action: #selector(infoBtnTap), for: .touchUpInside)
            }
            
            //상품 정보에 대한 글이 작성되어 있는 라벨
            itemInfotextLabel.do {
                //checkInfo = true
                $0.text = itemInfoText
                $0.textColor = UIColor.init(hex: 0x6D7582)
                $0.font = .tossBody1
                $0.numberOfLines = 2
                $0.textAlignment = .left
            }
            
            //움직이는 조그마한 바
            rectanglebarView.do {
                $0.backgroundColor = .black
                $0.layer.cornerRadius = 3
            }
}

ScrollView 위에 상품정보버튼+후기버튼+상품정보란+움직이는바 까지 한 번에 묶어서 올려주는 itemInfoView 를 구현하였다. 위 코드는 전반적인 layout 이다.

 

 

@objc
    func infoBtnTap() {
        checkInfo.toggle()
        if(checkInfo){
            infoButton.setTitleColor(UIColor(hex: 0x191919), for: .normal)
            reviewButton.setTitleColor(UIColor(hex: 0x999999), for: .normal)
            rectangleResetAnimation()
            itemInfotextLabel.text = itemInfoText
        }
        else {
            infoButton.setTitleColor(UIColor(hex: 0x999999), for: .normal)
            reviewButton.setTitleColor(UIColor(hex: 0x191919), for: .normal)
            rectangleAnimation()
            itemInfotextLabel.text = ""
        }
}

버튼을 tap 하면 구현되는 infoBtnTap() 의 코드다. 그렇다면 이번 미션의 하이라이트인 rectangleAnimation() 과 rectangleResetAnimation() 을 살펴보자!

 

 

 

🚫 TrobleShooting

 움직이는 애니메이션을 구현한다는 자체가 나에게는 TrobleShooting 이었다. 내 욕심엔 한 함수 안에서 구현하고 싶었지만 더 가독성있고 쉽고 짜도록 하여 두 함수로 구분하였다. 우선 익범오빠가 알려주었던 것처럼 remakeConstraints 를 사용하였다. 말 그대로 layout 의 위치를 다시 잡아주는 함수다. 이미 정의한 위치를 다시 잡아주고 싶을 때는 updateConstraints 와 remakeConstraints 을 사용할 수 있는데, 먼저 updateConstraints 는 말 그대로 업데이트의 개념으로 모든 위치를 재정의해줄 필요가 없다. remakeConstraints 로 위치를 재설정하게 된다면 모든 layout 을 다시 정해주어야 한다. 그렇다면 왜 remakeConstraints 를 사용하는 것일까? 축 자체가 바뀌었을 때는 remakeConstraints 을 써주는 것이 훨씬 더 간편하다. 즉, remakeConstraints 는 top, leading, bottom, trailing의 기준 자체가 변경되었을 때 사용하는 것이 좋다!

 

 

위치를 재설정해주었다면 애니메이션 효과를 넣어야 한다. rectanglebarView 에 layoutIfNeeded() 함수로 애니메이션을 구현하였다. layoutIfNeeded() 는 UI 업데이트 명령을 큐 뒤에 넣는 것이 아니라 맨 앞에 넣어서 바로 UI 의 변경을 명령하는 것이다. 다시 말하자면, 해당 메소드 호출 후 update cycle 이 올 때까지 기대린 뒤 호출하는 것이 아니라 해당 메소드 호출 즉시 layoutIfNeeded()를 호출하게 되는 것이다.  

func rectangleAnimation() {
        rectanglebarView.snp.remakeConstraints {
            $0.centerX.equalTo(reviewButton.snp.centerX)
            $0.bottom.equalTo(reviewButton.snp.bottom)
            $0.width.equalTo(reviewButton.titleLabel!.snp.width)
            $0.height.equalTo(3)
        }
        UIView.animate(withDuration: 0.5) { [self] in
            self.rectanglebarView.superview?.layoutIfNeeded()
        }
}
    
func rectangleResetAnimation() {
        rectanglebarView.snp.remakeConstraints {
            $0.centerX.equalTo(infoButton.snp.centerX)
            $0.bottom.equalTo(reviewButton.snp.bottom)
            $0.width.equalTo(infoButton.titleLabel!.snp.width)
            $0.height.equalTo(3)
        }
        UIView.animate(withDuration: 0.5) { [self] in
            self.rectanglebarView.superview?.layoutIfNeeded()
        }
}

 

 

 

 

- 두 번째 View

1. TextView 구현

 

 사실 난 Textview 를 처음 구현해보았다. TextField 는 했어도 view 로 구현하는 것은 처음이다. 위와 같이 편지 쓰는 부분을 구현했어야 했다. 

 

 

balloncardTextView.do {
                $0.isScrollEnabled = false
                $0.text = "고마운 마음을 담아\n선물을 보내요"
                $0.font = .tossHeader2
                $0.textColor = .init(hex: 0xC05FD0)
                $0.textAlignment = .center
                $0.backgroundColor = UIColor.clear
                $0.isEditable = true
                $0.textContainerInset = UIEdgeInsets(top: 20, left: 18, bottom: 18, right: 18)
                $0.inputAccessoryView = textViewAccessoryView
}

 TextView 안에서 TextField 와 같이 구현할 수 있었다. 그 중에서 "isScrollEnabled" 를 false 로 두고 "isEditable" 을 통하여 수정할 수 있도록 하였고, "textContainerInset" 을 이용하여 text 의 layout 을 구현할 수 있었다. "inputAccessoryView" 란 키보드 위에 뜨는 보조 뷰다. 이건 아래 2번째 미션이었던 키보드를 따라가는 완료 버튼을 구현하기 위함이다. 

 

 

textView 를 구현할 때 View 에 text 의 글자 수가 21자를 넘긴다면, 글자 크기를 줄이는 조건을 두어야 하는데, 그 코드가 아래와 같다.

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        let currentText = textView.text ?? ""
        guard let stringRange = Range(range, in: currentText) else  { return false }
        let changedText = currentText.replacingCharacters(in: stringRange, with: text)
        writeContentLabel.text = "\(changedText.count)자/86자"

        if(changedText.count > 21) {
            balloncardTextView.font = .tossTitle2
            balloncardTextView.textContainerInset = UIEdgeInsets(top: 20, left: 18, bottom: 18, right: 18)
        }
        else {
            balloncardTextView.font = .tossHeader2
            balloncardTextView.textContainerInset = UIEdgeInsets(top: 20, left: 18, bottom: 18, right: 18)
        }
        return changedText.count <= 85
}

위와 같이 구현하였다. replacingCharacters 란 문자열의 범위를 받고 그 범위에 with 문자열로 대체하여 복사본을 리턴하는 함수다. 말 자체만 들었을 때는 전-혀 무슨 말인지 모르겠다. 앞서 나온 뜻을 내 코드에 빗대어 말해보면, stringRange 를 범위로 받고 이 범위에 text 문자열로 대체하여 복사본을 리턴하는 것이다. 더 구체적으로 말해보자! stringRange 는 textView 에 작성되어 있는 글씨의 범위이며, 이 범위만큼 textView 의 text 를 대체한다. 아하! 다시 말하면, text 가 계속해서 update 될 때마다, 글자수를 계산하여 changedText 에 넣어주는 것이다! 그렇게 센 글자수가 21보다 크게 되면 font 를 변경하게 되는 것이다.

 

 

 

🚫 TrobleShooting

 clear 버튼을 눌렀을 때 글자 수 세는 label 이 사라져야 하고, 글씨를 쓰는 순간 다시 label 이 생겨야 한다. 이때 자꾸 에러가 났는데, textView 를 구현하는 모든 코드에 wirteContentLabel.isHidden 을 선언하였다. clean 버튼을 누르면 label 이 사라져야 하고, 쓰기 시작할 때, 쓰고 있을 때, 쓰고난 직후에는 label 이 있어야 한다. 만약 작성을 완료하였을 때, 메세지 작성 버튼이 있다면 label 을 사라지게 하고, 없다면 label 이 보여야 한다. 이건 editMessageButtonTap() 함수에서 wirteContentLabel 을 따로 다루어 textViewDidEndEdting() 에서 아래처럼 조건문을 굳이 쓰지 않아도 되지만, 한 곳에 모여있게 해서 보기 쉽게 하려고 아래 처럼 작성하였다 !~!

 

 

@objc
private func clean() {
        balloncardTextView.text = ""
        writeContentLabel.text = "0자/86자"
        writeContentLabel.isHidden = true
        
}

...

extension GiftcardViewController: UITextViewDelegate {
    
    func textViewDidBeginEditing(_ textView: UITextView) {
        
        if placeHolder == textView.text {
            balloncardTextView.text = ""
        }
        
        writeContentLabel.isHidden = false
    }

    func textViewDidChange(_ textView: UITextView) {
        writeContentLabel.isHidden = false
    }
    
    func textViewDidEndEditing(_ textView: UITextView) {

        if !textView.hasText {
            textView.text = placeHolder
        }
        if(!editMessageButton.isHidden){
            writeContentLabel.isHidden = true
        }
        else {
            writeContentLabel.isHidden = false
        }
    }
    
...

writeContentLabel 을 제외한 다른 버튼은 모두 없앤 코드다.

 

 

 

 

2. 완료 버튼이 키보드를 따라가도록 구현

 

 위 영상처럼 키보드가 없을 때는 파란색 수정 완료 버튼이 바닥에 있어야 하는데 키보드를 올리면 키보드 위로 따라 올라와야 한다. 사실 따라 올라오는 것처럼 보이지만, 사실은 버튼을 2개 만든 것이다. 그리고 이 부분은 내가 직접 구현한 부분이 거의 없고 많~은 도움을 받아서 코드를 더 자세히 보도록 하자!

 

 

editCompleteButton.do {
                $0.setTitle("수정 완료", for: .normal)
                $0.titleLabel?.font = .tossBody2
                $0.setTitleColor(.tossWhite, for: .normal)
                $0.makeCornerRound(radius: 20)
                $0.backgroundColor = .tossBlue
                $0.addTarget(self, action: #selector(editCompletedTap), for: .touchUpInside)
}

textViewAccessoryView.do {
            $0.frame = .init(x: 0, y: 0, width: 400, height: 67)
            $0.backgroundColor = .clear
            
            
            textViewEditMessageButton.do {
                $0.setTitle("수정 완료", for: .normal)
                $0.titleLabel?.font = .tossBody2
                $0.setTitleColor(.tossWhite, for: .normal)
                $0.makeCornerRound(radius: 20)
                $0.backgroundColor = .tossBlue
                $0.addTarget(self,
                             action: #selector(editCompletedTap),
                             for: .touchUpInside)
            }
}

editCompleteButton 은 분홍색 "완료" 버튼이 사라지면 하단에 위치해 있으며 키보드로 가려지는 버튼이다. textViewEditMessageButton 은 textViewAccessoryView 안에 위치하며 키보드를 따라가는 버튼이다. 따라서 모든 setStyle 을 똑같이 구현하였다.

 

 

balloncardTextView.do {
                $0.inputAccessoryView = textViewAccessoryView
}

키보드를 따라가기 위해선, 만든 버튼을 액세서리로 만들어야 한다. TextView 에 inputAccessoryView 를 위 코드에서 선언한  textViewAccessoryView 로 설정하였다. 따라서 키보드의 악세사리 역할을 하여 위에 살포시 올라가도록 하였다.

 

 

@objc
private func editCompletedTap() {
        let text = balloncardTextView.text
        print("수정 완료 버튼이 눌렸습니다.")
        balloncardTextView.resignFirstResponder()
        completeButton.isHidden = false
        writeContentLabel.isHidden = true
        deleteButton.isHidden = true
        editMessageButton.isHidden = false
}

editCompletedTap() 은 위와 같다. 

 

 

🚫 TrobleShooting

 키보드가 안올라오는 문제가 있었다. 자꾸 에러가 떠서 왜 그러지 만지작 거리다가 해결방법을 찾았다. Simulator 의 [ I/O - Keyboard - Connect Hardware Keyboard ] 가 체크되어있다면 체크를 풀어주면 된다. 그런데 체크가 되어있지 않은데도 키보드가 올라오지 않는 현상이 있었다. 난 분명 선언을 해주었는데 말이다!

 

 

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        balloncardTextView.resignFirstResponder()
}

private func editMessageButtonTap() {
        balloncardTextView.becomeFirstResponder()
}
    
@objc
private func editCompletedTap() {
        balloncardTextView.resignFirstResponder()
}

touchesBegan() 는 textView 나 키보드를 제외한 바탕을 터치하면 키보드가 내려가도록 하는 것이고, editMessageButtonTap() 은 메시지 수정 버튼을 누르면 키보드가 올라온다. editComletedTap() 은 수정 완료 버튼으로 tap 하면 키보드를 내리도록 한다. 하지만 위에서 말한대로 키보드가 올라오지 않았다! 사실 구글링을 해봐도 답을 찾을 수가 없었다. 체크되어있지 않더라도 다시 체크를 해주고, 체크를 풀어준 뒤 실행하면 잘 작동이 된다. 혹시 사이클이 문제 아닐까?

 

 

- 리팩토링

1. Mark 주석 작성

코딩 컨벤션을 정하였는데, 코드에 마크주석을 달기로 하였다.

// MARK: - Property
// MARK: - UI Components
// MARK: - Life Cycle
// MARK: - AddContent
// MARK: - Style
// MARK: - Layout
// MARK: - Actions
// MARK: - GIftViewDataSource

위의 마크주석으로 구분하였다. 마크주석으로 구분하니 코드에 칸이 나눠지는데 생각보다 예쁘다.

 

 

예뿌다! 깔끔하다!

 

 

 

2. 변수명, 함수명 직관적으로 설정

    private var scrollView = UIScrollView()
    private var contentView = UIView()
    private var itemMainView = UIView()
    
    private var itemInfoView = UIView()
    private var itemInfotextLabel = UILabel()
    private var itemInfoText : String?
    //private lazy var infoButton = UIButton()
    => private lazy var itemInfoButton = UIButton()
    //private lazy var reviewButton = UIButton()
    => private lazy var itemReviewButton = UIButton(
    //private lazy var rectanglebarView = UIView(frame: originFrame)
    => private lazy var itemRectangleBarView = UIView(frame: originFrame)
    //private var checkInfo = true
    => private var itemInfoCheck = true
    
    //private var itemEtcView = UIView()
    => private var productInfoView = UIView()
    private var productImage = UIImageView()
    private var productbrandLabel = UILabel()
    private var productnameLabel = UILabel()
    private var productpriceLabel = UILabel()
    
    private var cashbackView = UIView()
    private var cashbackIcon = UIImageView()
    //private var cashbackmessageLabel = UILabel()
    => private var cashbackMessageLabel = UILabel()
    //private var cashbackpointLabel = UILabel()
    => private var cashbackPointLabel = UILabel()
    
    //private var expirydateinfoLabel = UILabel()
    => private var expirydateNameLabel = UILabel()
    //private var expirydateLabel = UILabel()
    =>private var expirydateNumberLabel = UILabel()
    //private var noticeButton = UIButton()
    => private var noticeNameButton = UIButton()
    //private var brandconButton = UIButton()
    => private var noticeNameButton = UIButton()
    
    private var topNavBar = UIView()
    private var backButton = UIButton()
    private var searchButton = UIButton()
    private var heartButton = UIButton()
    
    private var bottomNavBar = UIView()
    private var buyButton = UIButton()
    private var giftButton = UIButton()
    
    private var topNavBar = UIView()
    private var backButton = UIButton()
    
    private var cardLabel = UILabel()
    //private var cardselectView = UIView()
    => private var cardSelectView = UIView()
    private var ballonButton = UIButton()
    private var turtleButton = UIButton()
    private var ghostButton = UIButton()
    private var spaceshipButton = UIButton()
    private var ballonLabel = UILabel()
    private var turtleLabel = UILabel()
    private var ghostLabel = UILabel()
    private var spaceshipLabel = UILabel()
    
    private var balloncardView = UIImageView()
    private var balloncardTextView = UITextView()
    private var editMessageButton = UIButton()
    private var editCompleteButton = UIButton()
    private var writeContentLabel = UILabel()
    private var cardEditState = false
    private var deleteButton = UIButton()
    
    private var completeButton = UIButton(type: .custom)

    private var textViewAccessoryView = UIView()
    private var textViewEditMessageButton = UIButton()

내가 두 가지의 View 를 사용하면서 선언한 모든 변수다. 이름을 바꾸어보았다. 함수 또한 btn 이라고 쓴 함수를 Button 으로, tapped 를 tap 으로 모두 바꿔주었다. 앞으로는 다른 좋은 코드들을 더 많이 보면서 작명 센스를 길러야 겠다!

 

 

3. 키보드 올라오지 않는 오류 해결

 - 우짜지? 내가 과연 할 수 있는걸까?

 

 

4. 한솔오빠가 해준 코드리뷰 반영

//MARK: - Life Cycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        addContentView()
        setStyle()
        setLayout()
        requestGiftAPI() 
}

기존 코드는 위와 같이 addContentView() 안에서 view.addSubviews 를 잡아주었다. 하지만 한솔 오빠의 리뷰를 보면 레이아웃을 잡기 전에 SuperView 에 먼저 addSubView 를 선언해야 한다.

 

 

//MARK: - Life Cycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubviews(scrollView, bottomNavBar, topNavBar)
        addContentView()
        
        setStyle()
        setLayout()
        requestGiftAPI() 
    }

 따라서 코드리뷰를 반영하여 위와 같이 구현하였다.

 

 

 

 

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        let currentText = textView.text ?? ""
        guard let stringRange = Range(range, in: currentText) else  { return false }
        let changedText = currentText.replacingCharacters(in: stringRange, with: text)
        writeContentLabel.text = "\(changedText.count)자/86자"

        if(changedText.count > 21) {
            balloncardTextView.font = .tossTitle2
            balloncardTextView.textContainerInset = UIEdgeInsets(top: 20, left: 18, bottom: 18, right: 18)
        }
        else {
            balloncardTextView.font = .tossHeader2
            balloncardTextView.textContainerInset = UIEdgeInsets(top: 20, left: 18, bottom: 18, right: 18)
        }
        return changedText.count <= 85
    }

 기존 textView 글자 수 세는 방식은 위와 같다. 코드리뷰 반영하여 다시 구현해보자!

 

 

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        let currentText = textView.text ?? ""
        guard let stringRange = Range(range, in: currentText) else  { return false }
        let changedText = currentText.replacingCharacters(in: stringRange, with: text)
        writeContentLabel.text = "\(changedText.count)자/86자"

        if(changedText.count <= 85) {
            if(changedText.count > 21) {
                balloncardTextView.font = .tossTitle2
                balloncardTextView.textContainerInset = UIEdgeInsets(top: 20, left: 18, bottom: 18, right: 18)
            }
            else {
                balloncardTextView.font = .tossHeader2
                balloncardTextView.textContainerInset = UIEdgeInsets(top: 20, left: 18, bottom: 18, right: 18)
            }
        }
        else {
            return false
        }
        
        return true
    }

 

 

 

@objc
    func backButtonTap() {
        if self.navigationController == nil {
            self.dismiss(animated: true, completion: nil)
        }
        else {
            self.navigationController?.popViewController(animated: true)
        }
    }

기존 코드는 위와 같다. backButton 을 누르는데, navigatinoController 가 nil 이라면 if 문을, 아니라면 pop 하여 전 화면으로 돌아가도록 하였는데, 코드리뷰를 보고 다시 보니 정말 navigation 으로 받고 있는 것이 없었다. 희재랑 코드랑 합치고 나서 수정했어야 하는데 ! 그래서 아래처럼 구현하였다.

 

 

@objc
    func backButtonTap() {
        self.navigationController?.popViewController(animated: true)
    }

위만 구현하여도 backButton 이 아주 실행되었다.

 

 

 

 

 

전체코드는 아래와 같다.

 

1. GiftViewController

더보기

//

//  GiftViewController.swift

//  Toss_iOS

//

//  Created by 신지원 on 2023/05/16.

//

 

import UIKit

 

import Kingfisher

import SnapKit

import Then

 

class GiftViewController: BaseViewController {

    

    // MARK: - Property

    

    private var productData: GiftproductModel?

    

    // MARK: - UI Components

    

    //scrollview 구현

    private var scrollView = UIScrollView()

    private var contentView = UIView()

    private var itemMainView = UIView()

    

    private var itemInfoView = UIView()

    private var itemInfotextLabel = UILabel()

    private var itemInfoText : String?

    private lazy var itemInfoButton = UIButton()

    private lazy var itemReviewButton = UIButton()

    private lazy var itemRectangleBarView = UIView(frame: originFrame)

    private var itemInfoCheck = true

    

    private var productInfoView = UIView()

    private var productImage = UIImageView()

    private var productbrandLabel = UILabel()

    private var productnameLabel = UILabel()

    private var productpriceLabel = UILabel()

    

    private var cashbackView = UIView()

    private var cashbackIcon = UIImageView()

    private var cashbackMessageLabel = UILabel()

    private var cashbackPointLabel = UILabel()

    

    private var expirydateNameLabel = UILabel()

    private var expirydateNumberLabel = UILabel()

    private var noticeNameButton = UIButton()

    private var brandconNameButton = UIButton()

    

    //상단 고정영역

    private var topNavBar = UIView()

    private var backButton = UIButton()

    private var searchButton = UIButton()

    private var heartButton = UIButton()

    

    //하단 고정영역

    private var bottomNavBar = UIView()

    private var buyButton = UIButton()

    private var giftButton = UIButton()

 

    //MARK: - Life Cycle

    

    override func viewDidLoad() {

        super.viewDidLoad()

        

        view.addSubviews(scrollView, bottomNavBar, topNavBar)

        addContentView()

        

        setStyle()

        setLayout()

        requestGiftAPI() 

    }

    

    // MARK: - AddContent

    

    func addContentView() {

        scrollView.addSubview(contentView)

        contentView.addSubviews(itemMainView, itemInfoView, productInfoView,

                                itemRectangleBarView,

                                productImage,productbrandLabel, productnameLabel, productpriceLabel,

                                cashbackView)

        cashbackView.addSubviews(cashbackIcon, cashbackMessageLabel, cashbackPointLabel)

        itemInfoView.addSubviews(itemInfoButton, itemReviewButton, itemInfotextLabel)

        productInfoView.addSubviews(expirydateNameLabel, expirydateNumberLabel,

                                noticeNameButton, brandconNameButton)

        topNavBar.addSubviews(backButton, searchButton, heartButton)

        bottomNavBar.addSubviews(giftButton, buyButton)

    }

    

    // MARK: - Style

    

    func setStyle() {

        view.backgroundColor = .tossWhite

        

        topNavBar.do {

            $0.backgroundColor = .tossWhite

            

            backButton.do {

                $0.setImage(Image.backArrow, for: .normal)

                $0.addTarget(self, action: #selector(backButtonDidTap), for: .touchUpInside)

            }

            searchButton.do {

                $0.setImage(Image.search, for: .normal)

            }

            heartButton.do {

                $0.setImage(Image.heart, for: .normal)

                $0.setImage(Image.heartFilled, for: .selected)

                

                $0.addTarget(self, action: #selector(heartButtonTap), for: .touchUpInside)

            }

        }

        

        contentView.do {

            $0.backgroundColor = .tossGrey200

        }

        itemMainView.do {

            $0.backgroundColor = .tossWhite

            

            productImage.do {

                $0.image = Image.coffee

            }

            productbrandLabel.do {

                //$0.text = "메가MGC커피"

                $0.font = .tossBody1

                $0.textColor = .tossGrey400

            }

            productnameLabel.do {

                //$0.text = "ime"

                $0.font = .tossSubTitle

                $0.textColor = .tossGrey500

            }

            productpriceLabel.do {

                //$0.text = "2,000원"

                $0.font = .tossHeader1

                $0.textColor = .tossGrey500

            }

            cashbackView.do {

                $0.backgroundColor = .tossWhite

                

                cashbackIcon.do {

                    $0.image = Image.point

                }

                cashbackMessageLabel.do {

                    $0.text = "3% 캐시백드려요"

                    $0.font = .tossSubTitle

                    $0.textColor = UIColor(hex: 0x6D7582)

                }

                cashbackPointLabel.do {

                    $0.text = "60원"

                    $0.font = .tossTitle2

                    $0.textColor = .tossBlue

                }

            }

        }

        

        itemInfoView.do {

            $0.backgroundColor = .tossWhite

            

            itemInfoButton.do {

                $0.backgroundColor = .tossWhite

                $0.setTitle("상품정보", for: .normal)

                $0.titleLabel?.font = .tossTitle2

                $0.titleLabel?.textAlignment = .center

                if(itemInfoCheck){

                    $0.setTitleColor(UIColor(hex: 0x191919), for: .normal)

                }

                else {

                    $0.setTitleColor(UIColor(hex: 0x999999), for: .normal)

                }

                $0.addTarget(self, action: #selector(infoButtonTap), for: .touchUpInside)

            }

            itemReviewButton.do {

                $0.backgroundColor = .tossWhite

                $0.setTitle("후기", for: .normal)

                $0.titleLabel?.font = .tossTitle2

                $0.titleLabel?.textAlignment = .center

                if(itemInfoCheck){

                    $0.setTitleColor(UIColor(hex: 0x999999), for: .normal)

                }

                else {

                    $0.setTitleColor(UIColor(hex: 0x191919), for: .normal)

                }

                $0.addTarget(self, action: #selector(infoButtonTap), for: .touchUpInside)

            }

            itemInfotextLabel.do {

                //checkInfo = true

                $0.text = itemInfoText

                $0.textColor = UIColor.init(hex: 0x6D7582)

                $0.font = .tossBody1

                $0.numberOfLines = 2

                $0.textAlignment = .left

            }

            itemRectangleBarView.do {

                $0.backgroundColor = .black

                $0.layer.cornerRadius = 3

            }

        }

        

        productInfoView.do {

            $0.backgroundColor = .tossWhite

            

            expirydateNameLabel.do {

                $0.text = "유효기간"

                $0.font = .tossTitle2

                $0.textColor = .tossGrey400

            }

            expirydateNumberLabel.do {

                //$0.text = "366일"

                $0.font = .tossTitle2

                $0.textColor = .tossGrey400

            }

            noticeNameButton.do {

                $0.backgroundColor = .tossWhite

                $0.setTitle("메가MGC커피 유의사항", for: .normal)

                $0.setTitleColor(.tossGrey400, for: .normal)

                $0.titleLabel?.font = .tossBody1

                $0.titleLabel?.textAlignment = .left

                $0.titleEdgeInsets = .init(top: 10, left: 19, bottom: 10, right: 210)

            }

            brandconNameButton.do {

                $0.backgroundColor = .tossWhite

                $0.setTitle("브랜드콘 안내", for: .normal)

                $0.setTitleColor(.tossGrey400, for: .normal)

                $0.titleLabel?.font = .tossBody1

                $0.titleLabel?.textAlignment = .left

                $0.titleEdgeInsets = .init(top: 10, left: 15, bottom: 10, right: 270)

            }

        }

        

        bottomNavBar.do {

            $0.backgroundColor = .tossWhite

            

            buyButton.do {

                $0.backgroundColor = .tossLightblue

                $0.setTitle("구매하기", for: .normal)

                $0.titleLabel?.font = .tossMedium18

                $0.setTitleColor(.tossBlue, for: .normal)

                $0.titleLabel?.textAlignment = .center

                $0.makeCornerRound(radius: 16)

            }

            giftButton.do {

                $0.backgroundColor = .tossBlue

                $0.setTitle("선물하기", for: .normal)

                $0.titleLabel?.font = .tossMedium18

                $0.setTitleColor(.tossLightblue, for: .normal)

                $0.titleLabel?.textAlignment = .center

                $0.makeCornerRound(radius: 16)

                $0.addTarget(self,

                             action: #selector(giftButtonTap),

                             for: .touchUpInside)

            }

        }

    }

    

    // MARK: - Layout

    

    func setLayout() {

        topNavBar.snp.makeConstraints {

            $0.top.equalTo(view.safeAreaLayoutGuide)

            $0.width.equalToSuperview()

            $0.height.equalTo(42)

            

            backButton.snp.makeConstraints {

                $0.centerY.equalToSuperview()

                $0.leading.equalToSuperview().inset(16)

                $0.size.equalTo(24)

            }

            searchButton.snp.makeConstraints {

                $0.centerY.equalToSuperview()

                $0.trailing.equalTo(heartButton.snp.leading).offset(-22)

            }

            heartButton.snp.makeConstraints {

                $0.centerY.equalToSuperview()

                $0.trailing.equalToSuperview().inset(22)

                $0.size.equalTo(24)

            }

        }

        

        scrollView.snp.makeConstraints {

            $0.top.equalTo(topNavBar.snp.bottom)

            $0.width.equalToSuperview()

            $0.bottom.equalTo(bottomNavBar.snp.top)

        }

        

        contentView.snp.makeConstraints {

            $0.top.bottom.equalToSuperview()

            $0.width.equalToSuperview()

        }

        

        itemMainView.snp.makeConstraints {

            $0.top.equalToSuperview()

            $0.leading.trailing.equalToSuperview()

            $0.height.equalTo(599)

            

            productImage.snp.makeConstraints {

                $0.top.equalToSuperview().inset(45)

                $0.width.equalToSuperview()

                $0.height.equalTo(329)

            }

            productbrandLabel.snp.makeConstraints {

                $0.top.equalTo(productImage.snp.bottom).offset(27)

                $0.leading.equalToSuperview().inset(26)

            }

            productnameLabel.snp.makeConstraints {

                $0.top.equalTo(productbrandLabel.snp.bottom).offset(10)

                $0.leading.equalToSuperview().inset(22)

            }

            productpriceLabel.snp.makeConstraints {

                $0.top.equalTo(productnameLabel.snp.bottom).offset(22)

                $0.leading.equalTo(productnameLabel.snp.leading)

            }

            cashbackView.snp.makeConstraints {

                $0.width.equalToSuperview()

                $0.top.equalTo(productpriceLabel.snp.bottom).offset(38)

                $0.height.equalTo(23)

                

                cashbackIcon.snp.makeConstraints {

                    $0.leading.equalToSuperview().inset(22)

                    $0.centerY.equalToSuperview()

                    $0.size.equalTo(22)

                }

                cashbackMessageLabel.snp.makeConstraints {

                    $0.leading.equalTo(cashbackIcon.snp.trailing).offset(16)

                    $0.centerY.equalToSuperview()

                }

                cashbackPointLabel.snp.makeConstraints {

                    $0.trailing.equalToSuperview().inset(24)

                    $0.centerY.equalToSuperview()

                }

            }

        }

        

        itemInfoView.snp.makeConstraints {

            $0.top.equalTo(itemMainView.snp.bottom).offset(16)

            $0.leading.trailing.equalToSuperview()

            $0.height.equalTo(188)

            

            itemInfoButton.snp.makeConstraints {

                $0.top.leading.equalToSuperview()

                $0.width.equalTo(187.5)

                $0.height.equalTo(58)

            }

            itemReviewButton.snp.makeConstraints {

                $0.top.trailing.equalToSuperview()

                $0.width.equalTo(187.5)

                $0.height.equalTo(58)

            }

            itemInfotextLabel.snp.makeConstraints {

                $0.top.equalTo(itemInfoButton.snp.bottom)

                $0.leading.equalToSuperview().inset(22)

                $0.bottom.equalToSuperview()

            }

            itemRectangleBarView.snp.makeConstraints {

                $0.top.equalTo(itemInfoButton.snp.bottom)

                $0.leading.equalToSuperview().offset(60)

                $0.width.equalTo(67)

                $0.height.equalTo(3)

            }

        }

        

        productInfoView.snp.makeConstraints {

            $0.top.equalTo(itemInfoView.snp.bottom).offset(16)

            $0.leading.trailing.equalToSuperview()

            $0.height.equalTo(242)

            $0.bottom.equalToSuperview()

            

            expirydateNameLabel.snp.makeConstraints {

                $0.top.equalToSuperview().offset(28)

                $0.leading.equalToSuperview().offset(22)

            }

            expirydateNumberLabel.snp.makeConstraints {

                $0.top.equalTo(expirydateNameLabel.snp.top)

                $0.trailing.equalToSuperview().inset(22)

            }

            noticeNameButton.snp.makeConstraints {

                $0.top.equalTo(expirydateNumberLabel.snp.bottom).offset(25)

                $0.width.equalToSuperview()

                $0.height.equalTo(40)

            }

            brandconNameButton.snp.makeConstraints {

                $0.top.equalTo(noticeNameButton.snp.bottom).offset(16)

                $0.width.equalToSuperview()

                $0.height.equalTo(40)

            }

        }

        

        bottomNavBar.snp.makeConstraints {

            $0.bottom.equalToSuperview()

            $0.width.equalToSuperview()

            $0.height.equalTo(87)

            

            buyButton.snp.makeConstraints {

                $0.top.equalToSuperview().inset(2)

                $0.leading.equalToSuperview().inset(19)

                $0.width.equalTo(164)

                $0.height.equalTo(55)

            }

            giftButton.snp.makeConstraints {

                $0.top.equalTo(buyButton.snp.top)

                $0.trailing.equalToSuperview().inset(19)

                $0.width.equalTo(164)

                $0.height.equalTo(55)

            }

        }

        

    }

    

    // MARK: - Actions

    

    @objc

    func infoButtonTap() {

        itemInfoCheck.toggle()

        if(itemInfoCheck){

            itemInfoButton.setTitleColor(UIColor(hex: 0x191919), for: .normal)

            itemReviewButton.setTitleColor(UIColor(hex: 0x999999), for: .normal)

            rectangleResetAnimation()

            itemInfotextLabel.text = itemInfoText

        }

        else {

            itemInfoButton.setTitleColor(UIColor(hex: 0x999999), for: .normal)

            itemReviewButton.setTitleColor(UIColor(hex: 0x191919), for: .normal)

            rectangleAnimation()

            itemInfotextLabel.text = ""

        }

    }

    

    @objc

    func heartButtonTap() {

        heartButton.isSelected.toggle()

        GiftAPI.shared.patchHeart { result in

            print("data response를 받았습니다")

        }

    }

    

    let originFrame = CGRect(x: 60, y: 0, width: 67, height: 3)

    let newFrame = CGRect(x: 100, y: 0, width: 67, height: 3)

    

    func rectangleAnimation() {

        itemRectangleBarView.snp.remakeConstraints {

            $0.centerX.equalTo(itemReviewButton.snp.centerX)

            $0.bottom.equalTo(itemReviewButton.snp.bottom)

            $0.width.equalTo(itemReviewButton.titleLabel!.snp.width)

            $0.height.equalTo(3)

        }

        UIView.animate(withDuration: 0.5) { [self] in

            

            self.itemRectangleBarView.superview?.layoutIfNeeded()

        }

    }

    

    func rectangleResetAnimation() {

        itemRectangleBarView.snp.remakeConstraints {

            $0.centerX.equalTo(itemInfoButton.snp.centerX)

            $0.bottom.equalTo(itemReviewButton.snp.bottom)

            $0.width.equalTo(itemInfoButton.titleLabel!.snp.width)

            $0.height.equalTo(3)

        }

        UIView.animate(withDuration: 0.5) { [self] in

            self.itemRectangleBarView.superview?.layoutIfNeeded()

        }

    }

    

    @objc

    func giftButtonTap() {

        let giftcardVC = GiftcardViewController()

        self.navigationController?.pushViewController(giftcardVC, animated: true)

    }

    

    @objc

    func backButtonDidTap() {

        self.dismiss(animated: true)

    }

}

 

// MARK: - GIftViewDataSource

 

extension GiftViewController {

    func requestGiftAPI() {

        GiftAPI.shared.getProduct { result in

            guard let result = self.validateResult(result) as? GiftproductModel else { return }

            self.productData = result

            self.dataBind(self.productData)

        }

    }

    

    func dataBind(_ productData: GiftproductModel?) {

        //productImage.kfSetImage(url: productData?.imageURL)

        guard let productData = self.productData else { return }

        productbrandLabel.text = productData.brandTitle

        productnameLabel.text = productData.productTitle

        productpriceLabel.text = String(productData.price ?? 0) + "원"

        cashbackPointLabel.text = String(productData.point ?? 0) + "원"

        expirydateNumberLabel.text = String(productData.expiration ?? 0) + "일"

        itemInfotextLabel.text = productData.productInfo

        itemInfoText = productData.productInfo

        

        let heart = productData.like ? Image.heartFilled : Image.heart

        if(heart == Image.heart){

            heartButton.isSelected = false

        }

        else {

            heartButton.isSelected = true

        }

    }

}

2. GiftcardViewController

더보기

//

//  GiftcardViewController.swift

//  Toss_iOS

//

//  Created by 신지원 on 2023/05/22.

//

 

import UIKit

 

import SnapKit

import Then

 

class GiftcardViewController: UIViewController {

    

    //MARK: - UI Components

    

    let placeHolder = "고마운 마음을 담아\n선물을 보내요"

    

    //상단 고정 영역

    private var topNavBar = UIView()

    private var backButton = UIButton()

    

    //카드 선택 영역

    private var cardLabel = UILabel()

    private var cardSelectView = UIView()

    private var ballonButton = UIButton()

    private var turtleButton = UIButton()

    private var ghostButton = UIButton()

    private var spaceshipButton = UIButton()

    private var ballonLabel = UILabel()

    private var turtleLabel = UILabel()

    private var ghostLabel = UILabel()

    private var spaceshipLabel = UILabel()

    

    //카드 작성 영역

    private var balloncardView = UIImageView()

    private var balloncardTextView = UITextView()

    private var editMessageButton = UIButton()

    private var editCompleteButton = UIButton()

    private var writeContentLabel = UILabel()

    private var cardEditState = false

    private var deleteButton = UIButton()

    

    //하단 고정 영역

    private var completeButton = UIButton(type: .custom)

    var indicator = UIActivityIndicatorView().then {

        $0.backgroundColor = UIColor(hex: 0xD478C5)

        $0.layer.cornerRadius = 14

    }

    

    

    // inputAccessoryView

    private var textViewAccessoryView = UIView()

    private var textViewEditMessageButton = UIButton()

    

    //MARK: - Life Cycle

    

    override func viewDidLoad() {

        super.viewDidLoad()

        

        addContentView()

        setStyle()

        setLayout()

        balloncardTextView.delegate = self

        self.navigationController?.navigationBar.isHidden = true

    }

    

    

    //MARK: - AddContent

    

    func addContentView() {

        view.addSubviews(topNavBar,

                         cardSelectView,

                         balloncardView,

                         editCompleteButton,

                         completeButton,

                         indicator)

        

        topNavBar.addSubview(backButton)

        

        cardSelectView.addSubviews(cardLabel,

                                   ballonButton,

                                   turtleButton,

                                   ghostButton,

                                   spaceshipButton,

                                   ballonLabel,

                                   turtleLabel,

                                   ghostLabel,

                                   spaceshipLabel)

        

        balloncardView.addSubviews(balloncardTextView,

                                   editMessageButton,

                                   writeContentLabel,

                                   deleteButton)

        

        textViewAccessoryView.addSubview(textViewEditMessageButton)

        

    }

    

    //MARK: - Style

    

    func setStyle() {

        view.backgroundColor = .tossWhite

        

        topNavBar.do {

            $0.backgroundColor = .tossWhite

            

            backButton.do {

                $0.setImage(Image.backArrow, for: .normal)

                $0.addTarget(self, action: #selector(backButtonTap), for: .touchUpInside)

            }

        }

        

        cardSelectView.do {

            $0.backgroundColor = .tossWhite

            

            cardLabel.do {

                $0.text = "카드를 골라주세요"

                $0.textColor = .tossGrey500

                $0.font = .tossTitle1

            }

            ballonButton.do {

                $0.setImage(Image.balloon, for: .normal)

            }

            ballonLabel.do {

                $0.text = "풍선"

                $0.font = .tossBody3

                $0.textColor = .tossGrey400

            }

            turtleButton.do {

                $0.setImage(Image.turtle, for: .normal)

            }

            turtleLabel.do {

                $0.text = "거북이"

                $0.font = .tossBody3

                $0.textColor = UIColor.init(hex: 0xB9BCC2)

            }

            ghostButton.do {

                $0.setImage(Image.ghost, for: .normal)

            }

            ghostLabel.do {

                $0.text = "유령"

                $0.font = .tossBody3

                $0.textColor = UIColor.init(hex: 0xB9BCC2)

            }

            spaceshipButton.do {

                $0.setImage(Image.spaceShip, for: .normal)

            }

            spaceshipLabel.do {

                $0.text = "우주선"

                $0.font = .tossBody3

                $0.textColor = UIColor.init(hex: 0xB9BCC2)

            }

        }

        

        balloncardView.do {

            $0.image = Image.giftCard

            $0.isUserInteractionEnabled = true

            $0.contentMode = .scaleAspectFill //비율 설정!

            

            balloncardTextView.do {

                $0.isScrollEnabled = false

                $0.text = "고마운 마음을 담아\n선물을 보내요"

                $0.font = .tossHeader2

                $0.textColor = .init(hex: 0xC05FD0)

                $0.textAlignment = .center

                $0.backgroundColor = UIColor.clear

                $0.isEditable = true

                $0.textContainerInset = UIEdgeInsets(top: 20, left: 18, bottom: 18, right: 18)

                $0.inputAccessoryView = textViewAccessoryView

            }

            editMessageButton.do {

                $0.setTitle("메시지 수정", for: .normal)

                $0.titleLabel?.font = .tossBody3

                $0.backgroundColor = UIColor.init(hex: 0x8D94A0).withAlphaComponent(0.15)

                $0.makeCornerRound(radius: 19)

                $0.setImage(Image.edit, for: .normal)

                $0.setTitleColor(.tossGrey500, for: .normal)

                $0.imageView?.contentMode = .scaleAspectFit

                $0.contentHorizontalAlignment = .center

                $0.semanticContentAttribute = .forceLeftToRight

                $0.addTarget(self, action: #selector(editMessageButtonTap), for: .touchUpInside)

            }

            editCompleteButton.do {

                $0.setTitle("수정 완료", for: .normal)

                $0.titleLabel?.font = .tossBody2

                $0.setTitleColor(.tossWhite, for: .normal)

                $0.makeCornerRound(radius: 20)

                $0.backgroundColor = .tossBlue

                $0.addTarget(self, action: #selector(completeButtonTapped), for: .touchUpInside)

            }

            writeContentLabel.do {

                $0.isHidden = true

                $0.textColor = .tossGrey400

                $0.font = .tossBody3

            }

            deleteButton.do {

                $0.isHidden = true

                $0.setImage(Image.delete, for: .normal)

                $0.addTarget(self, action: #selector(clean), for: .touchUpInside)

            }

        }

        

        textViewAccessoryView.do {

            $0.frame = .init(x: 0, y: 0, width: 400, height: 67)

            $0.backgroundColor = .clear

            

            

            textViewEditMessageButton.do {

                $0.setTitle("수정 완료", for: .normal)

                $0.titleLabel?.font = .tossBody2

                $0.setTitleColor(.tossWhite, for: .normal)

                $0.makeCornerRound(radius: 20)

                $0.backgroundColor = .tossBlue

                $0.addTarget(self,

                             action: #selector(completeButtonTap),

                             for: .touchUpInside)

            }

        }

        

        completeButton.do {

            $0.setTitle("완료", for: .normal)

            $0.titleLabel?.font = .spoqaHanSanNeo(.bold, size: 18)

            $0.setTitleColor(.tossWhite, for: .normal)

            $0.backgroundColor = UIColor(hex: 0xD478C5)

            $0.layer.cornerRadius = 14

            $0.addTarget(self, action: #selector(completeButtonTap), for: .touchUpInside)

        }

        

    }

    

    // MARK: - Layout

    

    func setLayout() {

        topNavBar.snp.makeConstraints {

            $0.top.equalTo(view.safeAreaLayoutGuide)

            $0.width.equalToSuperview()

            $0.height.equalTo(42)

            

            backButton.snp.makeConstraints {

                $0.centerY.equalToSuperview()

                $0.leading.equalToSuperview().inset(16)

                $0.size.equalTo(24)

            }

        }

        

        cardSelectView.snp.makeConstraints {

            $0.top.equalToSuperview().inset(97)

            $0.centerX.equalToSuperview()

            $0.height.equalTo(127)

            $0.width.equalTo(291)

            

            cardLabel.snp.makeConstraints {

                $0.top.equalToSuperview()

                $0.height.equalTo(22)

                $0.centerX.equalToSuperview()

            }

            ballonButton.snp.makeConstraints {

                $0.top.equalTo(cardLabel.snp.bottom).offset(20)

                $0.leading.equalToSuperview()

                $0.size.equalTo(60)

            }

            ballonLabel.snp.makeConstraints {

                $0.top.equalTo(ballonButton.snp.bottom).offset(10)

                $0.centerX.equalTo(ballonButton.snp.centerX)

            }

            turtleButton.snp.makeConstraints {

                $0.top.equalTo(ballonButton.snp.top)

                $0.leading.equalTo(ballonButton.snp.trailing).offset(17)

                $0.size.equalTo(60)

            }

            turtleLabel.snp.makeConstraints {

                $0.top.equalTo(turtleButton.snp.bottom).offset(10)

                $0.centerX.equalTo(turtleButton.snp.centerX)

            }

            ghostButton.snp.makeConstraints {

                $0.top.equalTo(ballonButton.snp.top)

                $0.trailing.equalTo(spaceshipButton.snp.leading).offset(-17)

                $0.size.equalTo(60)

            }

            ghostLabel.snp.makeConstraints {

                $0.top.equalTo(ghostButton.snp.bottom).offset(10)

                $0.centerX.equalTo(ghostButton.snp.centerX)

            }

            spaceshipButton.snp.makeConstraints {

                $0.top.equalTo(ballonButton.snp.top)

                $0.trailing.equalToSuperview()

                $0.size.equalTo(60)

            }

            spaceshipLabel.snp.makeConstraints {

                $0.top.equalTo(spaceshipButton.snp.bottom).offset(10)

                $0.centerX.equalTo(spaceshipButton.snp.centerX)

            }

        }

        

        balloncardView.snp.makeConstraints {

            $0.top.equalTo(cardSelectView.snp.bottom).offset(21)

            $0.bottom.equalTo(completeButton.snp.top).offset(-30)

            $0.leading.trailing.equalToSuperview().inset(22)

            

            balloncardTextView.snp.makeConstraints {

                $0.top.equalToSuperview().inset(26)

                $0.leading.trailing.equalToSuperview().inset(31)

                $0.height.greaterThanOrEqualTo(100)

            }

            editMessageButton.snp.makeConstraints {

                $0.top.equalTo(balloncardTextView.snp.bottom).offset(12)

                $0.centerX.equalToSuperview()

                $0.width.equalTo(117)

                $0.height.equalTo(38)

            }

            writeContentLabel.snp.makeConstraints {

                $0.top.equalTo(balloncardTextView.snp.bottom).offset(20)

                $0.centerX.equalToSuperview()

            }

            deleteButton.snp.makeConstraints {

                $0.leading.equalTo(writeContentLabel.snp.trailing)

                $0.centerY.equalTo(writeContentLabel.snp.centerY)

            }

        }

        

        textViewEditMessageButton.snp.makeConstraints {

            $0.center.equalToSuperview()

            $0.width.equalTo(132)

            $0.height.equalTo(40)

        }

        

        editCompleteButton.snp.makeConstraints {

            $0.centerX.equalToSuperview()

            $0.width.equalTo(132)

            $0.height.equalTo(40)

            $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(5)

        }

        

        completeButton.snp.makeConstraints {

            $0.bottom.equalToSuperview().inset(35)

            $0.centerX.equalToSuperview()

            $0.width.equalTo(335)

            $0.height.equalTo(54)

        }

        indicator.snp.makeConstraints {

            $0.bottom.equalToSuperview().inset(35)

            $0.centerX.equalToSuperview()

            $0.width.equalTo(335)

            $0.height.equalTo(54)

        }

    }

}

 

// MARK: - Actions

 

extension GiftcardViewController {

    

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

        balloncardTextView.resignFirstResponder()

        if(balloncardTextView.text == placeHolder){

            writeContentLabel.isHidden = true

            deleteButton.isHidden = true

        }

    }

    

    @objc

    private func editMessageButtonTap() {

        if(balloncardTextView.text == "고마운 마음을 담아\n선물을 보내요") {

            balloncardTextView.text = ""

        }

        balloncardTextView.becomeFirstResponder()

        editMessageButton.isHidden = true

    }

    

    @objc

    private func completeButtonTap() {

        let text = balloncardTextView.text

        print("수정 완료 버튼이 눌렸습니다.")

        balloncardTextView.resignFirstResponder()

        completeButton.isHidden = false

        writeContentLabel.isHidden = true

        deleteButton.isHidden = true

        editMessageButton.isHidden = false

    }

    

    @objc

    private func clean() {

        balloncardTextView.text = ""

        writeContentLabel.text = "0자/86자"

        writeContentLabel.isHidden = true

        deleteButton.isHidden = true

        

    }

    

    @objc

    func backButtonTap() {

        self.navigationController?.popViewController(animated: true)

    }

    

    @objc

    func completeButtonTapped() {

        indicator.startAnimating()

        

        // MARK: - GIftViewDataSource

        

        GiftManager().postGift(self, productID: 1) { statusCode in

            DispatchQueue.main.asyncAfter(deadline: .now() + 2 , execute: {

                self.indicator.stopAnimating()

                let toastMessageViewController = ToastMessageViewController()

                toastMessageViewController.modalPresentationStyle = .overFullScreen

                switch statusCode {

                case 200:

                    print("성공")

                    UIView.animate(withDuration: 1, delay: 0, options: .curveEaseOut, animations: {

                        self.present(toastMessageViewController, animated: false)

                    }) { (completed) in

                        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, execute: {

                            UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseOut) {

                                toastMessageViewController.dismiss(animated: false)

                                self.navigationController?.popViewController(animated: true)

                            }

                        })

                    }

                case 400:

                    print("유효하지 않은 입력")

                case 404:

                    print("존재하지 않는 상품")

                case 500:

                    print("서버 오류")

                default:

                    print(statusCode, "????/")

                }

            })

        }

    }

}

 

// MARK: - UITextViewDelegate

 

extension GiftcardViewController: UITextViewDelegate {

    

    func textViewDidBeginEditing(_ textView: UITextView) {

        

        

        if placeHolder == textView.text {

            balloncardTextView.text = ""

        }

        

        completeButton.isHidden = true

        editMessageButton.isHidden = true

        writeContentLabel.isHidden = false

        deleteButton.isHidden = false

    }

    

    func textViewDidChange(_ textView: UITextView) {

        deleteButton.isHidden = false

        writeContentLabel.isHidden = false

    }

    

    func textViewDidEndEditing(_ textView: UITextView) {

        editMessageButton.isHidden = textView.hasText

        if !textView.hasText {

            textView.text = placeHolder

        }

        if(!editMessageButton.isHidden){

            writeContentLabel.isHidden = true

            deleteButton.isHidden = true

        }

        else {

            writeContentLabel.isHidden = false

            deleteButton.isHidden = false

        }

    }

    

    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {

        let currentText = textView.text ?? ""

        guard let stringRange = Range(range, in: currentText) else  { return false }

        let changedText = currentText.replacingCharacters(in: stringRange, with: text)

        writeContentLabel.text = "\(changedText.count)자/86자"

        

        if(changedText.count <= 85) {

            if(changedText.count > 21) {

                balloncardTextView.font = .tossTitle2

                balloncardTextView.textContainerInset = UIEdgeInsets(top: 20, left: 18, bottom: 18, right: 18)

            }

            else {

                balloncardTextView.font = .tossHeader2

                balloncardTextView.textContainerInset = UIEdgeInsets(top: 20, left: 18, bottom: 18, right: 18)

            }

        }

        else {

            return false

        }

        

        return true

    }

}

 

 

 

 

 

- 마지막 후기

  리팩토링까지 마쳤다, 나 아무래도 많이 는것 같다. 그치,,,? 나름 코드 짜면서 재밌다고도 느꼈고 더 깔끔하고, 더 예쁘게 짜고 싶다는 욕심도 생겼다. 영광이다. 컴퓨터공학과 들어와서 처음으로 재밌게 한 코드 작성이다. 정말 많은 사람들의 도움을 받았고, 나중엔 누군가가 나에게 물어볼 수 있을만한 실력을 갖추고 싶다. 왜냐면 아직 아무도 나에게 도움을 요청하지 않는다 ㅎㅎ,,

 

시상식은 아니지만 신지원의 첫 프로젝트인 만큼 상 받은 기분과 다를게 없으니 고마운 사람들 이름이나 남기겠다. 실명 거론해서 미안하지만 이정도로 많이 고맙다는 뜻이다!!!

 

먼저 우리 팀 희재와 창희오빠가 정말 많이 도와주고 격려해주었다. 그리고 코드리뷰 해준 한솔 오빠도 너무 고맙고 엉엉, 완료 버튼 구현해준 장석우에게 고맙고, 날 잡고 아예 1대1 과외까지 해준 규보오빠에게도 너무 고맙다고 하고 싶다. 난 받기만해서 어떡하지, 나도 꼭 열심히 하여 도움이 될 것이다. 화이팅!