Implement basic support for views

This commit is contained in:
david-swift 2024-06-10 06:31:23 +02:00
commit 3c17404dc6
32 changed files with 1767 additions and 0 deletions

40
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Bug report
description: Something is not working as expected.
title: Description of the bug
labels: bug
body:
- type: textarea
attributes:
label: Describe the bug
description: >-
A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: >-
Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: >-
A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: >-
Add any other context about the problem here.

View File

@ -0,0 +1,29 @@
name: Components request
description: Suggest an idea for a new component
title: Description of the component request
labels: enhancement
body:
- type: textarea
attributes:
label: Why would you like to add a new component?
placeholder: >-
A clear and concise description of why the component should be added.
validations:
required: false
- type: textarea
attributes:
label: Describe your idea for the implementation.
placeholder: >-
What could the implementation be like in Adwaita?
validations:
required: false
- type: textarea
attributes:
label: Additional context
placeholder: >-
Add any other context about the component request here.
validations:
required: false

View File

@ -0,0 +1,36 @@
name: Feature request
description: Suggest an idea for this project
title: Description of the feature request
labels: enhancement
body:
- type: input
attributes:
label: Is your feature request related to a problem? Please describe.
placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: false
- type: textarea
attributes:
label: Describe the solution you'd like
placeholder: >-
A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
placeholder: >-
A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Additional context
placeholder: >-
Add any other context or screenshots about the feature request here.
validations:
required: true

14
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,14 @@
## Steps
- [ ] Add your name or username and a link to your GitHub profile into the [Contributors.md][1] file.
- [ ] Build the project on your machine. If it does not compile, fix the errors.
- [ ] Describe the purpose and approach of your pull request below.
- [ ] Submit the pull request. Thank you very much for your contribution!
## Purpose
_Describe the problem or feature._
_If there is a related issue, add the link._
## Approach
_Describe how this pull request solves the problem or adds the feature._
[1]: /Contributors.md

57
.github/workflows/docs.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: Deploy Docs
on:
push:
branches: ["main"]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
Deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Install Libadwaita
run: |
brew update
brew install libadwaita
sed -i '' 's/-I..includedir.//g' $(brew --prefix)/Library/Homebrew/os/mac/pkgconfig/*/libffi.pc
- name: Clone DocC Repo
run: |
git clone https://github.com/AparokshaUI/Adwaita.docc Sources/Adwaita/Adwaita.docc
rm Sources/Adwaita/Adwaita.docc/LICENSE.md
rm Sources/Adwaita/Adwaita.docc/README.md
y | rm -r Sources/Adwaita/Adwaita.docc/.git
- name: Build Docs
run: |
xcrun xcodebuild docbuild \
-scheme Adwaita \
-destination 'generic/platform=macOS' \
-derivedDataPath "$PWD/.derivedData"
xcrun docc process-archive transform-for-static-hosting \
"$PWD/.derivedData/Build/Products/Debug/Adwaita.doccarchive" \
--output-path "docs" \
--hosting-base-path "adwaita-swift"
- name: Modify Docs
run: |
echo "<script>window.location.href += \"/documentation/adwaita\"</script>" > docs/index.html;
sed -i '' 's/#06f/#ea3358/g' docs/css/documentation-topic~topic~tutorials-overview.d6f5411c.css
sed -i '' 's/,2px/,10px/g' docs/css/index.038e887c.css
- name: Upload Artifact
uses: actions/upload-pages-artifact@v3
with:
path: 'docs'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

30
.github/workflows/swiftlint.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: SwiftLint
on:
push:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
pull_request:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
workflow_dispatch:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
jobs:
SwiftLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --strict
env:
WORKING_DIRECTORY: Source

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
/Package.resolved
.Ulysses-Group.plist
/.docc-build
/io.github.AparokshaUI.Generation.json
/.vscode

165
.swiftlint.yml Normal file
View File

@ -0,0 +1,165 @@
# Opt-In Rules
opt_in_rules:
- anonymous_argument_in_multiline_closure
- array_init
- attributes
- closure_body_length
- closure_end_indentation
- closure_spacing
- collection_alignment
- comma_inheritance
- conditional_returns_on_newline
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
- contains_over_range_nil_comparison
- convenience_type
- discouraged_none_name
- discouraged_object_literal
- discouraged_optional_boolean
- discouraged_optional_collection
- empty_collection_literal
- empty_count
- empty_string
- enum_case_associated_values_count
- explicit_init
- fallthrough
- file_header
- file_name
- file_name_no_space
- first_where
- flatmap_over_map_reduce
- force_unwrapping
- function_default_parameter_at_end
- identical_operands
- implicit_return
- implicitly_unwrapped_optional
- joined_default_parameter
- last_where
- legacy_multiple
- let_var_whitespace
- literal_expression_end_indentation
- local_doc_comment
- lower_acl_than_parent
- missing_docs
- modifier_order
- multiline_arguments
- multiline_arguments_brackets
- multiline_function_chains
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- no_extension_access_modifier
- no_grouping_extension
- no_magic_numbers
- number_separator
- operator_usage_whitespace
- optional_enum_case_matching
- prefer_self_in_static_references
- prefer_self_type_over_type_of_self
- prefer_zero_over_explicit_init
- prohibited_interface_builder
- redundant_nil_coalescing
- redundant_type_annotation
- return_value_from_void_function
- shorthand_optional_binding
- sorted_first_last
- sorted_imports
- static_operator
- strict_fileprivate
- switch_case_on_newline
- toggle_bool
- trailing_closure
- type_contents_order
- unneeded_parentheses_in_closure_argument
- yoda_condition
# Disabled Rules
disabled_rules:
- block_based_kvo
- class_delegate_protocol
- dynamic_inline
- is_disjoint
- no_fallthrough_only
- notification_center_detachment
- ns_number_init_as_function_reference
- nsobject_prefer_isequal
- private_over_fileprivate
- redundant_objc_attribute
- self_in_property_initialization
- todo
- unavailable_condition
- valid_ibinspectable
- xctfail_message
# Custom Rules
custom_rules:
github_issue:
name: 'GitHub Issue'
regex: '//.(TODO|FIXME):.(?!.*(https://github\.com/AparokshaUI/Meta/issues/\d))'
message: 'The related GitHub issue must be included in a TODO or FIXME.'
severity: warning
fatal_error:
name: 'Fatal Error'
regex: 'fatalError.*\(.*\)'
message: 'Fatal error should not be used.'
severity: error
enum_case_parameter:
name: 'Enum Case Parameter'
regex: 'case [a-zA-Z0-9]*\([a-zA-Z0-9\.<>?,\n\t =]+\)'
message: 'The associated values of an enum case should have parameters.'
severity: warning
tab:
name: 'Whitespaces Instead of Tab'
regex: '\t'
message: 'Spaces should be used instead of tabs.'
severity: warning
# Thanks to the creator of the SwiftLint rule
# "empty_first_line"
# https://github.com/coteditor/CotEditor/blob/main/.swiftlint.yml
# in the GitHub repository
# "CotEditor"
# https://github.com/coteditor/CotEditor
empty_first_line:
name: 'Empty First Line'
regex: '(^[ a-zA-Z ]*(?:protocol|extension|class|struct) (?!(?:var|let))[ a-zA-Z:]*\{\n *\S+)'
message: 'There should be an empty line after a declaration'
severity: error
# Analyzer Rules
analyzer_rules:
- unused_declaration
- unused_import
# Options
file_header:
required_pattern: '(// swift-tools-version: .+)?//\n// .*.swift\n// Meta\n//\n// Created by .* on .*\.(\n// Edited by (.*,)+\.)*\n(\n// Thanks to .* for the .*:\n// ".*"\n// https://.* \(\d\d.\d\d.\d\d\))*//\n'
missing_docs:
warning: [internal, private]
error: [open, public]
excludes_extensions: false
excludes_inherited_types: false
type_contents_order:
order:
- case
- type_alias
- associated_type
- type_property
- instance_property
- ib_inspectable
- ib_outlet
- subscript
- initializer
- deinitializer
- subtype
- type_method
- view_life_cycle_method
- ib_action
- other_method
excluded:
- Tests/
- .build/

