iOS) 잠금 화면 위젯을 위해서 최소버전 대응하기

3 minute read

내용

  • 최소버전과 관련하여 위젯이 등장하지 않던 문제 원인 파악 및 해결
  • iOS deployment target 15.0 으로 설정된 상태에서 잠금 화면 위젯(16.0+) 대응해보기

🚨 문제 상황 인식

메인 앱 타겟의 최소 버전은 15.0 이었습니다. 16.0 버전의 아이폰에서 앱은 설치할 수 있지만, 위젯을 설치할 수 없었습니다.

그 이유는 widget extension 의 최소 버전이 16.2 로 설정되어 있었기 때문이었습니다.

왜 16.2 였었나?

PreviewProvider 프로토콜을 채택하여 preview 하는 struct 에서 Live Activity 의 미리보기를 만들 수 있는 메서드가 있는데 해당 메서드의 지원 버전이 16.2+ 이기 때문이었습니다.

[previewContext(_:isStale:viewKind:) Apple Developer Documentation](https://developer.apple.com/documentation/activitykit/activityattributes/previewcontext(_:isstale:viewkind:)?changes=latest_ma_10__4_3&language=objc)

widget extension 을 만들 때 Live Activity 를 포함하는 옵션을 선택하게 되었고 자연스레 widget extension target 의 deployment target 이 16.2 로 설정되었습니다.

추가적으로, 아래 링크에서는 현재 iOS 사용 현황에 대해서 살펴볼 수 있습니다. 현재 iOS 15 미만의 기기들은 전체에서 8% 정도 됩니다.(iOS 16 은 72%, iOS 15 는 20%)

App Store - 지원 - Apple Developer

✅ 문제 해결

현재 서비스는 메인 앱이 최소 버전 15.0, 홈 위젯은 14.0+, 잠금 화면 위젯은 16.0+, 추가적으로 프로젝트 내에는 Live Acitivty 지원하는 위젯(16.1+)이 있지만 아직 서비스를 제공하지 않습니다.

그렇기 때문에 운영체제의 버전별로 분기처리를 해주지 않으면 다음과 같이 에러가 등장합니다.

1

대표적으로 잠금 화면 위젯을 지원하기 위한 widget family 의 case 로 accessoryCircular 가 있습니다. 해당 case 를 사용하기 위해서 16.0 이상에서만 가능합니다.

  • 다음과 같이 코드에 분기처리를 해주겠습니다.
struct QRCodeWidget: Widget {
    // ✅ 지원하는 WidgetFamily 대응.
    private let supportedFamilies: [WidgetFamily] = {
        if #available(iOSApplicationExtension 16.0, *) {
            return [.systemSmall, .accessoryCircular]
        } else {
            return [.systemSmall]
        }
    }()
    
    let kind: String = "QRCodeWidget"
    
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: QRCodeProvider()) { entry in
            QRCodeEntryView(entry: entry)
        }
        .configurationDisplayName("QR Code 위젯")
        .description("QR Code 를 인식할 수 있도록 카메라로 빠르게 접근합니다.")
        // ✅ supportedFamilies([.systemSall, .accessoryCircular]) 에서 분기처리하기 위해서 수정.
        .supportedFamilies(supportedFamilies)
    }
}

// 출처: https://stackoverflow.com/questions/72819402/lockscreen-swift-widget-only-available-to-ios-16-users

struct QRCodeWidget_Previews: PreviewProvider {
    static var previews: some View {
        QRCodeEntryView(entry: QRCodeEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
        // ✅ 지원하는 WidgetFamily 대응.
        if #available(iOSApplicationExtension 16.0, *) {
            QRCodeEntryView(entry: QRCodeEntry(date: Date()))
                .previewContext(WidgetPreviewContext(family: .accessoryCircular))
        }
    }
}

iOS 15.0 시뮬레이터에서 정상 실행되었습니다.

🚨 문제 상황 인식 - WidgetBundel 에서 분기처리

iOS 16.0 시뮬레이터에서는 실행 시 다음과 같이 Fatal error: Unavailable 가 등장했습니다.

2

다음과 같이 분기처리를 하는데도 다음과 같이 에러가 등장했습니다.

Closure containing control flow statement cannot be used with result builder 'WidgetBundleBuilder’

21

WidgetBundleBuilder 인 body 에서 분기 처리는 할 수 없다는 에러였습니다.

✅ 문제 해결

WidgetBundle 프로토콜을 살펴보면 아래와 같이 구성되어 있습니다.

public protocol WidgetBundle {
    associatedtype Body : Widget
    /// Creates a widget bundle using the bundle's body as its content.
    init()

    /// Declares the group of widgets that an app supports.
    ///
    /// The order that the widgets appear in this property determines the order
    /// they are shown to the user when adding a widget. The following example
    /// shows how to use a widget bundle builder to define a body showing
    /// a game status widget first and a character detail widget second:
    ///
    ///     @main
    ///     struct GameWidgets: WidgetBundle {
    ///        var body: some Widget {
    ///            GameStatusWidget()
    ///            CharacterDetailWidget()
    ///        }
    ///     }
    ///
    @WidgetBundleBuilder var body: Self.Body { get }
}

Body 는 Widget 자료형을 가지기 때문에 분기 처리 과정의 결과로 Widget 을 반환해주어야 합니다.

그래서 직접 WidgetBundleBuilder.buildBock() 를 사용해서 하나의 단일 Widget 을 반환하였습니다.

(참고로 10개까지 한번에 Widget 으로 반환해줄 수 있습니다.)

3

import WidgetKit
import SwiftUI

@main
struct WidgetsBundle: WidgetBundle {
    var body: some Widget {
        // ✅ OpenAppLockScreenWidget 이 잠금화면 위젯이기 때문에 다음과 같이 분기처리.
        if #available(iOSApplicationExtension 16.0, *) {
            return WidgetBundleBuilder.buildBlock(MyCardWidget(), OpenAppLockScreenWidget(), QRCodeWidget())
        } else {
            return WidgetBundleBuilder.buildBlock(MyCardWidget(), QRCodeWidget())
        }
    }
}

// 출처: [https://developer.apple.com/forums/thread/711441](https://developer.apple.com/forums/thread/711441)

결과

iOS 15.0, 16.0 에서 정상작동을 확인할 수 있습니다.

4

관련 이슈들 출처:

[What is the proper way of adding A… Apple Developer Forums](https://developer.apple.com/forums/thread/711441)

Variable WidgetBundle configuration based on conditions

Categories:

Updated: