Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create meal widget #116

Merged
merged 8 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion Projects/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,31 @@ let project = Project(
dependencies: [
.project(target: "Feature", path: .relativeToRoot("Projects/Feature")),
.project(target: "Repository", path: .relativeToRoot("Projects/Data")),
.project(target: "DIContainer", path: .relativeToRoot("Projects/DIContainer"))
.project(target: "DIContainer", path: .relativeToRoot("Projects/DIContainer")),
.target(name: "DodamDodamWidget")
]
),
.target(
name: "DodamDodamWidget",
destinations: [.iPhone],
product: .appExtension,
bundleId: "com.b1nd.dodam.student.WidgetExtension",
deploymentTargets: .iOS("15.0"),
infoPlist: .extendingDefault(with: [
"CFBundleDisplayName": "$(PRODUCT_NAME)",
"NSExtension": [
"NSExtensionPointIdentifier": "com.apple.widgetkit-extension",
]
]),
sources: ["iOS-Widget/Source/**"],
resources: ["iOS-Widget/Resource/**"],
scripts: [.swiftLint],
dependencies: [
.project(target: "Domain", path: .relativeToRoot("Projects/Domain")),
.project(target: "Repository", path: .relativeToRoot("Projects/Data")),
.project(target: "DIContainer", path: .relativeToRoot("Projects/DIContainer")),
.project(target: "Shared", path: .relativeToRoot("Projects/Shared")),
.external(name: "DDS")
]
)
]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 패치에 대한 간단한 리뷰입니다:

  1. 의존성 중복: RepositoryDIContainer가 기존 의존성과 새로운 타겟 모두에 추가되었습니다. 각 타겟에서 필요 유무를 검토하여 중복을 제거하는 것이 좋습니다.

  2. 배포 대상: iOS 15.0에 배포하도록 설정한 것은 좋은 접근이지만, 사용자 기반에 따라 이전 버전과의 호환성을 고려할 필요가 있습니다.

  3. 스크립트 지정: .swiftLint 스크립트를 추가하였는데, 이를 통해 코드 일관성을 유지할 수 있습니다. 그러나 해당 스크립트 실행이 실패할 경우 빌드 오류가 발생할 수 있으므로, 적절한 환경설정을 필요합니다.

  4. 잘못된 정보 플리스트 키: infoPlist"NSExtensionPointIdentifier"형식은 맞으나 값이 올바른지 확인하고, 추가적인 필수가 필요한지 검토해야 합니다.

  5. 리소스 및 소스 경로: "iOS-Widget/Source/**""iOS-Widget/Resource/**" 경로가 존재하는지 확인하고, 확장자로 필터링이 제대로 이루어지는지도 체크해야 합니다.

제안:

  • 코드 주석을 추가하여 의존성 또는 설정 이유를 설명하면 가독성을 높일 수 있습니다.
  • 테스트 케이스 및 문서화를 강화하여 새롭게 추가된 위젯 관련 기능이 원활히 작동하는지 확인하는 절차를 마련하시기 바랍니다.

Expand Down
25 changes: 25 additions & 0 deletions Projects/App/iOS-Widget/Source/AppMainWidget.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// AppMainWidget.swift
// ProjectDescriptionHelpers
//
// Created by hhhello0507 on 7/23/24.
//

import Foundation
import WidgetKit
import SwiftUI
import DDS
import DIContainer

