Danny의 iOS 컨닝페이퍼
article thumbnail

[TIL #2] 23 / 03 / 17 ~ 23 / 03 / 18

앞서 배운 [Swift/TIL #1] CAShapeLayer, UIBezierPath 내용을 토대로 탭바를 구현해 봅시다.

 

역시 글을 쓸 땐 중간 정렬이 보기가 좋아요.

(나만 그런가...)

 

 

Frame & Bounds

들어가기 전에 Frame과 Bounds의 차이를 먼저 알아봅시다.

위치 및 크기의 차이가 있는데요.

한번 살펴봅시다.

 

그림으로 된 설명은 Zedd, 소들이 님이 잘해주셨어요.

 

🎯 Frame 

객체의 Frame 위치는 SuperView의 좌표를 기준으로 상대적인 좌표를 나타낸 것입니다.

특징으로는 Frame을 변경 시 객체의 위치만 바뀌고, 하위 뷰(SubView)는 영향을 받지 않습니다.

 

다시 말해 객체는 SuperView의 좌표를 기준으로 움직입니다.


🎯 Bounds 

객체 내부에서 자신만의 좌표계(VIewPort)를 사용해 나타냅니다.

크기(size)는 같고 처음 위치(origin)는 VIewPort의 원점(0, 0)에 위치합니다.

특징으로는 Bounds로 변경 시 객체 내 모든 하위뷰(SubView)가 영향을 받습니다.

 

Bounds가 엄청 헷갈리는데, 일단 객체는 무조건 SuperView와는 아무 관계없고 고정돼 있습니다.

그래서 위치를 바꾸더라고 항상 같은 위치, 크기로 존재하게 되죠.

 

다만, 여기서 영향을 받게 되는 건 SubView입니다.

객체 내부의 좌표계(VIewPort)를 사용하기 때문에 SubView가 이동하는 것처럼 보이죠.

 

 

⭐️ Bounds로 위치(origin) 변경 시

이걸 이해하려면, 먼저 객체는 SuperView 위에 고정돼 있다는 걸 기억해야 합니다!

(그래서 보이는 객체의 크기와 위치는 변하지 않죠)

 

객체를 Bounds로 변경하게 되면, 자동으로 View를 다시 그려주게 되는데,

일단 객체는 SuperView 위에서 고정이 돼있으므로, 객체는 가만히 있는 것처럼 보일 테고

내부 좌표를 이동하게 되면 내부의 SubView 위치만 이동한 것처럼 보이게 될 겁니다.

 

여기서 꼭 알고 가야 하는 개념인 ViewPort란 것이 있습니다.

ViewPort란? 현재 화면에서 보이고 있는 직사각형 영역을 말합니다. (출력 영역)

 

간단히 말해, bounds의 위치(origin) 이동은 ViewPort의 좌표가 이동하는 것과 같은 의미입니다.

bounds 좌표 ==  ViewPort 좌표

내부의 좌표계 == ViewPort 좌표

 

즉, bounds가 좌표가 변하게 되면, 보여지는 영역(ViewPort)이 움직이게 됩니다.

예를들어, bounds의 origin이 (100, 0)만큼 이동했을 때, 보여지는 영역이 이동하게 되므로

상대적으로 객체 내에 있는 SubView는 반대로 움직이는 것과 같이 보이게 되겠죠. (-100, 0)

 

ViewPort에 대한 자세한 내용은 소들이님 블로그 참고

 

 

Bounds로 크기(width, height)만을 변경 시 

이 내용은 참고만 하세요.

테스트만 해서 작성한 거라 부정확합니다.

 

크기를 변경하면 SubView가 확대 및 축소가 될 줄 알았는데 이동만 되더라고요...?

 

도형의 중간을 기점으로 객체의 frame의 좌표가 변하게 됩니다.

width를 변경 시 Y축 기준으로 (처음 width - 나중 width) / 2 만큼 변하게 됩니다.

height를 변경 시 X축 기준으로 (처음 height - 나중 height) / 2 만큼 변하게 됩니다.

 

즉, bounds의 크기를 변경하면 frame의 위치는

width 변경 시 frame.x의 값이 변화함.

height 변경 시 frame.y의 값이 변화함.

 

그리고 이유는 모르겠지만,

위의 크기 공식에서 위치까지 변경시키면 적용이 안 되더라고요...?!

 

 

탭바를 만들어보자

어제 배운 CAShapeLayer을 통해 생성을 해봅시다.

 

우리는 위에서 Frame와 Bounds에 대해서 알아봤습니다.

생각해 보면 기본적으로 탭바 컨트롤러는 ViewController의 하단에 위치해 있습니다.

 

그러므로 탭바 생성 시 만약 Frame을 사용하게 된다면,

SuperView(ViewController)를 기준으로 위치를 계산해야 되므로 계산이 복잡해지게 되겠죠?

 

위에서 공부를 해본 결과

SuperVIew와 상관관계가 없는 Bounds를 사용하면 조금 더 쉽게 모양을 그릴 수 있을 것 같네요.

 

 

이제 시작!

이런 모양의 탭바를 만들어 줄 거예요.

 

일단 탭바 컨트롤러를 만들고 필요한 설정을 해줬습니다.

final class CustomTabBarController: UITabBarController {
    
    // MARK: - LifeCycles
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupTabBarItems()
    }
    
    // MARK: - TabBar Item Setup
    func setupTabBarItems() {
        let mapViewController = MapViewController()
        mapViewController.tabBarItem.image = UIImage(named: "globe")
        mapViewController.tabBarItem.selectedImage = UIImage(named: "globe.fill")
        
        let addlistViewController = AddListViewController()
        addlistViewController.tabBarItem.image = UIImage(named: "add.fill")
        
        let userViewController = UserViewController()
        userViewController.tabBarItem.image = UIImage(named: "user")
        userViewController.tabBarItem.selectedImage = UIImage(named: "user.fill")
        
        viewControllers = [mapViewController, addlistViewController, userViewController]
    }
}

 

자 그러면 CAShapeLayer를 생성하고 UIBezierPath를 통해 모양을 그려봅시다.

func setupTabBar() {
    let layer = CAShapeLayer()
}

 

레이어를 만들 때 크기를 하드코딩으로 박아버리면 쉽지만,

나중에라도 크기를 변경할 수 있으므로 크기를 계산해서 만들어봤습니다.

(탭바 아이템 추가 등)

 

그리기 전, 계산을 위해 기본 TabBar 크기를 확인해 봅시다.

iPhone SE의 경우 (375, 49)

iPhone 12 Pro의 경우 (390, 83)

iPhone 12 Max의 경우 (428, 83)

iPhone 14 Pro의 경우 (393, 83)

 

뭐 기종마다 다 다르네요...

 

그런데 여기서 알 수 있는 건,

신형 iPhone의 경우, 너비(width)는 차이가 있지만 높이(height)는 83으로 고정이더라고요.

 

일단, 신형 iPhone 기준으로 한번 만들어 보겠습니다.

 

 

width를 만드는 것은 쉽습니다.

그냥 각 TabBar의 너비를 기준으로 값을 정해주면 되더라고요.

이런 식으로 만들어주면, 양쪽에 같은 크기의 여백이 생기고 중앙 정렬된 것처럼 보일 거예요.

// 탭바의 크기 (390, 83)

// x 축으로 이동한 거리 (여백이 생기겠죠)
let x: CGFloat = 70    

// 크기: 탭바의 너비(390) - (여백 * 2)
let width: CGFloat = tabBar.bounds.width - (x * 2)

 

 

높이(height)를 만들 때는

Y축 방향으로 0만큼 주고 높이를 설정해 봤습니다.

// 높이를 설정
let height: CGFloat = 49

// Y축 설정
let y: CGFloat = 0

 

만약 TabBar에서 타이틀과 이미지를 사용할 경우엔, 이렇게 사용해도 무방할 것 같네요.

글을 수정한거라 이미지가 조금 다릅니다.

 

그런데 저는 이미지만 사용할 것이므로, 타이틀을 제거해 봤더니,

그냥 타이틀만 사라지고 위치는 변하지 않네요... 아래로 조금이라도 내려올 줄 알았는데...

(뒤의 알약 모양 레이어가 살짝 밑으로 내려간 것처럼 보이게 됐어요)

너무 쉽더라니...

탭바 아이템 위치를 옮기는 방법을 찾다가... 못 찾아서 이건 포기

 

일단 보니까, TabBar Item은 '이미지'와 '타이틀'로 돼있는데,

타이틀을 사용하지 않더라도 타이틀 영역은 사라지지 않고 저런 식으로 레이아웃이 배치되니까...

 

흠 그럼 살짝 계산을 해야겠죠? ㅠ

 

여러 번의 삽질 결과

일단 기본 TabBar의 "이미지 + 타이틀" 영역은 크기가 49로 구형 iPhone과 같더라고요!

그리고 여기서 이미지만 사용할 때는 (-5.5) 만큼 이동시켜 주면, 이미지의 중간으로 위치하게 되더라고요...

 

일단 기본 높이로 만든 코드는 이렇습니다.

(이미지 + 타이틀 영역의 크기만큼 설정해 봄)

// 높이를 설정
let height: CGFloat = 49

// Y축 설정
let y: CGFloat = -5.5

 

조금 더 추가해서, 어떠한 높이를 주더라도 항상 중간으로 위치하게 만들어 봤습니다.

let x: CGFloat = 70                                   // x 축으로 이동한 거리 (여백)
let width: CGFloat = tabBar.bounds.width - (x * 2)    // 너비: 기본 탭바의 너비 - (여백 * 2)
let baseHeight: CGFloat = 49                          // 기본 높이
let currentHeight: CGFloat = baseHeight + height      // 높이를 설정 (기본높이 + 추가 높이)
let y: CGFloat = -(height/2 + 5.5)                    // Y축 = 아이콘의 중간으로 맞춤

 

이렇게 어떤 높이(height)를 주더라도 항상 아이템에 중간으로 위치하게 만들어 봤습니다.

 

색상 및 그림자 효과를 추가한 완성한 코드를 보시죠.

// MARK: - Layer Setup
func setupTabBar(eachSide space: CGFloat, addHeight: CGFloat) {
    // CAShapeLayer 객체 생성
    let layer = CAShapeLayer()
    
    // tab bar layer 세팅
    let x: CGFloat = space                                // x 축으로 이동한 거리 (여백)
    let width: CGFloat = tabBar.bounds.width - (x * 2)    // 너비: 기본 탭바의 너비 - (여백 * 2)
    let baseHeight: CGFloat = 49                          // 기본 높이 (변경 X)
    let currentHeight: CGFloat = baseHeight + addHeight   // 높이를 설정 (기본높이 + 추가 높이)
    let y: CGFloat = -(5.5 + addHeight/2)                 // Y축 = 아이콘의 중간으로 맞춤

    // 알약 모양으로 UIBezierPath 생성
    let path = UIBezierPath(roundedRect: CGRect(x: x,
                                                y: y,
                                                width: width,
                                                height: currentHeight),
                            cornerRadius: currentHeight / 2).cgPath
    layer.path = path

    layer.fillColor = HexCode.tabBarBackground.color.cgColor
    
    // tab bar layer 그림자 설정
    layer.shadowColor = HexCode.selected.color.cgColor
    layer.shadowOffset = CGSize(width: 0.0, height: 1.0)  // 밑면 그림자 크기
    layer.shadowRadius = 5.0                              // 흐려지는 반경
    layer.shadowOpacity = 0.5                             // 불투명도 (0 ~ 1)
    
    // tab bar layer 삽입: addSublayer대신 insertSublayer(0번째 Sublayer에 대치) 사용
    self.tabBar.layer.insertSublayer(layer, at: 0)
    
    // tab bar items의 위치 설정
    self.tabBar.itemWidth = width / 5
    self.tabBar.itemPositioning = .centered
    
    // 틴트 컬러 설정
    self.tabBar.tintColor = HexCode.selected.color
    self.tabBar.unselectedItemTintColor = HexCode.unselected.color
}

 

viewDidLayoutSubviews에 넣어 줍시다.

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    setupTabBar(eachSide: 70, addHeight: 11)
}

 

