티스토리 뷰

최근 운동 서비스 앱을 SwiftUI 로 작업하다가 뷰에 그림자를 만들었습니다.

이런 UI를 그리고 싶었거든요...

 

 

그런데 이런 보라색 오류가 보였습니다.

오류를 눌러봐도 어디에 오류가 있는건지 제대로 보여주진 않았습니다.

직역해보니

음.. 렌더링 비용이 많이 드는 동적 그림자를 사용하고 있다네요.

 

shadowPath를 설정하거나 그림자를 이미지로 만들어서 사용하라고 권해주는데,

저는 그림자를 하나하나 이미지로 만드는 비효율적인 행동을 하고싶지는 않으니

shadowPath 를 더 알아보기로 결정했습니다.

 

이번 글에서는 SwiftUI에서 그림자를 최적화하여 적용하는 방법에 대해 알아보겠습니다.

특히 shadowoverlay를 사용하는 방법을 비교하고, 최적화된 그림자 적용 방법에 대해 설명하겠습니다.


기존 코드: shadow

shadow modifier를 사용한 기존 코드입니다.

struct CustomTabBarView: View {
    @StateObject private var tabViewModel = TabViewModel()

    var body: some View {
        HStack(alignment: .center, spacing: 48) {
            ForEach(tabViewModel.tabs) { tab in
                TabButtonView(
                    iconName: tab.iconName,
                    isSelected: tabViewModel.selectedTab == tab.tab,
                    action: { tabViewModel.selectTab(tab.tab) }
                )
            }
        }
        .background(Color.white)
        .cornerRadius(20)
        .shadow(color: Color.black.opacity(0.12), radius: 10, x: 0, y: 0) // 여기!
        .offset(y: -15)
    }
}

이 코드는 간단하고 직관적이지만, 전체 뷰에 그림자를 적용하기 때문에 성능 측면에서는 최적화되지 않은 방법입니다.

SwiftUI는 그림자의 경로를 동적으로 계산해야 하기 때문에, 복잡한 뷰에서는 성능 저하가 발생할 수 있다고 합니다.

 


 

수정된 코드: overlay & RoundedRectangle

overlayRoundedRectangle을 사용해보면 아래와 같습니다.

.overlay(
    RoundedRectangle(cornerRadius: 20)
        .stroke(Color.clear, lineWidth: 0)
        .shadow(color: Color.black.opacity(0.12), radius: 10, x: 0, y: 0)
)

이 코드는 overlay를 사용하여 별도의 레이어에 그림자를 적용합니다.

RoundedRectangle을 사용하여 그림자의 형태를 명시적으로 정의하기 때문에 그림자의 경로를 최적화하여 렌더링할 수 있습니다.

 


shadowPath 안알려줌?

UIKit의 shadowPath는 그림자가 적용될 경로를 명시적으로 정의할 수 있게하는 CALayer 의 속성 중 하나입니다.

쓰려면 쓸수는 있는데 UIViewRepresentable을 사용해서 UIView를 만들고 이걸 또 SwiftUI에 통합하는 과정이 필요합니다.

어차피 그 방법을 지금 사용하지 않을거라서 따로 설명은 하지 않겠습니다.

#shadowPath 사용 예제

더보기

import UIKit

class ShadowedView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupShadow()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupShadow()
    }

    private func setupShadow() {
        // 뷰의 기본 설정
        self.backgroundColor = .white
        self.layer.cornerRadius = 20

        // 그림자 설정
        self.layer.shadowColor = UIColor.black.cgColor
        self.layer.shadowOpacity = 0.12
        self.layer.shadowRadius = 10
        self.layer.shadowOffset = CGSize(width: 0, height: 0)

        // shadowPath 설정
        let path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 20).cgPath
        self.layer.shadowPath = path
    }
}

결론

SwiftUI에서 그림자를 적용할 때, 단순히 shadow modifier를 사용하는 것보다 overlayRoundedRectangle을 사용하여 그림자의 형태를 명시적으로 정의하는 것이 더 효율적입니다. 시각적으로는 거의 동일하게 보이지만, 그림자의 렌더링을 최적화하기 때문에 성능 측면에서 중요한 차이를 만들 수 있습니다.

 

주요 차이점

  1. 그림자 적용 위치: 기존 코드는 전체 뷰에 직접 그림자를 적용하지만, 수정된 코드는 overlay를 사용하여 별도의 레이어에 그림자를 적용합니다. 이는 그림자의 렌더링을 분리하여 성능을 향상시킵니다.
  2. 그림자 형태: 수정된 코드는 RoundedRectangle을 사용하여 그림자의 형태를 명시적으로 정의합니다.
  3. 렌더링 최적화: overlay와 RoundedRectangle을 사용함으로써, SwiftUI는 그림자를 더 효율적으로 렌더링할 수 있습니다. 이는 shadowPath를 명시적으로 설정하는 것과 유사한 효과를 낼 수 있습니다.

라고해서 적용을 해보았는데요...

이랬던 뷰가 -> 요래됐슴다~~

 

하... GPT-4o, Claude 3.5 너무 신뢰하지 말걸... 블로그 글 다써놓고 적용하는 바람에 위의 글이 무용지물이 됐습니다...

 


찐찐찐막 최종 해결 코드 : drawingGroup

 

공식문서에 이런게 있었네요...

 

drawingGroup(opaque:colorMode:) | Apple Developer Documentation

Composites this view’s contents into an offscreen image before final display.

developer.apple.com

func drawingGroup(
    opaque: Bool = false,
    colorMode: ColorRenderingMode = .nonLinear
) -> some View

 

drawingGroup(opaque:colorMode:) modifier는 뷰의 하위 트리를 렌더링하기 전에 단일 뷰로 평평(flat)하게 만든다고 합니다.

즉 뷰를 하나의 정적 이미지로 만들어주어 뷰 대신 비트맵으로 표시해준다네요. 😮

 


오늘의 배움

코드 적용을 시켜보고 글을 쓰자...^^