@main
struct AppMainWidget: WidgetBundle {

init() {
Pretendard.register()
DependencyProvider.shared.register()
}

var body: some Widget {
DodamMealWidget()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// DataSourceAssembly.swift
// DodamDodam
//
// Created by Mercen on 3/28/24.
//

import Swinject
import DataSource
import Network
import Repository

struct DataSourceAssembly: Assembly {

func assemble(container: Container) {
container.register(MealDataSource.self) {
.init(remote: $0.resolve(MealRemote.self)!)
}.inObjectScope(.container)
}
}
18 changes: 18 additions & 0 deletions Projects/App/iOS-Widget/Source/DI/Assembly/RemoteAssembly.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// RemoteAssembly.swift
// DodamDodam
//
// Created by Mercen on 3/28/24.
//

import Swinject
import Network

struct RemoteAssembly: Assembly {

func assemble(container: Container) {
container.register(MealRemote.self) { _ in
.init()
}.inObjectScope(.container)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// RepositoryAssembly.swift
// DodamDodam
//
// Created by Mercen on 3/28/24.
//

import Swinject
import Repository
import DataSource
import Domain

struct RepositoryAssembly: Assembly {

func assemble(container: Container) {
container.register((any MealRepository).self) {
MealRepositoryImpl(dataSource: $0.resolve(MealDataSource.self)!)
}.inObjectScope(.container)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// DependencyProvider.swift
// DodamDodam
//
// Created by Mercen on 3/13/24.
//

import Swinject
import DIContainer

public extension DependencyProvider {

func register() {
Container.loggingFunction = nil
_ = Assembler(
[
DataSourceAssembly(),
RemoteAssembly(),
RepositoryAssembly()
],
container: self.container
)
}
}
27 changes: 27 additions & 0 deletions Projects/App/iOS-Widget/Source/Entry/MealEntry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// MealEntry.swift
// DodamDodam
//
// Created by hhhello0507 on 7/23/24.
//

import WidgetKit
import Domain

struct MealEntry: TimelineEntry {
let date: Date
let meal: MealResponse
}

extension MealEntry {
static let empty = MealEntry(
date: .now,
meal: MealResponse(
exists: true,
date: .now,
breakfast: nil,
lunch: nil,
dinner: nil
)
)
}
90 changes: 90 additions & 0 deletions Projects/App/iOS-Widget/Source/Provider/MealProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// DodamMealProvider.swift
// DodamDodam
//
// Created by hhhello0507 on 7/23/24.
//

import WidgetKit
import Domain
import Shared
import DIContainer

struct MealProvider: TimelineProvider {

@Inject var mealRepository: any MealRepository
func placeholder(in context: Context) -> MealEntry {

let meal = Meal(
details: [
.init(name: "퀴노아녹두죽", allergies: []),
.init(name: "채소샐러드", allergies: []),
.init(name: "우자드레싱", allergies: []),
.init(name: "깍두기", allergies: []),
.init(name: "초코첵스시리얼+우ㅁㅁㅁㅁㅁ유", allergies: []),
.init(name: "초코크로와상", allergies: [])
],
calorie: 941
)
let entry = MealEntry(
date: .now,
meal: MealResponse(
exists: true,
date: .now,
breakfast: meal,
lunch: meal,
dinner: meal
)
)
return entry
}

func getSnapshot(in context: Context, completion: @escaping (MealEntry) -> Void) {
Task {
var currentDate = Date.now
if getDate(.hour, date: currentDate) >= 20 {
currentDate = Calendar.current.date(byAdding: .day, value: 1, to: currentDate)!
}
do {
let year = getDate(.year, date: currentDate)
let month = getDate(.month, date: currentDate)
let day = getDate(.day, date: currentDate)
let request = FetchMealRequest(year: year, month: month, day: day)
let meal = try await mealRepository.fetchMeal(request)
let entry = MealEntry(
date: currentDate,
meal: meal
)
completion(entry)
} catch {
let entry = MealEntry.empty
completion(entry)
}
}
}

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) ?? .init()
Task {
var currentDate = Date()
// 오후 8시가 지나면 다음날로
if getDate(.hour, date: currentDate) >= 20 {
currentDate = Calendar.current.date(byAdding: .day, value: 1, to: currentDate)!
}

do {
let meal = try await mealRepository.fetchMeal(.init(year: getDate(.year, date: currentDate), month: getDate(.month, date: currentDate), day: getDate(.day, date: currentDate)))
let entry = MealEntry(
date: currentDate,
meal: meal
)
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
} catch {
let entry = MealEntry.empty
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
}
}
129 changes: 129 additions & 0 deletions Projects/App/iOS-Widget/Source/Widget/DodamMealWidget.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//
// DodamMealWidget.swift
// DodamDodam
//
// Created by hhhello0507 on 7/23/24.
//

import SwiftUI
import WidgetKit
import Shared
import DDS
import Domain

struct DodamMealWidget: Widget {

@Environment(\.widgetFamily) private var widgetFamily

private let widgetFamilyList: [WidgetFamily] = if #available(iOSApplicationExtension 16.0, *) {
[.systemSmall, .systemMedium, .accessoryRectangular, .accessoryCircular]
} else {
[.systemSmall, .systemMedium]
}

var body: some WidgetConfiguration {
StaticConfiguration(
kind: "DodamMealWidget",
provider: MealProvider()
) { entry in
SmallDodamMealWidget(entry: entry)
}
.configurationDisplayName("급식")
.description("아침, 점심, 저녁 위젯으로 빠르고 쉽게 확인해요")
.contentMarginsDisabled()
.supportedFamilies(widgetFamilyList)
}
}

struct SmallDodamMealWidget: View {

@State private var selection = 0

private let entry: MealProvider.Entry

init(entry: MealProvider.Entry) {
self.entry = entry
Pretendard.register()
}

var body: some View {
Group {
if #available(iOSApplicationExtension 17.0, *) {
label(meal: entry.meal)
.containerBackground(for: .widget) {
Dodam.color(DodamColor.Background.neutral)
}
} else {
label(meal: entry.meal)
}
}
.padding(8)
}

@ViewBuilder
private func label(meal: MealResponse) -> some View {
let idx = switch (getDate(.hour, date: .now), getDate(.minute, date: .now)) {
// 아침: ~ 8:20
case (0...8, _), (8, ..<20): 0
// 점심: 8:21 ~ 13:30
case (8, 21...60), (8...13, _), (13, 0..<30): 1
// 저녁: 13:31 ~ 19:10
case (13, 0...30), (13...19, _), (19, 0..<10): 2
default: -1
}
let (tag, meal): (String, Meal?) = switch idx {
case 0: ("아침", meal.breakfast)
case 1: ("점심", meal.lunch)
case 2: ("저녁", meal.dinner)
default: ("", nil)
}
content(tag: tag, meal: meal)
}

@ViewBuilder
private func content(tag: String, meal: Meal?) -> some View {
VStack(spacing: 4) {
HStack {
Text(tag)
.foreground(DodamColor.Static.white)
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(DodamColor.Primary.normal)
.clipShape(.large)
.font(.footnote)
Spacer()
if let meal {
Text("\(Int(meal.calorie.rounded()))Kcal")
.font(.caption)
.foreground(DodamColor.Label.alternative)
}
}
VStack(alignment: .leading, spacing: 0) {
if let meal {
ForEach(meal.details, id: \.self) {
Text($0.name)
.lineLimit(1)
.truncationMode(.tail)
.font(.caption)
.foreground(DodamColor.Label.normal)
.frame(maxWidth: .infinity, alignment: .leading)
}
} else {
Text("급식이 없어요")
.caption1(.medium)
.foreground(DodamColor.Label.normal)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
.padding(8)
.frame(maxHeight: .infinity)
.background(DodamColor.Background.normal)
.clipShape(.large)
}
.background(DodamColor.Background.neutral)
}
}

#Preview {
SmallDodamMealWidget(entry: .empty)
}
Loading