iOS

[iOS] Border과 CornerRadius에 대한 고찰

스츠흐 2024. 6. 24. 00:45

안녕하세요!

안드로이드, Flutter 개발도 하다 보니 느꼈는데, iOS는 Border와 CornerRadius 설정이 쉬운 편인 것 같습니다.

뷰의 layer에서 기본으로 제공하는 속성 값만 입력하면 쉽게 효과를 적용할 수 있습니다.

// Border 설정
view.layer.borderColor = UIColor.black.cgColor
view.layer.borderWidth = 1

// CornerRadius 설정
view.layer.cornerRadius = 5

그러다 문득, 디자인적 완성도를 높이려고 자세히 살펴보니 '그동안 너무 생각 없이 사용하고 있었구나...' 싶더라구요.

그래서 오늘은 Border와 CornerRadius에 대해서 더 고민해 보는 시간을 가졌습니다.

 

 

0. Border와 CornerRadius

Border는 뷰의 테두리(Stroke)를 의미합니다.

피그마를 기준으로 살펴보면, 색상, 두께, 위치(뷰를 기준으로 inside, center, outside)를 기본 속성으로 설정할 수 있습니다.

피그마에서 뷰에 Stroke를 추가하면, 기본으로 Inside에 추가됩니다.

피그마의 Stroke(Border) 기본 설정 패널(좌) / 위치 설정값 드롭다운(우)

 

CornerRadius는 뷰의 코너링 값을 의미합니다.

설정한 값을 반지름으로 삼는 원을 코너 둥글기 값으로 사용합니다.

{height / 2}보다 같거나 큰 값을 설정할 경우, 완전히 둥근 모서리를 가지게 됩니다.

 

 

1. iOS의 Border 기본값은?

iOS에서 아래와 같이 layer에 기본적인 방식으로 Border를 추가하면 어디에 추가될까요?

// border의 위치 확인을 위해 
// 뷰의 색상은 파랑색, border의 색상은 투명도 추가한 빨간색으로 지정
view.backgroundColor = .blue
view.layer.borderColor = UIColor.red.withAlphaComponent(0.5).cgColor
view.layer.borderWidth = 5

 

정답은 Inside입니다!

빨간색 border가 파란색 뷰 안에 완전히 들어가서 보라색으로 보이고 있습니다

 

 

아마 Center나 Outside의 경우,  Width와 Height을 변화시키게 되고, 결과적으로 전체적인 레이아웃에 영향을 끼치기 때문이 아닐까 추측합니다.

그렇다면 디자이너가 Center나 Outside로 디자인을 주면 어떤 속성을 변경하면 될까요?

아쉽게도, 현재 기준으로 이를 설정하는 속성은 없습니다.

대신 아래와 같이 border를 표현하는 CALayer를 생성하고, border의 frame을 조절하여서 동일한 디자인을 구현할 수 있습니다.

// border를 넣고 싶은 View를 targetView라고 명명했습니다.
// #1 위치 - Inside는 기본 방식으로 추가 가능

// #2 위치 - Center
let borderLayer = CALayer()
let borderWidth: CGFloat = 5
let borderFrame = CGRect(x: -(borderWidth / 2), y: -(borderWidth / 2),
                         width: targetView.frame.size.width + borderWidth,
                         height: targetView.frame.size.height + borderWidth)
borderLayer.frame = borderFrame
borderLayer.backgroundColor = UIColor.clear.cgColor
borderLayer.borderWidth = borderWidth
borderLayer.borderColor = UIColor.red.withAlphaComponent(0.5).cgColor
targetView.layer.addSublayer(borderLayer)

// #3 위치 - Outside
let borderLayer = CALayer()
let borderWidth: CGFloat = 5
let borderFrame = CGRect(x: -(borderWidth), y: -(borderWidth),
                         width: targetView.frame.size.width + (borderWidth * 2),
                         height: targetView.frame.size.height + (borderWidth * 2))
borderLayer.frame = borderFrame
borderLayer.backgroundColor = UIColor.clear.cgColor
borderLayer.borderWidth = borderWidth
borderLayer.borderColor = UIColor.red.withAlphaComponent(0.5).cgColor
targetView.layer.addSublayer(borderLayer)

Center border(좌), Outside border(우)를 추가한 뷰

 

 

 

이제 편안한 마음으로 글을 마무리하려고 했는데, 갑자기 이 상태에서 코너를 적용하면 어떻게 될까 궁금해졌습니다.

만들어진 borderLayer와 targetView에 cornerRadius를 추가해 보겠습니다!

// 앞뒤 중복되는 코드 생략
// CASE #1. borderWidth >= cornerRadius
let borderLayer = CALayer()
borderLayer.borderWidth = 10
borderLayer.cornerRadius = 5

targetView.layer.cornerRadius = 5

// CASE #2. borderWidth < cornerRadius
let borderLayer = CALayer()
borderLayer.borderWidth = 10
borderLayer.cornerRadius = 20

targetView.layer.cornerRadius = 20

 

아아 이상하군요... 

뷰와 테두리 사이에 공간이 생기거나, 코너값이 이질적인 느낌이 듭니다..

 

 

2. Border와 CornerRadius를 동시에 가진 뷰

두 값이 작을 때는 크게 눈에 띄지 않았는데, 값이 커질수록 이질감이 커졌습니다.

바깥 CornerRadius, 안쪽 CornerRadius, Border 사이의 관계를 어떻게 되는지 확인해 보았습니다.

 

{바깥 CornerRadius} = {CornerRadius}

