├── .github ├── release-drafter.yml └── workflows │ ├── docs.yml │ ├── release-drafter.yml │ ├── release.yml │ ├── swift.yml │ └── swiftlint.yml ├── .gitignore ├── .jazzy.yml ├── .swiftlint.yml ├── Images ├── helloworld-screenshot.png ├── swiftweb-architecture.png └── swiftweb.png ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── SwiftWeb │ ├── Extensions │ │ └── Sequence+keypath.swift │ ├── HTML │ │ ├── HTMLNode.swift │ │ └── HTMLTemplate.swift │ ├── Server │ │ ├── InputEvent.swift │ │ ├── InputEventResponder.swift │ │ └── SwiftWebServer.swift │ ├── Utility │ │ ├── Edge.swift │ │ ├── Font.swift │ │ ├── LayoutAxis.swift │ │ └── PreviewProvider.swift │ ├── View Modifiers │ │ ├── ColorFilterModifier.swift │ │ ├── FontModifier.swift │ │ └── ModifiedContent.swift │ ├── View Tree │ │ ├── Binding.swift │ │ ├── GrowingAxesModifying.swift │ │ ├── PreferenceProvider.swift │ │ ├── State.swift │ │ ├── StateStorageNode.swift │ │ ├── ViewNode+CustomStringConvertible.swift │ │ ├── ViewNode.swift │ │ └── ViewTree.swift │ ├── View.swift │ └── Views │ │ ├── AnyView.swift │ │ ├── Button.swift │ │ ├── Color.swift │ │ ├── ForEach.swift │ │ ├── Form.swift │ │ ├── Frame.swift │ │ ├── GlobalOverlayView.swift │ │ ├── HStack.swift │ │ ├── Image.swift │ │ ├── List.swift │ │ ├── NavigationView.swift │ │ ├── Picker.swift │ │ ├── PreferenceChangeListenerView.swift │ │ ├── PreferenceProvidingView.swift │ │ ├── Section.swift │ │ ├── Sheet.swift │ │ ├── Slider.swift │ │ ├── Spacer.swift │ │ ├── Stack.swift │ │ ├── TabItem.swift │ │ ├── TabView.swift │ │ ├── TaggedView.swift │ │ ├── TapGestureView.swift │ │ ├── Text.swift │ │ ├── TextField.swift │ │ ├── TupleView.swift │ │ ├── VStack.swift │ │ ├── ViewBuilder.swift │ │ └── ZStack.swift └── SwiftWebScript │ └── JavaScriptClient.swift └── Tests ├── LinuxMain.swift └── SwiftWebTests ├── SwiftWebTests.swift └── XCTestManifests.swift /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | branches: [release] 2 | name-template: '$NEXT_PATCH_VERSION' 3 | tag-template: '$NEXT_PATCH_VERSION' 4 | categories: 5 | - title: '🚀 Features' 6 | labels: 7 | - 'feature' 8 | - 'enhancement' 9 | - title: '🐛 Bug Fixes' 10 | labels: 11 | - 'fix' 12 | - 'bugfix' 13 | - 'bug' 14 | - title: '🧰 Maintenance' 15 | label: 'chore' 16 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 17 | template: | 18 | ## Changes 19 | 20 | $CHANGES 21 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish Documentation 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy_docs: 9 | runs-on: macos-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Publish Jazzy Docs 13 | uses: steven0351/publish-jazzy-docs@v1 14 | with: 15 | personal_access_token: ${{ secrets.ACCESS_TOKEN }} 16 | config: .jazzy.yml 17 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - release 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "release" 14 | - uses: release-drafter/release-drafter@v5 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | - name: Notify Example Project 18 | run: | 19 | curl -XPOST -u "${{ secrets.PAT_USERNAME}}:${{secrets.ACCESS_TOKEN}}"\ 20 | -H "Accept: application/vnd.github.everest-preview+json"\ 21 | -H "Content-Type: application/json" https://api.github.com/repos/YOURNAME/APPLICATION_NAME/dispatches --data '{"event_type": "build_application"}' 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: GH Release 13 | uses: softprops/action-gh-release@v0.1.5 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [ develop, release ] 6 | pull_request: 7 | branches: [ develop, release ] 8 | 9 | jobs: 10 | build: 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Xcode version check 15 | run: xcodebuild -version 16 | - name: Check version 17 | run: swift --version 18 | - name: Build 19 | run: swift build 20 | - name: Test 21 | run: swift test 22 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/swiftlint.yml' 7 | - '.swiftlint.yml' 8 | - '**/*.swift' 9 | 10 | jobs: 11 | swiftlint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: GitHub Action for SwiftLint 16 | uses: norio-nomura/action-swiftlint@3.1.0 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Apodini .gitignore File 2 | 3 | # Swift Package Manager 4 | Package.resolved 5 | *.xcodeproj 6 | .swiftpm 7 | .build/ 8 | 9 | # Xcode User settings 10 | xcuserdata/ 11 | 12 | # Other files 13 | .DS_Store 14 | 15 | # Fastlane 16 | fastlane/report.xml 17 | fastlane/Preview.html 18 | fastlane/screenshots/**/*.png 19 | fastlane/test_output -------------------------------------------------------------------------------- /.jazzy.yml: -------------------------------------------------------------------------------- 1 | module: SwiftWeb 2 | author: Quirin Schweigert 3 | theme: fullwidth 4 | output: ./docs 5 | documentation: ./*.md 6 | author_url: https://github.com/Apodini 7 | github_url: https://github.com/Apodini/SwiftWeb 8 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # Apodini SwiftLint file Apodini 2 | 3 | # The whitelist_rules configuration also includes rules that are enabled by default to provide a good overview of all rules. 4 | whitelist_rules: 5 | - anyobject_protocol 6 | # Prefer using AnyObject over class for class-only protocols. 7 | - array_init 8 | # Prefer using Array(seq) over seq.map { $0 } to convert a sequence into an Array. 9 | - block_based_kvo 10 | # Prefer the new block based KVO API with keypaths when using Swift 3.2 or later. 11 | - class_delegate_protocol 12 | # Delegate protocols should be class-only so they can be weakly referenced. 13 | - closing_brace 14 | # Closing brace with closing parenthesis should not have any whitespaces in the middle. 15 | # - closure_body_length 16 | # Closure bodies should not span too many lines. 17 | - closure_end_indentation 18 | # Closure end should have the same indentation as the line that started it. 19 | # - closure_parameter_position 20 | # Closure parameters should be on the same line as opening brace. 21 | - closure_spacing 22 | # Closure expressions should have a single space inside each brace. 23 | - collection_alignment 24 | # All elements in a collection literal should be vertically aligned 25 | - colon 26 | # Colons should be next to the identifier when specifying a type and next to the key in dictionary literals. 27 | - comma 28 | # There should be no space before and one after any comma. 29 | - compiler_protocol_init 30 | # The initializers declared in compiler protocols such as ExpressibleByArrayLiteral shouldn't be called directly. 31 | - conditional_returns_on_newline 32 | # Conditional statements should always return on the next line 33 | - contains_over_filter_count 34 | # Prefer contains over comparing filter(where:).count to 0. 35 | - contains_over_filter_is_empty 36 | # Prefer contains over using filter(where:).isEmpty 37 | - contains_over_first_not_nil 38 | # Prefer `contains` over `first(where:) != nil` 39 | - contains_over_range_nil_comparison 40 | # Prefer contains over range(of:) != nil and range(of:) == nil 41 | - control_statement 42 | # if, for, guard, switch, while, and catch statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses. 43 | # - convenience_type 44 | # Types used for hosting only static members should be implemented as a caseless enum to avoid instantiation. 45 | - cyclomatic_complexity 46 | # Complexity of function bodies should be limited. 47 | - discarded_notification_center_observer 48 | # When registering for a notification using a block, the opaque observer that is returned should be stored so it can be removed later. 49 | - discouraged_direct_init 50 | # Discouraged direct initialization of types that can be harmful. e.g. UIDevice(), Bundle() 51 | - discouraged_optional_boolean 52 | # Prefer non-optional booleans over optional booleans. 53 | # - discouraged_optional_collection # Enable as soon as https://github.com/realm/SwiftLint/issues/2298 is fixed 54 | # Prefer empty collection over optional collection. 55 | - duplicate_imports 56 | # Duplicate Imports 57 | - dynamic_inline 58 | # Avoid using 'dynamic' and '@inline(__always)' together. 59 | - empty_collection_literal 60 | # Prefer checking isEmpty over comparing collection to an empty array or dictionary literal. 61 | - empty_count 62 | # Prefer checking `isEmpty` over comparing `count` to zero. 63 | - empty_enum_arguments 64 | # Arguments can be omitted when matching enums with associated types if they are not used. 65 | - empty_parameters 66 | # Prefer () -> over Void ->. 67 | - empty_parentheses_with_trailing_closure 68 | # When using trailing closures, empty parentheses should be avoided after the method call. 69 | - empty_string 70 | # Prefer checking `isEmpty` over comparing string to an empty string literal. 71 | - empty_xctest_method 72 | # Empty XCTest method should be avoided. 73 | - enum_case_associated_values_count 74 | # Number of associated values in an enum case should be low 75 | - explicit_init 76 | # Explicitly calling .init() should be avoided. 77 | - fatal_error_message 78 | # A fatalError call should have a message. 79 | - file_length 80 | # Files should not span too many lines. 81 | # See file_length below for the exact configuration. 82 | - first_where 83 | # Prefer using ``.first(where:)`` over ``.filter { }.first` in collections. 84 | - flatmap_over_map_reduce 85 | # Prefer flatMap over map followed by reduce([], +). 86 | - for_where 87 | # where clauses are preferred over a single if inside a for. 88 | - force_cast 89 | # Force casts should be avoided. 90 | - force_try 91 | # Force tries should be avoided. 92 | - force_unwrapping 93 | # Force unwrapping should be avoided. 94 | # - function_body_length 95 | # Functions bodies should not span too many lines. 96 | # See function_body_length below for the exact configuration. 97 | - function_parameter_count 98 | # Number of function parameters should be low. 99 | # See function_parameter_count below for the exact configuration. 100 | - generic_type_name 101 | # Generic type name should only contain alphanumeric characters, start with an uppercase character and span between 1 and 20 characters in length. 102 | - identical_operands 103 | # Comparing two identical operands is likely a mistake. 104 | - identifier_name 105 | # Identifier names should only contain alphanumeric characters and start with a lowercase character or should only contain capital letters. 106 | # In an exception to the above, variable names may start with a capital letter when they are declared static and immutable. 107 | # Variable names should not be too long or too short. Excluded names are listed below. 108 | - implicit_getter 109 | # Computed read-only properties and subscripts should avoid using the get keyword. 110 | - implicit_return 111 | # Prefer implicit returns in closures. 112 | - implicitly_unwrapped_optional 113 | # Implicitly unwrapped optionals should be avoided when possible. 114 | - inert_defer 115 | # If defer is at the end of its parent scope, it will be executed right where it is anyway. 116 | - is_disjoint 117 | # Prefer using Set.isDisjoint(with:) over Set.intersection(_:).isEmpty. 118 | - joined_default_parameter 119 | # Discouraged explicit usage of the default separator. 120 | - large_tuple 121 | # Tuples shouldn't have too many members. Create a custom type instead. 122 | # See large_tuple below for the exact configuration. 123 | - last_where 124 | # Prefer using .last(where:) over .filter { }.last in collections. 125 | - leading_whitespace 126 | # Files should not contain leading whitespace. 127 | - legacy_cggeometry_functions 128 | # CGGeometry: Struct extension properties and methods are preferred over legacy functions 129 | - legacy_constant 130 | # Struct-scoped constants are preferred over legacy global constants (CGSize, CGRect, NSPoint, ...). 131 | - legacy_constructor 132 | # Swift constructors are preferred over legacy convenience functions (CGPointMake, CGSizeMake, UIOffsetMake, ...). 133 | - legacy_hashing 134 | # Prefer using the hash(into:) function instead of overriding hashValue 135 | - legacy_multiple 136 | # Prefer using the isMultiple(of:) function instead of using the remainder operator (%). 137 | - legacy_nsgeometry_functions 138 | # Struct extension properties and methods are preferred over legacy functions 139 | - legacy_random 140 | # Prefer using type.random(in:) over legacy functions. 141 | - line_length 142 | # Lines should not span too many characters. 143 | # See line_length below for the exact configuration. 144 | - literal_expression_end_indentation 145 | # Array and dictionary literal end should have the same indentation as the line that started it. 146 | - lower_acl_than_parent 147 | # Ensure definitions have a lower access control level than their enclosing parent 148 | - mark 149 | # MARK comment should be in valid format. e.g. '// MARK: ...' or '// MARK: - ...' 150 | # - missing_docs 151 | # Declarations should be documented. 152 | - modifier_order 153 | # Modifier order should be consistent. 154 | - multiline_arguments 155 | # Arguments should be either on the same line, or one per line. 156 | - multiline_function_chains 157 | # Chained function calls should be either on the same line, or one per line. 158 | - multiline_literal_brackets 159 | # Multiline literals should have their surrounding brackets in a new line. 160 | - multiline_parameters 161 | # Functions and methods parameters should be either on the same line, or one per line. 162 | - nesting 163 | # Types and statements should only be nested to a certain level deep. 164 | # See nesting below for the exact configuration. 165 | - nimble_operator 166 | # Prefer Nimble operator overloads over free matcher functions. 167 | - no_fallthrough_only 168 | # Fallthroughs can only be used if the case contains at least one other statement. 169 | - no_space_in_method_call 170 | # Don’t add a space between the method name and the parentheses. 171 | - notification_center_detachment 172 | # An object should only remove itself as an observer in deinit. 173 | - nslocalizedstring_key 174 | # Static strings should be used as key in NSLocalizedString in order to genstrings work. 175 | - nsobject_prefer_isequal 176 | # NSObject subclasses should implement isEqual instead of ==. 177 | - object_literal 178 | # Prefer object literals over image and color inits. 179 | - opening_brace 180 | # Opening braces should be preceded by a single space and on the same line as the declaration. 181 | - operator_usage_whitespace 182 | # Operators should be surrounded by a single whitespace when they are being used. 183 | - operator_whitespace 184 | # Operators should be surrounded by a single whitespace when defining them. 185 | - optional_enum_case_matching 186 | # Matching an enum case against an optional enum without ‘?’ is supported on Swift 5.1 and above. 187 | - orphaned_doc_comment 188 | # A doc comment should be attached to a declaration. 189 | - overridden_super_call 190 | # Some overridden methods should always call super 191 | - pattern_matching_keywords 192 | # Combine multiple pattern matching bindings by moving keywords out of tuples. 193 | - prefer_self_type_over_type_of_self 194 | # Prefer Self over type(of: self) when accessing properties or calling methods. 195 | - private_action 196 | # IBActions should be private. 197 | - private_outlet 198 | # IBOutlets should be private to avoid leaking UIKit to higher layers. 199 | - private_over_fileprivate 200 | # Prefer private over fileprivate declarations. 201 | - private_unit_test 202 | # Unit tests marked private are silently skipped. 203 | - prohibited_super_call 204 | # Some methods should not call super ( 205 | # NSFileProviderExtension: providePlaceholder(at:completionHandler:) 206 | # NSTextInput doCommand(by:) 207 | # NSView updateLayer() 208 | # UIViewController loadView()) 209 | - protocol_property_accessors_order 210 | # When declaring properties in protocols, the order of accessors should be get set. 211 | - reduce_boolean 212 | # Prefer using .allSatisfy() or .contains() over reduce(true) or reduce(false) 213 | - reduce_into 214 | # Prefer reduce(into:_:) over reduce(_:_:) for copy-on-write types 215 | - redundant_discardable_let 216 | # Prefer _ = foo() over let _ = foo() when discarding a result from a function. 217 | - redundant_nil_coalescing 218 | # nil coalescing operator is only evaluated if the lhs is nil, coalescing operator with nil as rhs is redundant 219 | - redundant_objc_attribute 220 | # Objective-C attribute (@objc) is redundant in declaration. 221 | - redundant_optional_initialization 222 | # Initializing an optional variable with nil is redundant. 223 | - redundant_set_access_control 224 | # Property setter access level shouldn't be explicit if it's the same as the variable access level. 225 | - redundant_string_enum_value 226 | # String enum values can be omitted when they are equal to the enumcase name. 227 | - redundant_type_annotation 228 | # Variables should not have redundant type annotation 229 | - redundant_void_return 230 | # Returning Void in a function declaration is redundant. 231 | - return_arrow_whitespace 232 | # Return arrow and return type should be separated by a single space or on a separate line. 233 | - shorthand_operator 234 | # Prefer shorthand operators (+=, -=, *=, /=) over doing the operation and assigning. 235 | - single_test_class 236 | # Test files should contain a single QuickSpec or XCTestCase class. 237 | - sorted_first_last 238 | # Prefer using `min()`` or `max()`` over `sorted().first` or `sorted().last` 239 | - statement_position 240 | # Else and catch should be on the same line, one space after the previous declaration. 241 | - static_operator 242 | # Operators should be declared as static functions, not free functions. 243 | - superfluous_disable_command 244 | # SwiftLint ‘disable’ commands are superfluous when the disabled rule would not have triggered a violation in the disabled region. Use “ - ” if you wish to document a command. 245 | - switch_case_alignment 246 | # Case statements should vertically align with their enclosing switch statement, or indented if configured otherwise. 247 | - syntactic_sugar 248 | # Shorthand syntactic sugar should be used, i.e. [Int] instead of Array. 249 | # - todo 250 | # TODOs and FIXMEs should be resolved. 251 | - toggle_bool 252 | # Prefer someBool.toggle() over someBool = !someBool. 253 | - trailing_closure 254 | # Trailing closure syntax should be used whenever possible. 255 | - trailing_comma 256 | # Trailing commas in arrays and dictionaries should be avoided/enforced. 257 | - trailing_newline 258 | # Files should have a single trailing newline. 259 | - trailing_semicolon 260 | # Lines should not have trailing semicolons. 261 | - trailing_whitespace 262 | # Lines should not have trailing whitespace. 263 | # Ignored lines are specified below. 264 | - type_body_length 265 | # Type bodies should not span too many lines. 266 | # See large_tuple below for the exact configuration. 267 | - type_name 268 | # Type name should only contain alphanumeric characters, start with an uppercase character and span between 3 and 40 characters in length. 269 | # Excluded types are listed below. 270 | - unavailable_function 271 | # Unimplemented functions should be marked as unavailable. 272 | - unneeded_break_in_switch 273 | # Avoid using unneeded break statements. 274 | - unneeded_parentheses_in_closure_argument 275 | # Parentheses are not needed when declaring closure arguments. 276 | - untyped_error_in_catch 277 | # Catch statements should not declare error variables without type casting. 278 | - unused_capture_list 279 | # Unused reference in a capture list should be removed. 280 | - unused_closure_parameter 281 | # Unused parameter in a closure should be replaced with _. 282 | - unused_control_flow_label 283 | # Unused control flow label should be removed. 284 | - unused_declaration 285 | # Declarations should be referenced at least once within all files linted. 286 | - unused_enumerated 287 | # When the index or the item is not used, .enumerated() can be removed. 288 | - unused_import 289 | # All imported modules should be required to make the file compile. 290 | - unused_optional_binding 291 | # Prefer != nil over let _ = 292 | - unused_setter_value 293 | # Setter value is not used. 294 | - valid_ibinspectable 295 | # @IBInspectable should be applied to variables only, have its type explicit and be of a supported type 296 | - vertical_parameter_alignment 297 | # Function parameters should be aligned vertically if they're in multiple lines in a declaration. 298 | - vertical_parameter_alignment_on_call 299 | # Function parameters should be aligned vertically if they're in multiple lines in a method call. 300 | - vertical_whitespace 301 | # Limit vertical whitespace to a single empty line. 302 | # See vertical_whitespace below for the exact configuration. 303 | - vertical_whitespace_closing_braces 304 | # Don’t include vertical whitespace (empty line) before closing braces. 305 | - vertical_whitespace_opening_braces 306 | # Don’t include vertical whitespace (empty line) after opening braces. 307 | - void_return 308 | # Prefer -> Void over -> (). 309 | - weak_delegate 310 | # Delegates should be weak to avoid reference cycles. 311 | - xctfail_message 312 | # An XCTFail call should include a description of the assertion. 313 | - yoda_condition 314 | # The variable should be placed on the left, the constant on the right of a comparison operator. 315 | 316 | excluded: # paths to ignore during linting. Takes precedence over `included`. 317 | - Carthage 318 | - Pods 319 | - .build 320 | - .swiftpm 321 | - R.generated.swift 322 | 323 | closure_body_length: # Closure bodies should not span too many lines. 324 | - 35 # warning - default: 20 325 | - 35 # error - default: 100 326 | 327 | enum_case_associated_values_count: # Number of associated values in an enum case should be low 328 | - 5 # warning - default: 5 329 | - 5 # error - default: 6 330 | 331 | file_length: # Files should not span too many lines. 332 | - 500 # warning - default: 400 333 | - 500 # error - default: 1000 334 | 335 | function_body_length: # Functions bodies should not span too many lines. 336 | - 50 # warning - default: 40 337 | - 50 # error - default: 100 338 | 339 | function_parameter_count: # Number of function parameters should be low. 340 | - 5 # warning - default: 5 341 | - 5 # error - default: 8 342 | 343 | identifier_name: 344 | excluded: # excluded names 345 | - id 346 | - ok 347 | - or 348 | - x 349 | - y 350 | - px 351 | 352 | large_tuple: # Tuples shouldn't have too many members. Create a custom type instead. 353 | - 4 # warning - default: 2 354 | - 5 # error - default: 3 355 | 356 | line_length: # Lines should not span too many characters. 357 | warning: 150 # default: 120 358 | error: 150 # default: 200 359 | ignores_comments: true # default: false 360 | ignores_urls: true # default: false 361 | ignores_function_declarations: false # default: false 362 | ignores_interpolated_strings: true # default: false 363 | 364 | nesting: # Types should be nested at most 2 level deep, and statements should be nested at most 5 levels deep. 365 | type_level: 366 | warning: 2 # warning - default: 1 367 | statement_level: 368 | warning: 5 # warning - default: 5 369 | 370 | trailing_closure: 371 | only_single_muted_parameter: true 372 | 373 | type_body_length: # Type bodies should not span too many lines. 374 | - 250 # warning - default: 200 375 | - 250 # error - default: 200 376 | 377 | type_name: 378 | excluded: # excluded names 379 | - ID 380 | 381 | trailing_whitespace: 382 | ignores_empty_lines: true # default: false 383 | ignores_comments: true # default: false 384 | 385 | unused_optional_binding: 386 | ignore_optional_try: true 387 | 388 | vertical_whitespace: # Limit vertical whitespace to a single empty line. 389 | max_empty_lines: 2 # warning - default: 1 390 | -------------------------------------------------------------------------------- /Images/helloworld-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apodini/SwiftWeb/54823c204b5dfdea97d570cb65c5e32cfafc0b7f/Images/helloworld-screenshot.png -------------------------------------------------------------------------------- /Images/swiftweb-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apodini/SwiftWeb/54823c204b5dfdea97d570cb65c5e32cfafc0b7f/Images/swiftweb-architecture.png -------------------------------------------------------------------------------- /Images/swiftweb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apodini/SwiftWeb/54823c204b5dfdea97d570cb65c5e32cfafc0b7f/Images/swiftweb.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Quirin Schweigert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "SwiftWeb", 6 | platforms: [ 7 | .macOS(.v10_15) 8 | ], 9 | products: [ 10 | .library(name: "SwiftWeb", targets: ["SwiftWeb"]), 11 | .library(name: "SwiftWebScript", targets: ["SwiftWebScript"]) 12 | ], 13 | targets: [ 14 | .target(name: "SwiftWeb"), 15 | .target(name: "SwiftWebScript"), 16 | .testTarget(name: "SwiftWebTests", dependencies: ["SwiftWeb"]) 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SwiftWeb logo 2 | 3 | ### SwiftWeb 4 | **Run Your SwiftUI App on a Swift Server. Serve Many Clients with Your SwiftUI Web App.** 5 | 6 | --- 7 | 8 | SwiftWeb logo 9 | 10 | With **SwiftWeb**, you can easily provide a web interface to your existing SwiftUI app. SwiftWeb renders SwiftUI code to HTML and CSS and keeps a WebSocket connection to connected Browsers. User input events are sent to your Swift server which runs your application logic. Screen updates are sent back to connected clients. 11 | 12 | ## Requirements 13 | 14 | The SwiftWeb framework is intentionally kept independant of any HTTP / WebSocket server implementation. In order to provide a user interface over the web, you need to 15 | 1. provide the SwiftWeb HTML template (`HTMLTemplate.withContent("")`) under a URL of your desire, 16 | 2. provide the JavaScript client script (`JavaScriptClient.script`) under `/script.js` and 17 | 3. implement a WebSocket endpoint under `/websocket` on your server and connect it to a `SwiftWebServer` instance. 18 | 19 | Have a look at the [example implementation](https://github.com/Apodini/SwiftWeb-Example) of an XCode project running an HTTP and WebSocket server together with SwiftWeb. 20 | 21 | ## Usage 22 | 23 | Simply instantiate a server instance with a view instance: 24 | 25 | ```let swiftWebServer = SwiftWebServer(contentView: Text("Hello World!")``` 26 | 27 |

