Skip and docs
This commit is contained in:
177
pedi-foods/Sources/PediFoods/ContentView.swift
Normal file
177
pedi-foods/Sources/PediFoods/ContentView.swift
Normal file
@@ -0,0 +1,177 @@
|
||||
import SwiftUI
|
||||
|
||||
enum ContentTab: String, Hashable {
|
||||
case welcome, home, settings
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@AppStorage("tab") var tab = ContentTab.welcome
|
||||
@AppStorage("name") var welcomeName = "Skipper"
|
||||
@AppStorage("appearance") var appearance = ""
|
||||
@State var viewModel = ViewModel()
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $tab) {
|
||||
NavigationStack {
|
||||
WelcomeView(welcomeName: $welcomeName)
|
||||
}
|
||||
.tabItem { Label("Welcome", systemImage: "heart.fill") }
|
||||
.tag(ContentTab.welcome)
|
||||
|
||||
NavigationStack {
|
||||
ItemListView()
|
||||
.navigationTitle(Text("\(viewModel.items.count) Items"))
|
||||
}
|
||||
.tabItem { Label("Home", systemImage: "house.fill") }
|
||||
.tag(ContentTab.home)
|
||||
|
||||
NavigationStack {
|
||||
SettingsView(appearance: $appearance, welcomeName: $welcomeName)
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
|
||||
.tag(ContentTab.settings)
|
||||
}
|
||||
.environment(viewModel)
|
||||
.preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct WelcomeView : View {
|
||||
@State var heartBeating = false
|
||||
@Binding var welcomeName: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Text("Hello [\(welcomeName)](https://skip.tools)!")
|
||||
.padding()
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundStyle(.red)
|
||||
.scaleEffect(heartBeating ? 1.5 : 1.0)
|
||||
.animation(.easeInOut(duration: 1).repeatForever(), value: heartBeating)
|
||||
.task { heartBeating = true }
|
||||
}
|
||||
.font(.largeTitle)
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemListView : View {
|
||||
@Environment(ViewModel.self) var viewModel: ViewModel
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(viewModel.items) { item in
|
||||
NavigationLink(value: item) {
|
||||
Label {
|
||||
Text(item.itemTitle)
|
||||
} icon: {
|
||||
if item.favorite {
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundStyle(.yellow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete { offsets in
|
||||
viewModel.items.remove(atOffsets: offsets)
|
||||
}
|
||||
.onMove { fromOffsets, toOffset in
|
||||
viewModel.items.move(fromOffsets: fromOffsets, toOffset: toOffset)
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: Item.self) { item in
|
||||
ItemView(item: item)
|
||||
.navigationTitle(item.itemTitle)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup {
|
||||
Button {
|
||||
withAnimation {
|
||||
viewModel.items.insert(Item(), at: 0)
|
||||
}
|
||||
} label: {
|
||||
Label("Add", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemView : View {
|
||||
@State var item: Item
|
||||
@Environment(ViewModel.self) var viewModel: ViewModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
TextField("Title", text: $item.title)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Toggle("Favorite", isOn: $item.favorite)
|
||||
DatePicker("Date", selection: $item.date)
|
||||
Text("Notes").font(.title3)
|
||||
TextEditor(text: $item.notes)
|
||||
.border(Color.secondary, width: 1.0)
|
||||
}
|
||||
.navigationBarBackButtonHidden()
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") {
|
||||
viewModel.save(item: item)
|
||||
dismiss()
|
||||
}
|
||||
.disabled(!viewModel.isUpdated(item))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView : View {
|
||||
@Binding var appearance: String
|
||||
@Binding var welcomeName: String
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
TextField("Name", text: $welcomeName)
|
||||
Picker("Appearance", selection: $appearance) {
|
||||
Text("System").tag("")
|
||||
Text("Light").tag("light")
|
||||
Text("Dark").tag("dark")
|
||||
}
|
||||
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
|
||||
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
Text("Version \(version) (\(buildNumber))")
|
||||
}
|
||||
HStack {
|
||||
PlatformHeartView()
|
||||
Text("Powered by [Skip](https://skip.tools)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A view that shows a blue heart on iOS and a green heart on Android.
|
||||
struct PlatformHeartView : View {
|
||||
var body: some View {
|
||||
#if os(Android)
|
||||
ComposeView {
|
||||
HeartComposer()
|
||||
}
|
||||
#else
|
||||
Text(verbatim: "💙")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if SKIP
|
||||
/// Use a ContentComposer to integrate Compose content. This code will be transpiled to Kotlin.
|
||||
struct HeartComposer : ContentComposer {
|
||||
@Composable func Compose(context: ComposeContext) {
|
||||
androidx.compose.material3.Text("💚", modifier: context.modifier)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
59
pedi-foods/Sources/PediFoods/PediFoodsApp.swift
Normal file
59
pedi-foods/Sources/PediFoods/PediFoodsApp.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
import Foundation
|
||||
import SkipFuse
|
||||
import SwiftUI
|
||||
|
||||
/// A logger for the PediFoods module.
|
||||
let logger: Logger = Logger(subsystem: "com.br.pedifoods.app", category: "PediFoods")
|
||||
|
||||
/// The shared top-level view for the app, loaded from the platform-specific App delegates below.
|
||||
///
|
||||
/// The default implementation merely loads the `ContentView` for the app and logs a message.
|
||||
/* SKIP @bridge */public struct PediFoodsRootView : View {
|
||||
/* SKIP @bridge */public init() {
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ContentView()
|
||||
.task {
|
||||
logger.info("Skip app logs are viewable in the Xcode console for iOS; Android logs can be viewed in Studio or using adb logcat")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Global application delegate functions.
|
||||
///
|
||||
/// These functions can update a shared observable object to communicate app state changes to interested views.
|
||||
/* SKIP @bridge */public final class PediFoodsAppDelegate : Sendable {
|
||||
/* SKIP @bridge */public static let shared = PediFoodsAppDelegate()
|
||||
|
||||
private init() {
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onInit() {
|
||||
logger.debug("onInit")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onLaunch() {
|
||||
logger.debug("onLaunch")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onResume() {
|
||||
logger.debug("onResume")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onPause() {
|
||||
logger.debug("onPause")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onStop() {
|
||||
logger.debug("onStop")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onDestroy() {
|
||||
logger.debug("onDestroy")
|
||||
}
|
||||
|
||||
/* SKIP @bridge */public func onLowMemory() {
|
||||
logger.debug("onLowMemory")
|
||||
}
|
||||
}
|
||||
563
pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings
Normal file
563
pedi-foods/Sources/PediFoods/Resources/Localizable.xcstrings
Normal file
@@ -0,0 +1,563 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"%lld Items" : {
|
||||
"comment" : "Header title for a list that contains the number of items",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld elementos"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld éléments"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "アイテム数 %lld"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld 个条目"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Add" : {
|
||||
"comment" : "Button in items list that will cause a new item to be added",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Añadir"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Ajouter"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "追加"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "添加"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Appearance" : {
|
||||
"comment" : "Settings select label for the interface style of the controls (light, dark, or default)",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Apariencia"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Apparence"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "外観"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "外观"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Cancel" : {
|
||||
"comment" : "Button title indicating that the operation should be cancelled",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cancelar"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Annuler"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "キャンセル"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "取消"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Dark" : {
|
||||
"comment" : "Menu item indicating that the appearance should be in dark mode",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Oscuro"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sombre"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ダーク"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "深色"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Date" : {
|
||||
"comment" : "Item editor form label for the Date field",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Fecha"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Date"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "日付"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "日期"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Favorite" : {
|
||||
"comment" : "Item editor title label for marking the item as a favorite",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Favorito"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Favori"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "お気に入り"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "收藏"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hello [%@](https://skip.tools)!" : {
|
||||
"comment" : "Welcome tab contents",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "¡Hola [%@](https://skip.tools)!"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Bonjour [%@](https://skip.tools)!"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "こんにちは [%@](https://skip.tools)"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "你好 [%@](https://skip.tools)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Home" : {
|
||||
"comment" : "Tab bar item title for the Home tab",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Inicio"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Accueil"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ホーム"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "首页"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Light" : {
|
||||
"comment" : "Menu item indicating that the appearance should be in light mode",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Claro"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Clair"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "明るい"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "浅色"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Name" : {
|
||||
"comment" : "Placeholder title for the Name field in a form",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nombre"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nom"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "名前"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "姓名"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Notes" : {
|
||||
"comment" : "Item editor form label for the Notes field",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Notas"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Notes"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ノート"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "笔记"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Powered by [Skip](https://skip.tools)" : {
|
||||
"comment" : "Link markdown text for the Powered by… label",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Impulsado por [Skip](https://skip.tools)"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Fonctionnalités offertes par [Skip](https://skip.tools)"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "[Skip](https://skip.tools) を使って動かしています"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "由[Skip](https://skip.tools)提供支持"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Save" : {
|
||||
"comment" : "Button title indicating that the current contents should be saved",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Guardar"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Enregistrer"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "保存"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "保存"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Settings" : {
|
||||
"comment" : "Tab bar item title for the Settings tab",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Configuración"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Paramètres"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "設定"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "设置"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"System" : {
|
||||
"comment" : "Menu item indicating that the appearance should be in the default system mode",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sistema"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Système"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "システム"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "系统"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Title" : {
|
||||
"comment" : "Label for the item editor form indicating the title of the item",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Título"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Titre"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "タイトル"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "标题"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Version %@ (%@)" : {
|
||||
"comment" : "Settings label showing the current version of the app",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Version %1$@ (%2$@)"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Versión %1$@ (%2$@)"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Version %1$@ (%2$@)"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "バージョン %1$@ (%2$@)"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "版本 %1$@ (%2$@)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Welcome" : {
|
||||
"comment" : "Tab bar item title for the Welcome tab",
|
||||
"localizations" : {
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Bienvenido"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Bienvenue"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ようこそ"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "欢迎"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
12
pedi-foods/Sources/PediFoods/Skip/skip.yml
Normal file
12
pedi-foods/Sources/PediFoods/Skip/skip.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
# Configuration file for https://skip.tools project
|
||||
#
|
||||
# Kotlin dependencies and Gradle build options for this module can be configured here
|
||||
#build:
|
||||
# contents:
|
||||
# - block: 'dependencies'
|
||||
# contents:
|
||||
# - 'implementation("androidx.compose.runtime:runtime")'
|
||||
|
||||
# this is a natively-compiled Skip Fuse module
|
||||
skip:
|
||||
mode: 'native'
|
||||
93
pedi-foods/Sources/PediFoods/ViewModel.swift
Normal file
93
pedi-foods/Sources/PediFoods/ViewModel.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import SkipFuse
|
||||
|
||||
/// The Observable ViewModel used by the application.
|
||||
@Observable public class ViewModel {
|
||||
var items: [Item] = loadItems() {
|
||||
didSet { saveItems() }
|
||||
}
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
func clear() {
|
||||
items.removeAll()
|
||||
}
|
||||
|
||||
func isUpdated(_ item: Item) -> Bool {
|
||||
item != items.first { i in
|
||||
i.id == item.id
|
||||
}
|
||||
}
|
||||
|
||||
func save(item: Item) {
|
||||
items = items.map { i in
|
||||
i.id == item.id ? item : i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An individual item held by the ViewModel
|
||||
struct Item : Identifiable, Hashable, Codable {
|
||||
let id: UUID
|
||||
var date: Date
|
||||
var favorite: Bool
|
||||
var title: String
|
||||
var notes: String
|
||||
|
||||
init(id: UUID = UUID(), date: Date = .now, favorite: Bool = false, title: String = "", notes: String = "") {
|
||||
self.id = id
|
||||
self.date = date
|
||||
self.favorite = favorite
|
||||
self.title = title
|
||||
self.notes = notes
|
||||
}
|
||||
|
||||
var itemTitle: String {
|
||||
!title.isEmpty ? title : dateString
|
||||
}
|
||||
|
||||
var dateString: String {
|
||||
date.formatted(date: .complete, time: .omitted)
|
||||
}
|
||||
|
||||
var dateTimeString: String {
|
||||
date.formatted(date: .abbreviated, time: .shortened)
|
||||
}
|
||||
}
|
||||
|
||||
/// Utilities for defaulting and persising the items in the list
|
||||
extension ViewModel {
|
||||
private static let savePath = URL.applicationSupportDirectory.appendingPathComponent("appdata.json")
|
||||
|
||||
fileprivate static func loadItems() -> [Item] {
|
||||
do {
|
||||
let start = Date.now
|
||||
let data = try Data(contentsOf: savePath)
|
||||
defer {
|
||||
let end = Date.now
|
||||
logger.info("loaded \(data.count) bytes from \(Self.savePath.path) in \(end.timeIntervalSince(start)) seconds")
|
||||
}
|
||||
return try JSONDecoder().decode([Item].self, from: data)
|
||||
} catch {
|
||||
// perhaps the first launch, or the data could not be read
|
||||
logger.warning("failed to load data from \(Self.savePath), using defaultItems: \(error)")
|
||||
let defaultItems = (1...365).map { Date(timeIntervalSinceNow: Double($0 * 60 * 60 * 24 * -1)) }
|
||||
return defaultItems.map({ Item(date: $0) })
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func saveItems() {
|
||||
do {
|
||||
let start = Date.now
|
||||
let data = try JSONEncoder().encode(items)
|
||||
try FileManager.default.createDirectory(at: URL.applicationSupportDirectory, withIntermediateDirectories: true)
|
||||
try data.write(to: Self.savePath)
|
||||
let end = Date.now
|
||||
logger.info("saved \(data.count) bytes to \(Self.savePath.path) in \(end.timeIntervalSince(start)) seconds")
|
||||
} catch {
|
||||
logger.error("error saving data: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user