Skip and docs

This commit is contained in:
Daniel Arantes Loverde
2026-02-04 08:42:32 -03:00
commit 78daaf1927
114 changed files with 3326 additions and 0 deletions

View 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

View 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")
}
}

View 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"
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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'

View 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)")
}
}
}