{안쪽 CornerRadius} = {(CornerRadius) - (Border의 Width)} 입니다.

 

* 추가적으로, 만약 borderWidth >= cornerRadius일 경우, 안쪽 CornerRadius는 코너를 둥글게 하지 않습니다! (=0)

 

 

3. Swift로 구현하기

그렇다면 디자이너가 의도한 정확한 Border와 CornerRadius를 적용하기 위해서는 어떻게 해야 할까요?

다양한 구현 방식이 있을 수 있겠지만, 저는 CALayer를 생성하고 path로 직접 그리는 방식으로 구현을 해봤습니다!

extension UIView {
    /**
     Border 위치 타입
     */
    enum BorderType {
        case inside
        case center
        case outside
    }
    
    /**
     Border 설정 메소드
     */
    func setBorder(borderWidth: CGFloat, borderColor: CGColor, cornerRadius: CGFloat, type: BorderType) {
        self.layer.cornerRadius = cornerRadius
        
        let layer = CAShapeLayer()
        layer.fillColor = borderColor
        
        // inside 기준으로 세팅
        var minX: CGFloat = 0
        var maxX: CGFloat = self.frame.width
        var minY: CGFloat = 0
        var maxY: CGFloat = self.frame.height
        var outerCornerRadius: CGFloat = cornerRadius
        var innerCorneRadius: CGFloat = cornerRadius - borderWidth
        
        switch type {
        case .inside:
            break
        case .center:
            minX = -(borderWidth / 2)
            maxX = maxX + (borderWidth / 2)
            minY = -(borderWidth / 2)
            maxY = maxY + (borderWidth / 2)
            outerCornerRadius = cornerRadius + (borderWidth / 2)
            innerCorneRadius = cornerRadius - (borderWidth / 2)
        case .outside:
            minX = -(borderWidth)
            maxX = maxX + borderWidth
            minY = -(borderWidth)
            maxY = maxY + borderWidth
            outerCornerRadius = cornerRadius + borderWidth
            innerCorneRadius = cornerRadius
        }
        
        // 테두리의 안쪽 면
        let innerPath = getRoundCornerRectanglePath(cornerRadius: max(innerCorneRadius, 0), // 음수일 경우 0으로 계산
                                                    minX: minX + borderWidth,
                                                    minY: minY + borderWidth,
                                                    maxX: maxX - borderWidth,
                                                    maxY: maxY - borderWidth)
        // 테두리의 바깥쪽 면
        let outerPath = getRoundCornerRectanglePath(cornerRadius: outerCornerRadius,
                                                    minX: minX,
                                                    minY: minY,
                                                    maxX: maxX,
                                                    maxY: maxY)
        
        let intersectionPath = outerPath.subtracting(innerPath)
        layer.path = intersectionPath
        self.layer.addSublayer(layer)
    }
    
    /**
     사각형/둥근 사각형 뷰 path 구하기
     */
    private func getRoundCornerRectanglePath(cornerRadius: CGFloat, minX: CGFloat, minY: CGFloat, maxX: CGFloat, maxY: CGFloat) -> CGMutablePath {
        let path = CGMutablePath()
        // 좌측 상단 - 시작점
        path.move(to: .init(x: minX, y: minY + cornerRadius))
        if cornerRadius > 0 { // 좌측 상단 모서리
            path.addArc(center: CGPoint(x: minX + cornerRadius, y: minY + cornerRadius), radius: cornerRadius, startAngle: .pi, endAngle: .pi * 3 / 2, clockwise: false)
        }
        
        // 우측 상단
        path.addLine(to: CGPoint(x: maxX - cornerRadius, y: minY))
        if cornerRadius > 0 { // 우측 상단 모서리
            path.addArc(center: CGPoint(x: maxX - cornerRadius, y: minY + cornerRadius), radius: cornerRadius, startAngle: .pi * 3 / 2, endAngle: .pi * 2, clockwise: false)
        }
        
        // 우측 하단
        path.addLine(to: CGPoint(x: maxX, y: maxY - cornerRadius))
        if cornerRadius > 0 { // 우측 하단 모서리
            path.addArc(center: CGPoint(x: maxX - cornerRadius, y: maxY - cornerRadius), radius: cornerRadius, startAngle: 0, endAngle: .pi / 2, clockwise: false)
        }
        
        // 좌측 하단
        path.addLine(to: CGPoint(x: minX + cornerRadius, y: maxY))
        if cornerRadius > 0 { // 좌측 하단 모서리
            path.addArc(center: CGPoint(x: minX + cornerRadius, y: maxY - cornerRadius), radius: cornerRadius, startAngle: .pi / 2, endAngle: .pi, clockwise: false)
        }
        
        // 좌측 상단으로 복귀하는 변 추가 - 끝점
        path.addLine(to: .init(x: minX, y: minY + cornerRadius))
        return path
    }
}

 

 

border의 위치(Inside, Center, Outside)에 따라서 테두리 위치값도 계산해줘야 하고,

꼭짓점마다 cornerRadius를 확인하여 path를 그리다 보니 코드가 굉장히 복잡해지네요..

그렇지만 이렇게 한다면 디자이너가 원하는 대로 border를 완벽하게 구현할 수 있을 것 같습니다! :)

 

해당 코드를 포함한 샘플 프로젝트는 아래 github에서 확인하실 수 있습니다

https://github.com/Chaehui-Seo/borderFramePoC

 

 

 

혹시 궁금하신 점이나, 잘못된 점이 있으면 댓글로 남겨주세요~!