완성!

 

 

추가 (탭바 이미지의 위치)

2023 / 07 / 25 추가

 

드디어 탭바의 아이템의 이미지 위치를 변경하는 방법을 찾았습니다!!!

 

탭바의 이미지의 inset을 조절해 주면 되더라고요.

 

이와 같이, 탭바 생성 시, imageInsets에 접근해서 위치를 조절해 수 있습니다.

 

그리고 항상 위치를 변경시킬 땐, top과 bottom은 한 쌍으로 조절해줘야 합니다.

만약, top 쪽으로 10만큼 올리면, bottom은 아래로 10만큼 내려줘서 이미지 중심 위치를 일정하게 해줘야 합니다

(한쪽만 변경 시, 이미지 프레임이 찌부러지기 때문에 Y축 계산이 힘들어집니다)

// MARK: - TabBar Item Setup
func setupTabBarItems() {
    let mapViewController = MapViewController()
    mapViewController.tabBarItem.image = UIImage(named: "globe")
    mapViewController.tabBarItem.selectedImage = UIImage(named: "globe.fill")
    mapViewController.tabBarItem.imageInsets = UIEdgeInsets(top: -10, left: 0, bottom: 10, right: 0)
    
    let addlistViewController = AddListViewController()
    addlistViewController.tabBarItem.image = UIImage(named: "add.filled")
    addlistViewController.tabBarItem.imageInsets = UIEdgeInsets(top: -10, left: 0, bottom: 10, right: 0)
    
    let userViewController = UserViewController()
    userViewController.tabBarItem.image = UIImage(named: "user")
    userViewController.tabBarItem.selectedImage = UIImage(named: "user.fill")
    userViewController.tabBarItem.imageInsets = UIEdgeInsets(top: -10, left: 0, bottom: 10, right: 0)

    viewControllers = [mapViewController, addlistViewController, userViewController]
}

 

 