40
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,40 @@
# Contributing
Thank you very much for taking the time for contributing to this project.
## Report a Bug
Just open a new issue on GitHub and describe the bug. It helps if your description is detailed. Thank you very much for your contribution!
## Suggest a New Feature
Just open a new issue on GitHub and describe the idea. Thank you very much for your contribution!
## Pull Requests
I am happy for every pull request, you do not have to follow these guidelines. However, it might help you to understand the project structure and make it easier for me to merge your pull request. Thank you very much for your contribution!
### 1. Fork & Clone this Project
Start by clicking on the `Fork` button at the top of the page. Then, clone this repository to your computer.
### 2. Open the Project
Open the project folder in GNOME Builder, Xcode or another IDE.
### 3. Understand the Project Structure
- The `README.md` file contains a description of the app or package.
- The `LICENSE.md` contains an MIT license.
- `CONTRIBUTING.md` is this file.
- Directory `Icons` that contains SVG files for the images used in the app and guides.
- `Sources` contains the source code of the project.
- `Model` contains the project's basis.
- `Data Flow` contains property wrappers and protocols required for managing the updates of a view.
- `Extensions` contains all the extensions of types that are not defined in this project.
- `User Interface` contains protocols and structures that are the basis of presenting content to the user.
- `View` contains structures that conform to the `AnyView` protocol.
- `Tests` contains a sample application using two sample backends for testing the package.
### 4. Edit the Code
Edit the code. If you add a new type, add documentation in the code.
### 5. Commit to the Fork
Commit and push the fork.
### 6. Pull Request
Open GitHub to submit a pull request. Thank you very much for your contribution!

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 david-swift
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

36
Package.swift Normal file
View File

@ -0,0 +1,36 @@
// swift-tools-version: 5.8
//
// Package.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
import PackageDescription
/// The Meta package is the foundation of the Aparoksha project.
let package = Package(
name: "Meta",
products: [
.library(
name: "Meta",
targets: ["Meta"]
)
],
targets: [
.target(
name: "Meta",
path: "Sources"
),
.target(
name: "SampleBackends",
dependencies: ["Meta"],
path: "Tests/SampleBackends"
),
.executableTarget(
name: "DemoApp",
dependencies: ["SampleBackends"],
path: "Tests/DemoApp"
)
]
)

141
README.md Normal file
View File

