호댕의 iOS 개발

[Widget] 위젯 사용자화 틴트 처리됨(tinted) 대응하기 (위젯이 흰 화면으로만 보인다...) 본문

Software Engineering/iOS

[Widget] 위젯 사용자화 틴트 처리됨(tinted) 대응하기 (위젯이 흰 화면으로만 보인다...)

호르댕댕댕 2025. 3. 11. 18:44

틴트 처리됨이 뭐지?

iOS 18에는 홈화면 편집에서 사용자화라는 부분이 새롭게 등장했다. 

여기서 앱 아이콘 및 폴더의 색상을 변경할 수 있는 틴트 처리됨 이라는 기능이 새롭게 등장했다. 

 

홈 화면을 롱프레스로 누른 후 좌측 상단 편집 버튼을 누르면 접근이 가능하다. 

https://support.apple.com/ko-kr/guide/iphone/iph385473442/ios

 

iPhone 홈 화면에서 앱 및 위젯 사용자화하기

iPhone의 홈 화면에서 앱 및 위젯의 색상 및 크기를 변경할 수 있습니다.

support.apple.com

 

 

안드로이드에선 이전부터 되던 기능들이 이제서야 되는 것이었지만 새로운 기능을 제공하는 것은 좋은 것이지라고 WWDC를 보며 생각을 했었는데...

 

문제 상황

틴트 처리됨을 통해 틴트 색상을 먹인 사용자의 경우 위젯이 하얀 색으로 아무 콘텐츠도 보이지 않는 문제가 발생했다. 

 

T맵의 위젯처럼 아무 콘텐츠도 보이지 않는데 웃긴건 탭 이벤트는 또 제대로 동작한다. 정말 보여지는 뷰만 문제가 있는 것이다 😅

그렇다면 이 문제는 어떻게 해결하면 좋을까...?

 

 

해결방법

widgetAccentable()

애플에선 iOS 16버전부터 적용이 가능한 해결책을 제공해주고 있었다. 

https://developer.apple.com/documentation/swiftui/view/widgetaccentable(_:)

 

widgetAccentable(_:) | Apple Developer Documentation

Adds the view and all of its subviews to the accented group.

developer.apple.com

이는 WidgetKit의 WidgetRenderingMode가 기본인 경우(fullColor) / 틴트 처리된 경우(accented)로 나눠서 다른 색상을 적용해 뷰를 그리게 된다. 

이를 상위 뷰에 적용하게 되면 하위 뷰까지 전부 적용된다. 

 

여기서 주의할 점은 한 번 widgetAccentable(true)를 적용하게 되면 추후 widgetAccentable(false)를 적용하더라도 accented를 위한 그룹에서 해당 뷰를 포함한 하위 뷰가 빠지지 않게 된다. 

 

일단은 이걸 적용하게 되면 일차원적인 해결은 가능하다. 

단순히 위젯이 텍스트 위주로 되어 있는 간단한 위젯이라면 요것만 적용해주면 잘 해결될 수 있다.

 

widgetAccentable()을 사용하며 발생했던 문제

뷰가 만약 아래 코드처럼 구성이 되어 있다고 해보자

ZStack {
    Color.black
    CustomView()
    	.foregroundColor(Color.white)
}

 

아주 간단하게 검은 배경에 흰 배경을 가진 커스텀 뷰가 있는 화면이다. 커스텀 뷰에는 간단한 리스트가 있었다고 가정해보자.

그런데 만약 여기서 widgetAccentable()을 ZStack에 먹인다면?

ZStack {
    Color.black
    CustomView()
    	.foregroundColor(Color.white)
}
.widgetAcceptable()

틴트 컬러를 먹이게 되면 

위 미리보기 화면처럼 컨텐츠가 보이지 않고 CustomView 영역 전체가 tint color로 바뀌게 된다. 

 

그래서 이 경우 ZStack을 따로 사용하지 않고, CustomView에 background Color를 주는 방식으로 수정을 했다. 

CustomView() // 내부에서 backgroundColor 변경
    .foregroundColor(Color.white)
    .widgetAcceptable()

이렇게 되면 원했던 것처럼 내부 콘텐츠도 제대로 보이게 된다.

 

ZStack 전체에 걸어주게 되면 하위 뷰 전체에 accent color가 적용되면서 배경에도 accent color가 적용되어 이런 문제가 발생했던 것 같다. 

