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 과외까지 해준 규보오빠에게도 너무 고맙다고 하고 싶다. 난 받기만해서 어떡하지, 나도 꼭 열심히 하여 도움이 될 것이다. 화이팅!
'32YB SOPT > iOS 세미나' 카테고리의 다른 글
[SOPT] iOS_7주차_정규세미나 (Realm을 사용한 LocalDB) (0) | 2023.08.21 |
---|---|
[SOPT] iOS_1주차_정규세미나 (Xcode사용법, iOS기초, 화면 전환, 데이터 전달 기초) (0) | 2023.06.27 |
[SOPT] iOS_2주차_과제 (0) | 2023.04.21 |
[SOPT] iOS_2주차_정규세미나 (데이터 전달 심화, SnapKit을 통한 AutoLayout, InjectIII, Then) (0) | 2023.04.08 |
[SOPT] iOS_1주차_과제 (final/private/lazy var/let/viewController/forEach/$0) (0) | 2023.04.07 |