추가 (Y축 위치 변경)

위에 설명했던 코드들은 탭바 아이템의 이미지 위치를 변경할 수가 없어서, Y축으로 이동이 불가능했습니다.

(꾸역꾸역 테스트를 통해서 이상적인 값을 찾아 만들어 주었다)

 

하지만, 이제 이미지의 위치를 변경하는 방법을 알았으니, 탭바를 Y축으로도 움직일 수 있겠네요.

비슷하긴 한데, 조금 더 깔끔한 코드를 만들어 봤습니다.

 

일단, 이미지 중심을 탭바 Y축의 원점(0)으로 맞추려면, 탭바를 아래로 19정도 내려줘야 하더라고요.

(위치 계산을 위해 원점을 찾았습니다. 아래 그림 참고)

class CustomTabBarController: UITabBarController {

    // MARK: - LifeCycles
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        setupTabBarItems()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        // 만약 Y축을 옮겨준다면, 아래 imageInsets의 크기도 Y축에 맞게 변경해줘야 합니다.
        setupTabBar(eachSide: 70, height: 60, y: -30)
    }
    
    // MARK: - TabBar Item Setup
    func setupTabBarItems() {
        let mapViewController = MapViewController()
        mapViewController.tabBarItem.image = UIImage(named: "globe")
        mapViewController.tabBarItem.selectedImage = UIImage(named: "globe.fill")
        mapViewController.tabBarItem.imageInsets = UIEdgeInsets(top: -30, left: 0, bottom: 30, right: 0)
        
        let addlistViewController = AddListViewController()
        addlistViewController.tabBarItem.image = UIImage(named: "add.filled")
        addlistViewController.tabBarItem.imageInsets = UIEdgeInsets(top: -30, left: 0, bottom: 30, right: 0)
        
        let userViewController = UserViewController()
        userViewController.tabBarItem.image = UIImage(named: "user")
        userViewController.tabBarItem.selectedImage = UIImage(named: "user.fill")
        userViewController.tabBarItem.imageInsets = UIEdgeInsets(top: -30, left: 0, bottom: 30, right: 0)

        viewControllers = [mapViewController, addlistViewController, userViewController]
    }
    
    // MARK: - Layer Setup
    func setupTabBar(eachSide space: CGFloat, height: CGFloat, y: CGFloat = 0) {
        // CAShapeLayer 객체 생성
        let layer = CAShapeLayer()
        
        // tab bar layer 세팅
        let x: CGFloat = space                              // X 축으로 이동한 거리 (여백)
        let width: CGFloat = tabBar.bounds.width - (x*2)    // 너비: 기본 탭바의 너비 - (여백 * 2)
        let height: CGFloat = height                        // 높이
        let centerImageY: CGFloat = 19 - (height/2)         // 이미지 중심을 탭바의 원점으로 이동(19) - (높이/2)
        let y: CGFloat = centerImageY + y                   // Y축: 변화된 높이 + 원하는 Y축 위치
      
        // 알약 모양으로 UIBezierPath 생성
        let frame: CGRect = CGRect(x: x, y: y, width: width, height: height)
        let path = UIBezierPath(roundedRect: frame,
                                cornerRadius: height/2).cgPath
        layer.path = path

        layer.fillColor = UIColor.white.cgColor
        
        // tab bar layer 그림자 설정
        layer.shadowColor = UIColor.red.cgColor
        layer.shadowOffset = CGSize(width: 0.0, height: 1.0)  // 밑면 그림자 크기
        layer.shadowRadius = 5.0                              // 흐려지는 반경
        layer.shadowOpacity = 0.5                             // 불투명도 (0 ~ 1)
        
        // tab bar layer 삽입: addSublayer대신 insertSublayer(0번째 Sublayer에 대치) 사용
        self.tabBar.layer.insertSublayer(layer, at: 0)
        
        // tab bar items의 위치 설정
        self.tabBar.itemWidth = width / 5
        self.tabBar.itemPositioning = .centered
        
        // 틴트 컬러 설정
        self.tabBar.tintColor = UIColor.orange
        self.tabBar.unselectedItemTintColor = UIColor.systemOrange
    }
}

 