틴트 컬러의 변경이 있을 때마다 즉각적으로 콘텐츠도 이에 맞춰 변경된다. 

 

만약 앱이 iOS 16 미만의 Deployment Target을 가지고 있다면?

앞서 말했듯 widgetAcceptable()의 경우 iOS 16이상부터 지원을 하고 있다. 

 

그렇다고 매번 if #available(iOS 16, *) 이렇게 분기 코드를 만들어 뷰를 만들면 복잡도가 너무 높아진다.

그래서 나는 커스텀 ViewModifier를 활용했다.

 

struct CustomWidgetRenderingModeModifier: ViewModifier {
    func body(content: Content) -> some View {
        if #available(iOS 16.0, *) {
            content
                .widgetAccentable()
        } else {
            content
        }
    }
}

 

코드는 간단하다. 내부 body 함수에서 버전 분기를 해서 16 미만인 경우 그대로 원래 뷰를 반환하고, 16 이상인 경우 .widgetAccentable()을 적용하는 코드이다. 

 

틴트 처리됨은 iOS 18부터 제공하는 기능이기 때문에 16부터 버전 분기를 하지 않고 iOS 18을 기준으로 분기를 해줘도 무방할 것 같다.

 

그럼 .widgetAccentable()을 사용했던 것과 유사하게 사용이 가능하며 버전 분기도 매번 필요하지 않게 된다. 

CustomView()
    .foregroundColor(Color.white)
    .modifier(CustomWidgetRenderingModeModifier())

 

widgetRenderingMode 환경변수

Text에 background를 주는 경우 Text의 영역만큼 TintColor로 채워지는 문제

예시는 카카오맵의 위젯이다. 

 

기존에는 Text가 있고 Background에 색이 칠해져있는 뷰이다. 

어떻게 구현되어 있는지는 직접 카카오맵 앱 코드를 확인해보진 않았지만 나도 이와 유사한 문제를 겪게 되었다. 

 

문제의 원인은 이전 ZStack 전체에 .widgetAccentable()을 적용했을 때와 동일한 원인이라고 판단했다. 

그렇다면 이걸 해결하기 위해선 내가 직접 accented / fullColor에 따라 각기 다른 뷰를 반환할 필요가 있었다.

 

그래서 widgetRenderingMode를 사용해야 했다. 

 

WidgetRenderingMode 또한 iOS 16부터 등장했다.

https://developer.apple.com/documentation/widgetkit/widgetrenderingmode

 

WidgetRenderingMode | Apple Developer Documentation

Constants that indicate the rendering mode for a widget.

developer.apple.com

 

이는 아래 환경 변수를 통해 가지고 올 수 있다. 

@Environment(\.widgetRenderingMode) var widgetRenderingMode

 

이는 3가지 Case가 존재한다. 

  • .accented : 틴트 적용됨을 선택한 상태
  • .fullColor : 일반적인 다크모드 / 일반모드를 선택한 상태
  • .vibrant : 잠금화면 위젯에서만 사용됨

 

그렇다면 우리는 widgetRenderingMode가 .accented일 때와 아닐 때로 구분해서 서로 다른 뷰를 전달시키면 된다. 

(accented일 때에는 Text에 background Color가 없고, 아닐 때에는 있도록)

 

일단 우리는 홈화면에 띄우는 일반적인 위젯을 보고 있기 때문에 vibrant는 논외로 보자.

 

그런데 나는 iOS 16 미만의 Deployment Target을 가지고 있다... 

그래서 처음에는 이런 계산 프로퍼티를 활용하면 되지 않을까란 생각을 했었다.

@available(iOS 16, *)
private var widgetRenderingMode: WidgetRenderingMode {
    @Environment(\.widgetRenderingMode) var widgetRenderingMode
    return widgetRenderingMode
}

흠.. 그런데 이렇게 하는 경우 항상 widgetRenderingMode가 .fullColor로 반환이 된다...

 

정확한 이유는 모르겠지만 계산프로퍼티 내부에 환경 변수를 선언하면 제대로 값일 가지고 오지 못했다. 

그래서 특정 버전 위에서 사용할 수 있는 커스텀 뷰와 특정 버전 이하에서 사용할 커스텀 뷰를 각각 만들었다.

 

