Danny의 iOS 컨닝페이퍼
article thumbnail

[TIL #4] 22 / 03 / 21

오늘은 [Swift/TIL #3] 탭바 아이템을 버튼으로 교체 with CGAffineTransform(회전) 에서 만든,

중간 add 버튼을 클릭하면, 팝 버튼이 생성되는 애니메이션을 만들어보려 합니다.

 

일단 아직 고쳐야 할 게 있지만, 중간 결과물을 먼저 봅시다.

 

오늘의 작업

1. Pop 버튼 생성 준비

2. Pop 버튼 생성 및 위치 설정

3. 애니메이션 및 버튼 처리

 

고쳐야 할 일

현재 만들고자 하는 건 원이 0에서 점점 커지는 애니메이션을 만들고 싶은데,

버튼의 원이 먼저 생성되고 스케일이 커지는 문제점이 있네요. (수정 완료)

 

 

Pop 버튼 생성 준비

일단 버튼에 사용할 데이터들을 구조체로 만들어 줬습니다.

// 탭바 pop 버튼에 사용할 데이터
struct PopButtonOption {
    var name: String
    var image: UIImage
}

 

그리고 사용하기 편하게, 옵션 데이터(이미지, 이름)를 다시 구조체로 묶어 줬어요.

그리고 생성자를 이용해서 사용할 버튼 옵션을 설정해 줬어요.

 

프로퍼티 buttons은 나중에 버튼의 애니메이션을 한 번에 관리하려고 만들어 줬습니다.

struct PopButtons {
    var buttons: [UIButton]
    var options: [PopButtonOption]
    
    init() {
        self.buttons = []
        self.options = [
            PopButtonOption(name: "Trash", image: UIImage(systemName: "trash")!),
            PopButtonOption(name: "Add", image: UIImage(systemName: "plus")!),
            PopButtonOption(name: "Edit", image: UIImage(systemName: "eraser")!)
            ]
    }
}

 

이제 버튼을 만드는 메서드를 생성해 봅시다.

 

일단 size를 받아 버튼을 만드는 메서드를 만들어 봅시다.

기본적인 버튼을 생성해 주고, 버튼 클릭하면 동작하는 애니메이션 구현해 줍시다.

(여기선 버튼 클릭 시 0에서 설정한 size만큼 점점 원이 커지는 애니메이션을 만들어 주었습니다.)

// pop 버튼 생성
class func createPopButton(size: CGFloat ,isTapped: Bool) -> UIButton {
    
    let button = UIButton(type: .custom)
    
    button.backgroundColor = HexCode.selected.color
    
    // 오토 레이아웃 설정
    button.snp.makeConstraints {
        $0.width.height.equalTo(size)
    }

    button.layer.cornerRadius = size / 2
    
    // ⭐️ 처음 값을 0으로 해줘야 점점 커지는 애니메이션이 가능
    button.transform = CGAffineTransform(scaleX: 0, y: 0)

    // 조건문: 버튼을 클릭 시 (애니메이션 동작)
    if isTapped {
        
        UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut) {
            // animations: 애니메이션 시작
            button.tintColor = .clear
            button.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
        } completion: { _ in
            // completion: 완료 된 후 동작을 정의 (마지막 동작)
            button.tintColor = HexCode.unselected.color
            button.transform = CGAffineTransform.identity  // identity 원래의 값으로 초기화
        }
    }
    return button
}

그리고 여기서 중요한 부분이, CGAffineTransform(scaleX: 0, y: 0)

 기본(처음) 크기를 0으로 만들어 줘야 합니다.

이렇게 설정해야 점점 커지는 애니메이션을 구현할 수 있습니다.

 

처음엔 button.bounds.size = CGSize(width: 0, height: 0) 처리해 주면 될 줄 알았지만,

이유는 모르겠지만, 값이 적용이 안되더라고요.

(여기서 시간 소비를...)

 

그리고 Pop 버튼은 타입 메서드(class func)로 만들어 주었습니다.

참고로 class로 만든 타입 메서드는 오버라이딩이 된다는 특징이 있습니다.

현재 하고 있는 프로젝트에서는

UI관련 생성은 모두 여기(싱글톤 UIFactory)에 몰아넣을 예정이에요.

 

 

Pop 버튼 생성 및 위치 설정

위에서 간편히 사용할 수 있게 버튼을 만들어 뒀습니다.

// 탭바에 사용할 수 있도록 프로퍼티로 생성해 줍시다.
var popButtons = PopButtons()

 

이제 만든 버튼을 갖고, 어떤 위치에 버튼을 생성할지 레이아웃을 잡아봅시다.

그림을 보면 버튼을 3개 만들어 줘야 되겠죠?

 

그전에 알아야 할 게 있는데, 지루한 수학입니다.

조금만 공부하고 가봅시다.

 

저는 중간 버튼의 중심으로 원의 반경을 따라 Pop버튼을 배치해주고 싶습니다.

 

원을 따라 배치시키려면, 일단 해당 위치의 x, y의 값이 필요하겠죠?

 

위의 그림의 나타내보면, 이와 같이 나타납니다.

 

다들 sin, cos은 알고 계실 테니 간단하게 공식만 보여드리겠습니다.

 

이렇게 표현되겠죠?

cos(a) = x / r

sin(a) = y / r

 

우리가 필요한 건 x, y 이므로, 이렇게 정리하고

x = cos(a) * r

y = sin(a) * r

 

마지막으로 angle(°)를 구하는 방법만 알면

x, y 값을 사용할 수 있게 됩니다.

1° = (π / 180) rad

a° = a * (π / 180) rad

 

이렇게  x, y 위치를 알 수 있게 됐습니다.

x = cos(a * π / 180) * r

y = sin(a * π / 180) * r

 

 

드디어 Pop 버튼을 생성 및 레이아웃을 잡아봅시다.

 

일단 45° 마다 배치를 시켜야 하므로, 각도를 정해주고 Pop 버튼을 생성해 줬습니다.

 // pop 버튼 셋팅
    func setupPopButton(count: Int, radius: CGFloat) {
        // 45° 마다 배치
        let degrees: CGFloat = 45
        
        for i in 0 ..< count {
            
            let button = UIFactory.createPopButton(size: 38, isTapped: self.buttonTapped)

            self.view.addSubview(button)
            self.popButtons.buttons.append(button)

            // 버튼 태그
            button.tag = i
            
            // 1° = (π / 180) rad
            // x = cos(a) * r = cos(각도) * 반지름
            // y = sin(a) * r = sin(각도) * 반지름
            let x = cos(degrees * CGFloat(i+1) * .pi/180) * radius
            let y = sin(degrees * CGFloat(i+1) * .pi/180) * radius
            
            button.snp.makeConstraints {
                $0.centerX.equalTo(self.tabBar).offset(-x)
                // top으로 중간 버튼 레이아웃을 잡았으므로 여기서도 top으로 설정
                $0.top.equalTo(self.tabBar).offset(-y)
            }
            
            button.setImage(popButtons.options[i].image, for: .normal)
            
            // pop 버튼이 가장 앞쪽으로 위치하게 만듬
            self.view.bringSubviewToFront(button)
            
            button.addTarget(self, action: #selector(popButtonHandler), for: .touchUpInside)
        }
    }
    
    @objc func popButtonHandler(sender: UIButton) {
        
        switch sender.tag {
        case 0:
            print("Trash")
        case 1:
            print("Add")
        default:
            print("Edit")
        }
    }

 

레이아웃 및 설정은 끝났고, 중간 버튼을 클릭 시 동작하게 넣어줍시다.

[Swift/TIL #3] 탭바 아이템을 버튼으로 교체 with CGAffineTransform(회전) 에서 buttonHandler의 Todo를 확인해 보세요.

let popButtonCount = self.popButtons.options.count
// 원하는 반지름으로 설정
self.setupPopButton(count: popButtonCount, radius: 72)

 

마찬가지로 중간 버튼 동작이 끝날 때도 Pop 버튼이 사라지는 애니메이션을 만들어 줍시다.

func removePopButton() {
    
    let popButtons = self.popButtons.buttons
    
    for button in popButtons {
        
        UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
            button.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
        } completion: { _ in
            // 버튼 제거
            button.removeFromSuperview()
        }
    }
}

 

 

마무리

완성

 

이렇게 오늘까지 커스텀 탭바 구현을 모두 마쳤습니다.

 

탭바 하나 구현하는데 정말 많은 시간과 코드가 들어갔네요.

생각대로 코드를 만든다고 해도, 원하는 애니메이션 구현하려면 여러 번 작업을 해봐야 되더라고요.

 

코어 그래픽, 레이어, 아핀 배열, 애니메이션, frame & bounds 등등

기존에 사용은 해봤지만, 어떤 식으로 동작하고 구현하는지 자세히는 몰랐지만,

커스텀 탭바를 만들어보면서 많이 배우게 됐네요.

 

다음 구현할 때는 시간이 절반으로 줄기를 빌며...

 

그리고 코드를 읽다가 틀린 내용이나 수정할 부분이 있으면, 지적 감사히 받겠습니다.

 

 

참고

https://betterprogramming.pub/design-dribbble-like-floating-buttons

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!