[Flutter]홈 위젯 만들기 iOS편 - 2) 위젯 커스텀하기
- -
지난 글에 이어 생성된 위젯을 변경해보겠습니다.
결과물을 먼저 보면 가계부의 지출액과 수입/지출 추가로 이어지는 버튼을 추가해봤습니다.
1. 위젯 커스텀
Xcode를 실행하여 위젯 코드를 수정해봅니다.
처음 코드를 보면 SimpleEntry가 존재하는데, 여기에 데이터를 추가로 받기 위해 content 파라미터를 추가합니다.
struct SimpleEntry: TimelineEntry {
let date: Date
let emoji: String
let content: String
}
SimpleEntry를 호출하는 코드에 content 파라미터를 추가합니다.
TimelineProvider안에 getSnapshot과 getTimeline은 다음과 같은 상황에 사용됩니다.
getSnapshot: 위젯 추가시, 위젯 리스트에 표시될 때 실행되는 함수
getTimeline: 위젯 추가후 홈 화면에 표시될 때 실행되는 함수
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), emoji: "😀", content: "1")
}
// 위젯 추가시 보이는 화면, 위젯 리스트 선택
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), emoji: "😀", content: "0")
completion(entry)
}
// 메인에 조회되는 위젯
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let userDefault = UserDefaults(suiteName: "group.woori.homeWidget")
let amount = userDefault?.string(forKey: "WooriWidgetKey") ?? "0"
let entryDate = Date()
let entry = SimpleEntry(date: entryDate, emoji: "😀", content: amount)
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
[코드설명]
let userDefault = UserDefaults(suiteName: "group.woori.homeWidget")
let amount = userDefault?.string(forKey: "WooriWidgetKey") ?? "0"
- App Group(group.woori.homeWidget)을 사용하여 앱과 위젯 간 데이터 공유를 가능하게 합니다.
- WooriWidgetKey는 Flutter에서 저장한 금액 문자열의 Key입니다. 이 값을 통해 가계부 지출액을 받아와 위젯에 보여줍니다.
VStack, HStack, Text 등을 활용하여 기본 위젯을 꾸몄습니다.
앱로고를 추가하기 위해 Image를 사용했고, Assets에 이미지를 추가하고 이미지명을 호출하면 됩니다.
struct WooriWidgetEntryView : View {
var entry: Provider.Entry
let buttonColor = Color.white.opacity(0.18) // 원하는 투명도에 맞게 조절
var body: some View {
let buttonColor = Color.white.opacity(0.18)
VStack(alignment: .leading, spacing: 4) {
// 1. 앱 로고
HStack(alignment: .center, spacing: 8) {
Image("AppLogo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 32, height: 32)
Text("이번달 지출")
.font(.system(size: 12, weight: .bold, design: .rounded))
.foregroundColor(.white)
}
.padding(.leading, 4)
// 2. 금액
Text(entry.content)
.font(.system(size: 22, weight: .bold, design: .rounded))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .center)
// 3. 버튼
HStack(spacing: 12) {
Link(destination: URL(string: "wooribudget://addincome")!) {
HStack {
Text("💰")
Text("수입 추가")
.font(.system(size: 18, design: .rounded))
}
.foregroundColor(.white)
.frame(maxWidth: .infinity, minHeight: 44)
.background(buttonColor)
.cornerRadius(12)
}
Link(destination: URL(string: "wooribudget://addexpense")!) {
HStack {
Text("🧾")
Text("지출 추가")
.font(.system(size: 18, design: .rounded))
}
.foregroundColor(.white)
.frame(maxWidth: .infinity, minHeight: 44)
.background(buttonColor)
.cornerRadius(12)
}
}
.padding(.top, 8)
}
.padding(.vertical, 6)
.cornerRadius(24)
}
}
[코드설명]
HStack(spacing: 12) {
Link(destination: URL(string: "wooribudget://addincome")!) {
// 수입 추가 버튼
}
Link(destination: URL(string: "wooribudget://addexpense")!) {
// 지출 추가 버튼
}
}
.padding(.top, 8)
- 각 버튼 클릭 시, wooribudget://addincome 또는 wooribudget://addexpense 딥링크를 호출해 Flutter 앱을 엽니다.
- 링크 사용을 위해 Info.plist에 CFBundleURLTypes - CFBundleURLSchemes에 wooribudget을 추가합니다.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>wooribudget</string>
</array>
</dict>
</array>
iOS 위젯 확장의 메인 진입점으로, 실제로 홈 화면이나 위젯 갤러리에서 보여질 위젯의 구성과 메타데이터를 정의합니다.
기본적으로 위젯은 3가지 종류의 모양이 존재하는데, 가로로 긴 버전만 제공하기 위해 supportedFamilies에 systemMedium을 추가했습니다.
struct WooriWidget: Widget {
let kind: String = "WooriWidget"
let bgColor = Color(red: 95/255, green: 148/255, blue: 247/255)
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
if #available(iOS 17.0, *) {
WooriWidgetEntryView(entry: entry)
.containerBackground(bgColor, for: .widget)
} else {
WooriWidgetEntryView(entry: entry)
.padding()
.background(bgColor)
}
}
.configurationDisplayName("우리") // 미리보기에 표시되는 문구
.description("우리가 쓰려고 만든 공유가계부")
.supportedFamilies([.systemMedium]) // 가로로 긴 버전만 제공
}
}
[코드설명]
let kind: String = "WooriWidget"
- 위젯의 고유 식별자입니다.
- Flutter에서 업데이트를 요청할 때 사용하는 androidName, iOSName에 해당합니다.
2. 앱 내부 데이터 연동하기
딥링크 사용을 위해 uni_link를 업데이트합니다.
flutter pub add uni_links
Flutter에서 위젯으로 연결하기 위한 키를 설정합니다.
groupId는 Widget 추가시 입력한 App Groups의 이름입니다.
const groupId = "group.woori.homeWidget";
const widgetName = "WooriWidget";
앱 진입시 지출액을 위젯으로 보여주기 위해 main.dart에 아래와 같이 함수를 추가했습니다.
void setHomeWidgetAmount(total) async {
await HomeWidget.setAppGroupId(groupId);
await HomeWidget.saveWidgetData<String>(
'WooriWidgetKey',
'${total >= 0 ? '+' : '-'}${LanguageUtil.money(
language: language,
money: FormatUtil.formatCurrency(total),
)}');
await HomeWidget.updateWidget(
androidName: widgetName, iOSName: widgetName);
}
setHomeWidgetAmount(total);
[코드설명]
await HomeWidget.setAppGroupId(groupId);
- groupId는 앱과 위젯 확장 모두 동일한 Group ID를 써야 합니다.
- iOS에서 앱과 위젯 간 데이터를 공유하기 위해 App Group을 설정합니다.
- Android에서는 내부적으로 무시됩니다.
await HomeWidget.saveWidgetData<String>(
'WooriWidgetKey',
'${total >= 0 ? '+' : '-'}${LanguageUtil.money(
language: language,
money: FormatUtil.formatCurrency(total),
)}');
- 이 값은 Swift 위젯에서 UserDefaults(suiteName:)로 읽어와 entry.content로 표시됩니다.(1. 참고)
마지막으로 수입/지출 버튼 클릭시 호출되는 딥링크 처리를 위해 메인 화면에 코드를 수정합니다.
_handleInitailUri() 함수는 앱이 최초 실행될 때 딥링크 URI를 받아 처리합니다.
StorageService().getInitialUriProcess()를 체크하는 이유는 딥링크 중복 호출을 방지하기 위해서였습니다.(앱 종료 상태에서 딥링크를 통해 화면이 이동되고, 메인 화면으로 왔을 때 다시 딥링크 URI로 이동되는 현상)
체크하는 방법은 SharedPreferences를 통해 호출 플래그값 true/false로 관리했습니다.
StreamSubscription? _sub;
@override
void initState() {
super.initState();
// 앱 실행 시 최초 딥링크 처리
_handleInitialUri();
// 위젯 수입,지출 버튼 클릭 리스너
_sub = uriLinkStream.listen((Uri? uri) {
if (uri != null) {
_handleUri(uri);
}
}, onError: (err) {});
WidgetsBinding.instance.addPostFrameCallback((_) async {
StorageService().saveInitialUri(false);
});
}
Future<void> _handleInitialUri() async {
final isHandled = await StorageService().getInitialUriProcess();
if (isHandled) return;
try {
final uri = await getInitialUri();
if (uri != null) {
_handleUri(uri);
StorageService().saveInitialUri(true);
}
} catch (e) {
// 에러 핸들링
}
}
Future<void> _handleUri(Uri uri) async {
String type = uri.host; //addincome(수입), addexpense(지출)
String date = FormatUtil.convertDateFormat(DateTime.now());
Token tokenInfo = await StorageService().getTokenInfo();
Map<String, String> queryParams = {
'userNo': '${tokenInfo.userNo}',
'date': date,
'type': type
};
Uri pageUri = Uri(
path: '/ledger/create/nstep1',
queryParameters: queryParams,
);
context.go(pageUri.toString());
}
[코드설명]
_sub = uriLinkStream.listen((Uri? uri) {
if (uri != null) {
_handleUri(uri);
}
}, onError: (err) {});
- 앱이 이미 실행 중일 때 위젯에서 wooribudget://addincome 같은 딥링크를 보내면 여기서 처리됩니다.
테스트 결과를 보겠습니다.
[시뮬레이션 실행 결과]
참고자료
'Framework > Flutter' 카테고리의 다른 글
[Flutter]홈 위젯 만들기 iOS편 - 1) 위젯 추가하기 (0) | 2025.06.11 |
---|---|
[Flutter]다국어 구현 (1) | 2025.02.21 |
[Flutter]Text Editor 적용하기 (0) | 2025.01.05 |
[Error]Linker command failed with exit code 1 (use -v to see invocation) (0) | 2025.01.02 |
[Error]Flutter 설치 에러 모음 (0) | 2024.12.18 |
소중한 공감 감사합니다.