@available(iOS 16, *)
struct CustomTextOveriOS18: View {
	@Environment(\.widgetRenderingMode) var widgetRenderingMode

	var body: some View {
    	if widgetRenderingMode == .accented {
        	Text("Hello")
                .font(.largeTitle)
                .foregroundColor(.white)
                .background(
                    RoundedRectangle(cornerRadius: 4)
                        .strokeBorder(Color.white, lineWidth: 1) // Color는 tint가 먹혀서 크게 중요 X
                )
        } else {
        	Text("Hello")
                .font(.largeTitle)
                .foregroundColor(.white)
                .background(
                    Color.orange
                )
        }
    }
}

struct CustomText: View {
    var body: some View {
    	Text("Hello")
                .font(.largeTitle)
                .foregroundColor(.white)
                .background(
                    Color.orange
                )
    }
}

 

그리고 사용할 때 버전 분기를 해서 각 뷰를 반환할 수 있도록 하면 이제 환경변수를 제대로 들고와서 사용자 설정 상태에 따라 뷰를 다르게 그릴 수 있게 된다.

 

 

그러면 이런 식으로 Text도 잘 보이면서 틴트도 제대로 먹일 수 있다.

흠... 그런데 만약 정말 background Color가 꼭 필요한 상황이라면..? 

 

아예 해당 부분을 이미지로 대체하거나, background Color에 opacity를 줘야 할 것 같은데 opacity를 주는 방법은 직접 테스트를 해보진 못했다. 

 

widgetAccentedRenderingMode

이미지가 제대로 tintColor가 적용되지 않는 문제

틴트 적용됨을 적용한 경우 이미지가 형태는 보이되 내부 디테일의 차이 없이 동일한 컬러로 채워져 있을 수 있다.

 

이 경우 사용할 수 있는 것이 바로 widgetAccentedRenderingMode이다. 이는 Image의 인스턴스 메서드로 이미지에서만 사용이 가능하다.

이건 iOS 16부터 사용할 수 있는 다른 것들과 다르게 iOS 18 이상부터 사용할 수 있는 따끈따끈한 함수/구조체이다.

 

여기서 중요한 점은 만약 Image의 상위 뷰에 .widgetAccentable()을 적용했다면 widgetAccentedRenderingMode 적용이 제대로 되지 않을 수 있다. 

 

 

https://developer.apple.com/documentation/widgetkit/widgetaccentedrenderingmode

 

WidgetAccentedRenderingMode | Apple Developer Documentation

Constants that indicate the rendering mode for an in when displayed in a widget in mode.

developer.apple.com

https://developer.apple.com/documentation/swiftui/image/widgetaccentedrenderingmode(_:)

 

widgetAccentedRenderingMode(_:) | Apple Developer Documentation

Specifies the how to render an when using the mode.

developer.apple.com

 

  • .accented : 기본 상태로 이미지 내부 디테일 없이 tint color가 적용된다.

  • .accentedDeceturated : tint color는 적용되지만, 이미지의 디테일은 유지된다.

  • .deceturated : 원래 이미지 컬러와 유사하지만 채도가 약간 빠진 것처럼 보이게 된다.

  • .fullColor : 원래 이미지 컬러 그대로를 보여준다.

 

 

틴트 컬러는 적용이 되는 것이 사용자 설정 상태에 맞다고 판단했고 나는 .accentedDeceturated 옵션을 선택했다.

 

struct ImageView: View {
    var body: some View {
        if #available(iOS 18, *) {
            Image(systemName: "star")
                .widgetAccentedRenderingMode(.accentedDesaturated)
        } else {
            Image(systemName: "star")
        }
    }
}

 

그래서 코드로 보면 이런 식으로 처리를 했다.

 

 

이 정도가 내가 확인했던 문제들과 해결 방법이다. 

 

 

참고 자료

- https://nemecek.be/blog/206/how-to-support-tinted-home-screen-widgets-in-ios-18

 

How to support tinted home screen widgets in iOS 18

Marking views to be rendered with colors, handling images and more.

nemecek.be

- https://www.createwithswift.com/adapting-widgets-for-tint-mode-and-dark-mode-in-swiftui/

 

Adapting widgets for tint mode and dark mode in SwiftUI

Explore the multiple rendering modes of widgets for different device settings with SwiftUI.

www.createwithswift.com

 

Comments