{"id":15038464,"url":"https://github.com/openalloc/swiftdetailer","last_synced_at":"2025-04-09T23:40:48.031Z","repository":{"id":43428408,"uuid":"463796829","full_name":"openalloc/SwiftDetailer","owner":"openalloc","description":"A multi-platform SwiftUI component for editing fielded data","archived":false,"fork":false,"pushed_at":"2023-05-01T23:59:20.000Z","size":488,"stargazers_count":6,"open_issues_count":3,"forks_count":3,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-04-08T17:53:24.060Z","etag":null,"topics":["data-editor","data-validation","details-view","swift-lang","swift-language","swift-library","swiftui","swiftui-components","swiftui-library"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/openalloc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-02-26T08:25:43.000Z","updated_at":"2025-01-26T20:02:49.000Z","dependencies_parsed_at":"2022-09-09T08:20:51.575Z","dependency_job_id":null,"html_url":"https://github.com/openalloc/SwiftDetailer","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openalloc%2FSwiftDetailer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openalloc%2FSwiftDetailer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openalloc%2FSwiftDetailer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openalloc%2FSwiftDetailer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/openalloc","download_url":"https://codeload.github.com/openalloc/SwiftDetailer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248131466,"owners_count":21052819,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data-editor","data-validation","details-view","swift-lang","swift-language","swift-library","swiftui","swiftui-components","swiftui-library"],"created_at":"2024-09-24T20:38:36.331Z","updated_at":"2025-04-09T23:40:48.012Z","avatar_url":"https://github.com/openalloc.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SwiftDetailer\n\nA multi-platform SwiftUI component for editing fielded data.\n\nAvailable as an open source library to be incorporated in SwiftUI apps.\n\n_SwiftDetailer_ is part of the [OpenAlloc](https://github.com/openalloc) family of open source Swift software tools.\n\nmacOS | iOS\n:---:|:---:\n![](https://github.com/openalloc/SwiftDetailer/blob/main/Images/macOSb.png)  |  ![](https://github.com/openalloc/SwiftDetailer/blob/main/Images/iOSc.png)\n\n## Features\n\n* Convenient editing (and viewing) of fielded data in your app\n* Presently targeting macOS v11+ and iOS v14+\\*\\*\n* Supporting both value and reference semantics (including Core Data, which uses the latter)\n* Can be used with various collection container types, such as `List`, `Table`, `LazyVStack`, etc.\\*\n* `.editDetailer` View modifier, to support (bound, read/write) view \n* `.viewDetailer` View modifier, to support (unbound, read-only) view\n* Option to add new item\n* Option to delete item\n* Option to validate at field-level, with indicators\n* Option to validate at record-level, with alert view\n* Optional `DetailerMenu` package available, for convenient invocation\n* Minimal use of View type erasure (i.e., use of `AnyView`)\n* No external dependencies!\n\n\\* And also the companion [Tabler](https://github.com/openalloc/SwiftTabler) component (by the same author)\n\n\\*\\* Other platforms like macCatalyst, iPad on Mac, watchOS, tvOS, etc. are poorly supported, if at all. Please contribute to improve support!\n\n## Detailer Example\n\nAn example, showing the basic use of _Detailer_. As a baseline, start with a display of rows of data in a `List`:\n\n```swift\nimport SwiftUI\n\nstruct Fruit: Identifiable {\n    var id: String\n    var name: String\n    var weight: Double\n    var color: Color\n}\n\nstruct ContentView: View {\n\n    @State private var fruits: [Fruit] = [\n        Fruit(id: \"🍌\", name: \"Banana\", weight: 118, color: .brown),\n        Fruit(id: \"🍓\", name: \"Strawberry\", weight: 12, color: .red),\n        Fruit(id: \"🍊\", name: \"Orange\", weight: 190, color: .orange),\n        Fruit(id: \"🥝\", name: \"Kiwi\", weight: 75, color: .green),\n        Fruit(id: \"🍇\", name: \"Grape\", weight: 7, color: .purple),\n        Fruit(id: \"🫐\", name: \"Blueberry\", weight: 2, color: .blue),\n    ]\n\n    var body: some View {\n        List(fruits) { fruit in\n            HStack {\n                Text(fruit.id)\n                Text(fruit.name).foregroundColor(fruit.color)\n                Spacer()\n                Text(String(format: \"%.0f g\", fruit.weight))\n            }\n        }\n    }\n}\n```\n\nThen, to add basic support for a detail page, targeting both macOS and iOS, you'll need to:\n\n* A. Import the `SwiftDetailer` and `SwiftDetailerMenu` packages.\n* B. Add state property for element to edit, and a typealias for cleaner code.\n* C. Give each row a menu (context for macOS; swipe for iOS).\n* D. Add a call to `editDetailer`, available as a modifier.\n* E. Include a `Form` containing the fields to edit, and ...\n* F. Add an action handler to save a modified `Fruit` element.\n\nThese are shown (and annotated) in the modified code below:\n\n```swift\nimport SwiftUI\nimport Detailer         // A\nimport DetailerMenu\n\nstruct Fruit: Identifiable {\n    var id: String\n    var name: String\n    var weight: Double\n    var color: Color\n}\n\nstruct ContentView: View {\n\n    @State private var fruits: [Fruit] = [\n        Fruit(id: \"🍌\", name: \"Banana\", weight: 118, color: .brown),\n        Fruit(id: \"🍓\", name: \"Strawberry\", weight: 12, color: .red),\n        Fruit(id: \"🍊\", name: \"Orange\", weight: 190, color: .orange),\n        Fruit(id: \"🥝\", name: \"Kiwi\", weight: 75, color: .green),\n        Fruit(id: \"🍇\", name: \"Grape\", weight: 7, color: .purple),\n        Fruit(id: \"🫐\", name: \"Blueberry\", weight: 2, color: .blue),\n    ]\n    \n    @State private var toEdit: Fruit? = nil // B\n\n    typealias Context = DetailerContext\u003cFruit\u003e\n\n    var body: some View {\n        List(fruits) { fruit in\n            HStack {\n                Text(fruit.id)\n                Text(fruit.name).foregroundColor(fruit.color)\n                Spacer()\n                Text(String(format: \"%.0f g\", fruit.weight))\n            }\n            .modifier(menu(fruit)) // C\n        }\n        .editDetailer(.init(onSave: saveAction),\n                      toEdit: $toEdit,\n                      originalID: toEdit?.id,\n                      detailContent: editDetail) // D\n    }\n    \n    // E\n    private func editDetail(ctx: Context, fruit: Binding\u003cFruit\u003e) -\u003e some View {\n        Form {\n            TextField(\"ID\", text: fruit.id)\n            TextField(\"Name\", text: fruit.name)\n            TextField(\"Weight\", value: fruit.weight, formatter: NumberFormatter())\n            ColorPicker(\"Color\", selection: fruit.color)\n        }\n    }\n    \n    // F\n    private func saveAction(ctx: Context, fruit: Fruit) {\n        if let n = fruits.firstIndex(where: { $0.id == fruit.id }) {\n            fruits[n] = fruit\n        }\n    }\n    \n    // C\n#if os(macOS)\n    private func menu(_ fruit: Fruit) -\u003e EditDetailerContextMenu\u003cFruit\u003e {\n        EditDetailerContextMenu(fruit) { toEdit = $0 }\n    }\n#elseif os(iOS)\n    private func menu(_ fruit: Fruit) -\u003e EditDetailerSwipeMenu\u003cFruit\u003e {\n        EditDetailerSwipeMenu(fruit) { toEdit = $0 }\n    }\n#endif\n}\n```\n\nOn macOS, ctrl-click (or right-click) on a row to invoke the context menu. On iOS, swipe the row to invoke the menu.\n\nFor a full implementation, with ability to add new records, see the _DetailerDemo_ project (link below). It extends the example with operations to add new records, delete records, and validate input. \n\nIt shows _Detailer_ used with `LazyVGrid` and `Table` containers.\n\n## Menuing\n\nYou can invoke _Detailer_ by various methods. One way is via context or swipe menus. For _optional_ menu support see [SwiftDetailerMenu](https://github.com/openalloc/SwiftDetailerMenu).\n\nThe use of context menus for macOS and iOS:\n\nmacOS | iOS\n:---:|:---:\n![](https://github.com/openalloc/SwiftDetailer/blob/main/Images/macOSa.png)  |  ![](https://github.com/openalloc/SwiftDetailer/blob/main/Images/iOSb.png)\n\nAnd swipe menu for iOS:\n\niOS\n:---:\n![](https://github.com/openalloc/SwiftDetailer/blob/main/Images/iOSa.png)\n\n## Validation\n\nYou can _optionally_ validate data using _Detailer_. Two approaches are available: field and record level.\n\nField and record level validation can be used individually or in concert.\n\n### Field-level validation\n\nThis is a *lightweight* form of validation where individual fields get a closure to test their validity. As they are executed with each change, they should NOT run expensive operations, like hitting a remote server.\n\nField-level validation is implemented as modifiers in the detail form, as in this example of three(3) validators used in the demo app:\n\n```swift\nprivate func editDetail(ctx: DetailerContext\u003cFruit\u003e, fruit: Binding\u003cFruit\u003e) -\u003e some View {\n    Form {\n        TextField(\"ID\", text: fruit.id)\n            .validate(ctx, fruit, \\.id) { $0.count \u003e 0 }\n        TextField(\"Name\", text: fruit.name)\n            .validate(ctx, fruit, \\.name) { $0.count \u003e 0 }\n        TextField(\"Weight\", value: fruit.weight, formatter: NumberFormatter())\n            .validate(ctx, fruit, \\.weight) { $0 \u003e 0 }\n        ColorPicker(\"Color\", selection: fruit.color)\n    }\n}\n```\n\nThe first two are testing string length. The third is testing the numerical value.\n\nBy default, invalid fields will be suffixed with a warning icon, currently an \"exclamationmark.triangle\", as displayed in the images above. This image is configurable.\n\nAll field-level validations must return `true` for the `Save` button to be enabled.\n\n**TIP**: for consistent margin spacing in layout, you can create a validation that always succeeds: `.validate(...) { _ in true }`.\n\n### Record-level validation\n\nThis can be a *heavyweight* form of validation executed when the user presses the `Save` button.\n\nIt's a parameter of the `DetailerConfig` initialization, specifically `onValidate: (Context, Element) -\u003e [String]`.\n\nIn your action handler, test the record and, if okay, return `[]`, an empty string array. Populate the array with messages if invalid. They will be presented to the user in an alert.\n\nIf this validation is used, the user will not be able to save changes until it returns `[]`.\n\n\n## Configuration\n\nDefaults can vary by platform. See the `DetailerConfigDefaults` code for specifics.\n\nThe `can` handlers are typically used to enable or disable controls, such as menu items. They are constrained by the definition of their `on` counterparts.\n\nThe `on` handlers, when defined, will enable the associated operation.\n\n- `minWidth: CGFloat` - minimum sheet width; default varies by platform\n- `canEdit: (Element) -\u003e Bool` - per-element modification enabling, if `onSave` defined; defaults to `{ _ in true }`\n- `canDelete: (Element) -\u003e Bool` - per-element deletion enabling, if `onDelete` defined; defaults to `{ _ in true }`\n- `onDelete: ((Element) -\u003e Void)?` - handler for deletion; defaults to `nil`\n- `onValidate: (Context, Element) -\u003e [String]` - handler for *heavyweight* validation; defaults to  `{ _, _ in [] }`\n- `onSave: ((Context, Element) -\u003e Void)?` - handler for user save; defaults to `nil`\n- `onCancel: (Context, Element) -\u003e Void` - handler for user cancel; defaults to `{ _, _ in }`\n- `titler: ((Element) -\u003e String)?` - handler for title generation; defaults to `nil`\n- `validateIndicator: (Bool) -\u003e AnyView` - defaults to \"exclamationmark.triangle\" image, with additional attributes\n\n## See Also\n\n* [SwiftDetailerMenu](https://github.com/openalloc/SwiftDetailerMenu) - optional menuing for _Detailer_, to avoid rolling your own\n\nApps demonstrating _Detailer_:\n\n* [DetailerDemo](https://github.com/openalloc/DetailerDemo) - basic use of _Detailer_\n\nThis library is a member of the _OpenAlloc Project_.\n\n* [_OpenAlloc_](https://openalloc.github.io) - product website for all the _OpenAlloc_ apps and libraries\n* [_OpenAlloc Project_](https://github.com/openalloc) - Github site for the development project, including full source code\n\n## License\n\nCopyright 2021, 2022 OpenAlloc LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at\n\n[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)\n\nUnless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.\n\n## Contributing\n\nContributions are welcome. You are encouraged to submit pull requests to fix bugs, improve documentation, or offer new features. \n\nThe pull request need not be a production-ready feature or fix. It can be a draft of proposed changes, or simply a test to show that expected behavior is buggy. Discussion on the pull request can proceed from there.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenalloc%2Fswiftdetailer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopenalloc%2Fswiftdetailer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenalloc%2Fswiftdetailer/lists"}