이번에 베이비스푼 앱에서 유저들의 요청이 많이 들어왔던 것중에 위젯을 개발하려고 했어요.
iOS 위젯을 개발하는 방법에 대해서 이야기 해보려 합니다.
제목에도 적혀있듯이 iOS Widget에서 버튼 기능을 추가하는 방법을 중점적으로 다뤄볼께요
우선 두 가지 타입의 위젯을 만들건데요 SummaryWidget과 DoTypeWidget을 만들어 볼거에요
SummaryWidget은 베이비스푼에서 입력받은 정보들을 Widget에서 Display만 하는 용도로 만들고
DoTypeWidget은 실제 앱에서 동작할 수 있도록 버튼 형식으로 구성해보았어요 (디자인은 아직 입히지 않았네요 ㅎ.ㅎ;)
기본적으로 Widget을 추가하는 방법은 (https://pub.dev/packages/home_widget) << 여기에서 참고하시면 되겠습니다.
기본적인 기능 구현에 대해서는 별도로 언급하지 않고 위젯을 개발하면서 어려움을 겪었던 부분 위주로 작성하려합니다.
메소드 채널 설정하기
Target > Runner > AppDelegate 파일에 FlutterMethodChannel을 설정합니다.
ApplicationService 클래스는 위젯에서 메소드채널을 사용하기위해서 추가한 클래스입니다. (아래에서 설명할께요)
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
private var methodChannel: FlutterMethodChannel?
// 위젯에서 메소드 채널을 사용하기 위해 추가
private var service: ApplicationService?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window.rootViewController as! FlutterViewController
methodChannel = FlutterMethodChannel(name: "<메소드채널 이름>", binaryMessenger: controller.binaryMessenger)
methodChannel?.setMethodCallHandler({ (call: FlutterMethodCall, result: FlutterResult) in
guard call.method == "initialLink" else {
// call.method != "initialLink" 일 경우
result(FlutterMethodNotImplemented)
return
}
// 위젯에서 메소드 채널을 사용하기 위해 추가
ApplicationService.instance.setMethodChannel(methodChannel: self.methodChannel!)
})
. . .
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
위와 같이 메소드 채널을 설정하고 home_widget 라이브러리를 이용하여,
베이비스푼 앱에서 Summary data가 변경되는 시점에 메소드 채널을 이용하여 SummaryWidget으로 data를 전송하고 update하도록 처리해주었습니다.
home_widget 라이브러리의 example을 그대로 사용하였기에 생략할께요 ㅋ
여러 가지 타입의 위젯 추가하기
우선 두가지 타입의 위젯을 추가 하기 위해서 다음과 같이 WidgetBundle을 구성해줍니다.
struct SummaryWidgetEntryView : View {
@Environment(\.widgetFamily) var family: WidgetFamily
var entry: Provider.Entry
let data = UserDefaults.init(suiteName:widgetGroupId)
var body: some View {
. . .
}
}
struct SummaryWidget: Widget {
let kind: String = "SummaryWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
SummaryWidgetEntryView(entry: entry)
}
.configurationDisplayName("베이비스푼 테스트 위젯")
.description("테스트용 위젯")
.supportedFamilies([.systemSmall])
}
}
struct DoTypeWidgetEntryView : View {
@Environment(\.widgetFamily) var family: WidgetFamily
var entry: Provider.Entry
let data = UserDefaults.init(suiteName:widgetGroupId)
var body: some View {
. . .
}
}
struct DoTypeWidget: Widget {
let kind: String = "DoTypeWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
DoTypeWidgetEntryView(entry: entry)
}
.configurationDisplayName("베이비스푼 육아활동 기록 위젯")
.description("베이비스푼 DoType 위젯 테스트")
.supportedFamilies([.systemMedium, .systemLarge]) // 위젯 크기 설정
}
}
@main
struct NSWidgetBundle: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
SummaryWidget()
DoTypeWidget()
}
}
다음으로 DoTypeWidget의 버튼영역 구현부를 살펴볼까요?
기저귀 버튼의 모습을 살펴보면 Link로 감싸진것을 볼 수 있는데요
struct DoTypeWidgetEntryView : View {
@Environment(\.widgetFamily) var family: WidgetFamily
var entry: Provider.Entry
let data = UserDefaults.init(suiteName:widgetGroupId)
var body: some View {
HStack(alignment: .center) {
Spacer()
Link(destination: URL(string: "testScheme://babyspoon/test?type=1")!, label: {
Button(action: {}) {
Text("기저귀")
.font(.system(size: 14))
.padding()
.foregroundColor(.black)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.cyan, lineWidth: 2)
)
}
})
Spacer()
. . .
}
}
}
iOS의 WidgetKit에서는 직접적인 handler를 호출할 수 없다고 하여 deepLink를 이용한 방식으로 구현하였어요.
deepLink 관련 링크는 아래 링크들을 살펴보시면 좋을것 같습니다.
(Flutter 공식 가이드) Deep Linking
: https://docs.flutter.dev/development/ui/navigation/deep-linking
(Apple 공식 가이드) 앱에 대한 사용자 정의 URL 스키마 정의
: https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app
(개념 이해) 딥링크와 다이나믹 링크의 차이
: https://miaow-miaow.tistory.com/185
저는 위젯의 버튼을 통해 앱에 직접 기능을 전달하는 용도로 개발하길 원하여 iOS에서 제공하는 방식 중 UrlScheme 방식을 이용해서 구현하기로 했어요. 위의 버튼 코드에 있는 LINK()가 하는 역할이 URL을 전달하는것인데요 AppDelegate 파일에서 해당 링크를 처리하는 함수를 추가해줘야합니다.
기저귀 버튼을 클릭한 경우 "testScheme://babyspoon/test?type=1" 가 호출될거고 아래의 코드에서는 각각 이런 정보가 전달됩니다.
url.scheme : testScheme
url.host : babyspoon
url.path : /test
url.arguments : type=1
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
let sourceApplication = options[.sourceApplication] as? String
let annotation = options[UIApplication.OpenURLOptionsKey.annotation] as? String
guard url.scheme == "testScheme" else {
return super.application(app, open: url, options: options)
}
print("AppDelegate application scheme: " + (url.scheme ?? "") + ", path: " + url.path)
if (url.path == "/home") {
self.methodChannel?.invokeMethod("bsTest", arguments: url.query)
}
return true
}
위와 같이 scheme, host, path, arguments를 설정한 후 methodChannel을 이용하여 flutter 앱으로 invoke 시켜줬습니다.
최종적으로 iOS widget에서 invokeMethod를 통해 Flutter에서 전달받아 앱에서는 다음과 같이 처리하는것으로 마무리 지었습니다.
static const methodChannel = const MethodChannel('<메소드채널 이름>');
methodChannel.setMethodCallHandler((methodCall) async {
controller.methodCall = call;
if (methodCall!.method == "bsTest") {
. . .
// iOS Widget에서 요청한 이벤트를 받아 Flutter 앱에서 처리
}
});
중간중간 불필요하다고 생각하는 부분과 보안에 관련된 부분은 제거하고 변수명을 변경하여 코드를 공유드려서 보기가 어려우실수도 있을것 같네요.
혹시 추가로 필요한 정보가 있거나 틀린 부분이 있으면 지적 해주세요. 수정토록 하겠습니다.
플러터로 개발하는 분들께 도움이 될까 싶어 작성해보았습니다 :)
* Flutter Widget for iOS 개발시 주의해야 할 사항
1. App Group 설정은 제대로 하였는지 확인하기
2. info.plist에 UrlScheme를 설정하였는지 확인하기
'Development > Flutter' 카테고리의 다른 글
GetX 어디까지 써봤나요? (0) | 2022.11.28 |
---|---|
Flutter 3.3 릴리즈 관련 링크 (0) | 2022.11.18 |
[해결방법] 문제1. The MinCompileSdk (31) specified in, 문제2. The binary version of its metadata is 1.5.1, expected version is 1.1.15. (0) | 2022.05.06 |
[해결방법] Vertical viewport was given unbounded height (0) | 2022.04.25 |
[해결방법] warning: The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0 (0) | 2022.04.15 |