Danny의 iOS 컨닝페이퍼
article thumbnail
반응형

[TIL #3] 22 / 03 / 19

[Swift/TIL #2] CAShapeLayer 이용한 탭바 구현 에 이어서

오늘도 역시나 탭바 작업을 할 예정입니다.

 

오늘 할 작업을 정리해 보자면

1. 탭바 아이템을 버튼으로 교체

2. 버튼에 회전 애니메이션 효과를 추가

 

 

문제

탭바의 아이템 특성상 클릭하게 되면 설정해 준 ViewController를 실행하게 됩니다.

 

저는 단순히 버튼 기능만을 원하기 때문에

오늘은 탭바 아이템 대신에 버튼으로 대체하는 작업을 해보려고 합니다.

 

한번 탭바 아이템 대신 버튼으로 대체해 봅시다.

 

완성 미리 보기

 

 

버튼 생성

먼저 탭바 가운데 아이템 대신 사용할 버튼을 만들어 줍시다.

 

이미지를 생성할 때 Configuration을 사용하면 기호의 크기, 색상, 스타일 등을 설정해 줄 수 있죠.

 

SymbolConfiguration

오직 SF Symbol의 이미지만 사용가능합니다. 이미지 내부 기호의 크기, 폰트, 배율 등 여러 설정을 할 수 있죠.

간편히 설정할 수 있게 여러 파라미터가 존재합니다.

 

SymbolConfiguration 통하여 할 수 있는 설정은 이와 같습니다.

 

point size :  기호의 크기를 설정

weight : 기호의 굵기를 설정

scale : 기호의 배율을 설정

font : 기호의 글꼴을 설정

textStyle : 기호의 글꼴의 TextStyle의 설정

 

 

버틀 클릭 시 회색으로 강조되는 거 막기

저는 iOS 15 이하를 사용하여 adjustsImageWhenHighlighted를 사용할 수 있었지만,

만약 iOS 15 이상 사용하게 되면 adjustsImageWhenHighlighted는 더 이상 지원하지 않더라고요.

 

대신, UIButton.ConfigurationUpdateHandler를 통해 isHighlighted를 변경을 해줘야 합니다.

이건 다음에 기회가 되면 UIButton의 Configuration에 대하여 다뤄보겠습니다.

 

 

이렇게 버튼 생성 완료

// 버튼 생성
let middleButton: UIButton = {
    let button = UIButton()
    // 현재 심볼 이미지를 변형(size, font 등)
    let configuation = UIImage.SymbolConfiguration(pointSize: 18,
                                                   weight: .heavy,
                                                   scale: .large)
    
    button.setImage(UIImage(systemName: "plus", withConfiguration: configuation),
                    for: .normal)
    // 강조 취소                
    button.adjustsImageWhenHighlighted = false
    
    // 버튼 색상
    button.tintColor = HexCode.unselected.color
    button.backgroundColor = HexCode.selected.color
    return button
}()

 

 

버튼 세팅

여기선 탭바 중간 아이템을 비활성화시킨 후, 모양을 잡기 위해 기본적인 설정을 해주었습니다.

// add 버튼 세팅
private func setupMiddleButton() {
    
    // 네이게이션 아이템 대신 버튼을 사용할 것이므로, 비활성화 시켜 클릭을 방지.
    DispatchQueue.main.async {
        if let items = self.tabBar.items {
            items[1].isEnabled = false
        }
    }
    
    self.tabBar.addSubview(middleButton)
    
    let size: CGFloat = 55
    let y: CGFloat = 30 - 11 - (size/2)   // (layer 높이/2 - layer에서 추가된 높이 - 버튼 사이즈/2)
 
    
    // layout 설정
    middleButton.snp.makeConstraints {
        $0.centerX.equalToSuperview()
        $0.top.equalToSuperview().offset(y)
        $0.width.height.equalTo(size)
    }
    
    // 83 차이
    
    
    // 버튼 모양 설정
    middleButton.layer.cornerRadius = size / 2
    
    // 버튼 그림자 설정
    middleButton.layer.shadowColor = HexCode.selected.color.cgColor
    middleButton.layer.shadowOffset = CGSize(width: 0, height: 1)
    middleButton.layer.shadowOpacity = 0.9
    middleButton.layer.shadowRadius = 5
    
    // 버튼 동작 생성
    middleButton.addTarget(self, action: #selector(middleButtonHandler), for: .touchUpInside)
}

 

viewDidLoad에서 setupMiddleButton 메서드를 넣어 줍시다.

override func viewDidLoad() {
    super.viewDidLoad()
    
    setupMiddleButton()
}

 

 

튼의 Layout 잡으면서...

레이아웃을 탭바(center)에 버튼(center)으로 맞추면 엉뚱한 곳에 위치하게 되더라고요?

 

생각해 보니까, layer만 만들어줬지 직접적으로 탭바의 크기는 건드린 적이 없던 것 같아요.

탭바의 모양을 확인을 해봤더니 역시나 이렇게 돼있었네요.

탭바 영역 (빨간색)

그래서 일단 계산을 나름 쉽게 하기 위해 top부분에 맞춰줬습니다.

 

일단 위의 코드처럼 버튼의 위치를 잡아주고,

나중에 탭바를 따로 만들어 줄 때 한 번 더 정리해줘야 할 것 같습니다. 뭔가 계산이 엄청 꼬였네요...

 

아무튼 정확히 중간은 맞았습니다.

 

팁: 객체들이 그려지는 건 origin기준 "+" 방향으로 그려집니다.

(즉, x축은 오른쪽으로 y축은 아래방향으로 그려집니다.)

 

 

 

버튼 동작

버튼을 만들어 줬으니 이번에는 동작을 설정해야겠죠?

 

버튼을 누르면 "+"가 "x"로 변하는 동작을 만들어 볼까 합니다.

 

회전을 시키기 위해선 CGAffineTransform가 사용이 되는데요.

일단 CGAffineTransform에 대해 간단히 알아봅시다.

 

 

CGAffineTransform

2D 공간에서 객체를 이동, 회전, 크기를 변환할 수 있습니다.

또한, 초기 모양을 CGAffineTransform으로 만들 수 없으며, 변환 목적으로만 사용합니다.

 

일단 CGAffineTransform의 변환에 대한 설정 값은,

 이와 같이 3 x 3 배열로 돼있습니다.

기존의 위치 값 * 생성된 배열 값

이런 식으로 계산이 돼서 위치 및 크기가 변한다고 하는데...

 

자세한 내용은 공식 문서 를 한번 참고해 보세요.

저는 배열의 곱도 가물가물하네요...

 

 

그래서 오늘은 간단하게 사용법만 설명드리겠습니다.

 

CGAffineTransform의 기본 생성자는 4가지 있습니다.

// 회전
CGAffineTransform(rotationAngle: CGFloat)
// 배율
CGAffineTransform(scaleX: CGFloat, y: CGFloat)
// 위치 이동
CGAffineTransform(translationX: CGFloat, y: CGFloat)

// 한번에 조절 (사용하기 까다롭다. 아니 사용하지 말자...)
CGAffineTransform(a: CGFloat, b: CGFloat, c: CGFloat, d: CGFloat, tx: CGFloat, ty: CGFloat)
// a: x축 방향의 scale 
// b: y축 회전 및 기울기? 
// c: x축 회전 및 기울기?
// d: y축 방향의 scale
// tx: x축으로 이동
// ty: y축으로 이동

// 만약 b, c를 만지게 된다면 a, b의 값도 수정을 해줘야 되네요...
// 현재 위치 * 변환할 값(3x3 배열)으로 값이 정해져서 그렇다는데 이해가...
// x' = ax + cy + tx
// y' = bx + dy + ty

 

간단한 예제를 만들어 봅시다.

 

뷰를 생성하고, 생성된 뷰의 모양을 변환시켜 보겠습니다.

let customView: UIView = {
    let view = UIView(frame: CGRect(x: 0, y: 200, width: 100, height: 100))
    view.backgroundColor = .red
    return view
}()

 

아주 간단합니다.

변환하고 싶은 값으로 CGAffineTransform의 객체를 만들어준 뒤,

사용하는 뷰의 객체의 transform에 넣어주면 됩니다.

// x, y 축으로 기존의 2배로 배율을 증가시킵니다.
self.customView.transform = CGAffineTransform(scaleX: 2, y: 2)
// x 축으로 100만큼, y 축으로 200만큼 이동합니다.
self.customView.transform = CGAffineTransform(translationX: 100, y: 200)
// 45° 만큼 회전을 시킵니다.  (π = 180°)
self.customView.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 4)

 

이와 같이, 메서드들도 존재하네요.

// 회전
func rotated(by: CGFloat) -> CGAffineTransform
// 배율
func scaledBy(x: CGFloat, y: CGFloat) -> CGAffineTransform
// 위치 이동
func translatedBy(x: CGFloat, y: CGFloat) -> CGAffineTransform
// 기존 배열을 반전시켜 리턴
func inverted() -> CGAffineTransform

 

위의 메서드를 보시면 생성자와 같은 기능(회전, 배율, 위치)을 하고 있습니다.

 

"왜 같은 기능을 만들었을까?"라고 생각할 수 있지만,

신기하게도, 이 메서드는 Dot(.)으로 연결해서 변환을 이어나갈 수 있습니다.

 

한번 위의 코드를 합쳐 사용해 봅시다.

(이 방법은 앞의 값이, 뒤의 값에도 영향을 주는 것 같네요.)

// x, y 축으로 기존의 2배로 배율을 증가시킨 후
// x축으로 200, y축으로 400 이동하고 (스케일을 2배로 키워서 그런가 이동도 2배가 커지네요)
// 마지막으로 회전을 45° 시킵니다.
let transform = CGAffineTransform(scaleX: 2, y: 2).translatedBy(x: 100, y: 200).rotated(by: CGFloat.pi/4)
 
self.customView.transform = transform

 

또한 위에 생성자가 한 개가 더 남아있었죠?

 

 CGAffineTransform(a:b:c:d:tx:ty:) 생성자를 이용하여,

위와 같은 작업을 구현할 수 있습니다.

 

아래와 같이 생성하면, 바로 위의 값과 동일해집니다.

하지만 뭔가 복잡하네요.

 

이건 이해가 안 돼요 ㅜ.ㅜ 시간낭비입니다!

scale, traslation, rotateAngle과 같이 만들어져 있는 생성자를 사용합시다!!!

let rotationAngle = CGFloat.pi/4
let sinValue = sin(rotationAngle)
let cosValue = cos(rotationAngle)

let transform = CGAffineTransform(a: cosValue * 2,
                                  b: sinValue * 2,
                                  c: -sinValue * 2,
                                  d: cosValue * 2,
                                  tx: 200,
                                  ty: 400)
                                   
self.customView.transform = transform

 

마지막으로 처음 위치로 만드는 방법을 소개하겠습니다.

CGAffineTransform.identity 사용하면 원래 위치로 값을 초기화할 수도 있습니다.

// 원래 위치로 초기화
self.customView.transform = CGAffineTransform.identity

 

이게 내용의 전부이긴 한데...

나중에 그림과 같이 정리해서 다시 올리겠습니다.

 

 

버튼 동작을 설정

다시 돌아와 버튼의 동작을 설정해 보겠습니다.

 

버튼을 클릭할 때 동작은 45° 회전, 취소 시 원점으로 복귀시키기 위해

전역변수로 플래그를 만들어 줬습니다. (Bool 값 사용)

var buttonTapped = false  // 버튼 클릭 시 Bool로 동작을 제어하기 위해 플래그를 박아둠

 

조건문으로 클릭 또는 취소할 때, 서로 다른 동작을 하도록 만들어 줬습니다.

바로 위에 배운 CGAffineTransform으로 회전을 시켜 봅시다.

@objc func buttonHandler(sender: UIButton) {

    if buttonTapped == false {
        
        UIView.animate(withDuration: 0.3) {
            // pi = 180°, (회전을 시켜준다.)
            let transform = CGAffineTransform(rotationAngle: CGFloat.pi / 4)
            self.middleButton.transform = transform
            // 버튼 색상 설정
            self.middleButton.tintColor = HexCode.selected.color
            self.middleButton.backgroundColor = HexCode.unselected.color
            
            // 버튼 테두리 설정
            self.middleButton.layer.borderWidth = 4
            self.middleButton.layer.borderColor = HexCode.selected.color.cgColor
            
            self.buttonTapped = true            
        }

        // TODO: - 버튼 클릭 시 할 작업을 추가해야 함
        
    } else {
        
        UIView.animate(withDuration: 0.3) {
            self.middleButton.transform = CGAffineTransform.identity
            // 버튼 색상 설정
            self.middleButton.tintColor = HexCode.unselected.color
            self.middleButton.backgroundColor = HexCode.selected.color
            
            // 버튼 테두리 설정
            self.middleButton.layer.borderWidth = 0
            
            self.buttonTapped = false
        }
        
        // TODO: - 버튼 취소 시 할 작업을 추가해야 함

    }
}

 

완성

 

 

마무리

다른 레이아웃은 쉽게 잡겠는데.... 탭바 레이아웃 잡는 건 쉽지 않네요...

(간단히 만드는 방법을 알고 계시면 공유 감사합니다... 꾸벅)

 

또한, 회전 시 CGAffineTransform(a, b, c, d, tx, ty)를 사용도 해봤지만, 

사용하기도 복잡해서, 조금 더 직관적인 rotate 생성자로 수정했습니다.

될 수 있으면 직관적인 생성자 또는 함수를 사용합시다!

 

다음 내용은 [Swift/TIL #4] Pop 버튼 애니메이션 (원 반경에 따라 버튼 생성 해보자) 입니다.

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

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