Y축으로 옮겨줘도, 탭바 터치가 반응하는 영역은 기존 탭바 위치인 것 같습니다.

정작 터치가 안되니 사용을 못하겠네요...

 

 

 

마무리

기본 탭바는 너무 iPhone스럽다 해야 하나요? 마치 설정창에 들어간 것 같은 느낌 때문에,

프로젝트를 할 때마다 항상 변경하고 싶었는데,

이번 기회에 CAShapeLayer를 통해서 이렇게 모양을 변경해 봤습니다.

 

이걸 만들기 위해 삼일을 날렸지만?!

(사실 몰랐던 내용과 개념을 공부해서 뿌듯하긴 합니다)

 

특히 위치 잡는 부분에서 시간을 진짜 많이 소비한 거 같네요. (공간 감각이 문젠가...ㅠㅠ)

 

일단 기억합시다!

"탭바에서 기본 높이(이미지 + 타이틀)는 크기 49로 고정돼 있다,

이미지만 사용할 경우 중간으로 맞추려면 5.5위로 올려주자."

 

그리고 CAShapeLayer는 생각보다 많이 사용가능 할 것 같더라고요.

사용방법이 단순해서 한번 배우면 간단히 이용할 수 있을 것 같습니다.

 

CALayer 사용할 때 가장 돋보이는 기술인 애니메이션 처리를 안 해봤지만...

다음에 기회가 되면 애니메이션도 기능도 한번 추가해 보겠습니다.

 

디자인은... 음...

아무튼 완성했습니다.

 

다음 내용은 [Swift/TIL #3] 탭바 아이템을 버튼으로 교체 with CGAffineTransform(회전) 입니다.

 

 

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

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