@ -0,0 +1,141 @@
<p align="center">
<img width="256" alt="Adwaita Icon" src="Icons/AdwaitaIcon.png">
<h1 align="center">Adwaita for Swift</h1>
</p>
<p align="center">
<a href="https://aparokshaui.github.io/adwaita-swift/">
Documentation
</a>
·
<a href="https://github.com/AparokshaUI/Adwaita">
GitHub
</a>
</p>
_Adwaita_ is a framework for creating user interfaces for GNOME with an API similar to SwiftUI.
The following code:
```swift
struct Counter: View {
@State private var count = 0
var view: Body {
HStack {
Button(icon: .default(icon: .goPrevious)) {
count -= 1
}
Text("\(count)")
.style("title-1")
.frame(minWidth: 100)
Button(icon: .default(icon: .goNext)) {
count += 1
}
}
}
}
```
Describes a simple counter view:
![Counter Example][image-1]
More examples are available in the [demo app][1].
## Table of Contents
- [Goals][2]
- [Widgets][3]
- [Installation][4]
- [Usage][5]
- [Thanks][6]
## Goals
_Adwaita_s main goal is to provide an easy-to-use interface for creating GNOME apps. The backend should stay as simple as possible, while not limiting the possibilities there are with [Libadwaita][7] and [GTK][8].
If you want to use _Adwaita_ in a project, but there are widgets missing, open an [issue on GitHub][9].
Find more information about the project's motivation in [this blog post](https://www.swift.org/blog/adwaita-swift/).
## Widgets
An overview of the widgets supported by _Adwaita_ is available [here](user-manual/Information/Widgets.md).
## Installation
### Dependencies
#### Flatpak
It is recommended to develop apps inside of a Flatpak.
That way, you don't have to install Swift or any of the dependencies on your system, and you always have access to the latest versions.
Take a look at the [template repository](https://github.com/AparokshaUI/AdwaitaTemplate).
This works on Linux only.
#### Directly on System
You can also run your apps directly on the system.
If you are using a Linux distribution, install `libadwaita-devel` or `libadwaita` (or something similar, based on the package manager) as well as `gtk4-devel`, `gtk4` or similar.
On macOS, follow these steps:
1. Install [Homebrew][11].
2. Install Libadwaita (and thereby GTK 4):
```
brew install libadwaita
```
### Swift Package
1. Open your Swift package in GNOME Builder, Xcode, or any other IDE.
2. Open the `Package.swift` file.
3. Into the `Package` initializer, under `dependencies`, paste:
```swift
.package(url: "https://github.com/AparokshaUI/Adwaita", from: "0.1.0")
```
## Usage
I recommend using the [template repository](https://github.com/AparokshaUI/AdwaitaTemplate) as a starting point.
Follow the [interactive tutorial](https://aparokshaui.github.io/adwaita-swift/tutorials/table-of-contents) or [read the docs](https://aparokshaui.github.io/adwaita-swift/documentation/adwaita) in order to get to know _Adwaita for Swift_.
## Thanks
### Dependencies
- [XMLCoder][18] licensed under the [MIT license][19]
- [Levenshtein Transformations](https://github.com/david-swift/LevenshteinTransformations) licensed under the [MIT license](https://github.com/david-swift/LevenshteinTransformations/blob/main/LICENSE.md)
### Other Thanks
- The [contributors][20]
- The auto-generation of widgets is based on [Swift Cross UI](https://github.com/stackotter/swift-cross-ui)
- [SwiftLint][21] for checking whether code style conventions are violated
- The programming language [Swift][22]
[1]: Tests/
[2]: #goals
[3]: #widgets
[4]: #installation
[5]: #usage
[6]: #thanks
[7]: https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1-latest/index.html
[8]: https://docs.gtk.org/gtk4/
[9]: https://github.com/AparokshaUI/Adwaita/issues
[10]: https://github.com/AparokshaUI/Libadwaita
[11]: https://brew.sh
[12]: user-manual/GettingStarted.md
[13]: user-manual/Basics/HelloWorld.md
[14]: user-manual/Basics/CreatingViews.md
[15]: user-manual/Basics/Windows.md
[16]: user-manual/Basics/KeyboardShortcuts.md
[17]: user-manual/Advanced/CreatingWidgets.md
[18]: https://github.com/CoreOffice/XMLCoder
[19]: https://github.com/CoreOffice/XMLCoder/blob/main/LICENSE
[20]: Contributors.md
[21]: https://github.com/realm/SwiftLint
[22]: https://github.com/apple/swift
[23]: https://github.com/SourceDocs/SourceDocs
[image-1]: Icons/Counter.png
[image-2]: Icons/Demo.png

View File

@ -0,0 +1,157 @@
//
// Binding.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
/// A property wrapper for a property of a view that binds the property of the parent view.
///
/// ```swift
/// struct Grandparent: View {
///
/// @State private var state = false
///
/// var view: Body {
/// Parent(value: $state)
/// }
/// }
///
/// struct Parent: View {
///
/// @Binding var value: Bool
///
/// var view: Body {
/// Child(value: $value)
/// }
///
/// }
///
/// struct Child: View {
///
/// @Binding var value: Bool
///
/// var view: Body {
/// // ...
/// }
///
/// }
/// ```
@propertyWrapper
@dynamicMemberLookup
public struct Binding<Value> {
/// The value.
public var wrappedValue: Value {
get {
getValue()
}
nonmutating set {
setValue(newValue)
for handler in handlers {
handler(newValue)
}
}
}
/// Get the value as a binding using the `$` prefix.
public var projectedValue: Binding<Value> {
.init {
wrappedValue
} set: { newValue in
wrappedValue = newValue
}
}
/// The closure for getting the value.
private let getValue: () -> Value
/// The closure for settings the value.
private let setValue: (Value) -> Void
/// Handlers observing whether the binding changes.
private var handlers: [(Value) -> Void] = []
/// Get a property of any content of a `Binding` as a `Binding`.
/// - Parameter dynamicMember: The path to the member.
/// - Returns: The binding.
public subscript<Subject>(dynamicMember keyPath: WritableKeyPath<Value, Subject>) -> Binding<Subject> {
.init {
wrappedValue[keyPath: keyPath]
} set: { newValue in
wrappedValue[keyPath: keyPath] = newValue
}
}
/// Initialize a property that is bound from a parent view.
/// - Parameters:
/// - get: The closure for getting the value.
/// - set: The closure for setting the value.
public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) {
self.getValue = get
self.setValue = set
}
/// Initialize a property that does not react to changes in the child view.
/// - Parameters:
/// - value: The constant value.
/// - Returns: The binding.
public static func constant(_ value: Value) -> Binding<Value> {
.init {
value
} set: { _ in
}
}
/// Observe whether data is changed over this binding.
/// - Parameter handler: The handler.
/// - Returns: The binding.
public func onSet(_ handler: @escaping (Value) -> Void) -> Self {
var newSelf = self
newSelf.handlers.append(handler)
return newSelf
}
}
extension Binding where Value: MutableCollection {
/// Get a child at a certain index of the array as a binding.
/// - Parameters:
/// - index: The child's index.
/// - defaultValue: The value used if the index is out of range does not exist.
/// - Returns: The child as a binding.
public subscript(safe index: Value.Index?, default defaultValue: Value.Element) -> Binding<Value.Element> {
.init {
if let index, wrappedValue.indices.contains(index) {
return wrappedValue[index]
}
return defaultValue
} set: { newValue in
if let index, wrappedValue.indices.contains(index) {
wrappedValue[index] = newValue
}
}
}
}
extension Binding where Value: MutableCollection, Value.Element: Identifiable {
/// Get a child of the array with a certain id as a binding.
/// - Parameters:
/// - id: The child's id.
/// - defaultValue: The value used if the index is out of range does not exist.
/// - Returns: The child as a binding.
public subscript(id id: Value.Element.ID?, default defaultValue: Value.Element) -> Binding<Value.Element> {
self[safe: wrappedValue.firstIndex { $0.id == id }, default: defaultValue]
}
}
extension Binding: CustomStringConvertible where Value: CustomStringConvertible {
/// A textual description of the wrapped value.
public var description: String {
wrappedValue.description
}
}

View File

@ -0,0 +1,30 @@
//
// Signal.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
import Foundation
/// A type that signalizes an action.
public struct Signal {
/// An action is signalized by toggling a boolean to `true` and back to `false`.
@State var boolean = false
/// A signal has a unique identifier.
public let id: UUID = .init()
/// Whether the action has caused an update.
public var update: Bool { boolean }
/// Initialize a signal.
public init() { }
/// Activate a signal.
public func signal() {
boolean = true
boolean = false
}
}

View File

@ -0,0 +1,78 @@
//
// State.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
import Foundation
/// A property wrapper for properties in a view that should be stored throughout view updates.
@propertyWrapper
public struct State<Value>: StateProtocol {
/// Access the stored value. This updates the views when being changed.
public var wrappedValue: Value {
get {
rawValue
}
nonmutating set {
rawValue = newValue
content.storage.update = true
UpdateManager.updateViews(force: forceUpdates)
}
}
/// Get the value as a binding using the `$` prefix.
public var projectedValue: Binding<Value> {
.init {
wrappedValue
} set: { newValue in
self.wrappedValue = newValue
}
}
// swiftlint:disable force_cast
/// Get and set the value without updating the views.
public var rawValue: Value {
get {
content.storage.value as! Value
}
nonmutating set {
content.storage.value = newValue
writeValue?(newValue)
}
}
// swiftlint:enable force_cast
/// The stored value.
let content: StateContent
/// Whether to force update the views when the value changes.
public var forceUpdates: Bool
/// The function for updating the value in the settings file.
private var writeValue: ((Value) -> Void)?
/// The value with an erased type.
public var value: Any {
get {
wrappedValue
}
nonmutating set {
if let newValue = newValue as? Value {
content.storage.value = newValue
}
}
}
/// Initialize a property representing a state in the view with an autoclosure.
/// - Parameters:
/// - wrappedValue: The wrapped value.
/// - forceUpdates: Whether to force update all available views when the property gets modified.
public init(wrappedValue: @autoclosure @escaping () -> Value, forceUpdates: Bool = false) {
content = .init(getInitialValue: wrappedValue)
self.forceUpdates = forceUpdates
}
}

View File

@ -0,0 +1,54 @@
//
// StateContent.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
/// A class storing the state's content.
class StateContent {
/// The storage.
var storage: Storage {
get {
if let internalStorage {
return internalStorage
}
let value = getInitialValue()
let storage = Storage(value: value)
internalStorage = storage
return storage
}
set {
internalStorage = newValue
}
}
/// The internal storage.
var internalStorage: Storage?
/// The initial value.
private var getInitialValue: () -> Any
/// Initialize the content without already initializing the storage or initializing the value.
/// - Parameter initialValue: The initial value.
init(getInitialValue: @escaping () -> Any) {
self.getInitialValue = getInitialValue
}
/// A class storing the value.
class Storage {
/// The stored value.
var value: Any
/// Whether to update the affected views.
var update = false
/// Initialize the storage.
/// - Parameters:
/// - value: The value.
init(value: Any) {
self.value = value
}
}
}

View File

@ -0,0 +1,14 @@
//
// StateProtocol.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
/// An interface for accessing `State` without specifying the generic type.
protocol StateProtocol {
/// The `StateContent`.
var content: StateContent { get }
}

View File

@ -0,0 +1,34 @@
//
// UpdateManager.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
/// This type manages view updates.
public enum UpdateManager {
/// Whether to block updates in general.
public static var blockUpdates = false
/// The functions handling view updates.
static var updateHandlers: [(Bool) -> Void] = []
/// Update all of the views.
/// - Parameter force: Whether to force all views to update.
///
/// Nothing happens if ``UpdateManager/blockUpdates`` is true.
static func updateViews(force: Bool = false) {
if !blockUpdates {
for handler in updateHandlers {
handler(force)
}
}
}
/// Add a handler that is called when the user interface should update.
/// - Parameter handler: The handler. The parameter defines whether the whole UI should be force updated.
static func addUpdateHandler(handler: @escaping (Bool) -> Void) {
updateHandlers.append(handler)
}
}

View File

@ -0,0 +1,134 @@
//
// Array.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
import Foundation
extension Array: AnyView where Element == AnyView {
/// The array's view body is the array itself.
public var viewContent: Body { self }
/// Get the debug tree for an array of views.
/// - Parameter parameters: Whether the widget parameters should be visible in the tree.
/// - Returns: The tree.
public func getBodyDebugTree<ViewType>(parameters: Bool, type: ViewType.Type) -> String {
var description = ""
for view in self where view as? ViewType != nil {
let viewDescription: String
if let widget = view as? Widget {
viewDescription = widget.getViewDescription(parameters: parameters, type: type)
} else {
viewDescription = view.getDebugTree(parameters: parameters, type: type)
}
description += viewDescription + "\n"
}
if !description.isEmpty {
description.removeLast()
}
return description
}
/// Get a widget from a collection of views.
/// - Parameter modifiers: Modify views before being updated.
/// - Returns: A widget.
public func widget(modifiers: [(AnyView) -> AnyView]) -> Widget {
if count == 1, let widget = self[safe: 0]?.widget(modifiers: modifiers) {
return widget
} else {
var modified = self
for (index, view) in modified.enumerated() {
for modifier in modifiers {
modified[safe: index] = modifier(view)
}
}
// TODO: Is wrapper correct choice?
return Wrapper { modified }
}
}
/// Update a collection of views with a collection of view storages.
/// - Parameters:
/// - storage: The collection of view storages.
/// - modifiers: Modify views before being updated.
/// - updateProperties: Whether to update properties.
public func update(_ storage: [ViewStorage], modifiers: [(AnyView) -> AnyView], updateProperties: Bool) {
for (index, element) in enumerated() {
if let storage = storage[safe: index] {
element
.widget(modifiers: modifiers)
.updateStorage(storage, modifiers: modifiers, updateProperties: updateProperties)
}
}
}
}
extension Array where Element == String {
/// Get the C version of the array.
var cArray: UnsafePointer<UnsafePointer<CChar>?>? {
let cStrings = self.map { $0.utf8CString }
let cStringPointers = cStrings.map { $0.withUnsafeBufferPointer { $0.baseAddress } }
let optionalCStringPointers = cStringPointers + [nil]
var optionalCStringPointersCopy = optionalCStringPointers
optionalCStringPointersCopy.withUnsafeMutableBufferPointer { bufferPointer in
bufferPointer.baseAddress?.advanced(by: cStrings.count).pointee = nil
}
let flatArray = optionalCStringPointersCopy.compactMap { $0 }
let pointer = UnsafeMutablePointer<UnsafePointer<CChar>?>.allocate(capacity: flatArray.count + 1)
for (index, element) in flatArray.enumerated() {
pointer.advanced(by: index).pointee = element
}
pointer.advanced(by: flatArray.count).pointee = nil
return UnsafePointer(pointer)
}
}
extension Array {
/// Accesses the element at the specified position safely.
/// - Parameters:
/// - index: The position of the element to access.
///
/// Access and set elements the safe way:
/// ```swift
/// var array = ["Hello", "World"]
/// print(array[safe: 2] ?? "Out of range")
/// ```
public subscript(safe index: Int?) -> Element? {
get {
if let index, indices.contains(index) {
return self[index]
}
return nil
}
set {
if let index, let value = newValue, indices.contains(index) {
self[index] = value
}
}
}
}
extension Array where Element: Identifiable {
/// Accesses the element with a certain id safely.
/// - Parameters:
/// - id: The child's id.
///
/// Access and set elements the safe way:
/// ```swift
/// var array = ["Hello", "World"]
/// print(array[safe: 2] ?? "Out of range")
/// ```
public subscript(id id: Element.ID) -> Element? {
self[safe: firstIndex { $0.id == id }]
}
}

View File

@ -0,0 +1,35 @@
//
// DefaultStringInterpolation.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
// Thanks to Eneko Alonso, Pyry Jahkola, cukr for the comments in this Swift forum discussion:
// "Multi-line string nested indentation with interpolation"
// https://forums.swift.org/t/multi-line-string-nested-indentation-with-interpolation/36933
//
extension DefaultStringInterpolation {
/// Preserve the indentation in a multi line string.
/// - Parameter string: The string.
///
/// Use it the following way:
/// """
/// Hello
/// \(indented: "World\n Test")
/// """
public mutating func appendInterpolation(indented string: String) {
// swiftlint:disable compiler_protocol_init
let indent = String(stringInterpolation: self).reversed().prefix { " \t".contains($0) }
// swiftlint:enable compiler_protocol_init
if indent.isEmpty {
appendInterpolation(string)
} else {
appendLiteral(
string.split(separator: "\n", omittingEmptySubsequences: false).joined(separator: "\n" + indent)
)
}
}
}

View File

@ -0,0 +1,13 @@
//
// String.swift
// Meta
//
// Created by david-swift on 09.06.24.
//
extension String {
/// A label for main content in a view storage.
static var mainContent: Self { "main" }
}

View File

@ -0,0 +1,76 @@
//
// AnyView.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
/// The view type used for any form of a view.
public protocol AnyView {
/// The view's content.
@ViewBuilder var viewContent: Body { get }
}
extension AnyView {
/// Get the view's debug tree.
/// - Parameter parameters: Whether the widget parameters should be included in the debug tree.
/// - Returns: A textual description.
public func getDebugTree<ViewType>(parameters: Bool, type: ViewType.Type) -> String {
if let body = self as? Body {
return body.getBodyDebugTree(parameters: parameters, type: type)
}
return """
\(Self.self) {
\(indented: viewContent.getBodyDebugTree(parameters: parameters, type: type))
}
"""
}
func getModified(modifiers: [(AnyView) -> AnyView]) -> AnyView {
var modified: AnyView = self
for modifier in modifiers {
modified = modifier(modified)
}
return modified
}
/// Update a storage to a view.
/// - Parameters:
/// - storage: The storage.
/// - modifiers: Modify views before being updated.
/// - updateProperties: Whether to update properties.
public func updateStorage(_ storage: ViewStorage, modifiers: [(AnyView) -> AnyView], updateProperties: Bool) {
let modified = getModified(modifiers: modifiers)
if let widget = modified as? Widget {
widget.update(storage, modifiers: modifiers, updateProperties: updateProperties)
} else {
Wrapper { viewContent }
.update(storage, modifiers: modifiers, updateProperties: updateProperties)
}
}
/// Get a storage.
/// - Parameter modifiers: Modify views before being updated.
/// - Returns: The storage.
public func storage(modifiers: [(AnyView) -> AnyView]) -> ViewStorage {
widget(modifiers: modifiers).container(modifiers: modifiers)
}
/// Wrap the view into a widget.
/// - Parameter modifiers: Modify views before being updated.
/// - Returns: The widget.
public func widget(modifiers: [(AnyView) -> AnyView]) -> Widget {
let modified = getModified(modifiers: modifiers)
if let peer = modified as? Widget {
return peer
}
return Wrapper { viewContent }
}
}
/// `Body` is an array of views.
public typealias Body = [AnyView]

View File

@ -0,0 +1,37 @@
//
// SimpleView.swift
// Meta
//
// Created by david-swift on 09.06.24.
//
/// A structure conforming to `SimpleView` is referred to as a view.
/// It can be part of a body.
///
/// ```swift
/// struct Test: SimpleView {
///
/// var view: Body {
/// AnotherView()
/// }
///
/// }
/// ```
///
/// A simple view cannot save state. Use ``View`` for saving state.
///
public protocol SimpleView: AnyView {
/// The view's content.
@ViewBuilder var view: Body { get }
}
extension SimpleView {
/// The view's content.
public var viewContent: Body {
view
}
}

View File

@ -0,0 +1,47 @@
//
// View.swift
// Meta
//
// Created by david-swift on 09.06.24.
//
/// A structure conforming to `View` is referred to as a view.
/// It can be part of a body.
///
/// ```swift
/// struct Test: View {
///
/// var view: Body {
/// AnotherView()
/// }
///
/// }
/// ```
///
/// Use ``SimpleView`` instead if a view does not have to save state.
///
public protocol View: AnyView {
/// The view's content.
@ViewBuilder var view: Body { get }
}
extension View {
/// The view's content.
public var viewContent: Body {
[StateWrapper(content: { view }, state: getState())]
}
func getState() -> [String: StateProtocol] {
var state: [String: StateProtocol] = [:]
for property in Mirror(reflecting: self).children {
if let label = property.label, let value = property.value as? StateProtocol {
state[label] = value
}
}
return state
}
}

View File

@ -0,0 +1,102 @@
//
// ViewBuilder.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
import Foundation
/// The ``ViewBuilder`` is a result builder for views.
@resultBuilder
public enum ViewBuilder {
/// A component used in the ``ArrayBuilder``.
public enum Component {
/// A view as a component.
case element(_: AnyView)
/// An array of components as a component.
case components(_: [Self])
}
/// Build combined results from statement blocks.
/// - Parameter components: The components.
/// - Returns: The components in a component.
public static func buildBlock(_ elements: Component...) -> Component {
.components(elements)
}
/// Translate an element into a ``ViewBuilder.Component``.
/// - Parameter element: The element to translate.
/// - Returns: A component created from the element.
public static func buildExpression(_ element: AnyView) -> Component {
.element(element)
}
/// Translate an array of elements into a ``ViewBuilder.Component``.
/// - Parameter elements: The elements to translate.
/// - Returns: A component created from the element.
public static func buildExpression(_ elements: [AnyView]) -> Component {
var components: [Component] = []
for element in elements {
components.append(.element(element))
}
return .components(components)
}
/// Fetch a component.
/// - Parameter component: A component.
/// - Returns: The component.
public static func buildExpression(_ component: Component) -> Component {
component
}
// TODO: Add support for optionals, building either
/*
/// Enables support for `if` statements without an `else`.
/// - Parameter component: An optional component.
/// - Returns: A nonoptional component.
public static func buildOptional(_ component: Component?) -> Component {
.element(
Bin()
.child {
if let component {
buildFinalResult(component)
} else {
[]
}
}
.visible(component != nil)
)
}
/// Enables support for `if`-`else` and `switch` statements.
/// - Parameter component: A component.
/// - Returns: The component.
public static func buildEither(first component: Component) -> Component {
.element(ViewStack(id: true) { _ in buildFinalResult(component) })
}
/// Enables support for `if`-`else` and `switch` statements.
/// - Parameter component: A component.
/// - Returns: The component.
public static func buildEither(second component: Component) -> Component {
.element(ViewStack(id: false) { _ in buildFinalResult(component) })
}
*/
/// Convert a component to an array of elements.
/// - Parameter component: The component to convert.
/// - Returns: The generated array of elements.
public static func buildFinalResult(_ component: Component) -> [AnyView] {
switch component {
case let .element(element):
return [element]
case let .components(components):
return components.flatMap { buildFinalResult($0) }
}
}
}

View File

@ -0,0 +1,42 @@
//
// ViewStorage.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
/// Store a rendered view in a view storage.
public class ViewStorage {
/// The pointer.
public var pointer: Any?
/// The view's content.
public var content: [String: [ViewStorage]]
/// The view's state (used in `StateWrapper`).
var state: [String: StateProtocol] = [:]
/// Other properties.
public var fields: [String: Any] = [:]
/// The pointer as an opaque pointer.
public var opaquePointer: OpaquePointer? {
get {
pointer as? OpaquePointer
}
set {
pointer = newValue
}
}
/// Initialize a view storage.
/// - Parameters:
/// - pointer: The pointer to the widget, its type depends on the backend.
/// - content: The view's content.
public init(
_ pointer: Any?,
content: [String: [ViewStorage]] = [:]
) {
self.pointer = pointer
self.content = content
}
}

View File

@ -0,0 +1,57 @@
//
// Widget.swift
// Meta
//
// Created by david-swift on 26.05.24.
//
/// A widget is a view that know about its GTUI widget.
public protocol Widget: AnyView {
/// The debug tree parameters.
var debugTreeParameters: [(String, value: CustomStringConvertible)] { get }
/// The debug tree's content.
var debugTreeContent: [(String, body: Body)] { get }
/// The view storage.
/// - Parameter modifiers: Modify views before being updated.
func container(modifiers: [(AnyView) -> AnyView]) -> ViewStorage
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
func update(_ storage: ViewStorage, modifiers: [(AnyView) -> AnyView], updateProperties: Bool)
}
extension Widget {
/// A widget's view is empty.
public var viewContent: Body { [] }
/// A description of the view.
public func getViewDescription<ViewType>(parameters: Bool, type: ViewType.Type) -> String {
var content = ""
for element in debugTreeContent {
if content.isEmpty {
content += """
{
\(indented: element.body.getDebugTree(parameters: parameters, type: type))
}
"""
} else {
content += """
\(element.0): {
\(indented: element.body.getDebugTree(parameters: parameters, type: type))
}
"""
}
}
if parameters {
let parametersString = debugTreeParameters.map { "\($0.0): \($0.value)" }.joined(separator: ", ")
return "\(Self.self)(\(parametersString))\(content)"
}
return "\(Self.self)\(content)"
}
}

View File

@ -0,0 +1,95 @@
//
// StateWrapper.swift
// Meta
//
// Created by david-swift on 09.06.24.
//
import Observation
/// A storage for `@State` properties.
public struct StateWrapper: Widget {
/// The content.
var content: () -> Body
/// The state information (from properties with the `State` wrapper).
var state: [String: StateProtocol] = [:]
/// The debug tree parameters.
public var debugTreeParameters: [(String, value: CustomStringConvertible)] {
[
("state", value: state)
]
}
/// The debug tree's content.
public var debugTreeContent: [(String, body: Body)] {
[("content", body: content())]
}
/// The identifier of the field storing whether to update the wrapper's content.
private var updateID: String { "update" }
/// Initialize a `StateWrapper`.
/// - Parameter content: The view content.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content
}
/// Initialize a `StateWrapper`.
/// - Parameters:
/// - content: The view content.
/// - state: The state information.
init(content: @escaping () -> Body, state: [String: StateProtocol]) {
self.content = content
self.state = state
}
/// Update a view storage.
/// - Parameters:
/// - storage: The view storage.
/// - modifiers: Modify views before being updated.
/// - updateProperties: Whether to update properties.
public func update(_ storage: ViewStorage, modifiers: [(AnyView) -> AnyView], updateProperties: Bool) {
var updateProperties = storage.fields[updateID] as? Bool ?? false
storage.fields[updateID] = false
for property in state {
if let storage = storage.state[property.key]?.content.storage {
property.value.content.storage = storage
}
if property.value.content.storage.update {
updateProperties = true
property.value.content.storage.update = false
}
}
if let storage = storage.content[.mainContent]?.first {
content()
.widget(modifiers: modifiers)
.update(storage, modifiers: modifiers, updateProperties: updateProperties)
}
}
/// Get a view storage.
/// - Parameter modifiers: Modify views before being updated.
/// - Returns: The view storage.
public func container(modifiers: [(AnyView) -> AnyView]) -> ViewStorage {
let content = content().widget(modifiers: modifiers).container(modifiers: modifiers)
let storage = ViewStorage(content.pointer, content: [.mainContent: [content]])
storage.state = state
observe(storage: storage)
return storage
}
/// Observe the observable properties accessed in the view.
/// - Parameter storage: The view storage
func observe(storage: ViewStorage) {
withObservationTracking {
_ = content().getDebugTree(parameters: true, type: AnyView.self)
} onChange: {
storage.fields[updateID] = true
UpdateManager.updateViews()
observe(storage: storage)
}
}
}

View File

@ -0,0 +1,51 @@
//
// Wrapper.swift
// Meta
//
// Created by david-swift on 27.05.24.
//
/// Wrap a view into a single widget.
public struct Wrapper: Widget {
/// The content.
var content: Body
/// The debug tree parameters.
public var debugTreeParameters: [(String, value: CustomStringConvertible)] {
[]
}
/// The debug tree's content.
public var debugTreeContent: [(String, body: Body)] {
[("content", body: content)]
}
/// Initialize a `Wrapper`.
/// - Parameter content: The view content.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content()
}
/// Update a view storage.
/// - Parameters:
/// - storage: The view storage.
/// - modifiers: Modify views before being updated.
/// - updateProperties: Whether to update properties.
public func update(_ storage: ViewStorage, modifiers: [(AnyView) -> AnyView], updateProperties: Bool) {
if let storage = storage.content[.mainContent]?.first {
content
.widget(modifiers: modifiers)
.update(storage, modifiers: modifiers, updateProperties: updateProperties)
}
}
/// Get a view storage.
/// - Parameter modifiers: Modify views before being updated.
/// - Returns: The view storage.
public func container(modifiers: [(AnyView) -> AnyView]) -> ViewStorage {
let content = content.widget(modifiers: modifiers).container(modifiers: modifiers)
return .init(content.pointer, content: [.mainContent: [content]])
}
}

View File

@ -0,0 +1,20 @@
import Meta
import SampleBackends
struct DemoView: SimpleView {
var view: Body {
Backend1.TestWidget()
testContent
}
@ViewBuilder
var testContent: Body {
Backend2.TestWidget()
Backend1.TestWidget()
}
}
print(DemoView().getDebugTree(parameters: true, type: Backend1.BackendView.self))
print(DemoView().getDebugTree(parameters: true, type: Backend2.BackendView.self))

View File

@ -0,0 +1,34 @@
import Meta
public enum Backend1 {
public struct TestWidget: BackendWidget {
public init() { }
public var debugTreeContent: [(String, body: Body)] {
[]
}
public var debugTreeParameters: [(String, value: CustomStringConvertible)] {
[]
}
public func container(modifiers: [(AnyView) -> AnyView]) -> ViewStorage {
print("Init Content")
let storage = ViewStorage(nil)
storage.fields["test"] = 0
return storage
}
public func update(_ storage: ViewStorage, modifiers: [(AnyView) -> AnyView], updateProperties: Bool) {
storage.fields["test"] = storage.fields["tests"] as? Int ?? 0 + 1
}
}
public protocol BackendView: AnyView { }
public protocol BackendWidget: BackendView, Widget { }
}

View File

@ -0,0 +1,34 @@
import Meta
public enum Backend2 {
public struct TestWidget: BackendWidget {
public init() { }
public var debugTreeContent: [(String, body: Body)] {
[]
}
public var debugTreeParameters: [(String, value: CustomStringConvertible)] {
[]
}
public func container(modifiers: [(AnyView) -> AnyView]) -> ViewStorage {
print("Init Content")
let storage = ViewStorage(nil)
storage.fields["test"] = 0
return storage
}
public func update(_ storage: ViewStorage, modifiers: [(AnyView) -> AnyView], updateProperties: Bool) {
storage.fields["test"] = storage.fields["tests"] as? Int ?? 0 + 1
}
}
public protocol BackendView: AnyView { }
public protocol BackendWidget: BackendView, Widget { }
}