28 | Hello World Screenshot 29 |

30 | 31 | The JavaScript client will connect to the server instance using a WebSocket connection and load the current state of the interface. 32 | 33 | Check out the [example project](https://github.com/Apodini/SwiftWeb-Example) implementing various view components with SwiftWeb. 34 | 35 | ## Contributing 36 | Contributions to this projects are welcome. Please make sure to read the [contribution guidelines](https://github.com/Apodini/.github/blob/master/CONTRIBUTING.md) first. 37 | 38 | ## License 39 | This project is licensed under the MIT License. See [License](https://github.com/Apodini/Template-Repository/blob/master/LICENSE) for more information. 40 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Extensions/Sequence+keypath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sequence+keypath.swift 3 | // App 4 | // 5 | // Created by Quirin Schweigert on 02.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Sequence { 11 | /// Returns an array containing the results of mapping the elements to the specified `KeyPath`. 12 | func map(_ keyPath: KeyPath) -> [T] { 13 | self.map { 14 | $0[keyPath: keyPath] 15 | } 16 | } 17 | 18 | /// Returns an array containing the non-nil results of mapping the elements of this sequence to the specified `KeyPath`. 19 | func compactMap(_ keyPath: KeyPath) -> [T] { 20 | self.compactMap { 21 | $0[keyPath: keyPath] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/HTML/HTMLNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLNode.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 02.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A structure representing an HTML element with means to modify its CSS style as well as custom attributes. 12 | */ 13 | public enum HTMLNode { 14 | case raw(String) 15 | 16 | case div( 17 | subNodes: [HTMLNode] = [], 18 | style: [CSSKey: CSSValue] = [:], 19 | customAttributes: [String: String?] = [:] 20 | ) 21 | 22 | case img( 23 | path: String, 24 | style: [CSSKey: CSSValue] = [:] 25 | ) 26 | 27 | case input( 28 | placeholder: String, 29 | value: String, 30 | style: [CSSKey: CSSValue] = [:], 31 | customAttributes: [String: String?] = [:] 32 | ) 33 | 34 | /// Creates a string representation of the `HTMLNode`. 35 | public func string() -> String { 36 | switch self { 37 | case .raw(let string): 38 | return string 39 | case let .div(subNodes, style, customAttributes): 40 | return """ 41 |
42 | \(subNodes.map { $0.string() } .joined()) 43 |
44 | """ 45 | case let .img(path, style): 46 | return """ 47 | 48 | """ 49 | case let .input(placeholder, value, style, customAttributes): 50 | return """ 51 | 52 | """ 53 | } 54 | } 55 | 56 | /// Creates a string representation of a set of CSS attributes. 57 | static func cssTag(from styleDictionary: [CSSKey: CSSValue]) -> String { 58 | guard !styleDictionary.isEmpty else { 59 | return .init() 60 | } 61 | 62 | let cssString = styleDictionary 63 | .compactMap { key, value in "\(key): \(value.cssString);" } 64 | .sorted() 65 | .joined(separator: " ") 66 | 67 | return "style=\"\(cssString)\"" 68 | } 69 | 70 | /** 71 | Returns an `HTMLNode` with an added CSS style attribute. 72 | 73 | - Parameters: 74 | - key: The CSS key of the added attribute. 75 | - value: The CSS value of the added attribute. 76 | */ 77 | public func withStyle(key: CSSKey, value newValue: CSSValue) -> Self { 78 | switch self { 79 | case .raw: 80 | return self 81 | case let .div(subnodes, style, customAttributes): 82 | var newStyle = style 83 | newStyle[key] = newValue 84 | return .div( 85 | subNodes: subnodes, 86 | style: newStyle, 87 | customAttributes: customAttributes 88 | ) 89 | 90 | case let .img(path, style): 91 | var newStyle = style 92 | newStyle[key] = newValue 93 | return .img(path: path, style: newStyle) 94 | 95 | case let .input(placeholder, value, style, customAttributes): 96 | var newStyle = style 97 | newStyle[key] = newValue 98 | return .input( 99 | placeholder: placeholder, 100 | value: value, 101 | style: newStyle, 102 | customAttributes: customAttributes 103 | ) 104 | } 105 | } 106 | 107 | /** 108 | Returns an `HTMLNode` with an added custom HTML attribute. 109 | 110 | - Parameters: 111 | - key: The `String ` key of the added attribute. 112 | - value: The `String ` value of the added attribute. 113 | */ 114 | public func withCustomAttribute(key: String, value newValue: String? = nil) -> Self { 115 | switch self { 116 | case let .div(subnodes, style, customAttributes): 117 | var newAttributes = customAttributes 118 | newAttributes[key] = newValue 119 | 120 | return .div( 121 | subNodes: subnodes, 122 | style: style, 123 | customAttributes: newAttributes 124 | ) 125 | 126 | case let .input(placeholder, value, style, customAttributes): 127 | var newAttributes = customAttributes 128 | newAttributes[key] = newValue 129 | 130 | return .input( 131 | placeholder: placeholder, 132 | value: value, 133 | style: style, 134 | customAttributes: newAttributes 135 | ) 136 | 137 | default: 138 | return self 139 | } 140 | } 141 | 142 | /// An enumeration representing a CSS key. 143 | public enum CSSKey: String, CustomStringConvertible { 144 | case backgroundColor = "background-color" 145 | case flexGrow = "flex-grow" 146 | case display 147 | case flexDirection = "flex-direction" 148 | case width 149 | case height 150 | case color 151 | case justifyContent = "justify-content" 152 | case alignItems = "align-items" 153 | case fontSize = "font-size" 154 | case fontWeight = "font-weight" 155 | case flexBasis = "flex-basis" 156 | case flexShrink = "flex-shrink" 157 | case alignSelf = "align-self" 158 | case marginLeft = "margin-left" 159 | case position 160 | case borderRadius = "border-radius" 161 | case boxShadow = "box-shadow" 162 | case padding 163 | case paddingLeft = "padding-left" 164 | case paddingTop = "padding-top" 165 | case paddingRight = "padding-right" 166 | case paddingBottom = "padding-bottom" 167 | case border 168 | case overflow 169 | case minWidth = "min-width" 170 | case minHeight = "min-height" 171 | case filter 172 | case top 173 | case left 174 | case right 175 | case bottom 176 | case fontFamily = "font-family" 177 | case pointerEvents = "pointer-events" 178 | 179 | public var description: String { 180 | self.rawValue 181 | } 182 | } 183 | 184 | /// An enumeration representing a CSS value. 185 | public enum CSSValue: Equatable { 186 | case raw(String) 187 | case px(Double) 188 | case flex 189 | case row 190 | case column 191 | case center 192 | case flexStart 193 | case flexEnd 194 | case stretch 195 | case int(Int) 196 | case zero 197 | case one 198 | case percent(Int) 199 | case color(Color) 200 | case initial 201 | case relative 202 | case absolute 203 | case shadow(offsetX: Double, offsetY: Double, radius: Double, color: Color) 204 | case border(width: Double, color: Color) 205 | case hidden 206 | case fixed 207 | case auto 208 | 209 | public var cssString: String { 210 | switch self { 211 | case .raw(let string): 212 | return string 213 | case .px(let value): 214 | return "\(value)px" 215 | case .int(let value): 216 | return String(describing: value) 217 | case .zero: 218 | return "0" 219 | case .one: 220 | return "1" 221 | case .percent(let value): 222 | return "\(value)%" 223 | case .color(let color): 224 | return color.cssString 225 | case .flexStart: 226 | return "flex-start" 227 | case .flexEnd: 228 | return "flex-end" 229 | case let .shadow(offsetX, offsetY, radius, color): 230 | return "\(offsetX)px \(offsetY)px \(radius)px \(color.cssString)" 231 | case let .border(width, color): 232 | return "\(width)px solid \(color.cssString)" 233 | default: 234 | return String(describing: self) 235 | } 236 | } 237 | 238 | public static func == (lhs: HTMLNode.CSSValue, rhs: HTMLNode.CSSValue) -> Bool { 239 | lhs.cssString == rhs.cssString 240 | } 241 | } 242 | 243 | public static func div(style: [CSSKey: CSSValue] = [:], buildSubnode: () -> HTMLNode) -> Self { 244 | .div(subNodes: [buildSubnode()], style: style) 245 | } 246 | } 247 | 248 | extension Dictionary where Key == String, Value == String? { 249 | var htmlAttributesString: String { 250 | self 251 | .map { key, value in 252 | if let value = value { 253 | return "\(key)=\"\(value)\"" 254 | } 255 | 256 | return key 257 | } 258 | .sorted() 259 | .joined(separator: " ") 260 | } 261 | } 262 | 263 | /// Provides functionality for an `Array` of `HTMLNode`s. 264 | public extension Array where Element == HTMLNode { 265 | /// Returns a single `HTMLNode` representing the array: An empty `raw` node if the array is empty, the single element of the 266 | /// array or a `div` node with the elements of this array as subnodes. 267 | func joined() -> HTMLNode { 268 | if isEmpty { 269 | print("joining empty html") 270 | return .raw(.init()) 271 | } else if count == 1, let first = first { 272 | return first 273 | } else { 274 | return .div(subNodes: self) 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/HTML/HTMLTemplate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTMLTemplate.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 05.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The HTML template provided by SwiftWeb. Serve this with your HTTP server implementation. 11 | public struct HTMLTemplate { 12 | /// Retrieve the template as a string. You can provide your custom content as an argument which will be replaced by the SwiftWeb 13 | /// JavaScript client once a connection to the server is established. 14 | public static func withContent(_ content: String) -> String { 15 | """ 16 | 17 | 18 | 19 | 20 | 21 | 22 | SwiftWeb 23 | 75 | 76 | 77 | \(content) 78 | 79 | 80 | """ 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Server/InputEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputEvent.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 29.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | enum InputEvent { 11 | case click(id: UUID) 12 | case change(id: UUID, newValue: String) 13 | } 14 | 15 | extension InputEvent: Codable { 16 | private enum CodingKeys: String, CodingKey { 17 | case click 18 | case change 19 | } 20 | 21 | private enum AssociatedValuesCodingKeys: String, CodingKey { 22 | case id 23 | case newValue 24 | } 25 | 26 | enum InputEventCodingError: Error { 27 | case decoding(String) 28 | } 29 | 30 | public func encode(to encoder: Encoder) throws { 31 | var container = encoder.container(keyedBy: CodingKeys.self) 32 | 33 | switch self { 34 | case .click(let id): 35 | var associatedValuesContainer = container.nestedContainer( 36 | keyedBy: AssociatedValuesCodingKeys.self, 37 | forKey: .click 38 | ) 39 | 40 | try associatedValuesContainer.encode(id, forKey: .id) 41 | 42 | case let .change(id, value): 43 | var associatedValuesContainer = container.nestedContainer( 44 | keyedBy: AssociatedValuesCodingKeys.self, 45 | forKey: .change 46 | ) 47 | 48 | try associatedValuesContainer.encode(id, forKey: .id) 49 | try associatedValuesContainer.encode(value, forKey: .newValue) 50 | } 51 | } 52 | 53 | public init(from decoder: Decoder) throws { 54 | let values = try decoder.container(keyedBy: CodingKeys.self) 55 | 56 | if values.contains(.click) { 57 | let clickAssociatedValuesContainer = try values.nestedContainer( 58 | keyedBy: AssociatedValuesCodingKeys.self, 59 | forKey: .click 60 | ) 61 | 62 | let id = try clickAssociatedValuesContainer.decode(UUID.self, forKey: .id) 63 | 64 | self = .click(id: id) 65 | } else if values.contains(.change) { 66 | let clickAssociatedValuesContainer = try values.nestedContainer( 67 | keyedBy: AssociatedValuesCodingKeys.self, 68 | forKey: .change 69 | ) 70 | 71 | let id = try clickAssociatedValuesContainer.decode(UUID.self, forKey: .id) 72 | let newValue = try clickAssociatedValuesContainer.decode(String.self, forKey: .newValue) 73 | 74 | self = .change(id: id, newValue: newValue) 75 | } else { 76 | throw InputEventCodingError.decoding(String(reflecting: values)) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Server/InputEventResponder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputEventResponder.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 30.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol InputEventResponder { } 11 | 12 | /// Implement this protocol with your `View` to receive HTML click events. 13 | public protocol ClickInputEventResponder { 14 | /// Called when a click event is received. 15 | func onClickInputEvent() 16 | } 17 | 18 | /// Implement this protocol with your `View` to receive HTML change events. 19 | public protocol ChangeInputEventResponder { 20 | /// Called when a change event is received. 21 | func onChangeInputEvent(newValue: String) 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Server/SwiftWebServer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftWebServer.swift 3 | // SwiftWebServer 4 | // 5 | // Created by Quirin Schweigert on 05.01.20. 6 | // Copyright © 2020 Quirin Schweigert. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | A server managing connections with client browsers providing a user interface via HTML. 13 | */ 14 | public class SwiftWebServer { 15 | let viewTree: ViewTree 16 | 17 | /// Instantiates a `SwiftWebServer` instance with a root `View` specifying the user interface which is provided to clients. 18 | public init(contentView: ContentView) where ContentView: View { 19 | viewTree = ViewTree(withRootView: contentView) 20 | } 21 | 22 | /// Call this method for the server instance to handle an incoming message from a WebSocket client. SwiftWeb sends user input 23 | /// events which the JavaScript client captures over this connection. 24 | public func handleClientMessage(session: WebSocketSession, message: String) { 25 | guard let data = message.data(using: .utf8) else { 26 | return 27 | } 28 | 29 | let inputEvent: InputEvent 30 | 31 | do { 32 | inputEvent = try JSONDecoder().decode(InputEvent.self, from: data) 33 | } catch { 34 | print("error decoding received input event: \(error)") 35 | return 36 | } 37 | 38 | print("received input event: \(inputEvent)") 39 | 40 | self.viewTree.handle(inputEvent: inputEvent) 41 | session.write(text: self.viewTree.render().string()) 42 | print(self.viewTree.description) 43 | } 44 | 45 | /// Call this method whenever a new client connects to the WebSocket server you provide. 46 | public func handleClientConnect(session: WebSocketSession) { 47 | print("client connected") 48 | session.write(text: self.viewTree.render().string()) 49 | print(self.viewTree.description) 50 | } 51 | 52 | /// Call this method whenever a client disconnects from your WebSocket server. 53 | public func handleClientDisconnect(session: WebSocketSession) { 54 | print("client disconnected") 55 | } 56 | } 57 | 58 | /// Represents a session of the WebSocket server you provide to a client browser. 59 | public protocol WebSocketSession { 60 | /// Sends a message to the respective WebSocket client. 61 | func write(text: String) 62 | } 63 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Utility/Edge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Edge.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 27.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An enumeration to indicate one edge of a rectangle. 11 | public enum Edge: Int8, CaseIterable { 12 | /// The bottom edge. 13 | case bottom 14 | 15 | /// The leading edge. 16 | case leading 17 | 18 | /// The top edge. 19 | case top 20 | 21 | /// The trailing edge. 22 | case trailing 23 | 24 | /// An efficient set of `Edge`s. 25 | public struct Set: OptionSet { 26 | public let rawValue: Int 27 | 28 | public init(rawValue: Int) { 29 | self.rawValue = rawValue 30 | } 31 | 32 | /// All edges. 33 | public static let all: Self = [.bottom, .top, .leading, .trailing] 34 | 35 | /// The bottom edge. 36 | public static let bottom: Self = .init(rawValue: 1 << Edge.bottom.rawValue) 37 | 38 | /// Horizontal edges. 39 | public static let horizontal: Self = [.leading, .trailing] 40 | 41 | /// The leading edge. 42 | public static let leading: Self = .init(rawValue: 1 << Edge.leading.rawValue) 43 | 44 | /// The top edge. 45 | public static let top: Self = .init(rawValue: 1 << Edge.top.rawValue) 46 | 47 | /// The trailing edge. 48 | public static let trailing: Self = .init(rawValue: 1 << Edge.trailing.rawValue) 49 | 50 | /// Vertical edges. 51 | public static let vertical: Self = [.top, .bottom] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Utility/Font.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Font.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 05.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An environment-dependent font. 11 | public struct Font { 12 | let size: Double 13 | let weight: Weight 14 | let design: Design 15 | 16 | /// Specifies a system font to use, along with the style, weight, and any design parameters you want applied to the text. 17 | public static func system(size: Double, 18 | weight: Weight = .regular, 19 | design: Design = .default) -> Font { 20 | Font(size: size, weight: weight, design: design) 21 | } 22 | 23 | /// A font with the title text style. 24 | public static var title: Font { 25 | .system(size: 26) 26 | } 27 | 28 | /// A font with the subheadline text style. 29 | public static var subheadline: Font { 30 | .system(size: 16) 31 | } 32 | 33 | /// A weight to use for fonts. 34 | public enum Weight: Int { 35 | case thin = 150 36 | case regular = 250 37 | case medium = 400 38 | case semibold = 500 39 | case bold = 600 40 | } 41 | 42 | /// A design to use for fonts. 43 | public enum Design: Hashable { 44 | case `default` 45 | case rounded 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Utility/LayoutAxis.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutAxis.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 06.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Primary layout axis of a view. Used by the SwiftWeb layout system to translate `GrowingLayoutAxis`s into CSS attributes at each level of the view hierarchy. 12 | */ 13 | public enum LayoutAxis { 14 | /// Indicates that a `View` lays out its children horizontally. 15 | case horizontal 16 | 17 | /// Indicates that a `View` lays out its children vertically. 18 | case vertical 19 | } 20 | 21 | /** 22 | An an axis in which the layout of a view can be extended to fill the available space. 23 | 24 | The growing axes of a view can be modified by the view itself by implementing the `GrowingAxesModifying` protocol. 25 | */ 26 | public enum GrowingLayoutAxis { 27 | /// An undetermined layout axis. Used by e.g. Spacers to indicate that its growing axes is only determined by a containing 28 | /// `HStack` or `VStack` 29 | case undetermined 30 | 31 | /// Indicates that a `View` can grow vertically. 32 | case vertical 33 | 34 | /// Indicates that a `View` can grow horizontally. 35 | case horizontal 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Utility/PreviewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewProvider.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 15.05.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A type that produces view previews in Xcode for SwiftUI. Currently has no effect with the SwiftWeb framework. 11 | public protocol PreviewProvider { 12 | associatedtype Previews: View 13 | static var previews: Self.Previews { get } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View Modifiers/ColorFilterModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorFilterModifier.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 10.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ColorFilterModifier: ViewModifier where Content: View { 11 | typealias Body = Content 12 | 13 | let cssFilterString: String 14 | 15 | func html(forHTMLOfContent htmlOfContent: HTMLNode) -> HTMLNode { 16 | htmlOfContent 17 | .withStyle(key: .filter, value: .raw(cssFilterString)) 18 | } 19 | } 20 | 21 | extension View { 22 | func systemBlueFilter() -> some View { 23 | let cssFilter = "contrast(0%) brightness(0%) invert(39%) sepia(81%) saturate(4741%) hue-rotate(202deg) brightness(103%) contrast(101%)" 24 | 25 | return ModifiedContent( 26 | content: self, 27 | modifier: ColorFilterModifier(cssFilterString: cssFilter) 28 | ) 29 | } 30 | 31 | func systemGrayFilter() -> some View { 32 | let cssFilter = "contrast(0%) brightness(0%) invert(60%) sepia(0%) saturate(0%) hue-rotate(111deg) brightness(98%) contrast(96%)" 33 | 34 | return ModifiedContent( 35 | content: self, 36 | modifier: ColorFilterModifier(cssFilterString: cssFilter) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View Modifiers/FontModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontModifier.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 10.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct FontModifier: ViewModifier where Content: View { 11 | typealias Body = Content 12 | 13 | let font: Font 14 | 15 | func html(forHTMLOfContent htmlOfContent: HTMLNode) -> HTMLNode { 16 | var newHTML = htmlOfContent 17 | .withStyle(key: .fontSize, value: .px(font.size)) 18 | .withStyle(key: .fontWeight, value: .int(font.weight.rawValue)) 19 | 20 | if font.design == .rounded { 21 | newHTML = newHTML.withStyle( 22 | key: .fontFamily, 23 | value: .raw("sf-pro-rounded-bold") 24 | ) 25 | } 26 | 27 | return newHTML 28 | } 29 | } 30 | 31 | extension View { 32 | public func font(_ font: Font) -> some View { 33 | ModifiedContent(content: self, modifier: FontModifier(font: font)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View Modifiers/ModifiedContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifiedContent.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 10.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ModifiedContent: View 11 | where Modifier: ViewModifier, Modifier.Content == Content { 12 | let content: Content 13 | let modifier: Modifier 14 | 15 | var body: some View { 16 | modifier.body(content: content) 17 | } 18 | 19 | func html(forHTMLOfSubnodes htmlOfSubnodes: [HTMLNode]) -> HTMLNode { 20 | modifier.html(forHTMLOfContent: htmlOfSubnodes.joined()) 21 | } 22 | } 23 | 24 | /// A modifier that you apply to a view or another view modifier, producing a different version of the original value. 25 | public protocol ViewModifier { 26 | /// The type of view representing the body. 27 | associatedtype Body: View 28 | 29 | /// The content view type. 30 | associatedtype Content: View 31 | 32 | /// Gets the current body of the caller. 33 | func body(content: Self.Content) -> Self.Body 34 | 35 | /// Specifies the transformation of HTML as implemented by this `ViewModifier` 36 | func html(forHTMLOfContent: HTMLNode) -> HTMLNode 37 | } 38 | 39 | public extension ViewModifier { 40 | func html(forHTMLOfContent htmlOfContent: HTMLNode) -> HTMLNode { // Shouldn't we somewhere delegate to body(content:).html(for... ? 41 | htmlOfContent 42 | } 43 | } 44 | 45 | public extension ViewModifier where Body == Content { 46 | /// Returns `content` as `body`. 47 | func body(content: Self.Content) -> Self.Body { 48 | content 49 | } 50 | } 51 | 52 | struct HTMLTransformingViewModifier: ViewModifier where Content: View { 53 | let transform: (HTMLNode) -> HTMLNode 54 | typealias Body = Content 55 | 56 | init(transform: @escaping (HTMLNode) -> HTMLNode) { 57 | self.transform = transform 58 | } 59 | 60 | func html(forHTMLOfContent htmlOfContent: HTMLNode) -> HTMLNode { 61 | transform(htmlOfContent) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View Tree/Binding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 17.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A property wrapper type that can read and write a value owned by a source of truth. 11 | @propertyWrapper public struct Binding { 12 | private var getValue: () -> Value 13 | private var setValue: (Value) -> Void 14 | 15 | /// The underlying value referenced by the binding variable. 16 | public var wrappedValue: Value { 17 | get { 18 | getValue() 19 | } 20 | 21 | nonmutating set { 22 | setValue(newValue) 23 | } 24 | } 25 | 26 | /// A projection of the binding value that returns a binding. 27 | public var projectedValue: Self { 28 | self 29 | } 30 | 31 | /// Creates a binding with closures that read and write the binding value. 32 | public init(getValue: @escaping () -> Value, setValue: @escaping (Value) -> Void) { 33 | self.getValue = getValue 34 | self.setValue = setValue 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View Tree/GrowingAxesModifying.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GrowingAxesModifying.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 16.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Implement this protocol with your `View` in order to customize its set of `GrowingLayoutAxis`. 12 | */ 13 | public protocol GrowingAxesModifying { 14 | /// Defines the set of `GrowingLayoutAxis` depending on the set of `GrowingLayoutAxis` of the subnodes of this view. 15 | func modifiedGrowingLayoutAxes(forGrowingAxesOfSubnodes: Set) 16 | -> Set 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View Tree/PreferenceProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewPreferences.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 22.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A named value produced by a view. 12 | 13 | A view with multiple children automatically combines its values for a given preference into a single value visible to its ancestors. 14 | */ 15 | public protocol PreferenceKey: AnyPreferenceKey { 16 | /// The type of value produced by this preference. 17 | associatedtype Value 18 | 19 | /// The default value of the preference. 20 | static var defaultValue: Self.Value { get } 21 | 22 | /// Combines a sequence of values by modifying the previously-accumulated value with the result of a closure that provides the 23 | /// next value. 24 | static func reduce(value: inout Self.Value, nextValue: () -> Self.Value) 25 | } 26 | 27 | protocol PreferenceProvider { 28 | var preferenceKeyType: AnyPreferenceKey.Type { get } 29 | var preferenceValue: Any { get } 30 | } 31 | 32 | /// Type-erased `PreferenceKey`. 33 | public protocol AnyPreferenceKey {} 34 | 35 | protocol PreferenceChangeListener { 36 | var preferenceKeyType: AnyPreferenceKey.Type { get } 37 | func onPreferenceChange(preferenceValue: Any?) 38 | } 39 | 40 | //protocol PreferenceProvider { 41 | // associatedtype P: PreferenceKey 42 | // var preferenceValue: P.Value { get } 43 | //} 44 | 45 | //extension TypeErasedPreferenceKey { 46 | // static func == (lhs: TypeErasedPreferenceKey, rhs: TypeErasedPreferenceKey) -> Bool { 47 | // return String(describing: lhs) == String(describing: rhs) 48 | // } 49 | // 50 | // func hash(into hasher: inout Hasher) { 51 | // hasher.combine(String(describing: self)) 52 | // } 53 | //} 54 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View Tree/State.swift: -------------------------------------------------------------------------------- 1 | // 2 | // State.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 08.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol TypeErasedState: AnyObject { 11 | func connect(to stateStorageNode: StateStorageNode, withPropertyName propertyName: String) 12 | func disconnect() 13 | } 14 | 15 | /** 16 | A property wrapper type that can read and write a value managed by SwiftWeb. 17 | 18 | SwiftWeb manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance 19 | and recomputes the body. Use the state as the single source of truth for a given view. 20 | 21 | A State instance isn’t the value itself; it’s a means of reading and writing the value. To access a state’s underlying value, use its variable 22 | name, which returns the `wrappedValue` property value. 23 | 24 | You should only access a state property from inside the view’s body, or from methods called by it. 25 | */ 26 | @propertyWrapper public class State: TypeErasedState { 27 | let defaultValue: Value 28 | 29 | private var propertyName: String? 30 | private var stateStorageNode: StateStorageNode? 31 | 32 | /// The underlying value referenced by the state variable. 33 | public var wrappedValue: Value { 34 | get { 35 | guard let propertyName = propertyName, let stateStorageNode = stateStorageNode else { 36 | print("property name or state storage node not set!") 37 | return defaultValue 38 | } 39 | 40 | return stateStorageNode.getProperty(forKey: propertyName) as? Value ?? defaultValue 41 | } 42 | 43 | set { 44 | guard let propertyName = propertyName, let stateStorageNode = stateStorageNode else { 45 | print("property name or state storage node not set!") 46 | return 47 | } 48 | 49 | stateStorageNode.setProperty(value: newValue, forKey: propertyName) 50 | } 51 | } 52 | 53 | func connect(to stateStorageNode: StateStorageNode, 54 | withPropertyName propertyName: String) { 55 | self.propertyName = propertyName 56 | self.stateStorageNode = stateStorageNode 57 | 58 | if stateStorageNode.getProperty(forKey: propertyName) == nil { 59 | stateStorageNode.setProperty(value: defaultValue, forKey: propertyName) 60 | } 61 | } 62 | 63 | func disconnect() { 64 | self.propertyName = nil 65 | self.stateStorageNode = nil 66 | } 67 | 68 | /// Creates the state with an initial wrapped value. 69 | public init(wrappedValue: Value) { 70 | self.defaultValue = wrappedValue 71 | } 72 | 73 | /// Returns the `binding` property of the `State` instance. 74 | public var projectedValue: Binding { 75 | binding 76 | } 77 | 78 | /// A binding to the state value. 79 | public var binding: Binding { 80 | Binding( 81 | getValue: { self.wrappedValue }, 82 | setValue: { self.wrappedValue = $0 } 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View Tree/StateStorageNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateStorageNode.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 08.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | class StateStorageNode { 11 | let viewInstanceID = UUID() 12 | var state: [String: Any] = [:] 13 | var onChange: (() -> Void)? 14 | 15 | func setProperty(value: Any, forKey key: String) { 16 | state[key] = value 17 | onChange?() 18 | } 19 | 20 | func getProperty(forKey key: String) -> Any? { 21 | state[key] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View Tree/ViewNode+CustomStringConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 31.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension ViewNode: CustomStringConvertible { 11 | public var description: String { 12 | var descriptionOfThisNode = "\(Self.simpleType(of: view)) \(stateStorageNode.state)" 13 | 14 | if let text = view as? Text { 15 | descriptionOfThisNode.append(" \"\(text.text)\"") 16 | } 17 | 18 | descriptionOfThisNode.append(" \(stateStorageNode.viewInstanceID.uuidString)") 19 | 20 | if subnodes.isEmpty { 21 | return "" 22 | } else { 23 | return """ 24 | 25 | \(subnodes.map { $0.description } .joined(separator: "\n").blockIndented()) 26 | 27 | """ 28 | } 29 | } 30 | 31 | static func simpleType(of value: Any) -> String { 32 | let typeString = String(describing: type(of: value)) 33 | if let simpleTypeString = typeString.split(separator: "<").first { 34 | return String(simpleTypeString) 35 | } else { 36 | return typeString 37 | } 38 | } 39 | } 40 | 41 | extension String { 42 | func blockIndented() -> String { 43 | self 44 | .split(separator: "\n") 45 | .map { 46 | " \($0)" 47 | } 48 | .joined(separator: "\n") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View Tree/ViewNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewNode.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 04.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | class ViewNode { 11 | var view: TypeErasedView 12 | let stateStorageNode: StateStorageNode 13 | var subnodes: [ViewNode] 14 | var isValid = true 15 | 16 | init(forView view: TypeErasedView, reconciling oldViewNode: ViewNode? = nil) { 17 | self.view = view 18 | subnodes = [] 19 | 20 | if let oldViewNode = oldViewNode, type(of: view) == type(of: oldViewNode.view) { 21 | // transfer state to the new node 22 | stateStorageNode = oldViewNode.stateStorageNode 23 | subnodes = buildSubtree(forView: view, reconcilingSubnodes: oldViewNode.subnodes) 24 | } else { 25 | // build subtree with new state 26 | stateStorageNode = StateStorageNode() 27 | subnodes = buildSubtree(forView: view, reconcilingSubnodes: []) 28 | } 29 | 30 | stateStorageNode.onChange = { 31 | self.isValid = false 32 | } 33 | 34 | // provideViewPreferences() 35 | } 36 | 37 | /** 38 | To adapt the SwiftUI layout system (the effect of Spacers and growing properties of views in general) we keep track of the set of 39 | growing axes for each view which we propagate through the view tree. This property is used when rendering to set CSS properties 40 | accordingly at each node of the tree. 41 | */ 42 | var growingLayoutAxes: Set { // TODO: buffer this 43 | let growingLayoutAxesOfSubnodes = subnodes 44 | .map(\.growingLayoutAxes) 45 | .reduce(Set()) { accumulator, growthAxes in 46 | accumulator.union(growthAxes) 47 | } 48 | 49 | if let growingAxesModifyingView = view as? GrowingAxesModifying { 50 | return growingAxesModifyingView 51 | .modifiedGrowingLayoutAxes(forGrowingAxesOfSubnodes: growingLayoutAxesOfSubnodes) 52 | } 53 | 54 | return growingLayoutAxesOfSubnodes 55 | } 56 | 57 | /** 58 | Put `view` into state context and render its HTML. Recursively render HTML of subnodes and hand the render functions an array 59 | of rendered HTML of the subcomponents so that composite views can compose it. While rendering, keep track of the growing axes 60 | of the view which can be determined by `View ` by implementing the protocol `GrowingLayoutAxesModifying`. 61 | */ 62 | func render() -> HTMLNode { 63 | executeInStateContext { view in 64 | let htmlOfSubnodes = subnodes.map { subnode in 65 | Self.applyGrowingProperties(toHTMLNode: subnode.render(), 66 | forGrowingLayoutAxes: subnode.growingLayoutAxes, 67 | inLayoutAxis: view.layoutAxis) 68 | } 69 | 70 | var html = view.html(forHTMLOfSubnodes: htmlOfSubnodes) 71 | .withCustomAttribute(key: "view", value: Self.simpleType(of: view)) 72 | 73 | if view is ClickInputEventResponder { 74 | html = html 75 | .withCustomAttribute(key: "click-event-responder") 76 | .withCustomAttribute(key: "id", value: stateStorageNode.viewInstanceID.uuidString) 77 | .withStyle(key: .pointerEvents, value: .auto) 78 | } 79 | 80 | if view is ChangeInputEventResponder { 81 | html = html 82 | .withCustomAttribute(key: "change-event-responder") 83 | .withCustomAttribute(key: "id", value: stateStorageNode.viewInstanceID.uuidString) 84 | } 85 | 86 | return html 87 | } 88 | } 89 | 90 | func handle(inputEvent: InputEvent) { 91 | executeInStateContext { view in 92 | switch inputEvent { 93 | case .click(let id): 94 | if id == stateStorageNode.viewInstanceID, 95 | let clickInputEventResponder = view as? ClickInputEventResponder { 96 | clickInputEventResponder.onClickInputEvent() 97 | } 98 | case let .change(id, newValue): 99 | if id == stateStorageNode.viewInstanceID, 100 | let changeInputEventResponder = view as? ChangeInputEventResponder { 101 | changeInputEventResponder.onChangeInputEvent(newValue: newValue) 102 | } 103 | } 104 | 105 | subnodes.forEach { subnode in 106 | subnode.handle(inputEvent: inputEvent) 107 | } 108 | } 109 | 110 | if !isValid { 111 | // rebuild the subtree while reconciling existing subnodes 112 | subnodes = buildSubtree(forView: view, reconcilingSubnodes: subnodes) 113 | } 114 | } 115 | 116 | func executeInStateContext(_ transaction: (TypeErasedView) -> T) -> T { 117 | let viewMirror = Mirror(reflecting: view) 118 | 119 | // This ties all `@State` properties of the view to the `StateStorageNode` associated with 120 | // this `ViewNode`. 121 | for child in viewMirror.children { 122 | if let typeErasedState = child.value as? TypeErasedState, let label = child.label { 123 | typeErasedState.connect(to: stateStorageNode, withPropertyName: label) 124 | } 125 | } 126 | 127 | // For testing we'll make sure to remove the reference for this storage container after the 128 | // transaction for now. It might be intended behaviour to keep this reference though because 129 | // then we could make mutating view state from outside (e.g. a scheduled closure) work. 130 | defer { 131 | for child in viewMirror.children { 132 | if let typeErasedState = child.value as? TypeErasedState { 133 | typeErasedState.disconnect() 134 | } 135 | } 136 | } 137 | 138 | return transaction(view) 139 | } 140 | 141 | func buildSubtree(forView view: TypeErasedView, 142 | reconcilingSubnodes oldSubnodes: [ViewNode]) -> [ViewNode] { 143 | executeInStateContext { view in 144 | let newSubviews = view.mapBody { $0 } 145 | 146 | // for now, if the number of subviews matches, we match pairs at the same indices 147 | if oldSubnodes.count == newSubviews.count { 148 | return zip(newSubviews, oldSubnodes).map { newSubview, oldSubnode in 149 | ViewNode(forView: newSubview, reconciling: oldSubnode) 150 | } 151 | } else { 152 | return newSubviews.map { subview in 153 | ViewNode(forView: subview) 154 | } 155 | } 156 | } 157 | } 158 | 159 | /** 160 | Apply CSS properties to `toHTMLNode` to make it grow in `forGrowingLayoutAxes` when placed in a CSS flex layout with 161 | the layout axis `inLayoutAxis`. 162 | */ 163 | static func applyGrowingProperties(toHTMLNode htmlNode: HTMLNode, 164 | forGrowingLayoutAxes growingLayoutAxes: Set, 165 | inLayoutAxis parentLayoutAxis: LayoutAxis) -> HTMLNode { 166 | growingLayoutAxes.reduce(htmlNode) { html, growthAxis in 167 | switch (growthAxis, parentLayoutAxis) { 168 | // For aligned axis of the layout direction of the parent node and this node the html 169 | // node can grow along the primary axis. 170 | case (.horizontal, .horizontal), (.vertical, .vertical): 171 | return html.withStyle(key: .flexGrow, value: .one) 172 | 173 | // For the perpendicular case it needs to stretch across the 174 | // secondary axis. 175 | case (.vertical, .horizontal), (.horizontal, .vertical): 176 | return html.withStyle(key: .alignSelf, value: .stretch) 177 | 178 | case (.undetermined, _): 179 | return html.withStyle(key: .flexGrow, value: .one) 180 | } 181 | } 182 | } 183 | 184 | func findPreferenceValue

(forKey preferenceKey: P.Type) -> P.Value? where P: PreferenceKey { 185 | let preferenceValueOfSubviews: P.Value? = subnodes.reduce(nil) { 186 | accumulatorValue, subnode -> P.Value? in 187 | 188 | let preferenceValueOfSubnode = subnode.findPreferenceValue(forKey: preferenceKey) 189 | 190 | // do we need to combine preference values because both accumulator and 191 | // preferenceValueOfSubnode are present? 192 | if let previousValue = accumulatorValue, 193 | let valueOfSubnode = preferenceValueOfSubnode { 194 | var value: P.Value = previousValue 195 | 196 | // reduce sibling preference values 197 | P.reduce(value: &value) { 198 | valueOfSubnode 199 | } 200 | 201 | // return the combined value 202 | return value 203 | } 204 | 205 | // return the one which is present 206 | return preferenceValueOfSubnode ?? accumulatorValue 207 | } 208 | 209 | let preferenceValueOfThisView: P.Value? 210 | 211 | // is the view that this node is holding providing a preference value for the supplied key? 212 | if let preferenceProviderView = view as? PreferenceProvider, 213 | preferenceProviderView.preferenceKeyType == P.self { 214 | preferenceValueOfThisView = preferenceProviderView.preferenceValue as? P.Value 215 | } else { 216 | preferenceValueOfThisView = nil 217 | } 218 | 219 | if let preferenceValueOfThisView = preferenceValueOfThisView, 220 | let preferenceValueOfSubviews = preferenceValueOfSubviews { 221 | var value: P.Value = preferenceValueOfSubviews 222 | 223 | // reduce preference value of subnodes and parent node 224 | P.reduce(value: &value) { 225 | preferenceValueOfThisView 226 | } 227 | 228 | return value 229 | } 230 | 231 | return preferenceValueOfThisView ?? preferenceValueOfSubviews 232 | } 233 | 234 | // func provideViewPreferences() { 235 | // if let preferenceChangeListener = view as? PreferenceChangeListener { 236 | //// preferenceChangeListener.onPreferenceChange( 237 | //// preferenceValue: findPreferenceValue(forKey: preferenceChangeListener.preferenceKey) 238 | //// ) 239 | // let preferenceValue = findPreferenceValue(forKey: preferenceChangeListener.) 240 | // } 241 | // } 242 | } 243 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View Tree/ViewTree.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewTree.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 05.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | class ViewTree { 11 | let rootNode: ViewNode 12 | 13 | init(withRootView rootView: TypeErasedView) { 14 | rootNode = ViewNode(forView: rootView) 15 | } 16 | 17 | func render() -> HTMLNode { 18 | rootNode.render() 19 | } 20 | 21 | func handle(inputEvent: InputEvent) { 22 | rootNode.handle(inputEvent: inputEvent) 23 | } 24 | } 25 | 26 | extension ViewTree: CustomStringConvertible { 27 | var description: String { 28 | rootNode.description 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 12.12.19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Type-erased `View` w.r.t. the `Body` associated type. 11 | public protocol TypeErasedView { 12 | /// The primary layout axis of the view. This is used by the SwiftWeb layout system to generate CSS properties mimicking the 13 | /// SwiftUI layout system. By default, all HTML elements in SwiftWeb are aligned in the `.vertical` axis. 14 | var layoutAxis: LayoutAxis { get } 15 | 16 | /// Defines the generation of HTML representing the view. 17 | func html(forHTMLOfSubnodes: [HTMLNode]) -> HTMLNode 18 | 19 | /// Transforms this view with the supplied closure argument. Can be overridden by `View`s by implementing the 20 | /// `CustomMappable` protocol in order to represent multiple subviews. This method is used by SwiftWeb for instantiating the 21 | /// view tree during runtime. 22 | func map(_ transform: (TypeErasedView) -> T) -> [T] 23 | 24 | /// Helper method which transforms the `body` of a view if the `Body` type is not equal to `Never`, returns an empty array 25 | /// otherwise. 26 | func mapBody(_ transform: (TypeErasedView) -> T) -> [T] 27 | } 28 | 29 | /** 30 | A type that represents part of your app’s user interface and provides modifiers that you use to configure views. 31 | 32 | You create custom views by declaring types that conform to the `View` protocol. Implement the required `body` computed property to provide the content for your custom view. 33 | */ 34 | public protocol View: TypeErasedView { 35 | /// The type of the `body` property defining the subtree of `View`s of this `View`. 36 | associatedtype Body: View 37 | 38 | /// The body property is used to delegate the generation of html for a specific node to another view. It should be set for all view 39 | /// which enclose other views in order to allow the layout system to propagate properties properly through the tree. 40 | var body: Body { get } 41 | } 42 | 43 | public extension View { 44 | var layoutAxis: LayoutAxis { 45 | .vertical 46 | } 47 | 48 | var body: some View { 49 | EmptyView() 50 | } 51 | } 52 | 53 | public extension View { 54 | func html(forHTMLOfSubnodes htmlOfSubnodes: [HTMLNode]) -> HTMLNode { 55 | htmlOfSubnodes.joined() 56 | } 57 | } 58 | 59 | /** 60 | This extension makes the `Never` type conform to `View` so that it can be used as the associated `Body` type of the latter for `View`s without subviews. 61 | */ 62 | extension Never: View { 63 | public var body: Never { fatalError("Never Type has no body") } 64 | } 65 | 66 | /// The `EmptyView` is used by the `ViewBuilder` as a return value for empty blocks. 67 | public struct EmptyView: View { 68 | public typealias Body = Never 69 | } 70 | 71 | public extension View where Body == Never { 72 | var body: Never { fatalError("\(type(of: self)) has no body") } 73 | } 74 | 75 | /// Implement this protocol with your `View` to define the elements to which a transformation of this view is applied. 76 | public protocol CustomMappable { 77 | /// Use the `customMap(_:)` method to e.g. give access to multiple subviews which this view represents. This method is used 78 | /// to construct the view tree during runtime. 79 | func customMap(_ transform: (TypeErasedView) -> T) -> [T] 80 | } 81 | 82 | extension View { 83 | public func mapBody(_ transform: (TypeErasedView) -> T) -> [T] { 84 | guard type(of: Body.self) != type(of: Never.self) else { 85 | return [] 86 | } 87 | 88 | if let customMappableBody = body as? CustomMappable { 89 | // This is where the recursion happens that makes composing TupleViews and ForEach views 90 | // possible 91 | return customMappableBody 92 | .customMap { 93 | $0.map(transform) 94 | } 95 | .flatMap { 96 | $0 97 | } 98 | } else { 99 | return [transform(body)] 100 | } 101 | } 102 | 103 | public func map(_ transform: (TypeErasedView) -> T) -> [T] { 104 | if let customMappableSelf = self as? CustomMappable { 105 | // This is where the recursion happens that makes composing TupleViews and ForEach views 106 | // possible 107 | return customMappableSelf 108 | .customMap { 109 | $0.map(transform) 110 | } 111 | .flatMap { 112 | $0 113 | } 114 | } else { 115 | return [transform(self)] 116 | } 117 | } 118 | 119 | /// Default implementation of `map(_:)` which 120 | public func map(_ keyPath: KeyPath) -> [T] { 121 | self.map { 122 | $0[keyPath: keyPath] 123 | } 124 | } 125 | } 126 | 127 | // MARK: View modifiers 128 | 129 | public extension View { 130 | /// Clips the view to its bounding frame, with the specified corner radius. 131 | func cornerRadius(_ radius: Double) -> some View { 132 | ModifiedContent(content: self, modifier: HTMLTransformingViewModifier { html in 133 | html 134 | .withStyle(key: .borderRadius, value: .px(radius)) 135 | .withStyle(key: .overflow, value: .hidden) 136 | }) 137 | } 138 | 139 | /// Adds a shadow to the view. 140 | func shadow(color: Color = Color(white: 0.0).opacity(0.20), 141 | radius: Double = 40.0, 142 | x: Double = 0.0, 143 | y: Double = 2.0 144 | ) -> some View { 145 | ModifiedContent(content: self, modifier: HTMLTransformingViewModifier { html in 146 | html.withStyle(key: .boxShadow, 147 | value: .shadow(offsetX: x, 148 | offsetY: y, 149 | radius: radius, 150 | color: color)) 151 | }) 152 | } 153 | 154 | /// Pads the view using the specified edge insets. 155 | func padding(_ edges: Edge.Set = .all, _ length: Double? = nil) -> some View { 156 | let length = length ?? 10.0 157 | 158 | let paddingPropertyMapping: [(cssKey: HTMLNode.CSSKey, edgeSet: Edge.Set)] = [ 159 | (.paddingTop, .top), 160 | (.paddingLeft, .leading), 161 | (.paddingRight, .trailing), 162 | (.paddingBottom, .bottom) 163 | ] 164 | 165 | let paddingStyle = Dictionary(uniqueKeysWithValues: paddingPropertyMapping.compactMap { 166 | edges.contains($0.edgeSet) ? ($0.cssKey, HTMLNode.CSSValue.px(length)) : nil 167 | }) 168 | 169 | return ModifiedContent(content: self, modifier: HTMLTransformingViewModifier { html in 170 | .div( 171 | subNodes: [ 172 | html 173 | .withStyle(key: .flexGrow, value: .one) 174 | .withStyle(key: .alignSelf, value: .stretch) 175 | ], 176 | style: paddingStyle 177 | ) 178 | }) 179 | } 180 | 181 | /// Pads the view using the specified length. 182 | func padding(_ length: Double? = nil) -> some View { 183 | padding(.all, length) 184 | } 185 | 186 | /// Adds a border to the view with the specified style and width. 187 | func border(_ color: Color, width: Double = 1) -> some View { 188 | ModifiedContent(content: self, modifier: HTMLTransformingViewModifier { html in 189 | html.withStyle(key: .border, 190 | value: .border(width: width, color: color)) 191 | }) 192 | } 193 | } 194 | 195 | 196 | // MARK: View description 197 | 198 | public extension TypeErasedView { 199 | /// Description of this view instance. 200 | var debugDescription: String { 201 | String(describing: Self.self) 202 | } 203 | } 204 | 205 | extension View { 206 | func withDebugReference(_ action: (Self) -> Void) -> Self { 207 | action(self) 208 | return self 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/AnyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyView.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 30.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A type-erased `View`. 12 | 13 | An AnyView allows changing the type of view used in a given view hierarchy. 14 | */ 15 | public struct AnyView: View, CustomMappable { 16 | /// The `Never` type indicates that this view doesn't define its subviews via the `body` property. 17 | public typealias Body = Never 18 | let containedView: TypeErasedView 19 | 20 | /// The implementation of this method delegates to the `content` view. 21 | public func html(forHTMLOfSubnodes htmlOfSubnodes: [HTMLNode]) -> HTMLNode { 22 | containedView.html(forHTMLOfSubnodes: htmlOfSubnodes) 23 | } 24 | 25 | /// Instantiates an `AnyView` by wrapping `content`. 26 | public init(content: Content) where Content: View { 27 | containedView = content 28 | } 29 | 30 | /// Instantiates an `AnyView` by wrapping `content`. 31 | public init(content: TypeErasedView) { 32 | containedView = content 33 | } 34 | 35 | public func customMap(_ transform: (TypeErasedView) -> T) -> [T] { 36 | [transform(containedView)] 37 | } 38 | } 39 | 40 | public extension TypeErasedView { 41 | func anyView() -> AnyView { 42 | AnyView(content: self) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/Button.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Button.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 17.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A control that performs an action when triggered. 11 | public struct Button

35 |
36 |
37 | 40 |
41 | 85 | """ 86 | ) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/Spacer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Spacer.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 04.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A flexible space that expands along the major axis of its containing stack layout, or on both axes if not contained in a stack. 11 | public struct Spacer: View, GrowingAxesModifying { 12 | public typealias Body = Never 13 | 14 | /// Initializes the `Spacer`. 15 | public init() { } 16 | 17 | public func modifiedGrowingLayoutAxes(forGrowingAxesOfSubnodes: Set) 18 | -> Set { 19 | // A Spacer can grow horizontally as well as vertically, dependent on the primary axis of 20 | // the containing stack view. 21 | [.undetermined] 22 | } 23 | 24 | public func html(forHTMLOfSubnodes htmlOfSubnodes: [HTMLNode]) -> HTMLNode { 25 | .div() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/Stack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stack.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 04.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol Stack: View { } 11 | 12 | extension Stack { 13 | static func insertSpacers(forSpacing spacing: Double?, 14 | inNodes nodes: [HTMLNode], 15 | axis: LayoutAxis) -> [HTMLNode] { 16 | let spacing = spacing ?? 8 17 | 18 | var spacedNodes: [HTMLNode] = [] 19 | 20 | for view in nodes { 21 | if !spacedNodes.isEmpty { 22 | switch axis { 23 | case .horizontal: 24 | spacedNodes.append(.div(subNodes: [], style: [.width: .px(spacing)])) 25 | case .vertical: 26 | spacedNodes.append(.div(subNodes: [], style: [.height: .px(spacing)])) 27 | } 28 | } 29 | 30 | spacedNodes.append(view) 31 | } 32 | 33 | return spacedNodes 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/TabItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabItem.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 08.02.20. 6 | // 7 | 8 | protocol TypeErasedTabItem { 9 | var image: Image? { get } 10 | var text: Text? { get } 11 | } 12 | 13 | struct TabItem: View, TypeErasedTabItem where Body: View { 14 | let image: Image? 15 | let text: Text? 16 | let body: Body 17 | } 18 | 19 | public extension View { 20 | func tabItem(@ViewBuilder _ label: () -> V) -> some View where V: View { 21 | let labelView = label() 22 | let subViews = labelView.map(\.self) 23 | 24 | let image: Image? = subViews.compactMap { $0 as? Image } .first 25 | let text: Text? = subViews.compactMap { $0 as? Text } .first 26 | 27 | return TabItem(image: image, text: text, body: self) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/TabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabView.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 07.02.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A view that switches between multiple child views using interactive user interface elements. 11 | public struct TabView: View where Content: View { 12 | let content: Content 13 | @State var selectionValue: Int = 0 14 | 15 | /// Initializes the `TabView` with the provided content and selection value specifying the active tab. 16 | public init(selection: Int = 0, @ViewBuilder content: () -> Content) { 17 | self.content = content() 18 | self.selectionValue = selection 19 | } 20 | 21 | public var body: some View { 22 | VStack(spacing: 0) { 23 | tabBar 24 | content.map(\.self)[selectionValue].anyView() 25 | } 26 | } 27 | 28 | var tabBar: some View { 29 | VStack(spacing: 0) { 30 | HStack(spacing: 136.0) { 31 | Spacer() 32 | 33 | ForEach(Array(content.map(\.self).enumerated())) { index, tab -> AnyView in 34 | let tabItem = tab as? TypeErasedTabItem 35 | 36 | let text = tabItem?.text ?? Text(String(describing: index)) 37 | let image = tabItem?.image ?? Image("placeholder.png") 38 | 39 | let stack = VStack(spacing: 7) { 40 | text 41 | .font(.system(size: 11, weight: .medium)) 42 | image 43 | .resizable() 44 | .frame(width: 22.0, height: 22.0) 45 | } 46 | 47 | let tabItemView = index == self.selectionValue ? 48 | stack.systemBlueFilter().anyView() 49 | : stack.systemGrayFilter().anyView() 50 | 51 | return tabItemView 52 | .onTapGesture { 53 | self.selectionValue = index 54 | } 55 | .anyView() 56 | } 57 | 58 | Spacer() 59 | } 60 | .frame(height: 73.0) 61 | 62 | Color(white: 0.77) 63 | .frame(height: 0.5) 64 | } 65 | .background(Color(white: 0.97)) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/TaggedView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaggedView.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 30.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol TypeErasedTaggedView { 11 | var tag: Any? { get } 12 | } 13 | 14 | struct TaggedView: View, TypeErasedTaggedView where Body: View { 15 | let tag: Any? 16 | let body: Body 17 | } 18 | 19 | public extension View { 20 | func tag(_ tag: V) -> some View where V: Hashable { 21 | TaggedView(tag: tag, body: self) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/TapGestureView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TapGestureView.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 04.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TapGestureView: View, ClickInputEventResponder where Content: View { 11 | var body: Content 12 | var action: () -> Void 13 | 14 | func html(forHTMLOfSubnodes htmlOfSubnodes: [HTMLNode]) -> HTMLNode { 15 | htmlOfSubnodes 16 | .joined() 17 | } 18 | 19 | func onClickInputEvent() { 20 | action() 21 | } 22 | } 23 | 24 | public extension View { 25 | func onTapGesture(perform action: @escaping () -> Void) -> some View { 26 | TapGestureView(body: self, action: action) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/Text.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Text.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 12.12.19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A view that displays one or more lines of read-only text. 11 | public struct Text: View { 12 | public typealias Body = Never 13 | 14 | let text: String 15 | let isBold: Bool 16 | public let html: HTMLNode = .raw("deprecated") 17 | 18 | /// Creates a text view that displays a string literal without localization. 19 | public init(_ text: String) { 20 | self.text = text 21 | isBold = false 22 | } 23 | 24 | init(_ text: String, isBold: Bool) { 25 | self.text = text 26 | self.isBold = isBold 27 | } 28 | 29 | public func html(forHTMLOfSubnodes htmlOfSubnodes: [HTMLNode]) -> HTMLNode { 30 | var html: HTMLNode = 31 | .div(style: [.justifyContent: .center, .alignItems: .center]) { 32 | .div(style: [.flexGrow: .zero]) { 33 | .raw(text) 34 | } 35 | } 36 | 37 | if isBold { 38 | html = html.withStyle(key: .fontWeight, value: .int(Font.Weight.bold.rawValue)) 39 | } 40 | 41 | return html 42 | } 43 | } 44 | 45 | public extension Text { 46 | /// Applies a bold font weight to the text. 47 | func bold() -> Text { 48 | Text(text, isBold: true) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/TextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextField.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 29.03.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A control that displays an editable text interface. 11 | public struct TextField: View, ChangeInputEventResponder { 12 | public typealias Body = Never 13 | 14 | let title: String 15 | let text: Binding 16 | 17 | public func onChangeInputEvent(newValue: String) { 18 | text.wrappedValue = newValue 19 | } 20 | 21 | public func html(forHTMLOfSubnodes htmlOfSubnodes: [HTMLNode]) -> HTMLNode { 22 | .input(placeholder: title, value: text.wrappedValue, style: [.pointerEvents: .auto]) 23 | } 24 | 25 | /// Creates an instance with a title string and a binding for the editable value. 26 | public init(_ title: S, text: Binding) where S: StringProtocol { 27 | self.title = String(title) 28 | self.text = text 29 | } 30 | 31 | /// Creates an instance which passes the value of this `TextField` through the provided `Formatter`. 32 | public init(_ title: S, value: Binding, formatter: Formatter) where S: StringProtocol { 33 | self.title = String(title) 34 | 35 | self.text = Binding(getValue: { 36 | formatter.string(for: value.wrappedValue) ?? "" 37 | }, setValue: { newText in 38 | var parsedAnyObject: AnyObject? 39 | var errorString: NSString? 40 | 41 | formatter.getObjectValue(&parsedAnyObject, for: newText, errorDescription: &errorString) 42 | 43 | guard let parsedObject = parsedAnyObject as? T else { 44 | print("TextField formatter couldn't parse object") 45 | return 46 | } 47 | 48 | value.wrappedValue = parsedObject 49 | }) 50 | } 51 | } 52 | 53 | /// The type of keyboard to display for a given text-based view. 54 | public enum UIKeyboardType { 55 | /// Specifies a keyboard with numbers and a decimal point. 56 | case decimalPad 57 | } 58 | 59 | public extension View { 60 | func keyboardType(_ type: UIKeyboardType) -> some View { 61 | self 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/TupleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TupleView.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 11.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol TypeErasedTupleView: CustomMappable { 11 | func map(_ transform: (TypeErasedView) -> T) -> [T] 12 | func map(_ keyPath: KeyPath) -> [T] 13 | } 14 | 15 | /// A View created from a swift tuple of View values. 16 | public struct TupleView: View, TypeErasedTupleView { 17 | public typealias Body = Never 18 | 19 | public var value: T 20 | 21 | public init(_ value: T) { 22 | self.value = value 23 | } 24 | 25 | // TODO: this doesn't have any effect since TupleViews are not included in the ViewTree 26 | public func html(forHTMLOfSubnodes htmlOfSubnodes: [HTMLNode]) -> HTMLNode { 27 | htmlOfSubnodes.joined() 28 | } 29 | 30 | public func customMap(_ transform: (TypeErasedView) -> T) -> [T] { 31 | let mirror = Mirror(reflecting: value) 32 | return mirror.children 33 | .map { child in 34 | guard let view = child.value as? TypeErasedView else { 35 | fatalError("TupleView must contain a tuple of Views") 36 | } 37 | 38 | return view 39 | } 40 | .map(transform) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/VStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStack.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 12.12.19. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A view that arranges its children in a vertical line. 11 | public struct VStack: Stack, GrowingAxesModifying where Content: View { 12 | public let body: Content 13 | let horizontalAlignment: HorizontalAlignment 14 | let spacing: Double? 15 | 16 | public func modifiedGrowingLayoutAxes(forGrowingAxesOfSubnodes growingAxesOfSubnodes: Set) 17 | -> Set { 18 | // `.undetermined` means that there is a spacer among the subviews which is not 19 | // contained in another stack. This means that this horizontal stack view can grow among 20 | // its primary axis. 21 | Set(growingAxesOfSubnodes.map { $0 == .undetermined ? .vertical : $0 }) 22 | } 23 | 24 | public init(alignment: HorizontalAlignment = .center, 25 | spacing: Double? = nil, 26 | @ViewBuilder buildSubviews: () -> Content) { 27 | body = buildSubviews() 28 | horizontalAlignment = alignment 29 | self.spacing = spacing 30 | } 31 | 32 | public func html(forHTMLOfSubnodes htmlOfSubnodes: [HTMLNode]) -> HTMLNode { 33 | let htmlOfSpacedSubnodes = Self.insertSpacers( 34 | forSpacing: spacing, 35 | inNodes: htmlOfSubnodes, // in vertical layout axis? 36 | axis: .vertical 37 | ) 38 | 39 | return .div(subNodes: htmlOfSpacedSubnodes, style: [ 40 | .display: .flex, 41 | .flexDirection: .column, 42 | .alignItems: horizontalAlignment.cssValue 43 | ]) 44 | } 45 | } 46 | 47 | /// An alignment position along the horizontal axis. 48 | public enum HorizontalAlignment { 49 | /// A guide marking the horizontal center of the view. 50 | case center 51 | 52 | /// A guide marking the leading edge of the view. 53 | case leading 54 | 55 | /// A guide marking the trailing edge of the view. 56 | case trailing 57 | 58 | var cssValue: HTMLNode.CSSValue { 59 | switch self { 60 | case .center: 61 | return .center 62 | case .leading: 63 | return .flexStart 64 | case .trailing: 65 | return .flexEnd 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/ViewBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewBuilder.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 11.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | // swiftlint:disable large_tuple line_length function_parameter_count identifier_name 11 | 12 | /// A custom parameter attribute that constructs views from closures. 13 | @_functionBuilder public struct ViewBuilder { 14 | /// Builds an empty view from an block containing no statements, `{ }`. 15 | public static func buildBlock() -> EmptyView { 16 | EmptyView() 17 | } 18 | 19 | /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through unmodified. 20 | public static func buildBlock(_ content: Content) -> Content where Content: View { 21 | content 22 | } 23 | 24 | public static func buildBlock(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0: View, C1: View { 25 | TupleView((c0: c0, c1: c1)) 26 | } 27 | 28 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0: View, C1: View, C2: View { 29 | TupleView((c0: c0, c1: c1, c2: c2)) 30 | } 31 | 32 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> TupleView<(C0, C1, C2, C3)> where C0: View, C1: View, C2: View, C3: View { 33 | TupleView((c0: c0, c1: c1, c2: c2, c3: c3)) 34 | } 35 | 36 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4) -> TupleView<(C0, C1, C2, C3, C4)> where C0: View, C1: View, C2: View, C3: View, C4: View { 37 | TupleView((c0: c0, c1: c1, c2: c2, c3: c3, c4: c4)) 38 | } 39 | 40 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5) -> TupleView<(C0, C1, C2, C3, C4, C5)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View { 41 | TupleView((c0: c0, c1: c1, c2: c2, c3: c3, c4: c4, c5: c5)) 42 | } 43 | 44 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View { 45 | TupleView((c0: c0, c1: c1, c2: c2, c3: c3, c4: c4, c5: c5, c6: c6)) 46 | } 47 | 48 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View { 49 | TupleView((c0: c0, c1: c1, c2: c2, c3: c3, c4: c4, c5: c5, c6: c6, c7: c7)) 50 | } 51 | 52 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View { 53 | TupleView((c0: c0, c1: c1, c2: c2, c3: c3, c4: c4, c5: c5, c6: c6, c7: c7, c8: c8)) 54 | } 55 | 56 | public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View, C9: View { 57 | TupleView((c0: c0, c1: c1, c2: c2, c3: c3, c4: c4, c5: c5, c6: c6, c7: c7, c8: c8, c9: c9)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/SwiftWeb/Views/ZStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZStack.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 04.01.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A view that overlays its children, aligning them in both axes. 11 | public struct ZStack: Stack, GrowingAxesModifying where Content: View { 12 | public let body: Content 13 | 14 | public func modifiedGrowingLayoutAxes(forGrowingAxesOfSubnodes: Set) 15 | -> Set { 16 | [.vertical, .horizontal] 17 | } 18 | 19 | public func html(forHTMLOfSubnodes htmlOfSubnodes: [HTMLNode]) -> HTMLNode { 20 | let stackedSubnodes = htmlOfSubnodes.map { node -> HTMLNode in 21 | .div(style: [ 22 | .position: .absolute, 23 | .width: .percent(100), 24 | .height: .percent(100), 25 | .display: .flex, 26 | .flexDirection: .column 27 | ]) { 28 | node 29 | } 30 | } 31 | 32 | return .div(subNodes: stackedSubnodes, style: [.position: .relative, .flexGrow: .one]) 33 | } 34 | 35 | /// Creates an instance with the given content 36 | public init(@ViewBuilder content: () -> Content) { 37 | body = content() 38 | } 39 | } 40 | 41 | public extension View { 42 | func background(_ background: Background) -> some View where Background: View { 43 | if let backgroundColor = background as? Color { 44 | return ModifiedContent(content: self, modifier: HTMLTransformingViewModifier { html in 45 | html.withStyle( 46 | key: .backgroundColor, 47 | value: .color(backgroundColor) 48 | ) 49 | }).anyView() 50 | } else { 51 | return ZStack { 52 | background 53 | self 54 | } 55 | .anyView() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SwiftWebScript/JavaScriptClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Quirin Schweigert on 15.05.20. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The JavaScript client provided by SwiftWeb. Serve this script for the '/script.js' path on your HTTP server. 11 | public struct JavaScriptClient { 12 | /// The script code as a `String`. 13 | public static var script: String { 14 | """ 15 | console.log("connecting..."); 16 | let socket = new WebSocket(`ws://${window.location.host}/websocket`); 17 | 18 | socket.onopen = (event) => { 19 | console.log("connected to application"); 20 | }; 21 | 22 | socket.onmessage = (event) => { 23 | document.body.innerHTML = event.data; 24 | console.log("body updated") 25 | }; 26 | 27 | document.onclick = (event) => { 28 | let viewID = findResponderID(event.target, "click-event-responder") 29 | 30 | if (event.target.tagName === "INPUT") { 31 | return 32 | } 33 | 34 | if (viewID !== null) { 35 | console.log(`click on ${viewID}`); 36 | 37 | socket.send(JSON.stringify({ 38 | click: { 39 | id: viewID, 40 | } 41 | })); 42 | } 43 | }; 44 | 45 | document.onchange = (event) => { 46 | let viewID = findResponderID(event.target, "change-event-responder") 47 | 48 | if (viewID !== null) { 49 | socket.send(JSON.stringify({ 50 | change: { 51 | id: viewID, 52 | newValue: event.srcElement.value, 53 | } 54 | })); 55 | } 56 | }; 57 | 58 | // bubbles the event up from `target` to return the id of the first element with `attribute` 59 | function findResponderID(target, attribute) { 60 | let currentElement = target; 61 | 62 | while (true) { 63 | if (currentElement.hasAttribute(attribute)) { 64 | return currentElement.getAttribute("id"); 65 | } 66 | 67 | if (currentElement.parentElement !== null) { 68 | currentElement = currentElement.parentElement; 69 | } else { 70 | return null; 71 | } 72 | } 73 | } 74 | """ 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | // A LinuxMain.swift file is no longer needed since `swift test --enable-test-discovery` is possible 2 | // Provide an error message when testing on Linux with no automatic test discovery 3 | #error(""" 4 | ----------------------------------------------------- 5 | Please test with `swift test --enable-test-discovery` 6 | ----------------------------------------------------- 7 | """) 8 | -------------------------------------------------------------------------------- /Tests/SwiftWebTests/SwiftWebTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftWeb 3 | 4 | final class SwiftWebTests: XCTestCase { 5 | func testHTMLTemplateContent() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssert(HTMLTemplate.withContent("testcontent").contains("testcontent"), 10 | "HTML template doesn't render supplied content") 11 | } 12 | 13 | static var allTests = [ 14 | ("testHTMLTemplateContent", testHTMLTemplateContent) 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /Tests/SwiftWebTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | [ 6 | testCase(SwiftWebTests.allTests) 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------