├── .gitignore ├── .travis.d ├── before-install.sh └── install.sh ├── .travis.yml ├── CONTRIBUTING.md ├── CoreDataToSwiftUI.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── DirectToSwiftUI-Mac.xcscheme │ ├── DirectToSwiftUI-Mobile.xcscheme │ └── DirectToSwiftUI-Watch.xcscheme ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── DirectToSwiftUI │ ├── CoreDataRules │ ├── KVCRulePredicate.swift │ ├── KVCRuleSelfAssignment.swift │ ├── README.md │ ├── RuleKeyPathAssignment.swift │ ├── RuleKeyPathPredicateExtras.swift │ ├── RuleModelExtras.swift │ ├── RuleOperatorExtras.swift │ └── ZeeQLRuleParser.swift │ ├── D2SMainView.swift │ ├── DefaultRules.swift │ ├── Environment │ ├── ContextKVC.swift │ ├── DefaultAssignment │ │ ├── DefaultAssignment.swift │ │ ├── EntityDefaults.swift │ │ ├── ManagedContextDefaults.swift │ │ ├── ManagedObjectDefaults.swift │ │ ├── ModelDefaults.swift │ │ └── README.md │ ├── EnvironmentKeys.swift │ ├── EnvironmentPathes.swift │ ├── README.md │ └── ViewModifiers.swift │ ├── README.md │ ├── Support │ ├── AppKit │ │ ├── D2SInspectWindow.swift │ │ └── D2SMainWindow.swift │ ├── AttributeValue.swift │ ├── ComparisonOperation.swift │ ├── CoreData │ │ ├── AttributeExtras.swift │ │ ├── D2SEditValidation.swift │ │ ├── DataSource.swift │ │ ├── DetailDataSource.swift │ │ ├── DummyImplementations.swift │ │ ├── EntityExtras.swift │ │ ├── FetchRequestExtras.swift │ │ ├── KVCBindings.swift │ │ ├── ManagedObjectBindings.swift │ │ ├── ModelExtras.swift │ │ └── PredicateExtras.swift │ ├── EquatableType.swift │ ├── FoundationExtras.swift │ ├── Hashes.swift │ ├── KeyValueCodingType.swift │ ├── Logger.swift │ ├── Platform.swift │ ├── ReExport.swift │ └── SwiftUI │ │ ├── D2STransformingFormatter.swift │ │ └── FormatterBinding.swift │ ├── TRANSITION.md │ ├── ViewModel │ ├── D2SDisplayGroup.swift │ ├── D2SFault.swift │ ├── D2SObjectAction.swift │ ├── D2SRuleEnvironment.swift │ ├── D2SToOneFetch.swift │ └── SparseFaultArray.swift │ └── Views │ ├── BasicLook │ ├── BasicLook.swift │ ├── PageWrapper │ │ ├── EntityMasterDetailPage.swift │ │ ├── EntitySidebar.swift │ │ ├── MasterDetail.swift │ │ └── NavigationPage.swift │ ├── Pages │ │ ├── AppKit │ │ │ └── WindowQueryList.swift │ │ ├── Edit.swift │ │ ├── EntityList.swift │ │ ├── Inspect.swift │ │ ├── Login.swift │ │ ├── QueryList.swift │ │ ├── Select.swift │ │ ├── SmallQueryList.swift │ │ └── UIKit │ │ │ ├── MobileQueryList.swift │ │ │ └── MobileSelect.swift │ ├── Properties │ │ ├── Display │ │ │ ├── DisplayBool.swift │ │ │ ├── DisplayDate.swift │ │ │ ├── DisplayEmail.swift │ │ │ ├── DisplayPassword.swift │ │ │ └── DisplayString.swift │ │ ├── Edit │ │ │ ├── EditBool.swift │ │ │ ├── EditDate.swift │ │ │ ├── EditLargeString.swift │ │ │ ├── EditNumber.swift │ │ │ └── EditString.swift │ │ ├── README.md │ │ └── Relationships │ │ │ ├── DisplayToOneSummary.swift │ │ │ ├── DisplayToOneTitle.swift │ │ │ └── EditToOne.swift │ ├── Reusable │ │ ├── D2SComponentView.swift │ │ ├── D2SDisplayProperties.swift │ │ ├── D2SDisplayPropertiesList.swift │ │ ├── D2SFaultContainer.swift │ │ ├── D2SFaultObjectLink.swift │ │ ├── D2SNavigationLink.swift │ │ ├── D2SNilText.swift │ │ ├── D2SPropertyName.swift │ │ ├── D2SRowFault.swift │ │ ├── D2SSummaryView.swift │ │ ├── D2STitleText.swift │ │ ├── D2STitledSummaryView.swift │ │ ├── D2SToOneContainer.swift │ │ └── D2SToOneLink.swift │ └── Rows │ │ ├── NamedToManyLink.swift │ │ ├── PropertyNameAsTitle.swift │ │ ├── PropertyNameValue.swift │ │ └── PropertyValue.swift │ ├── Debug │ ├── D2SDebugBox.swift │ ├── D2SDebugEntityDetails.swift │ ├── D2SDebugEntityInfo.swift │ ├── D2SDebugFormatter.swift │ ├── D2SDebugLabel.swift │ ├── D2SDebugMOCInfo.swift │ └── D2SDebugObjectEditInfo.swift │ ├── DefaultLook.swift │ ├── Generic │ ├── D2SEntityPageView.swift │ └── D2SPageView.swift │ ├── Misc │ ├── ListEnabledDatePicker.swift │ ├── MultilineEditor.swift │ ├── SearchField.swift │ └── Spinner.swift │ └── README.md └── xcconfig └── Base.xcconfig /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | 70 | # hh 71 | .DS_Store 72 | Package.resolved 73 | .docker.build 74 | 75 | -------------------------------------------------------------------------------- /.travis.d/before-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ "$TRAVIS_OS_NAME" == "Linux" ]]; then 4 | sudo apt-get install -y wget \ 5 | clang-3.6 libc6-dev make git libicu52 libicu-dev \ 6 | git autoconf libtool pkg-config \ 7 | libblocksruntime-dev \ 8 | libkqueue-dev \ 9 | libpthread-workqueue-dev \ 10 | systemtap-sdt-dev \ 11 | libbsd-dev libbsd0 libbsd0-dbg \ 12 | curl libcurl4-openssl-dev \ 13 | libedit-dev \ 14 | python2.7 python2.7-dev \ 15 | libxml2 16 | 17 | sudo update-alternatives --quiet --install /usr/bin/clang clang /usr/bin/clang-3.6 100 18 | sudo update-alternatives --quiet --install /usr/bin/clang++ clang++ /usr/bin/clang++-3.6 100 19 | fi 20 | -------------------------------------------------------------------------------- /.travis.d/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # our path is: 4 | # /home/travis/build/NozeIO/Noze.io/ 5 | 6 | if ! test -z "$SWIFT_SNAPSHOT_NAME"; then 7 | # Install Swift 8 | wget "${SWIFT_SNAPSHOT_NAME}" 9 | 10 | TARBALL="`ls swift-*.tar.gz`" 11 | echo "Tarball: $TARBALL" 12 | 13 | TARPATH="$PWD/$TARBALL" 14 | 15 | cd $HOME # expand Swift tarball in $HOME 16 | tar zx --strip 1 --file=$TARPATH 17 | pwd 18 | 19 | export PATH="$PWD/usr/bin:$PATH" 20 | which swift 21 | 22 | if [ `which swift` ]; then 23 | echo "Installed Swift: `which swift`" 24 | else 25 | echo "Failed to install Swift?" 26 | exit 42 27 | fi 28 | fi 29 | 30 | swift --version 31 | 32 | 33 | # Environment 34 | 35 | TT_SWIFT_BINARY=`which swift` 36 | 37 | echo "${TT_SWIFT_BINARY}" 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | 3 | notifications: 4 | slack: 5 | rooms: 6 | - zeeql:odi4PEJUdmDPkBfjhHIaSdrS 7 | 8 | matrix: 9 | include: 10 | - os: osx 11 | osx_image: xcode11 12 | 13 | before_install: 14 | - ./.travis.d/before-install.sh 15 | 16 | install: 17 | - ./.travis.d/install.sh 18 | 19 | script: 20 | - export PATH="$HOME/usr/bin:$PATH" 21 | - set -o pipefail 22 | - xcodebuild -scheme DirectToSwiftUI-All -configuration Debug -target DirectToSwiftUI-All | xcpretty 23 | - xcodebuild -scheme DirectToSwiftUI-All -configuration Release -target DirectToSwiftUI-All | xcpretty 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license 4 | your contribution to ZeeZide GmbH and the community, and agree by submitting 5 | the patch that your contributions are licensed under the Apache 2.0 license. 6 | -------------------------------------------------------------------------------- /CoreDataToSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CoreDataToSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CoreDataToSwiftUI.xcodeproj/xcshareddata/xcschemes/DirectToSwiftUI-Mac.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /CoreDataToSwiftUI.xcodeproj/xcshareddata/xcschemes/DirectToSwiftUI-Mobile.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /CoreDataToSwiftUI.xcodeproj/xcshareddata/xcschemes/DirectToSwiftUI-Watch.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | TBD 2 | 3 | Copyright 2019 ZeeZide GmbH 4 | 5 | Provided on an "AS IS" BASIS, WITHOUT WARRANTIES OR 6 | CONDITIONS OF ANY KIND, either express or implied. 7 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | 7 | name: "CoreDataToSwiftUI", 8 | 9 | platforms: [ 10 | .macOS(.v10_15), .iOS(.v13), .watchOS(.v6) 11 | ], 12 | 13 | products: [ 14 | .library(name: "DirectToSwiftUI", targets: [ "DirectToSwiftUI" ]) 15 | ], 16 | 17 | dependencies: [ 18 | .package(url: "https://github.com/DirectToSwift/SwiftUIRules.git", 19 | from: "0.1.3") 20 | ], 21 | 22 | targets: [ 23 | .target(name: "DirectToSwiftUI", 24 | dependencies: [ "SwiftUIRules" ]) 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

CoreData to SwiftUI 2 | 4 |

5 | 6 | ![Swift5.1](https://img.shields.io/badge/swift-5.1-blue.svg) 7 | ![macOS](https://img.shields.io/badge/os-macOS-green.svg?style=flat) 8 | ![iOS](https://img.shields.io/badge/os-iOS-green.svg?style=flat) 9 | ![watchOS](https://img.shields.io/badge/os-watchOS-green.svg?style=flat) 10 | ![Travis](https://api.travis-ci.org/DirectToSwift/CoreDataToSwiftUI.svg?branch=chore/replace-zeeql-1&style=flat) 11 | 12 | _Going fully declarative_: Direct to SwiftUI. 13 | 14 | WORK IN PROGRESS: 15 | Supposedly this will eventually be a 16 | [Direct to SwiftUI](https://github.com/DirectToSwift/DirectToSwiftUI), 17 | just using 18 | [CoreData](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/index.html#//apple_ref/doc/uid/TP40001075-CH2-SW1) 19 | instead of [ZeeQL](http://zeeql.io). 20 | 21 | **Direct to SwiftUI** 22 | is an adaption of an old 23 | [WebObjects](https://en.wikipedia.org/wiki/WebObjects) 24 | technology called 25 | [Direct to Web](https://developer.apple.com/library/archive/documentation/WebObjects/Developing_With_D2W/WalkThrough/WalkThrough.html#//apple_ref/doc/uid/TP30001015-DontLinkChapterID_5-TPXREF101). 26 | This time for Apple's new framework: 27 | [SwiftUI](https://developer.apple.com/xcode/swiftui/). 28 | Instant 29 | [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 30 | apps, configurable using 31 | [a declarative rule system](http://www.alwaysrightinstitute.com/swiftuirules/), 32 | yet fully integrated with SwiftUI. 33 | 34 | There is a blog entry explaining how to use this: 35 | [Introducing Direct to SwiftUI](http://www.alwaysrightinstitute.com/directtoswiftui/). 36 | 37 | 38 | ## Notes 39 | 40 | The library name is intentionally kept as DirectToSwiftUI. Only the package 41 | is a different one. 42 | Which implies that you can't use CoreData to SwiftUI and Direct to SwiftUI 43 | together! 44 | 45 | 46 | ## Requirements 47 | 48 | CoreData to SwiftUI requires an environment capable to run SwiftUI. 49 | That is: macOS Catalina, iOS 13 or watchOS 6. 50 | In combination w/ Xcode 11. 51 | 52 | Note that you can run iOS 13/watchOS 6 apps on Mojave in the emulator, 53 | so that is fine as well. 54 | 55 | ## Using the Package 56 | 57 | You can either just drag the Direct to SwiftUI Xcode project into your own 58 | project, 59 | or you can use Swift Package Manager. 60 | 61 | The package URL is: 62 | [https://github.com/DirectToSwift/CoreDataToSwiftUI.git 63 | ](https://github.com/DirectToSwift/CoreDataToSwiftUI.git). 64 | 65 | 66 | ## Misc 67 | 68 | - [The Environment](Sources/DirectToSwiftUI/Environment/README.md) 69 | - [Views](Sources/DirectToSwiftUI/Views/README.md) 70 | - [Database Setup](Sources/DirectToSwiftUI/DatabaseSetup.md) 71 | 72 | ## What it looks like 73 | 74 | A demo application using the Sakila database is provided: 75 | [DVDRentalCoreData](https://github.com/DirectToSwift/DVDRentalCoreData). 76 | 77 | ### Watch 78 | 79 |

80 | 81 | 82 | 83 | 84 |

85 | 86 | ### Phone 87 | 88 |

89 | 90 | 91 |

92 | 93 | ### macOS 94 | 95 | Still too ugly to show, but works in a very restricted way ;-) 96 | 97 | ## Who 98 | 99 | Brought to you by 100 | [The Always Right Institute](http://www.alwaysrightinstitute.com) 101 | and 102 | [ZeeZide](http://zeezide.de). 103 | We like 104 | [feedback](https://twitter.com/ar_institute), 105 | GitHub stars, 106 | cool [contract work](http://zeezide.com/en/services/services.html), 107 | presumably any form of praise you can think of. 108 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/CoreDataRules/KVCRulePredicate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KVCRulePredicate.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import protocol SwiftUIRules.RulePredicate 10 | import struct SwiftUIRules.RuleContext 11 | 12 | extension NSPredicate : RulePredicate { 13 | 14 | public func evaluate(in ruleContext: RuleContext) -> Bool { 15 | // FIXME: need to wrap ruleContext in KVC trampoline 16 | evaluate(with: ruleContext) 17 | } 18 | 19 | } 20 | 21 | // TBD: I think conformance has to be declared manually and can't be attached 22 | // to the protocol? 23 | 24 | extension NSCompoundPredicate { 25 | public var rulePredicateComplexity : Int { 26 | return subpredicates.reduce(0) { 27 | let complexity = ($1 as? RulePredicate)?.rulePredicateComplexity ?? 1 28 | return $0 + complexity 29 | } 30 | } 31 | } 32 | 33 | public extension SwiftUIRules.RuleComparisonOperation { 34 | 35 | init?(_ op: NSComparisonPredicate.Operator) { 36 | switch op { 37 | case .matches, .like, .beginsWith, .endsWith, 38 | .`in`, .customSelector, .contains, .between: 39 | return nil 40 | case .equalTo: self = .equal 41 | case .notEqualTo: self = .notEqual 42 | case .greaterThan: self = .greaterThan 43 | case .greaterThanOrEqualTo: self = .greaterThanOrEqual 44 | case .lessThan: self = .lessThan 45 | case .lessThanOrEqualTo: self = .lessThanOrEqual 46 | @unknown default: return nil 47 | } 48 | } 49 | } 50 | 51 | public extension NSComparisonPredicate.Operator { 52 | 53 | init(_ op: SwiftUIRules.RuleComparisonOperation) { 54 | switch op { 55 | case .equal: self = .equalTo 56 | case .notEqual: self = .notEqualTo 57 | case .lessThan: self = .lessThan 58 | case .greaterThan: self = .greaterThan 59 | case .lessThanOrEqual: self = .lessThanOrEqualTo 60 | case .greaterThanOrEqual: self = .greaterThanOrEqualTo 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/CoreDataRules/KVCRuleSelfAssignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KVCRuleSelfAssignment.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import protocol SwiftUIRules.DynamicEnvironmentKey 9 | import protocol SwiftUIRules.RuleAction 10 | import protocol SwiftUIRules.RuleCandidate 11 | import struct SwiftUIRules.RuleContext 12 | 13 | /* 14 | * KVCRuleSelfAssignment 15 | * 16 | * This is an abstract assignment class which evaluates the right side of the 17 | * assignment as a keypath against itself. E.g. 18 | * 19 | * color = currentColor 20 | * 21 | * Will call 'currentColor' on the assignment object. Due to this the class is 22 | * abstract since the subclass must provide appropriate KVC keys for the 23 | * operation. 24 | */ 25 | open class KVCRuleSelfAssignment 26 | : NSObject, RuleCandidate, RuleAction 27 | { 28 | let key : K.Type 29 | let keyPath : String 30 | 31 | public init(key: K.Type, keyPath: String) { 32 | self.key = key 33 | self.keyPath = keyPath 34 | } 35 | 36 | public var candidateKeyType: ObjectIdentifier { 37 | return ObjectIdentifier(key) 38 | } 39 | public func isCandidateForKey(_ key: K.Type) 40 | -> Bool 41 | { 42 | return self.key == key 43 | } 44 | 45 | public func fireInContext(_ context: RuleContext) -> Any? { 46 | self.value(forKeyPath: keyPath) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/CoreDataRules/README.md: -------------------------------------------------------------------------------- 1 |

SwiftUI Rule Enhancements for CoreData 2 | 4 |

5 | 6 | This directory contains enhancements so that ZeeQL KeyValueCoding can be used 7 | together with 8 | [SwiftUIRules](https://github.com/DirectToSwift/SwiftUIRules/blob/develop/README.md), 9 | and that NSPredicates can be used as rule predicates. 10 | 11 | It also comes with a rule parser which can parse and build rule models 12 | dynamically. 13 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/CoreDataRules/RuleKeyPathAssignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RuleKeyPathAssignment.swift 3 | // SwiftUIRules 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUIRules 9 | 10 | /** 11 | * RuleKeyPathAssignment 12 | * 13 | * This RuleAction object evaluates the action value as a lookup against the 14 | * _context_. Which then can trigger recursive rule evaluation (if the queried 15 | * key is itself a rule based value). 16 | * 17 | * In a model it looks like: 18 | *
  user.role = 'Manager' => bannerColor = defaultColor
19 | * 20 | * The bannerColor = defaultColor represents the 21 | * D2SRuleKeyPathAssignment. 22 | * When executed, it will query the RuleContext for the 'defaultColor' and 23 | * will return that in fireInContext(). 24 | *

25 | * Note that this object does *not* perform a 26 | * takeValueForKey(value, 'bannerColor'). It simply returns the value in 27 | * fireInContext() for further processing at upper layers. 28 | * 29 | * @see RuleAction 30 | * @see RuleAssignment 31 | */ 32 | public struct RuleKeyPathAssignment 33 | : RuleCandidate, RuleAction 34 | { 35 | public let key : K.Type 36 | public let keyPath : String 37 | 38 | public init(_ key: K.Type, _ keyPath: [ String ]) { 39 | self.key = key 40 | self.keyPath = keyPath.joined(separator: ".") 41 | } 42 | public init(_ key: K.Type, _ keyPath: String) { 43 | self.key = key 44 | self.keyPath = keyPath 45 | } 46 | 47 | public var candidateKeyType: ObjectIdentifier { 48 | return ObjectIdentifier(key) 49 | } 50 | public func isCandidateForKey(_ key: K.Type) 51 | -> Bool 52 | { 53 | return self.key == key 54 | } 55 | 56 | public func fireInContext(_ context: RuleContext) -> Any? { 57 | KeyValueCoding.value(forKeyPath: keyPath, inObject: context) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/CoreDataRules/RuleKeyPathPredicateExtras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RuleKeyPathPredicateExtras.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | public extension RuleKeyPathPredicate { 9 | // Any Predicates to support NSManagedObject dynamic properties. 10 | 11 | init(keyPath: Swift.KeyPath, value: Value) { 12 | self.init { ruleContext in 13 | eq(ruleContext[keyPath: keyPath], value) 14 | } 15 | } 16 | init(keyPath: Swift.KeyPath, value: Value?) { 17 | self.init { ruleContext in 18 | eq(ruleContext[keyPath: keyPath], value) 19 | } 20 | } 21 | 22 | init(keyPath: Swift.KeyPath, 23 | operation: SwiftUIRules.RuleComparisonOperation, 24 | value: Value) 25 | { 26 | let op = NSComparisonPredicate.Operator(operation) 27 | self.init() { ruleContext in 28 | op.compare(ruleContext[keyPath: keyPath], value) 29 | } 30 | } 31 | init(keyPath: Swift.KeyPath, 32 | operation: SwiftUIRules.RuleComparisonOperation, 33 | value: Value?) 34 | { 35 | let op = NSComparisonPredicate.Operator(operation) 36 | self.init() { ruleContext in 37 | op.compare(ruleContext[keyPath: keyPath], value) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/CoreDataRules/RuleModelExtras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RuleModelExtras.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import class Foundation.NSPredicate 9 | import SwiftUIRules 10 | 11 | public extension RuleModel { 12 | 13 | @discardableResult 14 | func when(_ predicate: NSPredicate, 15 | set key: K.Type, to value: K.Value) -> Self 16 | where K: DynamicEnvironmentKey 17 | { 18 | addRule(Rule(when: predicate, 19 | do: RuleValueAssignment(key, value))) 20 | return self 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/D2SMainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainView.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import class SwiftUIRules.RuleModel 9 | import struct SwiftUIRules.RuleContext 10 | import SwiftUI 11 | 12 | public struct D2SMainView: View { 13 | 14 | @ObservedObject private var viewModel : D2SRuleEnvironment 15 | 16 | public init(managedObjectContext : NSManagedObjectContext, 17 | ruleModel : RuleModel) 18 | { 19 | viewModel = D2SRuleEnvironment( 20 | managedObjectContext : managedObjectContext, 21 | ruleModel : ruleModel.fallback(D2SDefaultRules) 22 | ) 23 | 24 | viewModel.resume() 25 | } 26 | 27 | public var body: some View { 28 | Group { 29 | if viewModel.isReady { 30 | PageWrapperSelect() 31 | .ruleContext(viewModel.ruleContext) 32 | .environment(\.managedObjectContext, viewModel.managedObjectContext) 33 | .environmentObject(viewModel) 34 | } 35 | else if viewModel.hasError { 36 | ErrorView(error: viewModel.error!) 37 | .padding() 38 | } 39 | else { 40 | ConnectProgressView() 41 | .padding() 42 | } 43 | } 44 | } 45 | 46 | struct PageWrapperSelect: View { 47 | 48 | @Environment(\.pageWrapper) var wrapper 49 | @Environment(\.firstTask) var firstTask 50 | 51 | var body: some View { 52 | wrapper 53 | .task(firstTask) 54 | } 55 | } 56 | 57 | struct ErrorView: View { 58 | // TODO: Make nice. Make generic. Detect specific types. 59 | // Maybe add an error env key. I think D2W even has an error task. 60 | 61 | let error : Swift.Error 62 | 63 | var body: some View { 64 | VStack { 65 | Spacer() 66 | Text(verbatim: "\(error)") 67 | Spacer() 68 | } 69 | } 70 | } 71 | 72 | struct ConnectProgressView: View { 73 | 74 | @State var isSpinning = false 75 | 76 | var body: some View { 77 | VStack { 78 | Text("Connecting database ...") 79 | Spacer() 80 | Spinner(isAnimating: isSpinning, speed: 1.8, size: 64) 81 | Spacer() 82 | } 83 | .onAppear { self.isSpinning = true } // seems necessary 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Environment/ContextKVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SContextKVC.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUIRules 9 | import struct SwiftUI.EnvironmentValues 10 | 11 | /** 12 | * SwiftUI.EnvironmentValues dispatches KVC calls to its attached 13 | * `ruleContext`. 14 | */ 15 | extension EnvironmentValues: KeyValueCodingType { 16 | 17 | public func setValue(_ value: Any?, forKey key: String) { 18 | ruleContext.setValue(value, forKey: key) 19 | } 20 | public func value(forKey key: String) -> Any? { 21 | ruleContext.value(forKey: key) 22 | } 23 | public func setValue(_ value: Any?, forKeyPath path: String) { 24 | ruleContext.setValue(value, forKeyPath: path) 25 | } 26 | public func value(forKeyPath path: String) -> Any? { 27 | ruleContext.value(forKeyPath: path) 28 | } 29 | } 30 | 31 | extension RuleContext: KeyValueCodingType { 32 | 33 | /** 34 | * RuleContext KVC uses a static map `kvcToEnvKey` which is setup in the 35 | * `D2SEnvironmentKeys.swift` file. 36 | * 37 | * Users can expose additional `DynamicEnvironmentKey` via KVC using the 38 | * `D2SContextKVC.expose()` static function. 39 | */ 40 | public func value(forKey k: String) -> Any? { 41 | guard let entry = D2SContextKVC.kvcToEnvKey[k] else { return nil } 42 | return entry.value(self) 43 | } 44 | 45 | /// Not possible yet 46 | public func setValue(_ value: Any?, forKey key: String) { 47 | // FIXME: Should be possible? 48 | globalD2SLogger.error("cannot set rulecontext values via KVC yet:", key) 49 | assertionFailure("cannot set rulecontext values via KVC yet: \(key)") 50 | return 51 | } 52 | 53 | } 54 | 55 | public enum D2SContextKVC { 56 | // kvcToEnvKey is in `D2SEnvironmentKeys.swift` 57 | 58 | /** 59 | * Expose a custom `DynamicEnvironmentKey` using a `KeyValueCoding` 60 | * key. 61 | */ 62 | public static func expose(_ environmentKey: K.Type, as kvcKey: String) 63 | where K: DynamicEnvironmentKey 64 | { 65 | kvcToEnvKey[kvcKey] = KVCMapEntry(environmentKey) 66 | } 67 | 68 | class AnyKVCMapEntry { 69 | func value(_ ctx: RuleContext) -> Any? { 70 | fatalError("subclass responsibility: \(#function)") 71 | } 72 | func isType(_ type: K2.Type) -> Bool { 73 | fatalError("subclass responsibility: \(#function)") 74 | } 75 | 76 | func makeValueAssignment(_ value: Any?) -> (RuleCandidate & RuleAction)? { 77 | fatalError("subclass responsibility: \(#function)") 78 | } 79 | 80 | func makeKeyAssignment(_ rhsEntry: AnyKVCMapEntry) 81 | -> (RuleCandidate & RuleAction)? 82 | { 83 | fatalError("subclass responsibility: \(#function)") 84 | } 85 | func makeKeyAssignment(to lhs: K.Type) 86 | -> (RuleCandidate & RuleAction)? 87 | { 88 | fatalError("subclass responsibility: \(#function)") 89 | } 90 | func makeKeyPathAssignment(_ keyPath: String) 91 | -> (RuleCandidate & RuleAction)? 92 | { 93 | fatalError("subclass responsibility: \(#function)") 94 | } 95 | } 96 | final class KVCMapEntry: AnyKVCMapEntry { 97 | init(_ key: K.Type) {} 98 | 99 | override 100 | func isType(_ type: K2.Type) -> Bool { 101 | return K.self == type 102 | } 103 | 104 | override func value(_ ctx: RuleContext) -> Any? { 105 | return ctx[dynamic: K.self] 106 | } 107 | 108 | override func makeValueAssignment(_ value: Any?) 109 | -> (RuleCandidate & RuleAction)? 110 | { 111 | guard let typedValue = value as? K.Value else { 112 | assertionFailure("invalid value for envkey: \(value as Any) \(K.self)") 113 | return nil 114 | } 115 | return RuleValueAssignment(K.self, typedValue) 116 | } 117 | 118 | override 119 | func makeKeyAssignment(_ rhsEntry: AnyKVCMapEntry) 120 | -> (RuleCandidate & RuleAction)? 121 | { 122 | return rhsEntry.makeKeyAssignment(to: K.self) 123 | } 124 | 125 | override 126 | func makeKeyAssignment(to lhs: K2.Type) 127 | -> (RuleCandidate & RuleAction)? 128 | { 129 | return RuleKeyAssignment(K2.self, K.self) 130 | } 131 | 132 | override 133 | func makeKeyPathAssignment(_ keyPath: String) 134 | -> (RuleCandidate & RuleAction)? 135 | { 136 | return RuleKeyPathAssignment(K.self, keyPath) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Environment/DefaultAssignment/DefaultAssignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDefaultAssignment.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUIRules 9 | 10 | // A test whether `D2SDefaultAssignment` makes sense. 11 | 12 | public enum D2SDefaultAssignments { 13 | // Namespace for assignments. Needs the `s` because we can't use the generic 14 | // class in a convenient way. 15 | } 16 | 17 | public extension D2SDefaultAssignments { 18 | // Note: Why can't we access he ruleContext values using KP wrappers? 19 | // Because we get it as `DynamicEnvironmentValues`. 20 | 21 | typealias A = D2SDefaultAssignment 22 | 23 | static var model: A { 24 | .init { ruleContext in 25 | // Hm, this recursion won't fly: 26 | // \.model.d2s.isDefault == true => \.model <= \.ruleObjectContext.model // '!' 27 | guard let model = ruleContext[D2SKeys.ruleObjectContext] 28 | .persistentStoreCoordinator?.managedObjectModel else { 29 | return D2SKeys.model.defaultValue 30 | } 31 | return model 32 | } 33 | } 34 | 35 | static var attribute: A { 36 | .init { ruleContext in 37 | let entity = ruleContext[D2SKeys.entity] 38 | let propertyKey = ruleContext[D2SKeys.propertyKey] 39 | return entity[attribute: propertyKey] 40 | ?? D2SKeys.attribute.defaultValue 41 | } 42 | } 43 | static var relationship: A { 44 | .init { ruleContext in 45 | let entity = ruleContext[D2SKeys.entity] 46 | let propertyKey = ruleContext[D2SKeys.propertyKey] 47 | return entity[relationship: propertyKey] 48 | ?? D2SKeys.relationship.defaultValue 49 | } 50 | } 51 | 52 | static var isEntityReadOnly: A { 53 | .init { ruleContext in 54 | let entity = ruleContext[D2SKeys.entity] 55 | let roEntities = ruleContext[D2SKeys.readOnlyEntityNames] 56 | return roEntities.contains(entity.name ?? "") 57 | } 58 | } 59 | static var isObjectEditable: A { 60 | .init { ruleContext in !ruleContext[D2SKeys.isEntityReadOnly] } 61 | } 62 | static var isObjectDeletable: A { 63 | .init { ruleContext in ruleContext[D2SKeys.isObjectEditable] } 64 | } 65 | 66 | static var propertyValue: A { 67 | .init { ruleContext in 68 | let object = ruleContext[D2SKeys.object] 69 | let propertyKey = ruleContext[D2SKeys.propertyKey] 70 | return KeyValueCoding.value(forKeyPath: propertyKey, inObject: object) 71 | } 72 | } 73 | 74 | static var loginEntity: A { 75 | .init { ruleContext in 76 | let model = ruleContext[D2SKeys.model] 77 | return model.lookupUserDatabaseEntity() ?? D2SKeys.entity.defaultValue 78 | } 79 | } 80 | } 81 | 82 | 83 | // MARK: - D2SDefaultAssignment 84 | 85 | public struct D2SDefaultAssignment: RuleLiteral { 86 | // So the advantage here is that we can use the assignment in a rule model, 87 | // like `true <= D2SDefaultAssignments.relationship` 88 | 89 | let action : ( DynamicEnvironmentValues ) -> K.Value 90 | 91 | public init(action : @escaping ( DynamicEnvironmentValues ) -> K.Value) { 92 | self.action = action 93 | } 94 | 95 | public func value(in context: DynamicEnvironmentValues) -> K.Value { 96 | return action(context) 97 | } 98 | } 99 | 100 | extension D2SDefaultAssignment: RuleCandidate { 101 | 102 | public func isCandidateForKey(_ key: K2.Type) 103 | -> Bool 104 | { 105 | return K.self == K2.self 106 | } 107 | 108 | public var candidateKeyType: ObjectIdentifier { ObjectIdentifier(K.self) } 109 | } 110 | 111 | extension D2SDefaultAssignment: RuleAction { 112 | 113 | public func fireInContext(_ context: RuleContext) -> Any? { 114 | return value(in: context) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Environment/DefaultAssignment/EntityDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SEntity.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import CoreData 9 | 10 | public extension NSEntityDescription { 11 | 12 | var attributes : [ NSAttributeDescription ] { 13 | properties.compactMap { $0 as? NSAttributeDescription } 14 | } 15 | var relationships : [ NSRelationshipDescription ] { 16 | properties.compactMap { $0 as? NSRelationshipDescription } 17 | } 18 | 19 | } 20 | 21 | public extension NSEntityDescription { 22 | var d2s : EntityD2S { return EntityD2S(entity: self) } 23 | } 24 | 25 | public struct EntityD2S { 26 | let entity : NSEntityDescription 27 | } 28 | 29 | public extension EntityD2S { 30 | 31 | var isDefault : Bool { entity is D2SDefaultEntity } 32 | 33 | var defaultTitle : String { return entity.name ?? "" } 34 | 35 | var defaultSortDescriptors : [ NSSortDescriptor ] { 36 | // This is not great, but there is no reasonable option? 37 | guard let firstAttribute = entity.attributes.first else { return [] } 38 | return [ NSSortDescriptor(key: firstAttribute.name, ascending: true) ] 39 | } 40 | 41 | var defaultAttributeAndRelationshipPropertyKeys : [ String ] { 42 | // Note: It is a speciality of AR that we keep the IDs as class properties. 43 | // That would not be the case for real, managed, EOs. 44 | // Here we want to keep the primary key for display, but drop all the 45 | // keys of the relationships. 46 | return entity.properties.map { $0.name } 47 | } 48 | 49 | var defaultAttributeAndToOnePropertyKeys : [ String ] { 50 | entity.properties.compactMap { 51 | if $0 is NSAttributeDescription { return $0.name } 52 | guard let rs = $0 as? NSRelationshipDescription else { return nil } 53 | return rs.isToMany ? nil : rs.name 54 | } 55 | } 56 | 57 | var defaultDisplayPropertyKeys : [ String ] { 58 | // Note: We do not sort but assume proper ordering 59 | return entity.attributes.map { $0.name } 60 | } 61 | 62 | var defaultSortPropertyKeys : [ String ] { 63 | return entity.attributes.map { $0.name } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Environment/DefaultAssignment/ManagedContextDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDatabase.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import CoreData 9 | 10 | public extension NSManagedObjectContext { 11 | 12 | var d2s : D2S { return D2S(moc: self) } 13 | 14 | struct D2S { 15 | let moc: NSManagedObjectContext 16 | 17 | public var isDefault : Bool { moc === D2SKeys.ruleObjectContext.defaultValue } 18 | 19 | public var hasDefaultTitle: Bool { 20 | guard let psc = moc.persistentStoreCoordinator else { return false } 21 | return (psc.persistentStores.first?.url != nil) || psc.name != nil 22 | } 23 | 24 | public var defaultTitle : String { 25 | if let psc = moc.persistentStoreCoordinator { 26 | if let p = psc.persistentStores.first?.url?.deletingPathExtension() 27 | .lastPathComponent, 28 | !p.isEmpty 29 | { 30 | return p 31 | } 32 | if let n = psc.name, !n.isEmpty { return n } 33 | } 34 | 35 | return "CoreData" // /shrug 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Environment/DefaultAssignment/ManagedObjectDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManagedObjectExtras.swift 3 | // Direct to SwiftUI (Mobile) 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import CoreData 9 | 10 | public extension NSManagedObject { 11 | 12 | var d2s : D2S { return D2S(object: self) } 13 | 14 | struct D2S { 15 | 16 | let object : NSManagedObject 17 | 18 | public var isDefault : Bool { 19 | return object === D2SKeys.object.defaultValue 20 | } 21 | 22 | public var defaultTitle : String { 23 | return Self.title(for: object) 24 | } 25 | 26 | 27 | // MARK: - Title 28 | 29 | static func title(for object: NSManagedObject) -> String { 30 | return title(for: object, entity: object.entity) 31 | } 32 | 33 | static func title(for object: NSManagedObject, entity: NSEntityDescription) 34 | -> String 35 | { 36 | // Look for string attributes, prefer 'title' exact match 37 | var firstString : NSAttributeDescription? 38 | var containsTitle : NSAttributeDescription? 39 | 40 | func string(for attribute: NSAttributeDescription?) -> String? { 41 | guard let attribute = attribute else { return nil } 42 | guard let value = object.value(forKey: attribute.name) else { return nil } 43 | guard let s = value as? String else { return nil } 44 | guard !s.isEmpty else { return nil } 45 | return s 46 | } 47 | 48 | for attribute in entity.attributes { 49 | guard attribute.isStringAttribute else { continue } 50 | let lowname = attribute.name.lowercased() 51 | if lowname == "title" { 52 | if let s = string(for: attribute) { return s } 53 | } 54 | else if containsTitle == nil, lowname.contains("title") { 55 | containsTitle = attribute 56 | } 57 | else if firstString == nil { 58 | firstString = attribute 59 | } 60 | } 61 | 62 | if let s = string(for: containsTitle) { return s } 63 | if let s = string(for: firstString) { return s } // TBD 64 | 65 | // TBD 66 | let s = object.objectID.uriRepresentation().lastPathComponent 67 | if !s.isEmpty { return s } 68 | 69 | return String("\(object.objectID)") 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Environment/DefaultAssignment/ModelDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SModel.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import CoreData 9 | 10 | public extension NSManagedObjectModel { 11 | var d2s : ModelD2S { return ModelD2S(model: self) } 12 | } 13 | public extension NSAttributeDescription { 14 | var d2s : AttributeD2S { return AttributeD2S(attribute: self) } 15 | } 16 | public extension NSRelationshipDescription { 17 | var d2s : RelationshipD2S { return RelationshipD2S(relationship: self) } 18 | } 19 | 20 | public struct ModelD2S { 21 | let model : NSManagedObjectModel 22 | 23 | public var isDefault : Bool { D2SKeys.model.defaultValue === model } 24 | 25 | public var defaultVisibleEntityNames : [ String ] { 26 | // Loop through rule system to derive displayName? 27 | // No, that would be the job of a view (set the entity, query the title) 28 | return model.entities.compactMap { $0.name } 29 | .sorted() 30 | } 31 | } 32 | 33 | public struct AttributeD2S { 34 | let attribute : NSAttributeDescription 35 | 36 | public var isDefault : Bool { attribute is D2SDefaultAttribute } 37 | } 38 | 39 | public struct RelationshipD2S { 40 | let relationship : NSRelationshipDescription 41 | 42 | public enum RelationshipType: Hashable { 43 | case none, toOne, toMany 44 | 45 | public var isRelationship: Bool { 46 | switch self { 47 | case .none: return false 48 | case .toOne, .toMany: return true 49 | } 50 | } 51 | } 52 | 53 | public var isDefault : Bool { relationship is D2SDefaultRelationship } 54 | public var type : RelationshipType { 55 | if relationship is D2SDefaultRelationship { return .none } 56 | if relationship.isToMany { return .toMany } 57 | return .toOne 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Environment/DefaultAssignment/README.md: -------------------------------------------------------------------------------- 1 |

Direct to SwiftUI Default Assignments 2 | 4 |

5 | 6 | Some environment keys need values which are derived from a base object, 7 | for example the displayed properties. 8 | 9 | In D2W this is handled by a special action class: `DefaultAssignment`. 10 | 11 | In addition we provide `d2s` trampolines on relevant objects which namespace 12 | common D2S properties one might need. 13 | 14 | > Also added DefaultAssignment things because that can derive values from 15 | > multiple keys! 16 | 17 | For example: 18 | 19 | - `\.object.d2s.isDefault` 20 | - `\.object.d2s.defaultTitle` 21 | - `\.entity.d2s.isDefault` 22 | - `\.entity.d2s.defaultTitle` 23 | - `\.ruleObjectContext.d2s.isDefault` 24 | - `\.ruleObjectContext.d2s.defaultTitle` 25 | - `\.ruleObjectContext.d2s.hasDefaultTitle` 26 | - `\.attribute.d2s.isDefault` 27 | - `\.relationship.d2s.isDefault` 28 | - `\.relationship.d2s.type`: `.none`, `.toOne`, `.toMany` 29 | - `\.model.d2s.isDefault` 30 | - `\.model.d2s.defaultVisibleEntityNames` 31 | 32 | ## `isDefault` keys 33 | 34 | Most D2S keys are structured so that they are not optionals, that includes 35 | `object`, `entity` or `model`. 36 | The rational is that Views should be able to declare their expected environment 37 | without optionality, e.g.: 38 | ```swift 39 | struct MyView: View { 40 | @Environment(\.object) var object // <== this NEEDs an object 41 | } 42 | ``` 43 | 44 | That has the sideeffect, that we need to provide empty dummy objects if those 45 | values are not explicitly set. 46 | 47 | To check for those "non" objects, the `.d2s.isDefault` property is provided. 48 | It can be used like so: 49 | ```swift 50 | \.object.d2s.isDefault == false => \.title <= \.object.d2s.defaultTitle, 51 | ``` 52 | 53 | > TBD: Is this a good idea? Maybe we should just use optionals ... 54 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Environment/README.md: -------------------------------------------------------------------------------- 1 |

Direct to SwiftUI Environment Keys 2 | 4 |

5 | 6 | A lot of the functionality of D2S is built around "Environment Keys". 7 | 8 | "Environment Keys" are keys which you can use like so in SwiftUI: 9 | 10 | ```swift 11 | public struct D2SInspectPage: View { 12 | 13 | @Environment(\.ruleObjectContext) private var database : Database // retrieve a key 14 | 15 | var body: some View { 16 | BlaBlub() 17 | .environment(\.task, "edit") // set a key 18 | } 19 | } 20 | ``` 21 | 22 | They are scoped along the view hierarchy. D2S uses them to pass down its rule 23 | execution context. 24 | 25 | ## Builtin environment keys 26 | 27 | D2S has quiet a set of builtin environment keys, including: 28 | - ZeeQL Objects: 29 | - `database` 30 | - `object` 31 | - ZeeQL Model: 32 | - `model` 33 | - `entity` 34 | - `attribute` 35 | - `relationship` 36 | - `propertyKey` 37 | - Rendering 38 | - `title` 39 | - `displayNameForEntity` 40 | - `displayNameForProperty` 41 | - `displayStringForNil` 42 | - `hideEmptyProperty` 43 | - `formatter` 44 | - `displayPropertyKeys` 45 | - `visibleEntityNames` 46 | - `navigationBarTitle` 47 | - Components and Pages 48 | - `task` 49 | - `nextTask` 50 | - `page` 51 | - `rowComponent` 52 | - `component` 53 | - `pageWrapper` 54 | - `debugComponent` 55 | - Permissions 56 | - `user` 57 | - `isObjectEditable` 58 | - `isObjectDeletable` 59 | - `isEntityReadOnly` 60 | - `readOnlyEntityNames` 61 | - Misc 62 | - `look` 63 | - `platform` 64 | - `debug` 65 | - `initialPropertyValues` 66 | 67 | Checkout the `D2SKeys` for the full set. 68 | 69 | 70 | ## Rule based environment keys 71 | 72 | A key concept of D2S is that environment keys are not just static keys, 73 | but that the value of a key can be derived from a "Rule Model". 74 | 75 | For example: 76 | 77 | ``` 78 | entity.name = 'Movie' AND attribute.name = 'name' 79 | => displayNameForProperty = 'Movie' 80 | *true* 81 | => displayNameForProperty = attribute.name 82 | ``` 83 | 84 | The value of `displayNameForProperty` will be different depending on the context 85 | which arounds it. 86 | 87 | All environment keys which are of that kind conform to the new 88 | `RuleEnvironmentKey` protocol, which also requires `EnvironmentKey` 89 | conformance. 90 | 91 | ### RuleContext 92 | 93 | > Unfortunately the builtin SwiftUI `EnvironmentValues` struct lacks a few 94 | > operations to allow us to directly make any environment key dynamic. 95 | 96 | Dynamic environment keys are stored in a `RuleContext`. `RuleContext` is a 97 | struct similar to SwiftUI's `EnvironmentValues`, but in addition to providing 98 | key storage, it can also evaluate keys against a rule model. 99 | 100 | > The `RuleContext` itself is stored as a regular environment key! 101 | 102 | The `RuleContext` is also the root object passed into the rule engine. So its 103 | keys are exposed to the rule engine. 104 | 105 | 106 | ## Adding a new dynamic environment key 107 | 108 | Since we want to support D2S keys as `EnvironmentKey` keys, 109 | but also as `KeyValueCoding` keys, 110 | and everything should still be as typesafe as possible, 111 | it is quite some work to set one up ... 112 | 113 | ### Step A: Create a `DynamicEnvironmentKey` 114 | 115 | This is the same as creating a regular `EnvironmentKey`. 116 | Define a struct representing the key (D2S ones are in the `D2S` namespacing 117 | enum): 118 | ```swift 119 | struct object: DynamicEnvironmentKey { 120 | public static let defaultValue : NSManagedObject = NSManagedObject() 121 | } 122 | ``` 123 | A requirement of `EnvironmentKey` is that all keys have a default value which is 124 | active when no explicit key was set. 125 | 126 | > If you want to make an optional key, just define it as an optional type! 127 | > Note that the `defaultValue` is _always_ queried (at least as of beta6). 128 | 129 | ### Step B: Property on `D2SDynamicEnvironmentValues` 130 | 131 | SwiftUI accesses environment keys using keypathes, e.g. the `\.ruleObjectContext` in 132 | here: 133 | 134 | ```swift 135 | @Environment(\.ruleObjectContext) var database : Database 136 | ``` 137 | 138 | Those need to be declared as an extension to `D2SDynamicEnvironmentValues`: 139 | 140 | ```swift 141 | public extension D2SDynamicEnvironmentValues { 142 | var database : Database { 143 | set { self[dynamic: D2SKeys.ruleObjectContext.self] = newValue } 144 | get { self[dynamic: D2SKeys.ruleObjectContext.self] } 145 | } 146 | ``` 147 | 148 | The `rule` subscript dispatches the set/get calls to the `RuleContext`, which 149 | either 150 | - returns a value previously set, 151 | - retrieves a value from the rule system 152 | - or falls back to the default value (two variants are provided). 153 | 154 | NOTE: Please keep all D2S system keypathes together in 155 | `D2SEnvironmentKeys.swift`. 156 | 157 | 158 | ### Step C: Expose the key to `KeyValueCoding` 159 | 160 | This needs the stringly mapping ... The internal ones are declared in a map in 161 | `D2SEnvironmentKeys.swift`. 162 | ```swift 163 | private static var kvcToEnvKey : [ String: KVCMapEntry ] = [ 164 | "database" : .init(D2SKeys.ruleObjectContext.self), 165 | ... 166 | ] 167 | ``` 168 | 169 | A custom key can be added using `D2SContextKVC.expose(Key.self, "kvcname")` 170 | by a framework consumer. 171 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Environment/ViewModifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModifiers.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUIRules 10 | 11 | public extension View { 12 | // @inlinable crashes swiftc - SR-11444 13 | 14 | /** 15 | * Push the given object to the `object` environment key, 16 | * *AND* as an environmentObject! 17 | */ 18 | //@inlinable 19 | func ruleObject(_ object: NSManagedObject) -> some View { 20 | self 21 | .environment(\.object, object) 22 | .environmentObject(object) // TBD: is this using the dynamic type? 23 | } 24 | 25 | //@inlinable 26 | func ruleContext(_ ruleContext: RuleContext) -> some View { 27 | self.environment(\.ruleContext, ruleContext) 28 | } 29 | 30 | //@inlinable 31 | func task(_ task: D2STask) -> some View { 32 | self.environment(\.task, task.stringValue) // TBD 33 | } 34 | //@inlinable 35 | func task(_ task: String) -> some View { 36 | self.environment(\.task, task) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/README.md: -------------------------------------------------------------------------------- 1 |

Direct to SwiftUI 2 | 4 |

5 | 6 | ![Swift5.1](https://img.shields.io/badge/swift-5.1-blue.svg) 7 | ![macOS](https://img.shields.io/badge/os-macOS-green.svg?style=flat) 8 | ![iOS](https://img.shields.io/badge/os-iOS-green.svg?style=flat) 9 | ![watchOS](https://img.shields.io/badge/os-watchOS-green.svg?style=flat) 10 | ![Travis](https://api.travis-ci.org/DirectToSwift/CoreDataToSwiftUI.svg?branch=develop&style=flat) 11 | 12 | - [The Environment](Environment/README.md) 13 | - [Views](Views/README.md) 14 | - [Database Setup](DatabaseSetup.md) 15 | 16 | ## Using the Package 17 | 18 | You can either just drag the DirectToSwiftUI Xcode project into your own 19 | project, 20 | or you can use Swift Package Manager. 21 | 22 | The package URL is: 23 | [https://github.com/DirectToSwift/DirectToSwiftUI.git 24 | ](https://github.com/DirectToSwift/DirectToSwiftUI.git). 25 | 26 | ## Who 27 | 28 | Brought to you by 29 | [The Always Right Institute](http://www.alwaysrightinstitute.com) 30 | and 31 | [ZeeZide](http://zeezide.de). 32 | We like 33 | [feedback](https://twitter.com/ar_institute), 34 | GitHub stars, 35 | cool [contract work](http://zeezide.com/en/services/services.html), 36 | presumably any form of praise you can think of. 37 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/AppKit/D2SInspectWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SInspectWindow.swift 3 | // Direct to SwiftUI (Mac) 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | #if os(macOS) 9 | import Cocoa 10 | import SwiftUI 11 | 12 | class D2SInspectWindow: NSWindowController { 13 | convenience init(rootView: RootView) { 14 | let hostingController = NSHostingController( 15 | rootView: rootView 16 | .frame(minWidth: 300 as CGFloat, maxWidth: .infinity, 17 | minHeight: 400 as CGFloat, maxHeight: .infinity) 18 | ) 19 | let window = NSWindow(contentViewController: hostingController) 20 | window.setContentSize(NSSize(width: 400, height: 400)) 21 | self.init(window: window) 22 | } 23 | } 24 | 25 | #endif // macOS 26 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/AppKit/D2SMainWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SMainWindow.swift 3 | // Direct to SwiftUI (Mac) 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | #if os(macOS) 9 | 10 | import class SwiftUI.NSHostingView 11 | import Cocoa 12 | 13 | /** 14 | * Function to create a main window. 15 | */ 16 | public func D2SMakeWindow(managedObjectContext : NSManagedObjectContext, 17 | ruleModel : RuleModel) 18 | -> NSWindow 19 | { 20 | let window = NSWindow( 21 | contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), 22 | styleMask: [ 23 | .titled, 24 | .closable, .miniaturizable, .resizable, 25 | .fullSizeContentView 26 | ], 27 | backing: .buffered, defer: false 28 | ) 29 | window.center() 30 | window.setFrameAutosaveName("D2SWindow") 31 | 32 | window.titleVisibility = .hidden // just hides the title string 33 | window.titlebarAppearsTransparent = true 34 | window.isMovableByWindowBackground = true 35 | 36 | let view = D2SMainView(managedObjectContext : managedObjectContext, 37 | ruleModel : ruleModel) 38 | .frame(maxWidth: .infinity, maxHeight: .infinity) 39 | window.contentView = NSHostingView(rootView: view) 40 | return window 41 | } 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/ComparisonOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComparisonOperation.swift 3 | // CoreDataToSwiftUI 4 | // 5 | // Created by Helge Heß on 22.09.19. 6 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSComparisonPredicate.Operator: Equatable { 12 | 13 | public static func ==(lhs: NSComparisonPredicate.Operator, 14 | rhs: NSComparisonPredicate.Operator) 15 | -> Bool 16 | { 17 | switch ( lhs, rhs ) { 18 | case ( equalTo, equalTo ): return true 19 | case ( notEqualTo, notEqualTo ): return true 20 | case ( greaterThan, greaterThan ): return true 21 | case ( greaterThanOrEqualTo, greaterThanOrEqualTo ): return true 22 | case ( lessThan, lessThan ): return true 23 | case ( lessThanOrEqualTo, lessThanOrEqualTo ): return true 24 | case ( contains, contains ): return true 25 | case ( between, between ): return true 26 | case ( like, like ): return true 27 | case ( beginsWith, beginsWith ): return true 28 | case ( endsWith, endsWith ): return true 29 | case ( matches, matches ): return true 30 | case ( customSelector, customSelector ): return true //TBD 31 | default: return false 32 | } 33 | } 34 | } 35 | 36 | public extension NSComparisonPredicate.Operator { 37 | // TODO: Evaluation is a "little" harder in Swift, also coercion 38 | 39 | func compare(_ a: Any?, _ b: Any?) -> Bool { 40 | // Everytime you compare an Any, a 🐄 dies. 41 | switch self { 42 | case .equalTo: return eq(a, b) 43 | case .notEqualTo: return !eq(a, b) 44 | case .lessThan: return isSmaller(a, b) 45 | case .greaterThan: return isSmaller(b, a) 46 | case .lessThanOrEqualTo: return isSmaller(a, b) || eq(a, b) 47 | case .greaterThanOrEqualTo: return isSmaller(b, a) || eq(a, b) 48 | 49 | case .contains: // firstname in ["donald"] or firstname in "donald" 50 | guard let b = b else { return false } 51 | guard let list = b as? ContainsComparisonType else { 52 | globalD2SLogger.error( 53 | "attempt to evaluate an ComparisonOperation dynamically:", 54 | self, a, b 55 | ) 56 | assertionFailure("comparison not supported for dynamic evaluation") 57 | return false 58 | } 59 | return list.contains(other: a) 60 | 61 | case .like: // firstname like *Donald* 62 | let ci = false // TBD 63 | if a == nil && b == nil { return true } // nil is like nil 64 | guard let value = a as? LikeComparisonType else { 65 | globalD2SLogger.error( 66 | "attempt to evaluate an ComparisonOperation dynamically:", 67 | self, a, b 68 | ) 69 | assertionFailure("comparison not supported for dynamic evaluation") 70 | return false 71 | } 72 | return value.isLike(other: b, caseInsensitive: ci) 73 | 74 | // TODO: support many more, geez :-) 75 | 76 | default: 77 | globalD2SLogger.error( 78 | "attempt to evaluate an NSComparisonPredicate dynamically:", 79 | self, a, b 80 | ) 81 | assertionFailure("comparison not supported for dynamic evaluation") 82 | return false 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/CoreData/D2SEditValidation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SEditValidation.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import CoreData 9 | 10 | public protocol D2SAttributeValidator { 11 | 12 | associatedtype Object : NSManagedObject 13 | 14 | var attribute : NSAttributeDescription { get } 15 | var object : Object { get } 16 | 17 | var isValid : Bool { get } 18 | } 19 | 20 | public extension D2SAttributeValidator { 21 | 22 | var isValid : Bool { 23 | return object.isNew 24 | ? attribute.validateForInsert(object) 25 | : attribute.validateForUpdate(object) 26 | } 27 | } 28 | 29 | 30 | public protocol D2SRelationshipValidator { 31 | 32 | associatedtype Object : NSManagedObject 33 | 34 | var relationship : NSRelationshipDescription { get } 35 | var object : Object { get } 36 | 37 | var isValid : Bool { get } 38 | } 39 | 40 | public extension D2SRelationshipValidator { 41 | 42 | var isValid : Bool { 43 | return object.isNew 44 | ? relationship.validateForInsert(object) 45 | : relationship.validateForUpdate(object) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/CoreData/DataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataSource.swift 3 | // CoreDataToSwiftUI 4 | // 5 | // Created by Helge Heß on 23.09.19. 6 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | 11 | public protocol DataSource { 12 | 13 | associatedtype Object : NSManagedObject 14 | 15 | var fetchRequest : NSFetchRequest? { set get } 16 | 17 | func fetchObjects() throws -> [ Object ] 18 | func fetchCount() throws -> Int 19 | func fetchGlobalIDs() throws -> [ NSManagedObjectID ] 20 | 21 | func fetchRequestForFetch() throws -> NSFetchRequest 22 | 23 | func _primaryFetchObjects (_ fr: NSFetchRequest) throws -> [ Object ] 24 | func _primaryFetchCount (_ fr: NSFetchRequest) throws -> Int 25 | func _primaryFetchGlobalIDs(_ fr: NSFetchRequest) throws 26 | -> [ NSManagedObjectID ] 27 | } 28 | 29 | public extension DataSource { 30 | 31 | func fetchObjects(_ fr: NSFetchRequest) throws -> [ Object ] { 32 | try _primaryFetchObjects(fr) 33 | } 34 | 35 | func fetchObjects() throws -> [ Object ] { 36 | try _primaryFetchObjects(try fetchRequestForFetch()) 37 | } 38 | func fetchCount() throws -> Int { 39 | try _primaryFetchCount(try fetchRequestForFetch()) 40 | } 41 | } 42 | 43 | public class ManagedObjectDataSource: DataSource { 44 | 45 | public let managedObjectContext : NSManagedObjectContext 46 | public let entity : NSEntityDescription 47 | public var fetchRequest : NSFetchRequest? 48 | 49 | public init(managedObjectContext : NSManagedObjectContext, 50 | entity : NSEntityDescription) 51 | { 52 | self.managedObjectContext = managedObjectContext 53 | self.entity = entity 54 | } 55 | 56 | public func fetchRequestForFetch() throws -> NSFetchRequest { 57 | if let c = fetchRequest?.typedCopy() { return c } 58 | return NSFetchRequest(entityName: entity.name ?? "") 59 | } 60 | 61 | public func _primaryFetchObjects(_ fr: NSFetchRequest) throws 62 | -> [ Object ] 63 | { 64 | try managedObjectContext.fetch(fr) 65 | } 66 | public func _primaryFetchCount(_ fr: NSFetchRequest) throws -> Int { 67 | if fr.resultType != .countResultType { 68 | let fr = fr.countCopy() 69 | fr.resultType = .countResultType 70 | return try managedObjectContext.count(for: fr) 71 | } 72 | else { 73 | return try managedObjectContext.count(for: fr) 74 | } 75 | } 76 | public func _primaryFetchGlobalIDs(_ fr: NSFetchRequest) 77 | throws -> [ NSManagedObjectID ] 78 | { 79 | if fr.resultType != .managedObjectIDResultType { 80 | let fr = fr.objectIDsCopy() 81 | fr.resultType = .managedObjectIDResultType 82 | return try managedObjectContext.fetch(fr) 83 | } 84 | else { 85 | return try managedObjectContext.fetch(fr) 86 | } 87 | } 88 | 89 | public func fetchGlobalIDs() throws -> [ NSManagedObjectID ] { 90 | let fr = fetchRequest?.objectIDsCopy() 91 | ?? NSFetchRequest(entityName: entity.name ?? "") 92 | fr.resultType = .managedObjectIDResultType 93 | return try _primaryFetchGlobalIDs(fr) 94 | } 95 | public func fetchGlobalIDs(_ fr: NSFetchRequest) 96 | throws -> [ NSManagedObjectID ] 97 | { 98 | let fr = fr.objectIDsCopy() 99 | fr.resultType = .managedObjectIDResultType 100 | return try _primaryFetchGlobalIDs(fr) 101 | } 102 | 103 | public func fetchCount(_ fr: NSFetchRequest) throws -> Int { 104 | return try _primaryFetchCount(fr) 105 | } 106 | 107 | public func createObject() -> Object { 108 | NSEntityDescription.insertNewObject( 109 | forEntityName: entity.name ?? "", 110 | into: managedObjectContext 111 | ) as! Object 112 | } 113 | } 114 | 115 | public extension ManagedObjectDataSource { 116 | 117 | func find() throws -> Object? { 118 | let fr = try fetchRequestForFetch() 119 | fr.fetchLimit = 2 120 | 121 | let objects = try _primaryFetchObjects(fr) 122 | assert(objects.count < 2) 123 | return objects.first 124 | } 125 | 126 | } 127 | 128 | public extension NSManagedObjectContext { 129 | 130 | func dataSource(for entity: NSEntityDescription) 131 | -> ManagedObjectDataSource 132 | { 133 | ManagedObjectDataSource(managedObjectContext: self, entity: entity) 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/CoreData/DetailDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailDataSource.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import CoreData 9 | 10 | extension NSRelationshipDescription { 11 | 12 | func predicateInDestinationForSource(_ source: NSManagedObject) 13 | -> NSPredicate? 14 | { 15 | guard let inverse = inverseRelationship else { 16 | globalD2SLogger.error("relationship misses inverse:", self) 17 | return nil 18 | } 19 | 20 | return NSComparisonPredicate( 21 | leftExpression : NSExpression(forKeyPath: inverse.name), 22 | rightExpression : NSExpression(forConstantValue: source), 23 | modifier: .direct, type: .equalTo, options: [] 24 | ) 25 | } 26 | } 27 | 28 | 29 | extension NSManagedObject { 30 | 31 | func wire(destination: NSManagedObject?, 32 | to relationship: NSRelationshipDescription) 33 | { 34 | self.setValue(destination, forKey: relationship.name) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/CoreData/DummyImplementations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DummyImplementations.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import CoreData 9 | 10 | // Those are here to workaround the issue that we don't want any 11 | // optionals in Views. Which may or may not be a good decision. 12 | 13 | internal final class D2SDummyObjectContext: NSManagedObjectContext { 14 | static let shared : NSManagedObjectContext = D2SDummyObjectContext() 15 | init() { 16 | super.init(concurrencyType: .mainQueueConcurrencyType) 17 | let psc = NSPersistentStoreCoordinator( 18 | managedObjectModel: D2SDefaultModel.shared) 19 | persistentStoreCoordinator = psc 20 | } 21 | required init?(coder: NSCoder) { 22 | fatalError("\(#function) has not been implemented") 23 | } 24 | } 25 | 26 | internal final class D2SDefaultModel: NSManagedObjectModel { 27 | static let shared : NSManagedObjectModel = D2SDefaultModel() 28 | override init() { 29 | super.init() 30 | } 31 | required init?(coder: NSCoder) { 32 | fatalError("\(#function) has not been implemented") 33 | } 34 | 35 | override var entities: [NSEntityDescription] { 36 | set { 37 | fatalError("unexpected call to set `entities`") 38 | } 39 | get { [ D2SDefaultEntity.shared ] } 40 | } 41 | override var entitiesByName: [String : NSEntityDescription] { 42 | [ "_dummy": D2SDefaultEntity.shared ] 43 | } 44 | } 45 | 46 | internal final class D2SDefaultEntity: NSEntityDescription { 47 | static let shared = D2SDefaultEntity() 48 | override init() { 49 | super.init() 50 | name = "_dummy" 51 | managedObjectClassName = NSStringFromClass(D2SDefaultObject.self) 52 | } 53 | required init?(coder: NSCoder) { 54 | fatalError("\(#function) has not been implemented") 55 | } 56 | override var managedObjectModel: NSManagedObjectModel { 57 | return D2SDefaultModel.shared 58 | } 59 | } 60 | internal final class D2SDefaultAttribute: NSAttributeDescription {} 61 | 62 | internal final class D2SDefaultRelationship: NSRelationshipDescription {} 63 | 64 | internal final class D2SDefaultObject: NSManagedObject { 65 | init() { 66 | // fails in class for entity 67 | super.init(entity : D2SDefaultEntity .shared, 68 | insertInto : D2SDummyObjectContext.shared) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/CoreData/FetchRequestExtras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchRequestExtras.swift 3 | // CoreDataToSwiftUI 4 | // 5 | // Created by Helge Heß on 23.09.19. 6 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | 11 | public extension NSFetchRequest { 12 | 13 | @objc convenience init(entity: NSEntityDescription) { 14 | assert(entity.name != nil) 15 | self.init(entityName: entity.name ?? "") 16 | } 17 | 18 | @objc func typedCopy() -> NSFetchRequest { 19 | let me = copy() 20 | guard let typed = me as? NSFetchRequest else { 21 | fatalError("fetch request lost its type! \(type(of: me))") 22 | } 23 | return typed 24 | } 25 | 26 | @objc func objectIDsCopy() -> NSFetchRequest { 27 | let me = copy() 28 | guard let typed = me as? NSFetchRequest else { 29 | fatalError("can't convert fetch request type! \(type(of: me))") 30 | } 31 | return typed 32 | } 33 | @objc func countCopy() -> NSFetchRequest { 34 | let me = copy() 35 | guard let typed = me as? NSFetchRequest else { 36 | fatalError("can't convert fetch request type! \(type(of: me))") 37 | } 38 | return typed 39 | } 40 | } 41 | 42 | public extension NSFetchRequest { 43 | 44 | @objc func limit(_ limit: Int) -> NSFetchRequest { 45 | let fr = typedCopy() 46 | fr.fetchLimit = limit 47 | return fr 48 | } 49 | @objc func offset(_ offset: Int) -> NSFetchRequest { 50 | let fr = typedCopy() 51 | fr.fetchOffset = offset 52 | return fr 53 | } 54 | 55 | @objc func `where`(_ predicate: NSPredicate) -> NSFetchRequest { 56 | let fr = typedCopy() 57 | fr.predicate = predicate 58 | return fr 59 | } 60 | 61 | #if false // doesn't fly, needs @objc which doesn't work w/ Range 62 | func range(_ range: Range) -> NSFetchRequest { 63 | let fr = typedCopy() 64 | fr.fetchOffset = range.lowerBound 65 | fr.fetchLimit = range.count 66 | return fr 67 | } 68 | #endif 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/CoreData/KVCBindings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KVCBindings.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | import CoreData 10 | 11 | public extension KeyValueCodingType { 12 | 13 | func binding(_ key: String) -> Binding { 14 | return KeyValueCoding.binding(key, for: self) 15 | } 16 | } 17 | 18 | public extension KeyValueCoding { // bindings for KVC keys 19 | 20 | static func binding(_ key: String, for object: KeyValueCodingType?) 21 | -> Binding 22 | { 23 | if let object = object { 24 | return Binding(get: { 25 | KeyValueCoding.value(forKey: key, inObject: object) 26 | }) { 27 | newValue in 28 | KeyValueCoding.setValue(newValue, forKey: key, inObject: object) 29 | } 30 | } 31 | else { 32 | return Binding(get: { return nil }) { newValue in 33 | globalD2SLogger.error("attempt to write to nil binding:", key) 34 | assertionFailure("attempt to write to nil binding: \(key)") 35 | } 36 | } 37 | } 38 | 39 | static func binding(_ key: String, for object: KeyValueCodingType) 40 | -> Binding 41 | { 42 | return Binding(get: { object.value(forKey: key) }, 43 | set: { object.setValue($0, forKey: key) }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/CoreData/ModelExtras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelExtras.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import CoreData 9 | 10 | public extension NSManagedObjectModel { 11 | 12 | /** 13 | * Try to find an entity which might form a user database (one which can be 14 | * queried using login/password) 15 | */ 16 | func lookupUserDatabaseEntity() -> NSEntityDescription? { 17 | var lcNameToUserEntity = [ String : NSEntityDescription ]() 18 | for entity in entities { 19 | guard let _ = entity.lookupUserDatabaseProperties() else { continue } 20 | guard let name = entity.name else { continue } 21 | lcNameToUserEntity[name.lowercased()] = entity 22 | } 23 | if lcNameToUserEntity.isEmpty { return nil } 24 | if lcNameToUserEntity.count == 1 { return lcNameToUserEntity.values.first } 25 | 26 | globalD2SLogger.log("multiple entities have passwords:", 27 | lcNameToUserEntity.keys.joined(separator: ",")) 28 | return lcNameToUserEntity["staff"] 29 | ?? lcNameToUserEntity["userdb"] 30 | ?? lcNameToUserEntity["accounts"] 31 | ?? lcNameToUserEntity["account"] 32 | ?? lcNameToUserEntity["person"] 33 | ?? lcNameToUserEntity.values.first // any, good luck 34 | } 35 | } 36 | 37 | public extension NSManagedObjectModel { 38 | 39 | subscript(entity name: String) -> NSEntityDescription? { 40 | entitiesByName[name] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/CoreData/PredicateExtras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PredicateExtras.swift 3 | // CoreDataToSwiftUI 4 | // 5 | // Created by Helge Heß on 23.09.19. 6 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension NSPredicate { 12 | 13 | var not : NSPredicate { 14 | NSCompoundPredicate(notPredicateWithSubpredicate: self) 15 | } 16 | func or(_ q: NSPredicate?) -> NSPredicate { 17 | guard let q = q else { return self } 18 | return NSCompoundPredicate(orPredicateWithSubpredicates: [self, q ]) 19 | } 20 | func and(_ q: NSPredicate?) -> NSPredicate { 21 | guard let q = q else { return self } 22 | return NSCompoundPredicate(andPredicateWithSubpredicates: [self, q ]) 23 | } 24 | } 25 | 26 | 27 | // MARK: - Factory 28 | 29 | public func and(_ a: NSPredicate?, _ b: NSPredicate?) -> NSPredicate? { 30 | if let a = a, let b = b { return a.and(b) } 31 | if let a = a { return a } 32 | return b 33 | } 34 | public func or(_ a: NSPredicate?, _ b: NSPredicate?) -> NSPredicate? { 35 | if let a = a, let b = b { return a.or(b) } 36 | if let a = a { return a } 37 | return b 38 | } 39 | fileprivate func and1(_ a: NSPredicate?, _ b: NSPredicate?) -> NSPredicate? { 40 | and(a, b) 41 | } 42 | fileprivate func or1(_ a: NSPredicate?, _ b: NSPredicate?) -> NSPredicate? { 43 | or(a, b) 44 | } 45 | 46 | public extension Sequence where Element : NSPredicate { 47 | 48 | func and() -> NSPredicate { 49 | return reduce(nil, { and1($0, $1) }) ?? NSPredicate(value: true) 50 | } 51 | func or() -> NSPredicate { 52 | return reduce(nil, { or1($0, $1) }) ?? NSPredicate(value: false) 53 | } 54 | func compactingOr() -> NSPredicate { 55 | return NSCompoundPredicate(orPredicateWithSubpredicates: Array(self)) 56 | } 57 | } 58 | public extension Collection where Element : NSPredicate { 59 | 60 | func and() -> NSPredicate { 61 | if isEmpty { return NSPredicate(value: false) } 62 | if count == 1 { return self[self.startIndex] } 63 | return NSCompoundPredicate(andPredicateWithSubpredicates: Array(self)) 64 | } 65 | func or() -> NSPredicate { 66 | if isEmpty { return NSPredicate(value: false) } 67 | if count == 1 { return self[self.startIndex] } 68 | return NSCompoundPredicate(orPredicateWithSubpredicates: Array(self)) 69 | } 70 | func compactingOr() -> NSPredicate { 71 | if isEmpty { return NSPredicate(value: false) } 72 | if count == 1 { return self[self.startIndex] } 73 | return Array(self).compactingOr() 74 | } 75 | } 76 | public extension Array where Element : NSPredicate { 77 | func compactingOr() -> NSPredicate { 78 | if isEmpty { return NSPredicate(value: false) } 79 | if count == 1 { return self[self.startIndex] } 80 | return NSCompoundPredicate(orPredicateWithSubpredicates: self) 81 | } 82 | } 83 | public extension Collection where Element == NSComparisonPredicate { 84 | 85 | /// TODO: Not implemented for CoreData, maybe not necessary either 86 | func compactingOr() -> NSPredicate { 87 | if isEmpty { return NSPredicate(value: false) } 88 | if count == 1 { return self[self.startIndex] } 89 | 90 | #if true 91 | return NSCompoundPredicate(orPredicateWithSubpredicates: Array(self)) 92 | #else 93 | var keyToValues = [ String : [ Any? ] ]() 94 | var extra = [ NSPredicate ]() 95 | 96 | for kvq in self { 97 | if kvq.predicateOperatorType != .equalTo { 98 | extra.append(kvq) 99 | continue 100 | } 101 | 102 | let lhs = kvq.leftExpression, rhs = kvq.rightExpression 103 | if keyToValues[key] == nil { keyToValues[key] = [ value ] } 104 | else { keyToValues[key]!.append(value) } 105 | } 106 | 107 | for ( key, values ) in keyToValues { 108 | if values.isEmpty { continue } 109 | if values.count == 1 { 110 | extra.append(NSComparisonPredicate(key, .equalTo, values.first!)) 111 | } 112 | else { 113 | extra.append(NSComparisonPredicate(key, .Contains, values)) 114 | } 115 | } 116 | 117 | if extra.count == 1 { return extra[extra.startIndex] } 118 | return NSCompoundPredicate(orPredicateWithSubpredicates: extra) 119 | #endif 120 | } 121 | } 122 | 123 | 124 | public func predicateToMatchAnyValue(_ values: [ String : Any? ]?, 125 | _ op: NSComparisonPredicate.Operator 126 | = .equalTo, 127 | caseInsensitive: Bool = false) 128 | -> NSPredicate? 129 | { 130 | guard let values = values, !values.isEmpty else { return nil } 131 | let kvq = values.map { key, value in 132 | NSComparisonPredicate( 133 | leftExpression : NSExpression(forKeyPath: key), 134 | rightExpression : NSExpression(forConstantValue: value), 135 | modifier: .direct, type: op, 136 | options: caseInsensitive ? [ .caseInsensitive ] : [] 137 | ) 138 | } 139 | if kvq.count == 1 { return kvq[0] } 140 | return NSCompoundPredicate(orPredicateWithSubpredicates: kvq) 141 | } 142 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/FoundationExtras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FoundationExtras.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2017-2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import struct Foundation.CharacterSet 9 | 10 | enum UObject { 11 | static func boolValue(_ v: Any?, default: Bool = false) -> Bool { 12 | guard let v = v else { return `default` } 13 | if let b = v as? Bool { return b } 14 | if let i = v as? Int { return i != 0 } 15 | let s = ((v as? String) ?? String(describing: v)).lowercased() 16 | return s == "true" || s == "yes" || s == "1" 17 | } 18 | } 19 | 20 | extension String { 21 | 22 | var isMixedCase : Bool { 23 | guard !isEmpty else { return false } 24 | let upper = CharacterSet.uppercaseLetters 25 | let lower = CharacterSet.lowercaseLetters 26 | 27 | var hadUpper = false 28 | var hadLower = false 29 | for c in self.unicodeScalars { 30 | if upper.contains(c) { 31 | if hadLower { return true } 32 | hadUpper = true 33 | } 34 | else if lower.contains(c) { 35 | if hadUpper { return true } 36 | hadLower = true 37 | } 38 | } 39 | return false 40 | } 41 | } 42 | 43 | extension String { 44 | 45 | var capitalizedWithPreUpperSpace: String { 46 | guard !isEmpty else { return "" } 47 | 48 | var s = "" 49 | s.reserveCapacity(count) 50 | 51 | var wasLastUpper = false 52 | var isFirst = true 53 | for c in self { 54 | let isUpper = c.isUppercase 55 | 56 | if isFirst { 57 | s += c.uppercased() 58 | isFirst = false 59 | wasLastUpper = true 60 | continue 61 | } 62 | 63 | defer { wasLastUpper = isUpper } 64 | 65 | if isUpper { 66 | if !wasLastUpper { s += " " } 67 | } 68 | s += String(c) 69 | } 70 | 71 | return s 72 | } 73 | 74 | } 75 | 76 | extension String { 77 | 78 | func range(of needle: String, skippingQuotes quotes: CharacterSet, 79 | escapeUsing escape: Character) -> Range? 80 | { 81 | // Note: stupid port of GETobjects version 82 | // TODO: speed ... 83 | // TODO: check correctness with invalid input ! 84 | guard !needle.isEmpty else { return nil } 85 | if quotes.isEmpty { 86 | return needle.range(of: needle) 87 | } 88 | 89 | let len = count 90 | let slen = needle.count 91 | let sc = needle.first! 92 | 93 | var i = startIndex 94 | while i < endIndex { 95 | let c = self[i] 96 | defer { i = index(after: i) } 97 | 98 | if c == sc { 99 | if slen == 1 { return i..<(index(after: i)) } 100 | if self[i.. String { 14 | Insecure.SHA1.hash(data: Data(self.utf8)).hexEncoded 15 | } 16 | func md5() -> String { 17 | Insecure.MD5.hash(data: Data(self.utf8)).hexEncoded 18 | } 19 | } 20 | 21 | extension Sequence where Element == UInt8 { 22 | 23 | var hexEncoded : String { 24 | lazy.map { 25 | $0 > 15 26 | ? String($0, radix: 16, uppercase: false) 27 | : "0" + String($0, radix: 16, uppercase: false) 28 | }.joined() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/KeyValueCodingType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueCodingType.swift 3 | // CoreDataToSwiftUI 4 | // 5 | // Created by Helge Heß on 22.09.19. 6 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum KeyValueCoding { 12 | 13 | static func setValue(_ value: Any?, forKey key: String, 14 | inObject object: KeyValueCodingType?) 15 | { 16 | guard let object = object else { return } 17 | object.setValue(value, forKey: key) 18 | } 19 | static func value(forKey key: String, 20 | inObject object: KeyValueCodingType?) -> Any? 21 | { 22 | guard let object = object else { return nil } 23 | return object.value(forKey: key) 24 | } 25 | 26 | static func setValue(_ value: Any?, forKeyPath path: String, 27 | in object: KeyValueCodingType?) 28 | { 29 | guard let object = object else { return } 30 | object.setValue(value, forKeyPath: path) 31 | } 32 | static func value(forKeyPath path: String, 33 | inObject object: KeyValueCodingType?) -> Any? 34 | { 35 | guard let object = object else { return nil } 36 | return object.value(forKeyPath: path) 37 | } 38 | 39 | } 40 | 41 | public protocol KeyValueCodingType { 42 | func setValue(_ value: Any?, forKey key: String) 43 | func value(forKey key: String) -> Any? 44 | 45 | func setValue(_ value: Any?, forKeyPath path: String) 46 | func value(forKeyPath path: String) -> Any? 47 | } 48 | 49 | extension NSObject: KeyValueCodingType {} 50 | 51 | public extension KeyValueCodingType { 52 | 53 | func setValue(_ value: Any?, forKeyPath path: String) { 54 | guard let r = path.range(of: ".") else { 55 | return setValue(value, forKey: path) 56 | } 57 | let k1 = String(path[.. Any? { 69 | guard let r = path.range(of: ".") else { return value(forKey: path) } 70 | 71 | let k1 = String(path[.. Any? { 94 | set { object.setValue(newValue, forKey: member) } 95 | get { object.value(forKey: member) } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // ZeeQL3 4 | // 5 | // Created by Helge Hess on 14/04/17. 6 | // Copyright © 2017-2019 ZeeZide GmbH. All rights reserved. 7 | // 8 | 9 | /** 10 | * Protocol used by ZeeQL to perform logging. Implement it using your favorite 11 | * logging framework ... 12 | * 13 | * Defaults to a simple Print based logger. 14 | */ 15 | public protocol D2SLogger { 16 | 17 | func primaryLog(_ logLevel: D2SLoggerLogLevel, _ msgfunc: () -> String, 18 | _ values: [ Any? ] ) 19 | 20 | } 21 | 22 | public extension D2SLogger { // Actual logging funcs 23 | 24 | func error(_ msg: @autoclosure () -> String, _ values: Any?...) { 25 | primaryLog(.Error, msg, values) 26 | } 27 | func warn (_ msg: @autoclosure () -> String, _ values: Any?...) { 28 | primaryLog(.Warn, msg, values) 29 | } 30 | func log (_ msg: @autoclosure () -> String, _ values: Any?...) { 31 | primaryLog(.Log, msg, values) 32 | } 33 | func info (_ msg: @autoclosure () -> String, _ values: Any?...) { 34 | primaryLog(.Info, msg, values) 35 | } 36 | func trace(_ msg: @autoclosure () -> String, _ values: Any?...) { 37 | primaryLog(.Trace, msg, values) 38 | } 39 | 40 | } 41 | 42 | public enum D2SLoggerLogLevel : Int8 { // cannot nest types in generics 43 | case Error 44 | case Warn 45 | case Log 46 | case Info 47 | case Trace 48 | } 49 | 50 | 51 | // MARK: - Global Logger 52 | 53 | import class Foundation.ProcessInfo 54 | 55 | /** 56 | * Other objects initialize their logger from this. Can be assigned to 57 | * something else if you care. 58 | * Log-level can be set using the `ZEEQL_LOGLEVEL` global. 59 | */ 60 | public var globalD2SLogger : D2SLogger = { 61 | #if DEBUG 62 | let defaultLevel = D2SLoggerLogLevel.Log 63 | #else 64 | let defaultLevel = D2SLoggerLogLevel.Error 65 | #endif 66 | let logEnv = ProcessInfo.processInfo.environment["D2S_LOGLEVEL"]? 67 | .lowercased() 68 | ?? "" 69 | let level : D2SLoggerLogLevel 70 | 71 | if logEnv == "error" { level = .Error } 72 | else if logEnv.hasPrefix("warn") { level = .Warn } 73 | else if logEnv.hasPrefix("info") { level = .Info } 74 | else if logEnv == "trace" { level = .Trace } 75 | else if logEnv == "log" { level = .Log } 76 | else { level = defaultLevel } 77 | 78 | return D2SPrintLogger(level: level) 79 | }() 80 | 81 | 82 | // MARK: - Simple Implementation 83 | 84 | #if os(Linux) 85 | import Glibc 86 | #else 87 | import Darwin 88 | #endif 89 | 90 | fileprivate let stderrLogLevel : D2SLoggerLogLevel = .Error 91 | 92 | public struct D2SPrintLogger : D2SLogger { 93 | // public, maybe useful for ZeeQL users as well. 94 | 95 | let logLevel : D2SLoggerLogLevel 96 | 97 | public init(level: D2SLoggerLogLevel = .Error) { 98 | logLevel = level 99 | } 100 | 101 | public func primaryLog(_ logLevel : D2SLoggerLogLevel, 102 | _ msgfunc : () -> String, 103 | _ values : [ Any? ] ) 104 | { 105 | guard logLevel.rawValue <= self.logLevel.rawValue else { return } 106 | 107 | var s = logLevel.logPrefix + msgfunc() 108 | for v in values { 109 | s += " " 110 | if let v = v as? String { s += v } 111 | else if let v = v as? CustomStringConvertible { s += v.description } 112 | else if let v = v { s += " \(v)" } 113 | else { s += "" } 114 | } 115 | 116 | if logLevel.rawValue <= stderrLogLevel.rawValue { 117 | fputs(s, stderr) 118 | } 119 | else { 120 | print(s) 121 | } 122 | } 123 | 124 | } 125 | 126 | fileprivate extension D2SLoggerLogLevel { 127 | 128 | var logPrefix : String { 129 | switch self { 130 | case .Error: return "ERROR: " 131 | case .Warn: return "WARN: " 132 | case .Info: return "INFO: " 133 | case .Trace: return "Trace: " 134 | case .Log: return "" 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/Platform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Platform.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | public enum Platform: Hashable { 9 | 10 | case desktop, watch, phone, pad, tv 11 | 12 | public static var `default`: Platform { 13 | #if os(macOS) 14 | return .desktop 15 | #elseif os(iOS) 16 | return .phone // TODO: .pad?! 17 | #elseif os(watchOS) 18 | return .watch 19 | #elseif os(tvOS) 20 | return .tv 21 | #else 22 | return .phone 23 | #endif 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/ReExport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReExport.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | @_exported import SwiftUI 9 | @_exported import CoreData 10 | @_exported import SwiftUIRules 11 | 12 | infix operator => : AssignmentPrecedence 13 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/SwiftUI/D2STransformingFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2STransformingFormatter.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import class Foundation.Formatter 9 | import class Foundation.NSCoder 10 | import class Foundation.NSString 11 | 12 | /** 13 | * This one pipes the value to be formatted through a closure before passing it 14 | * on to another formatter. 15 | * 16 | * This is useful if the other formatter expects the value as a specific type 17 | * and/or unit. 18 | * 19 | * Example: 20 | * 21 | * let minuteDurationFormatter: Formatter = { 22 | * let mf : DateComponentsFormatter = { 23 | * let f = DateComponentsFormatter() 24 | * f.allowedUnits = [ .hour, .minute ] 25 | * f.unitsStyle = .short 26 | * return f 27 | * }() 28 | * return D2STransformingFormatter(mf) { ( minutes : Int ) in 29 | * TimeInterval(minutes * 60) 30 | * } 31 | * }() 32 | * 33 | */ 34 | public final class D2STransformingFormatter: Formatter { 35 | 36 | let wrapped : Formatter 37 | let string : ( In ) -> Out 38 | let value : ( ( Out ) -> In )? 39 | 40 | public init(_ wrapped: Formatter, string: @escaping ( In ) -> Out) { 41 | self.wrapped = wrapped 42 | self.string = string 43 | self.value = nil 44 | super.init() 45 | } 46 | required init?(coder: NSCoder) { 47 | fatalError("\(#function) has not been implemented") 48 | } 49 | 50 | override public func string(for obj: Any?) -> String? { 51 | guard let inValue = obj as? In else { 52 | return wrapped.string(for: obj as? Out) 53 | } 54 | return wrapped.string(for: string(inValue)) 55 | } 56 | 57 | override public func editingString(for obj: Any) -> String? { 58 | return string(for: obj) 59 | } 60 | 61 | override public 62 | func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, 63 | for string: String, errorDescription 64 | error: AutoreleasingUnsafeMutablePointer?) 65 | -> Bool 66 | { 67 | var o : AnyObject? = nil 68 | let ok = wrapped.getObjectValue(&o, for: string, errorDescription: error) 69 | guard ok, let out = o as? Out else { 70 | obj?.pointee = nil 71 | return false 72 | } 73 | 74 | if let value = value { 75 | obj?.pointee = value(out) as AnyObject 76 | return true 77 | } 78 | else if let i = out as? In { 79 | obj?.pointee = i as AnyObject 80 | return true 81 | } 82 | else { 83 | obj?.pointee = o 84 | return false 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Support/SwiftUI/FormatterBinding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormatterBinding.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import class Foundation.Formatter 9 | import class Foundation.NSString 10 | import struct SwiftUI.Binding 11 | 12 | public extension Binding { 13 | 14 | /** 15 | * Creates a String binding from an arbitrary value binding which pipes the 16 | * value binding through a formatter. 17 | * 18 | * This is a workaround to fix the `TextField` not doing the same when a 19 | * formatter is attached. 20 | */ 21 | func format(with formatter: Formatter, editing: Bool = true) 22 | -> Binding 23 | { 24 | Binding( 25 | get: { 26 | if editing { 27 | guard let s = formatter.editingString(for: self.wrappedValue) else { 28 | globalD2SLogger.trace("could not format:", self.wrappedValue, 29 | "\n using:", formatter, 30 | "\n to string.") 31 | return "" 32 | } 33 | return s 34 | } 35 | else { 36 | guard let s = formatter.string(for: self.wrappedValue) else { 37 | globalD2SLogger.trace("could not format:", self.wrappedValue, 38 | "\n using:", formatter, 39 | "\n to string.") 40 | return "" 41 | } 42 | return s 43 | } 44 | }, 45 | set: { string in 46 | var value : AnyObject? = nil 47 | var error : NSString? = nil 48 | guard formatter.getObjectValue(&value, for: string, 49 | errorDescription: &error) else { 50 | globalD2SLogger.warn("could not format:", string, 51 | "\n using:", formatter, 52 | "\n to: ", Value.self) 53 | return 54 | } 55 | guard let typedValue = value as? Value else { 56 | globalD2SLogger.warn("could not format:", string, 57 | "\n value:", value, 58 | "\n using:", formatter, 59 | "\n to: ", Value.self) 60 | return 61 | } 62 | self.wrappedValue = typedValue 63 | } 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/TRANSITION.md: -------------------------------------------------------------------------------- 1 | # Transition TODOs 2 | 3 | - Dynamic Member lookup 4 | - I think we can't add this to NSManagedObject? But do we 5 | need to? We have concrete classes? Hmm... 6 | 7 | - NSManagedObject's are faults? 8 | 9 | - snapshot in managed objects, how? (and where?) 10 | 11 | - validation is builtin, can drop the own 12 | 13 | - how to determine predicate complexity 14 | 15 | - what about RulePredicate's, those cannot be used as qualifiers. We would 16 | need to wrap them. 17 | 18 | - dropped `RuleClosurePredicate` 19 | 20 | - AttributeValue things 21 | 22 | - CD doesn't have (or need) primary/foreign keys! 23 | - the whole JoinTargetID story is superfluous 24 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/ViewModel/D2SFault.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SFault.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import protocol Swift.Identifiable 9 | 10 | public protocol D2SFaultResolver: AnyObject { 11 | 12 | func resolveFaultWithID(_ id: NSManagedObjectID) 13 | } 14 | 15 | public enum D2SFault 16 | where Resolver: D2SFaultResolver 17 | { 18 | // In the CoreData context we still need faults. They represent objects 19 | // for which we don't even know the object id. 20 | // Note: Using AnyObject in the generic here breaks everything! 21 | 22 | /// Keep an object reference as unowned, to break cycles 23 | public struct Unowned { 24 | unowned let object: Object 25 | } 26 | 27 | init(_ id: NSManagedObjectID, _ resolver: Resolver) { 28 | self = .fault(id, Unowned(object: resolver)) 29 | } 30 | init(index: Int, resolver: Resolver) { 31 | self.init(IndexGlobalID.make(index), resolver) 32 | } 33 | 34 | case object(NSManagedObjectID, Object) 35 | case fault(NSManagedObjectID, Unowned) 36 | 37 | public func accessingFault() -> Bool { 38 | switch self { 39 | case .object: return false 40 | case .fault(let id, let resolver): 41 | resolver.object.resolveFaultWithID(id) 42 | return true 43 | } 44 | } 45 | 46 | public var isFault: Bool { 47 | switch self { 48 | case .object: return false 49 | case .fault: return true 50 | } 51 | } 52 | 53 | public var object : Object { 54 | switch self { 55 | case .object(_, let object): return object 56 | case .fault: 57 | fatalError("attempt to access fault as resolved object \(self)") 58 | } 59 | } 60 | 61 | /** 62 | * Returns nil if it is still a fault, but triggers a fetch. 63 | */ 64 | public subscript(dynamicMember keyPath: 65 | ReferenceWritableKeyPath) 66 | -> V? 67 | { 68 | guard !accessingFault() else { return nil } 69 | return object[keyPath: keyPath] 70 | } 71 | } 72 | 73 | extension D2SFault: Equatable { 74 | public static func == (lhs: D2SFault, rhs: D2SFault) -> Bool { 75 | switch ( lhs, rhs ) { 76 | case ( .fault(let lhs, _), .fault(let rhs, _) ): 77 | return lhs == rhs 78 | case ( .object(_, let lhs), .object(_, let rhs) ): 79 | // Yeah, because putting the AnyObject into the generic signature 80 | // mysteriously breaks everything. 81 | return (lhs as AnyObject) === (rhs as AnyObject) 82 | default: 83 | return false 84 | } 85 | } 86 | } 87 | extension D2SFault: Hashable { 88 | public func hash(into hasher: inout Hasher) { 89 | id.hash(into: &hasher) 90 | } 91 | } 92 | 93 | extension D2SFault: Identifiable { 94 | 95 | // TODO: This is still pending. It now works because we replace the index GIDs 96 | // w/ real GIDs. But as soon as we can fault a real GID, it _might_ 97 | // fail again. 98 | 99 | // I think this might not work because SwiftUI doesn't notice changes to the 100 | // fault state? Even though the enum _does_ change. 101 | public var id: NSManagedObjectID { 102 | switch self { 103 | case .object(let id, _): return id 104 | case .fault (let id, _): return id 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/ViewModel/D2SObjectAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SObjectAction.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | public enum D2STask : Hashable { 9 | // Note: We don't want to use this enum instead of the string in the 10 | // ruleContext, because it wouldn't fly well w/ KVC based predicates. 11 | 12 | case query 13 | case list 14 | case inspect 15 | case edit 16 | case select 17 | case login 18 | case error 19 | case custom(String) 20 | 21 | public init(_ string: S) { 22 | switch string { 23 | case "inspect" : self = .inspect 24 | case "edit" : self = .edit 25 | case "query" : self = .query 26 | case "list" : self = .list 27 | case "select" : self = .select 28 | case "login" : self = .login 29 | case "error" : self = .error 30 | default : self = .custom(String(string)) 31 | } 32 | } 33 | 34 | public var stringValue: String { 35 | switch self { 36 | case .inspect : return "inspect" 37 | case .edit : return "edit" 38 | case .query : return "query" 39 | case .list : return "list" 40 | case .select : return "select" 41 | case .login : return "login" 42 | case .error : return "error" 43 | case .custom(let s) : return s 44 | } 45 | } 46 | } 47 | extension D2STask : ExpressibleByStringLiteral { 48 | public init(stringLiteral value: String) { 49 | self.init(value) 50 | } 51 | } 52 | 53 | // Like a D2STask, but has the additional `task` and `nextTask` cases for 54 | // context sensitive actions. 55 | public enum D2SObjectAction : Hashable { 56 | // FIXME: better name, this is not really an `action`? 57 | 58 | case task 59 | case nextTask 60 | 61 | case query 62 | case list 63 | case inspect 64 | case edit 65 | case select 66 | case login 67 | case error 68 | 69 | case custom(String) 70 | 71 | func action(task: String, nextTask: String) -> String { 72 | switch self { 73 | case .task : return task 74 | case .nextTask : return nextTask 75 | 76 | case .inspect : return "inspect" 77 | case .edit : return "edit" 78 | case .query : return "query" 79 | case .list : return "list" 80 | case .select : return "select" 81 | case .login : return "login" 82 | case .error : return "error" 83 | 84 | case .custom(let s) : return s 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/ViewModel/D2SRuleEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SRuleEnvironment.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import class SwiftUIRules.RuleModel 9 | import struct SwiftUIRules.RuleContext 10 | import SwiftUI 11 | import Combine 12 | import CoreData 13 | 14 | /** 15 | * Used to fetch the model from the database, if necessary. 16 | */ 17 | public final class D2SRuleEnvironment: ObservableObject { 18 | 19 | public var isReady : Bool { databaseModel != nil } 20 | public var hasError : Bool { error != nil } 21 | 22 | @Published public var databaseModel : NSManagedObjectModel? 23 | @Published public var error : Swift.Error? 24 | @Published public var ruleContext : RuleContext 25 | 26 | public let managedObjectContext : NSManagedObjectContext 27 | public let ruleModel : RuleModel 28 | 29 | public init(managedObjectContext : NSManagedObjectContext, 30 | ruleModel : RuleModel) 31 | { 32 | self.managedObjectContext = managedObjectContext 33 | self.databaseModel = 34 | managedObjectContext.persistentStoreCoordinator?.managedObjectModel 35 | self.ruleModel = ruleModel 36 | 37 | ruleContext = RuleContext(ruleModel: ruleModel) 38 | ruleContext[D2SKeys.ruleObjectContext] = managedObjectContext 39 | 40 | if let model = self.databaseModel { 41 | ruleContext[D2SKeys.model] = model 42 | } 43 | } 44 | 45 | public func resume() {} 46 | } 47 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/ViewModel/D2SToOneFetch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SToOneFetch.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import Combine 9 | import SwiftUI 10 | 11 | /** 12 | * This is used to fetch the toOne relship of an object. 13 | */ 14 | public final class D2SToOneFetch: ObservableObject { 15 | 16 | @Published var destination : NSManagedObject? 17 | 18 | let object : NSManagedObject 19 | let propertyKey : String 20 | 21 | var isReady : Bool { destination != nil } 22 | 23 | public init(object: NSManagedObject, propertyKey: String) { 24 | self.object = object 25 | self.propertyKey = propertyKey 26 | self.destination = object.value(forKeyPath: propertyKey) as? NSManagedObject 27 | } 28 | 29 | func resume() { 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/BasicLook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicLook.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | /** 9 | * A basic theme for Direct to SwiftUI. Uses a lot of Lists to show sets of 10 | * attributes. 11 | */ 12 | public enum BasicLook { 13 | 14 | public enum PageWrapper {} 15 | public enum Page { 16 | public enum AppKit {} 17 | public enum UIKit {} 18 | } 19 | public enum Row {} 20 | public enum Property { 21 | public enum Display {} 22 | public enum Edit {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/PageWrapper/EntityMasterDetailPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SEntityMasterDetailPage.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.PageWrapper.MasterDetail { 11 | 12 | /** 13 | * NavigationView really works badly on macOS. This is kinda like a replacement 14 | * but lacks the split view ... 15 | * 16 | * Note: This is only intended for macOS. 17 | */ 18 | struct EntityMasterDetailPage: View { 19 | 20 | @Environment(\.model) private var model 21 | @State private var selectedEntityName : String? 22 | 23 | private var selectedEntity: NSEntityDescription? { 24 | guard let entityName = selectedEntityName else { return nil } 25 | guard let entity = model[entity: entityName] else { 26 | #if os(macOS) 27 | globalD2SLogger.error("did not find entity:", entityName, 28 | "in:", model) 29 | return nil 30 | #else 31 | fatalError("did not find entity: \(entityName) in \(model)") 32 | #endif 33 | } 34 | return entity 35 | } 36 | 37 | struct EntityContent: View { 38 | 39 | @Environment(\.entity) private var entity 40 | 41 | var body: some View { 42 | D2SPageView() 43 | .environment(\.entity, entity) 44 | .task("list") 45 | } 46 | } 47 | 48 | struct EmptyContent: View { 49 | // FIXME: Show something useful as the default, maybe a query page 50 | var body: some View { 51 | Text("Select an Entity") 52 | .frame(maxWidth: .infinity, maxHeight: .infinity) 53 | } 54 | } 55 | 56 | private var backgroundColor: Color? { 57 | #if os(macOS) 58 | return Color(NSColor.textBackgroundColor) 59 | #else 60 | return nil 61 | #endif 62 | } 63 | 64 | // FIXME: show navbar title? 65 | // TODO: Async login page 66 | public var body: some View { 67 | HStack(spacing: 0 as CGFloat) { 68 | //HSplitView { // this works, but we get a window title bar again 69 | 70 | Sidebar(selectedEntityName: $selectedEntityName) 71 | .task(.query) 72 | .frame(minWidth: 120 as CGFloat, maxWidth: 200 as CGFloat) 73 | 74 | Group { 75 | if selectedEntityName == nil { 76 | EmptyContent() 77 | } 78 | else { 79 | EntityContent() 80 | .environment(\.entity, 81 | selectedEntity ?? D2SKeys.entity.defaultValue) 82 | } 83 | } 84 | .frame(minWidth: 400 as CGFloat, idealWidth: 600 as CGFloat, 85 | maxWidth: .infinity) 86 | .background(self.backgroundColor) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/PageWrapper/EntitySidebar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SEntityListSidebar.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.PageWrapper.MasterDetail.EntityMasterDetailPage { 11 | 12 | /** 13 | * A sidebar listing the entities for the D2SEntityMasterDetailPage. 14 | * 15 | * Primarily intended for macOS. 16 | */ 17 | struct Sidebar: View { 18 | 19 | @Binding public var selectedEntityName : String? 20 | public let autoselect = true 21 | 22 | @Environment(\.model) private var model 23 | @Environment(\.visibleEntityNames) private var names 24 | 25 | struct EntityName: View { 26 | @Environment(\.displayNameForEntity) private var title 27 | var body: some View { Text(title) } 28 | } 29 | 30 | private func colorForEntityName(_ name: String) -> Color { 31 | /* looks wrong on macOS: 32 | .background(self.selectedEntityName == name 33 | ? Color(NSColor.selectedContentBackgroundColor) : nil) 34 | */ 35 | // hence our non-standard highlighting 36 | #if os(macOS) 37 | return selectedEntityName == name 38 | ? Color(NSColor.selectedTextColor) 39 | : Color(NSColor.secondaryLabelColor) 40 | #elseif os(iOS) 41 | return selectedEntityName == name 42 | ? Color(UIColor.label) 43 | : Color(UIColor.secondaryLabel) 44 | #else 45 | return selectedEntityName == name 46 | ? Color.black 47 | : Color.gray 48 | #endif 49 | } 50 | 51 | public var body: some View { 52 | Group { 53 | #if false 54 | // This is now almost right. but the 1st item does not select in b7 55 | List(names, id: \String.self, selection: $selectedEntityName) { name in 56 | Group { 57 | EntityName() 58 | .tag(name) 59 | } 60 | .environment(\.entity, { self.model[entity: name]! }()) 61 | } 62 | .listStyle(SidebarListStyle()) // requires Section 63 | #elseif os(macOS) 64 | List { // works but wrong color: (selection: $selectedEntityName) { 65 | Section(header: Text("Entities")) { // this is required for sidebar! 66 | ForEach(names, id: \String.self) { name in 67 | HStack { 68 | EntityName() 69 | Spacer() 70 | } 71 | .environment(\.entity, { 72 | self.model[entity: name] ?? D2SKeys.entity.defaultValue 73 | }()) 74 | .frame(maxWidth: .infinity, maxHeight: .infinity) 75 | .foregroundColor(self.colorForEntityName(name)) 76 | .onTapGesture { 77 | self.selectedEntityName = name 78 | } 79 | .tag(name) 80 | } 81 | } 82 | .collapsible(false) 83 | } 84 | .listStyle(SidebarListStyle()) // requires Section 85 | #elseif os(macOS) 86 | List { 87 | ForEach(names, id: \String.self) { name in 88 | HStack { 89 | EntityName() 90 | Spacer() 91 | } 92 | .environment(\.entity, { self.model[entity: name]! }()) 93 | .frame(maxWidth: .infinity, maxHeight: .infinity) 94 | .foregroundColor(self.colorForEntityName(name)) 95 | .onTapGesture { 96 | self.selectedEntityName = name 97 | } 98 | .tag(name) 99 | } 100 | } 101 | .listStyle(SidebarListStyle()) // requires Section 102 | #elseif os(macOS) 103 | List { // (selection: $selection) { 104 | Section(header: Text("Entities")) { // this is required for sidebar! 105 | ForEach(names, id: \String.self) { name in 106 | EntityName() 107 | .tag(name) 108 | .onTapGesture { 109 | self.selectedEntityName = name 110 | } 111 | } 112 | } 113 | .collapsible(false) 114 | } 115 | // macOS only: 116 | // .listStyle(SidebarListStyle()) // requires Section 117 | #else // currently use for iOS etc 118 | List { // (selection: $selection) { 119 | Section(header: Text("Entities")) { // this is required for sidebar! 120 | ForEach(names, id: \String.self) { name in 121 | EntityName() 122 | .tag(name) 123 | .onTapGesture { 124 | self.selectedEntityName = name 125 | } 126 | } 127 | } 128 | } 129 | // macOS only: 130 | // .listStyle(SidebarListStyle()) // requires Section 131 | #endif 132 | } 133 | .onAppear { 134 | if self.autoselect && self.selectedEntityName == nil { 135 | self.selectedEntityName = self.names.first 136 | } 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/PageWrapper/MasterDetail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SMasterDetailPageWrapper.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.PageWrapper { 11 | /** 12 | * NavigationView really works badly on macOS. This is kinda like a 13 | * replacement but lacks the split view ... 14 | * 15 | * Note: This is only really intended for macOS to workaround navview issues. 16 | */ 17 | struct MasterDetail: View { 18 | 19 | @Environment(\.ruleContext) private var ruleContext 20 | 21 | #if os(macOS) // use an own, custom component 22 | public var body: some View { 23 | EntityMasterDetailPage() 24 | .ruleContext(ruleContext) 25 | } 26 | #elseif os(watchOS) // TODO: master detail for watchOS? 27 | public var body: some View { 28 | D2SPageView() 29 | .ruleContext(ruleContext) 30 | } 31 | #else 32 | public var body: some View { 33 | NavigationView { 34 | D2SPageView() 35 | .ruleContext(ruleContext) 36 | 37 | // FIXME: Show something useful as the default, maybe a query page 38 | Text("Select an Entity ") 39 | .frame(maxWidth: .infinity, maxHeight: .infinity) 40 | } 41 | .ruleContext(ruleContext) 42 | } 43 | #endif 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/PageWrapper/NavigationPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SMobilePageWrapper.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.PageWrapper { 11 | 12 | /** 13 | * Wraps the page in a navigation view. 14 | */ 15 | struct Navigation: View { 16 | 17 | @EnvironmentObject var ruleEnvironment : D2SRuleEnvironment 18 | 19 | @available(OSX, unavailable) 20 | struct RootTitledPageView: View { 21 | 22 | #if os(macOS) // no .navigationBarTitle on macOS 23 | var body: some View { D2SPageView() } 24 | #else 25 | @Environment(\.navigationBarTitle) private var title 26 | var body: some View { 27 | D2SPageView() 28 | .navigationBarTitle(title) // explict override for 1st page 29 | } 30 | #endif 31 | } 32 | 33 | #if os(macOS) // no .navigationBarTitle on macOS 34 | public var body: some View { 35 | NavigationView { 36 | D2SPageView() 37 | } 38 | } 39 | #elseif os(watchOS) 40 | public var body: some View { // no NavigationView on watchOS 41 | RootTitledPageView() 42 | } 43 | #else // iOS, watchOS 44 | public var body: some View { 45 | NavigationView { 46 | RootTitledPageView() 47 | } 48 | } 49 | #endif 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Pages/AppKit/WindowQueryList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowQueryList.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Page.AppKit { 11 | 12 | #if !os(macOS) 13 | static func WindowQueryList() -> some View { 14 | Text("\(#function) is not available on this platform") 15 | } 16 | #else 17 | 18 | /** 19 | * Shows a page containing the contents of an entity. 20 | * 21 | * Backed by a D2SDisplayGroup. 22 | * 23 | * This variant is opening windows for actions. Intended for macOS. 24 | */ 25 | struct WindowQueryList: View { 26 | 27 | @Environment(\.ruleObjectContext) private var moc 28 | @Environment(\.entity) private var entity 29 | @Environment(\.auxiliaryPredicate) private var auxiliaryPredicate 30 | 31 | public init() {} 32 | 33 | private func makeDataSource() -> ManagedObjectDataSource { 34 | return moc.dataSource(for: entity) 35 | } 36 | 37 | public var body: some View { 38 | Bound(dataSource: makeDataSource(), 39 | auxiliaryPredicate: auxiliaryPredicate) 40 | .environment(\.auxiliaryPredicate, nil) // reset! 41 | } 42 | 43 | struct Bound: View { 44 | 45 | @Environment(\.ruleContext) private var context 46 | @State private var showSortSelector = false 47 | @Environment(\.isEntityReadOnly) private var isReadOnly 48 | @Environment(\.title) private var title 49 | @Environment(\.nextTask) private var nextTask 50 | 51 | // This seems to crash on macOS b7 52 | @ObservedObject private var displayGroup : D2SDisplayGroup 53 | 54 | private var entity: NSEntityDescription { displayGroup.dataSource.entity } 55 | 56 | init(dataSource: ManagedObjectDataSource, 57 | auxiliaryPredicate: NSPredicate?) 58 | { 59 | self.displayGroup = D2SDisplayGroup( 60 | dataSource: dataSource, 61 | auxiliaryPredicate: auxiliaryPredicate 62 | ) 63 | } 64 | 65 | func handleDoubleTap(on object: NSManagedObject?) { 66 | guard let object = object else { return } // still a fault 67 | 68 | let view = D2SPageView() 69 | .task(nextTask) 70 | .ruleObject(object) 71 | .ruleContext(context) 72 | 73 | let wc = D2SInspectWindow(rootView: view) 74 | wc.window?.title = title 75 | wc.window?.setFrameAutosaveName("Inspect:\(title)") 76 | wc.showWindow(nil) 77 | } 78 | 79 | var body: some View { 80 | VStack { 81 | SearchField(search: $displayGroup.queryString) 82 | .background(Color(NSColor.windowBackgroundColor)) 83 | 84 | List(displayGroup.results) { fault in 85 | Group { 86 | if fault.accessingFault() { D2SRowFault() } 87 | else { 88 | HStack { 89 | D2STitledSummaryView() // TODO: select via rule! 90 | .frame(maxWidth: .infinity) 91 | .ruleObject(fault.object) 92 | .ruleContext(self.context) // req on macOS 93 | } 94 | } 95 | } 96 | .onTapGesture(count: 2) { // this looses the D2SCtx! 97 | // If we put this too far inside, it doesn't detect clicks 98 | // on the title text. 99 | // In b6 it still doesn't click on empty sections of the view. 100 | self.handleDoubleTap(on: fault.isFault ? nil : fault.object) 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | #endif // macOS 108 | } 109 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Pages/Edit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Edit.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Page { 11 | 12 | /** 13 | * A view for editing the properties of an object. 14 | * 15 | * Iterates over the `displayPropertyKeys` and shows the respective property 16 | * edit views. 17 | */ 18 | struct Edit: View { 19 | 20 | struct Content: View { 21 | 22 | @Environment(\.debugComponent) private var debugComponent 23 | @EnvironmentObject private var object : NSManagedObject 24 | 25 | #if os(iOS) // do not use Form on iOS 26 | public var body: some View { 27 | VStack { 28 | D2SDisplayPropertiesList() 29 | debugComponent 30 | } 31 | .task(.edit) 32 | } 33 | #else 34 | public var body: some View { 35 | VStack { 36 | Form { 37 | D2SDisplayProperties() // form scrolls etc 38 | } 39 | debugComponent 40 | } 41 | .task(.edit) 42 | } 43 | #endif 44 | } 45 | 46 | #if os(iOS) 47 | struct ContentWithNavigationBar: View { 48 | 49 | @EnvironmentObject private var object : NSManagedObject 50 | 51 | @Environment(\.presentationMode) private var presentationMode 52 | @Environment(\.ruleObjectContext) private var moc 53 | @Environment(\.updateTimestampPropertyKey) private var updateTS 54 | 55 | @State private var lastError : Swift.Error? 56 | @State private var isShowingError = false 57 | 58 | private var errorMessage: String { 59 | guard let error = lastError else { return "No Error" } 60 | return String(describing: error) 61 | } 62 | 63 | private var hasChanges: Bool { 64 | object.hasChanges // TODO: probably need to subscribe to object for this! 65 | } 66 | 67 | func goBack() { 68 | // that feels dirty 69 | // programmatically POP the edit page 70 | // I think a NavLink is necessary here (plus a binding to pass along) 71 | presentationMode.wrappedValue.dismiss() 72 | } 73 | 74 | private func save() { 75 | guard hasChanges else { return goBack() } 76 | 77 | do { 78 | if let pkey = updateTS { 79 | object.setValue(Date(), forKey: pkey) 80 | } 81 | try moc.save() 82 | goBack() 83 | } 84 | catch { 85 | globalD2SLogger.error("failed to save object:", error) 86 | lastError = error 87 | isShowingError = true 88 | } 89 | } 90 | private func discard() { 91 | // TBD: is this the right way? 92 | moc.refresh(object, mergeChanges: false) 93 | } 94 | 95 | private func errorAlert() -> Alert { 96 | // TODO: Improve on the error message 97 | if object.isNew { 98 | return Alert(title: Text("Create Failed"), 99 | message: Text(errorMessage), 100 | dismissButton: .default(Text("Retry"), 101 | action: self.save) 102 | ) 103 | } 104 | else { 105 | return Alert(title: Text("Save Failed"), 106 | message: Text(errorMessage), 107 | primaryButton: .destructive(Text("Discard"), 108 | action: self.discard), 109 | secondaryButton: .default(Text("Retry"), 110 | action: self.save) 111 | ) 112 | } 113 | } 114 | 115 | var body: some View { 116 | Content() 117 | .navigationBarItems(trailing: Group { 118 | HStack { 119 | Button(action: self.discard) { 120 | Image(systemName: "trash.circle") 121 | } 122 | Button(action: self.save) { // text looks better 123 | Text(object.isNew ? "Create" : "Save") 124 | } 125 | } 126 | .disabled(!hasChanges) 127 | }) 128 | .alert(isPresented: $isShowingError, content: errorAlert) 129 | } 130 | } 131 | 132 | public var body: some View { 133 | ContentWithNavigationBar() 134 | } 135 | #else 136 | public var body: some View { 137 | Content() 138 | } 139 | #endif 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Pages/EntityList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityList.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Page { 11 | 12 | /** 13 | * Shows a list of entities w/ a navigation link. 14 | */ 15 | struct EntityList: View { 16 | 17 | @Environment(\.model) private var model 18 | @Environment(\.visibleEntityNames) private var names 19 | 20 | struct EntityName: View { 21 | @Environment(\.displayNameForEntity) private var title 22 | var body: some View { Text(title) } 23 | } 24 | 25 | struct Cell: View { 26 | 27 | @Environment(\.nextTask) private var nextTask 28 | 29 | let name : String 30 | var body : some View { 31 | D2SNavigationLink(destination: D2SEntityPageView(entityName: name) 32 | .task(nextTask)) 33 | { 34 | EntityName() 35 | } 36 | } 37 | } 38 | 39 | #if os(macOS) // macOS needs the section header in the sidebar style 40 | #if true 41 | public var body: some View { 42 | List { // (selection: $selection) { 43 | Section(header: Text("Entities")) { // header is req for sidebar b6! 44 | ForEach(names, id: \String.self) { name in 45 | Cell(name: name) 46 | .environment(\.entity, self.entity(for: name)) 47 | } 48 | } 49 | .collapsible(false) 50 | } 51 | .listStyle(SidebarListStyle()) // requires Section 52 | } 53 | #else // this kinda tracks a selection, but still doesn't work 54 | @State var selection: String? 55 | 56 | public var body: some View { 57 | List(names, id: \String.self, selection: $selection) { name in 58 | Section { 59 | Cell(name: name) 60 | } 61 | .environment(\.entity, self.entity(for: name)) 62 | } 63 | } 64 | #endif 65 | #else // not macOS 66 | @Environment(\.debugComponent) private var debugComponent 67 | 68 | public var body: some View { 69 | VStack { 70 | List(names, id: \String.self) { name in 71 | Cell(name: name) 72 | .environment(\.entity, self.entity(for: name)) 73 | } 74 | debugComponent 75 | } 76 | } 77 | #endif 78 | 79 | private func entity(for name: String) -> NSEntityDescription { 80 | guard let entity = self.model[entity: name] else { 81 | #if false 82 | globalD2SLogger.warn("did not find entity:", name, "\n in:", model) 83 | #endif 84 | return D2SKeys.entity.defaultValue 85 | } 86 | return entity 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Pages/Inspect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Inspect.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Page { 11 | /** 12 | * A readonly view showing the properties of an object. 13 | * 14 | * Iterates over the `displayPropertyKeys` and shows the respective views. 15 | */ 16 | struct Inspect: View { 17 | 18 | @Environment(\.debugComponent) private var debugComponent 19 | 20 | public init() {} 21 | 22 | // TBD: this should also do the prefetching of relationships? 23 | 24 | #if os(iOS) 25 | struct NavbarItems: View { 26 | 27 | @Environment(\.isObjectEditable) private var isEditable 28 | // TODO: add keys to make this per object 29 | 30 | var body: some View { 31 | Group { 32 | if isEditable { 33 | D2SNavigationLink(destination: D2SPageView().task(.edit)) { 34 | Text("Edit") 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | public var body: some View { 42 | VStack { 43 | D2SDisplayPropertiesList() 44 | debugComponent 45 | } 46 | .navigationBarItems(trailing: NavbarItems()) 47 | } 48 | #else // TBD: how on others? 49 | public var body: some View { 50 | VStack { 51 | D2SDisplayPropertiesList() 52 | debugComponent 53 | } 54 | } 55 | #endif 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Pages/Login.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Login.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | import CoreData 10 | 11 | public extension BasicLook.Page { 12 | 13 | /** 14 | * Shows a simple login page. 15 | * 16 | * D2S tries to find the table which contains the user information by matching 17 | * the attributes of the entities in the model. 18 | * If you don't want that, just add a rule as usual: 19 | * 20 | * \.task == "login" => \.entity <= "UserDatabase" 21 | * 22 | * Note: By default the `firstTask` is not login, rather the system jumps 23 | * directly into the DB. 24 | * To enable the login page, add this to the rule model: 25 | * 26 | * \.firstTask <= "login" 27 | * 28 | * And to configure the next page, as usual: 29 | * 30 | * \.task == "login" => \.nextTask <= "query" 31 | * 32 | * Or whatever you like (defaults to "query"). 33 | */ 34 | struct Login: View { 35 | // TODO: Add keychain etc etc 36 | 37 | @Environment(\.ruleObjectContext) private var moc 38 | @Environment(\.entity) private var entity 39 | @Environment(\.attribute) private var attribute 40 | @Environment(\.nextTask) private var nextTask 41 | 42 | @State var username : String = "" 43 | @State var password : String = "" 44 | @State var loginUser : NSManagedObject? = nil 45 | 46 | private var hasValidInput: Bool { 47 | return !username.isEmpty 48 | } 49 | 50 | private func loginAttributes() 51 | -> ( NSAttributeDescription, NSAttributeDescription )? 52 | { 53 | // We allow the user to specify the login property, but not the 54 | // password yet. 55 | let authProps = entity.lookupUserDatabaseProperties() 56 | if !attribute.d2s.isDefault { 57 | guard let pwd = authProps?.password 58 | ?? entity[attribute: "password"] else { 59 | return nil 60 | } 61 | return ( attribute, pwd ) 62 | } 63 | return authProps 64 | } 65 | 66 | private func login() { 67 | // FIXME: Make async, add spinner, all the good stuff ;-) 68 | let pwd = password 69 | defer { password = "" } 70 | 71 | loginUser = nil 72 | 73 | guard let ( la, pa ) = loginAttributes() else { 74 | globalD2SLogger.error("cannot login using entity:", entity) 75 | return 76 | } 77 | 78 | // TBD: This TextField on iOS always produces capitalized strings, which 79 | // is often wrong, so lets also compare to the lowercase variant. 80 | let userNamePredicate = la.eq(username).or(la.eq(username.lowercased())) 81 | 82 | // For password we just go brute force. Managed to resist the urge to 83 | // also check for plain. More options might make sense. 84 | let pwdPredicate = pa.eq(password.md5()).or(pa.eq(password.sha1())) 85 | 86 | let ds = ManagedObjectDataSource( 87 | managedObjectContext: moc, entity: entity) 88 | ds.fetchRequest = NSFetchRequest(entity: entity) 89 | .where(userNamePredicate.and(pwdPredicate)) 90 | 91 | if let user = try? ds.find() { 92 | loginUser = user 93 | } 94 | else { 95 | globalD2SLogger.error("did not find user or pwd") 96 | } 97 | } 98 | 99 | public var body: some View { 100 | Group { 101 | if loginUser == nil { 102 | // Designers welcome. 103 | VStack { 104 | VStack { 105 | #if os(watchOS) // no RoundedBorderTextFieldStyle 106 | TextField ("Username", text: $username) 107 | SecureField("Password", text: $password) 108 | #else 109 | TextField ("Username", text: $username) 110 | .textFieldStyle(RoundedBorderTextFieldStyle()) 111 | SecureField("Password", text: $password) 112 | .textFieldStyle(RoundedBorderTextFieldStyle()) 113 | #endif 114 | Button(action: self.login) { 115 | Text("Login") 116 | } 117 | .disabled(!hasValidInput) 118 | } 119 | .padding() 120 | .padding() 121 | .background(RoundedRectangle(cornerRadius: 16) 122 | .stroke() 123 | .foregroundColor(.secondary)) 124 | .padding() 125 | .frame(maxWidth: 320) 126 | Spacer() 127 | } 128 | } 129 | else { 130 | D2SPageView() 131 | .task(nextTask) 132 | .environment(\.user, loginUser!) 133 | // TODO: clear entity 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Pages/QueryList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryList.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Page { 11 | 12 | /** 13 | * Those functions select a proper query list view for the current platform. 14 | * It is useful if you want to embed a D2SQueryListPage page in your own 15 | * View which binds to the `query` task itself, so we can't find it using the 16 | * rule system. 17 | */ 18 | #if os(macOS) 19 | static func QueryList() -> some View { AppKit.WindowQueryList() } 20 | #elseif os(iOS) 21 | static func QueryList() -> some View { UIKit.QueryList() } 22 | #elseif os(watchOS) 23 | static func QueryList() -> some View { SmallQueryList() } 24 | #endif 25 | } 26 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Pages/Select.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Select.swift 3 | // DirectToSwitUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Page { 11 | 12 | /** 13 | * Those functions select a proper query list view for the current platform. 14 | * It is useful if you want to embed a D2SQueryListPage page in your own 15 | * View which binds to the `query` task itself, so we can't find it using the 16 | * rule system. 17 | */ 18 | #if os(macOS) 19 | static func Select() -> some View { return Text("TODO") } 20 | #elseif os(iOS) 21 | static func Select() -> some View { return UIKit.Select() } 22 | #elseif os(watchOS) 23 | static func Select() -> some View { return Text("TODO") } 24 | #endif 25 | } 26 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Pages/SmallQueryList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SmallQueryList.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Page { 11 | /** 12 | * Shows a page containing the contents of an entity. 13 | * 14 | * Backed by a D2SDisplayGroup. 15 | * 16 | * This simple variant is intended for watchOS. 17 | */ 18 | struct SmallQueryList: View { 19 | 20 | @Environment(\.ruleObjectContext) private var moc 21 | @Environment(\.entity) private var entity 22 | @Environment(\.auxiliaryPredicate) private var auxiliaryPredicate 23 | 24 | public init() {} 25 | 26 | private func makeDataSource() -> ManagedObjectDataSource { 27 | moc.dataSource(for: entity) 28 | } 29 | 30 | public var body: some View { 31 | Bound(dataSource: makeDataSource(), 32 | auxiliaryPredicate: auxiliaryPredicate) 33 | .environment(\.auxiliaryPredicate, nil) // reset! 34 | } 35 | 36 | struct Bound: View { 37 | 38 | // This seems to crash on macOS b7 39 | @ObservedObject private var displayGroup : D2SDisplayGroup 40 | 41 | init(dataSource : ManagedObjectDataSource, 42 | auxiliaryPredicate : NSPredicate?) 43 | { 44 | self.displayGroup = D2SDisplayGroup( 45 | dataSource : dataSource, 46 | auxiliaryPredicate : auxiliaryPredicate 47 | ) 48 | } 49 | 50 | var body: some View { 51 | VStack { 52 | List(displayGroup.results) { fault in 53 | D2SFaultObjectLink(fault: fault) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Display/DisplayBool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDisplayPropertyViews.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Property.Display { 11 | 12 | /** 13 | * Displays the value of a bool property. 14 | */ 15 | struct Bool: View { 16 | 17 | typealias String = Swift.String 18 | typealias Bool = Swift.Bool 19 | 20 | public init() {} 21 | 22 | @EnvironmentObject var object : NSManagedObject 23 | 24 | @Environment(\.propertyValue) private var propertyValue 25 | 26 | private var boolValue : Bool { 27 | return UObject.boolValue(propertyValue, default: false) 28 | } 29 | private var stringValue: String { 30 | return boolValue ? "✓" : "⨯" 31 | } 32 | 33 | public var body: some View { 34 | D2SDebugLabel("[DB]") { 35 | Text(stringValue) 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Display/DisplayDate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateProperty.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | import struct Foundation.Date 10 | 11 | public extension BasicLook.Property.Display { 12 | 13 | struct Date: View { 14 | 15 | typealias String = Swift.String 16 | typealias Date = Foundation.Date 17 | 18 | public init() {} 19 | 20 | @EnvironmentObject var object : NSManagedObject 21 | 22 | @Environment(\.attribute) private var attribute 23 | @Environment(\.propertyValue) private var propertyValue 24 | @Environment(\.displayStringForNil) private var stringForNil 25 | 26 | private var stringValue: String { 27 | guard let v = propertyValue else { return stringForNil } 28 | 29 | if let date = v as? Date { 30 | return attribute.dateFormatter().string(from: date) 31 | } 32 | 33 | if let s = v as? String { return s } 34 | 35 | return String(describing: v) 36 | } 37 | 38 | public var body: some View { 39 | D2SDebugLabel("[DD]") { Text(stringValue) } 40 | } 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Display/DisplayEmail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDisplayEmail.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if canImport(AppKit) 11 | import class AppKit.NSWorkspace 12 | #elseif canImport(UIKit) && !os(watchOS) 13 | import class UIKit.UIApplication 14 | #endif 15 | 16 | public extension BasicLook.Property.Display { 17 | 18 | /** 19 | * Shows the email as a tappable text (where supported). 20 | * 21 | * To make it work on iOS, you need to add `mailto` to the 22 | * `LSApplicationQueriesSchemes` array property in the Info.plist. 23 | * Note: Doesn't work in the simulator. 24 | */ 25 | struct Email: View { 26 | 27 | typealias String = Swift.String 28 | 29 | public init() {} 30 | 31 | @EnvironmentObject var object : NSManagedObject 32 | 33 | @Environment(\.propertyKey) private var propertyKey 34 | @Environment(\.propertyValue) private var propertyValue 35 | @Environment(\.displayStringForNil) private var stringForNil 36 | @Environment(\.debug) private var debug 37 | 38 | private var stringValue: String { 39 | guard let v = propertyValue else { return stringForNil } 40 | return object.coerceValueToString(v, formatter: nil, forKey: propertyKey) 41 | } 42 | 43 | #if os(watchOS) 44 | public var body: some View { 45 | D2SDebugLabel("[DM]") { 46 | Text(stringValue) 47 | } 48 | } 49 | #else // not watchOS 50 | private var url: URL? { 51 | // Needs `LSApplicationQueriesSchemes` 52 | guard let v = propertyValue else { return nil } 53 | let s = object.coerceValueToString(v, formatter: nil, forKey: propertyKey) 54 | guard !s.isEmpty else { return nil } 55 | return URL(string: "mailto:" + s) 56 | } 57 | 58 | private func openMail() { 59 | guard let url = url else { return } 60 | 61 | #if os(macOS) 62 | NSWorkspace.shared.open(url) 63 | #elseif os(iOS) 64 | // For this to work, the Info.plist must have "mailto" 65 | // in the `LSApplicationQueriesSchemes` property. 66 | if UIApplication.shared.canOpenURL(url) { 67 | UIApplication.shared.open(url) 68 | } 69 | #endif 70 | } 71 | 72 | public var body: some View { 73 | D2SDebugLabel("[DM]") { 74 | Text(stringValue) 75 | .onTapGesture(perform: self.openMail) 76 | } 77 | } 78 | #endif // not watchOS 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Display/DisplayPassword.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDisplayPassword.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Property.Display { 11 | 12 | /** 13 | * This just shows a lock or `-` if the password is empty. 14 | * 15 | * Guess you wanted to see the actualy password, didn't you? Feel free to 16 | * provided your own View which can reverse stored hashes using some 17 | * haxor databases. 18 | */ 19 | struct Password: View { 20 | 21 | typealias String = Swift.String 22 | typealias Bool = Swift.Bool 23 | 24 | public init() {} 25 | 26 | @EnvironmentObject var object : NSManagedObject 27 | 28 | @Environment(\.propertyKey) private var propertyKey 29 | @Environment(\.propertyValue) private var propertyValue 30 | @Environment(\.displayStringForNil) private var stringForNil 31 | 32 | private var hasPassword: Bool { 33 | guard let v = propertyValue else { return false } 34 | if let s = v as? String { return !s.isEmpty } 35 | globalD2SLogger.warn("unexpected type for password field:", 36 | type(of: v)) 37 | return !String(describing: v).isEmpty 38 | } 39 | 40 | public var body: some View { 41 | if hasPassword { return Text(verbatim: "🔐") } 42 | else { return Text(verbatim: stringForNil)} 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Display/DisplayString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringProperty.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Property.Display { 11 | 12 | struct String: View { 13 | 14 | typealias String = Swift.String 15 | 16 | public init() {} 17 | 18 | @EnvironmentObject private var object : NSManagedObject 19 | 20 | @Environment(\.propertyKey) private var propertyKey 21 | @Environment(\.propertyValue) private var propertyValue 22 | @Environment(\.formatter) private var formatter 23 | @Environment(\.displayStringForNil) private var stringForNil 24 | @Environment(\.debug) private var debug 25 | 26 | private var stringValue: String { 27 | guard let v = propertyValue else { return stringForNil } 28 | return object.coerceValueToString(v, formatter: formatter, 29 | forKey: propertyKey) 30 | } 31 | 32 | public var body: some View { 33 | D2SDebugLabel("[DS]") { Text(stringValue) } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Edit/EditBool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SEditBool.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Property.Edit { 11 | 12 | struct Bool: View, D2SAttributeValidator { 13 | 14 | typealias String = Swift.String 15 | typealias Bool = Swift.Bool 16 | 17 | public init() {} 18 | 19 | @EnvironmentObject public var object : NSManagedObject 20 | 21 | @Environment(\.propertyKey) private var propertyKey 22 | @Environment(\.propertyValue) private var propertyValue 23 | @Environment(\.attribute) public var attribute 24 | 25 | private var boolValue : Bool { 26 | return UObject.boolValue(propertyValue, default: false) 27 | } 28 | private var stringValue: String { 29 | return boolValue ? "✓" : "⨯" 30 | } 31 | 32 | public var body: some View { 33 | // FIXME: use toggle 34 | D2SDebugLabel("[EB]") { 35 | HStack { 36 | D2SEditPropertyName(isValid: isValid) 37 | Spacer() 38 | Toggle(isOn: object.boolBinding(propertyKey)) { EmptyView() } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Edit/EditDate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SEditDate.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Property.Edit { 11 | 12 | struct Date: View, D2SAttributeValidator { 13 | // Note: When used inside a Form, the DatePicker (and presumably other 14 | // pickers on iOS) uses multiple List rows. 15 | // Aka the Form is a List itself, so we can't nest it in another. 16 | // (which is a little weird, it should just expand its own cell). 17 | 18 | typealias String = Swift.String 19 | typealias Bool = Swift.Bool 20 | typealias Date = Foundation.Date 21 | 22 | public init() {} 23 | 24 | @EnvironmentObject public var object : NSManagedObject 25 | 26 | @Environment(\.propertyKey) private var propertyKey 27 | @Environment(\.propertyValue) private var propertyValue 28 | @Environment(\.attribute) public var attribute 29 | @Environment(\.displayNameForProperty) private var label 30 | 31 | private var isOptional : Bool { attribute.isOptional } 32 | 33 | private var isNull : Bool { 34 | !(propertyValue is Date) 35 | } 36 | 37 | private func setNull(_ wantsValue: Bool) { 38 | object.setValue(wantsValue ? Date() : nil, forKeyPath: propertyKey) 39 | } 40 | 41 | #if os(iOS) // use non-Form DatePicker on iOS 42 | public var body: some View { 43 | D2SDebugLabel("[ED\(isOptional ? "?" : "")]") { 44 | if isOptional { 45 | ListEnabledDatePicker(label, 46 | selection: object.dateBinding(propertyKey)) 47 | Toggle(isOn: Binding(get: { self.isNull }, set: self.setNull)) { 48 | Text("") // TBD 49 | } 50 | } 51 | else { 52 | ListEnabledDatePicker(label, selection: object.dateBinding(propertyKey)) 53 | } 54 | } 55 | } 56 | #elseif os(watchOS) 57 | public var body: some View { 58 | D2SDebugLabel("[ED\(isOptional ? "?" : "")]") { 59 | Text("FIXME") // no DatePicker on watchOS b7 60 | } 61 | } 62 | #else // regular datepicker on others 63 | public var body: some View { 64 | D2SDebugLabel("[ED\(isOptional ? "?" : "")]") { 65 | if isOptional { 66 | DatePicker(label, selection: object.dateBinding(propertyKey)) 67 | Toggle(isOn: Binding(get: { self.isNull }, set: self.setNull)) { 68 | Text("") // TBD 69 | } 70 | } 71 | else { 72 | DatePicker(label, selection: object.dateBinding(propertyKey)) 73 | } 74 | } 75 | } 76 | #endif 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Edit/EditLargeString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SEditLargeString.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Property.Edit { 11 | 12 | /** 13 | * Edit long strings ... 14 | */ 15 | struct LargeString: View, D2SAttributeValidator { 16 | 17 | typealias String = Swift.String 18 | typealias Bool = Swift.Bool 19 | 20 | public init() {} 21 | 22 | @EnvironmentObject public var object : NSManagedObject 23 | 24 | @Environment(\.propertyKey) private var propertyKey 25 | @Environment(\.displayNameForProperty) private var label 26 | @Environment(\.attribute) public var attribute 27 | 28 | #if os(iOS) // use UIView, NSView is prepared, needs testing. 29 | public var body: some View { 30 | Group { 31 | D2SDebugLabel("[ELS]") { 32 | MultilineEditor(text: object.stringBinding(propertyKey)) 33 | .frame(height: (UIFont.systemFontSize * 1.2) * 3) 34 | } 35 | } 36 | } 37 | #else 38 | public var body: some View { 39 | Group { 40 | D2SDebugLabel("[ELS]") { 41 | TextField(label, text: object.stringBinding(propertyKey)) 42 | //.lineLimit(5) // Doesn't actually work 43 | .multilineTextAlignment(.leading) 44 | //.frame(minHeight: 100) // doesn't help, still wrapps inside 45 | } 46 | } 47 | } 48 | #endif 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Edit/EditNumber.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SEditNumber.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public extension BasicLook.Property.Edit { 12 | 13 | struct Number: View, D2SAttributeValidator { 14 | 15 | public init() {} 16 | 17 | @EnvironmentObject public var object : NSManagedObject 18 | 19 | @Environment(\.propertyKey) private var propertyKey 20 | @Environment(\.formatter) private var formatter 21 | @Environment(\.attribute) public var attribute 22 | 23 | // E.g. configure for Double etc 24 | private static let formatter : NumberFormatter = { 25 | let f = NumberFormatter() 26 | f.allowsFloats = false 27 | return f 28 | }() 29 | 30 | private var formatterToUse: Formatter { 31 | return formatter ?? Number.formatter 32 | } 33 | 34 | public var body: some View { 35 | // b7 `formatter` init of TextField does not work properly. 36 | D2SDebugLabel("[EN]") { 37 | HStack { 38 | D2SEditPropertyName(isValid: isValid) 39 | Spacer() 40 | TextField("", text: object.binding(propertyKey) 41 | .format(with: formatterToUse)) 42 | .multilineTextAlignment(.trailing) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Edit/EditString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SEditString.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Property.Edit { 11 | 12 | struct String: View { 13 | 14 | typealias String = Swift.String 15 | typealias Bool = Swift.Bool 16 | typealias Date = Foundation.Date 17 | 18 | public init() {} 19 | 20 | @EnvironmentObject var object : NSManagedObject 21 | 22 | @Environment(\.propertyKey) private var propertyKey 23 | @Environment(\.formatter) private var formatter 24 | @Environment(\.displayNameForProperty) private var label 25 | 26 | private struct Labeled: View, D2SAttributeValidator { 27 | 28 | @ObservedObject var object : NSManagedObject 29 | 30 | @Environment(\.displayNameForProperty) private var label 31 | @Environment(\.attribute) var attribute 32 | 33 | let content : V 34 | 35 | var body: some View { 36 | VStack(alignment: .leading) { 37 | D2SPropertyNameHeadline(isValid: isValid) 38 | content 39 | } 40 | } 41 | } 42 | 43 | public var body: some View { 44 | Group { 45 | if formatter != nil { 46 | D2SDebugLabel("[ESF]") { 47 | Labeled(object: object, content: 48 | TextField("", text: object.binding(propertyKey) 49 | .format(with: formatter!))) 50 | } 51 | } 52 | else { 53 | D2SDebugLabel("[ES]") { 54 | Labeled(object: object, content: 55 | TextField("", text: object.stringBinding(propertyKey))) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/README.md: -------------------------------------------------------------------------------- 1 |

Direct to SwiftUI Property Components 2 | 4 |

5 | 6 | Those components are usually selected using the `component` environment key. 7 | 8 | They display or edit one property of an entity, i.e. they inspect the 9 | `propertyKey`. 10 | 11 | By default there is essentially one `View` and one `Edit` property component for 12 | each type. E.g. `D2SDisplayString` to display strings, `D2SEditString` to edit 13 | strings and `D2SDisplayDate` to display dates. 14 | 15 | Which one is displayed for a property is selected by the rule system, and there 16 | are quite a few builtin rules to select the basic types, e.g. 17 | ```swift 18 | (\.task == "edit" && \.attribute.attributeType == .dateAttributeType 19 | => \.component <= D2SEditDate()) 20 | .priority(3), 21 | (\.task == "edit" && \.attribute.attributeType == .booleanAttributeType 22 | => \.component <= D2SEditBool()) 23 | .priority(3), 24 | ``` 25 | 26 | As usual you are not restricted to the builtin property View's. You can build a 27 | completely custom one. For example you could build a `DisplayLocationOnMap` view 28 | which instead of showing a lat/lon property as values, shows an actual MapKit 29 | map. 30 | 31 | Also note that you can use "fake" propertyKey's which do not map to real entity 32 | properties, for example you could use a fake "name" propertyKey which then 33 | actually inspects the "firstname" and "lastname" properties and shows a combined 34 | View. 35 | You just have to manually set the `displayPropertyKeys` to pull them up. 36 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Relationships/DisplayToOneSummary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDisplaySummary.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Property.Display { 11 | 12 | /** 13 | * Fetches a to one relationship of an object and displays the 14 | * summary for that (using `D2SSummaryView`). 15 | */ 16 | struct ToOneSummary: View { 17 | 18 | public typealias String = Swift.String 19 | 20 | private let navigationTask : String 21 | // TBD: maybe make this an environment key (aka `nextTask`?) 22 | 23 | public init(navigationTask: String = "inspect") { 24 | self.navigationTask = navigationTask 25 | } 26 | 27 | public var body: some View { 28 | D2SToOneLink(navigationTask: navigationTask) { 29 | D2SSummaryView() 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Relationships/DisplayToOneTitle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDisplayToOneTitle.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Property.Display { 11 | 12 | /** 13 | * Fetches a to one relationship of an object and displays the 14 | * title for that (using `D2STitleText`). 15 | */ 16 | struct ToOneTitle: View { 17 | 18 | public typealias String = Swift.String 19 | 20 | private let navigationTask : String 21 | 22 | public init(navigationTask: String = "inspect") { 23 | self.navigationTask = navigationTask 24 | } 25 | 26 | public var body: some View { 27 | D2SToOneLink(navigationTask: navigationTask) { 28 | D2STitleText() 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Properties/Relationships/EditToOne.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SEditToOne.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Property.Edit { 11 | 12 | struct ToOne: View, D2SRelationshipValidator { 13 | // Note: Wanted to do this using a "sheet". But FB7270069. 14 | // So going w/ a navigation link for now. 15 | 16 | public init() {} 17 | 18 | @EnvironmentObject public var object : NSManagedObject 19 | 20 | @Environment(\.relationship) public var relationship 21 | 22 | public var body: some View { 23 | D2SNavigationLink(destination: 24 | // Note: The `object` is still the source object here! 25 | // Which conflicts w/ the `title` binding. 26 | D2SPageView() 27 | .environment(\.entity, relationship.destinationEntity!) 28 | .environment(\.relationship, relationship) 29 | .environment(\.navigationBarTitle, relationship.name) // TBD: Hm hm 30 | .ruleObject(object) 31 | .task(.select) 32 | ) 33 | { 34 | VStack(alignment: .leading) { 35 | D2SPropertyNameHeadline(isValid: isValid) 36 | D2SToOneContainer { 37 | RowComponent() 38 | .task(.select) 39 | } 40 | } 41 | } 42 | } 43 | 44 | private struct RowComponent: View { 45 | @Environment(\.rowComponent) var body 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SComponentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SPropertyValue.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * This shows the value of the property using the View applicable for the 12 | * current environment. 13 | * 14 | * It queries the `component` environment key which resolves to the proper 15 | * View for the property + entity + task. 16 | * 17 | * It is the same (but less typing) as: 18 | * 19 | * @Environment(\.component) var component 20 | * 21 | */ 22 | public struct D2SComponentView: View { 23 | 24 | @Environment(\.component) public var body 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SDisplayProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDisplayProperties.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * A ForEach over the `displayPropertyKeys`. 12 | */ 13 | public struct D2SDisplayProperties: View { 14 | 15 | @Environment(\.displayPropertyKeys) private var displayPropertyKeys 16 | 17 | public var body: some View { 18 | ForEach(displayPropertyKeys, id: \String.self) { propertyKey in 19 | PropertySwitch() 20 | .environment(\.propertyKey, propertyKey) 21 | } 22 | } 23 | 24 | private struct PropertySwitch: View { 25 | @Environment(\.rowComponent) var body 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SDisplayPropertiesList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDisplayPropertiesList.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * Shows a `List` w/ the `displayPropertyKeys` of the object. 12 | * 13 | * Supports `hideEmptyProperty`. 14 | */ 15 | public struct D2SDisplayPropertiesList: View { 16 | 17 | @Environment(\.entity) private var entity 18 | @Environment(\.object) private var object 19 | @Environment(\.displayPropertyKeys) private var displayPropertyKeys 20 | @Environment(\.hideEmptyProperty) private var hideEmptyProperty 21 | 22 | public init() {} 23 | 24 | typealias PropertyType = RelationshipD2S.RelationshipType 25 | 26 | private var propertiesToDisplay: [ String ] { 27 | if !hideEmptyProperty { return displayPropertyKeys } 28 | 29 | return displayPropertyKeys.filter { propertyKey in 30 | if propertyType(propertyKey).isRelationship { return true } 31 | 32 | guard let value = object.value(forKeyPath: propertyKey) else { 33 | return false 34 | } 35 | if let s = value as? String, s.isEmpty { return false } 36 | return true 37 | } 38 | } 39 | 40 | private func propertyType(_ propertyKey: String) -> PropertyType { 41 | entity[relationship: propertyKey]?.d2s.type ?? .none 42 | } 43 | 44 | public var body: some View { 45 | List(propertiesToDisplay, id: \String.self) { propertyKey in 46 | PropertySwitch() 47 | .environment(\.propertyKey, propertyKey) 48 | } 49 | } 50 | 51 | private struct PropertySwitch: View { 52 | @Environment(\.rowComponent) var body 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SFaultContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SFaultContainer.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * This takes a `D2SFault`. If it still is a fault, it shows some wildcard 12 | * view. If not, it shows the content. 13 | */ 14 | public struct D2SFaultContainer: View { 15 | 16 | public typealias Fault = D2SFault> 17 | 18 | private let fault : Fault 19 | private let content : ( Object ) -> Content 20 | 21 | init(fault: Fault, @ViewBuilder content: @escaping ( Object ) -> Content) { 22 | self.fault = fault 23 | self.content = content 24 | } 25 | 26 | public var body: some View { 27 | Group { 28 | if fault.accessingFault() { D2SRowFault() } 29 | else { content(fault.object).ruleObject(fault.object) } 30 | } 31 | } 32 | } 33 | 34 | public extension D2SFaultContainer where Content == D2STitledSummaryView { 35 | init(fault: Fault) { 36 | self.fault = fault 37 | self.content = { _ in D2STitledSummaryView() } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SFaultObjectLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SFaultObjectLink.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * This takes a `D2SFault`. If it still is a fault, it shows some wildcard 12 | * view. 13 | * If it is an object, it embeds the object in a `NavigationLink` which shows 14 | * the `nextTask` with the object bound. 15 | */ 16 | public struct D2SFaultObjectLink: View 18 | { 19 | public typealias Fault = D2SFault> 20 | 21 | @Environment(\.task) var task 22 | @Environment(\.nextTask) var nextTask 23 | 24 | private let action : D2SObjectAction 25 | private let fault : Fault 26 | private let destination : Destination 27 | private let content : Content 28 | // TBD: should the content get selected using the `rowComponent`? 29 | // Probably. 30 | 31 | private let isActive : Binding? 32 | 33 | init(fault: Fault, destination: Destination, 34 | action: D2SObjectAction = .nextTask, 35 | isActive: Binding? = nil, 36 | @ViewBuilder content: () -> Content) 37 | { 38 | self.isActive = isActive 39 | self.action = action 40 | self.fault = fault 41 | self.destination = destination 42 | self.content = content() 43 | } 44 | 45 | private var taskToInvoke: String { 46 | return action.action(task: task, nextTask: nextTask) 47 | } 48 | 49 | public var body: some View { 50 | // This has sizing issues on the first load. The cells have the wrong 51 | // height. 52 | // Maybe we need to make the Fault an observed object? 53 | Group { 54 | if fault.accessingFault() { D2SRowFault() } 55 | else { 56 | D2SNavigationLink(destination: destination.task(taskToInvoke) 57 | .ruleObject(fault.object), 58 | isActive: isActive) 59 | { 60 | content.ruleObject(fault.object) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | public extension D2SFaultObjectLink where Content == D2STitledSummaryView { 68 | init(fault: Fault, destination: Destination, 69 | action: D2SObjectAction = .nextTask, isActive: Binding? = nil) 70 | { 71 | self.isActive = isActive 72 | self.action = action 73 | self.fault = fault 74 | self.destination = destination 75 | self.content = D2STitledSummaryView() 76 | } 77 | } 78 | 79 | public extension D2SFaultObjectLink where Destination == D2SPageView, 80 | Content == D2STitledSummaryView 81 | { 82 | init(fault: Fault, 83 | action: D2SObjectAction = .nextTask, isActive: Binding? = nil) 84 | { 85 | self.isActive = isActive 86 | self.fault = fault 87 | self.destination = D2SPageView() 88 | self.content = D2STitledSummaryView() 89 | self.action = action 90 | } 91 | } 92 | public extension D2SFaultObjectLink where Destination == D2SPageView { 93 | init(fault: Fault, 94 | action: D2SObjectAction = .nextTask, isActive: Binding? = nil, 95 | @ViewBuilder content: () -> Content) 96 | { 97 | self.isActive = isActive 98 | self.fault = fault 99 | self.destination = D2SPageView() 100 | self.content = content() 101 | self.action = action 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SNavigationLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SNavigationLink.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * The same like `NavigationLink`, but this preserves the `D2SContext` 12 | * in the environment OF THE DESTINATION. Which otherwise starts with 13 | * a fresh one! 14 | * 15 | * On b6 watchOS/macOS the environment is lost on navigation. 16 | * That is no good :-) So we copy our keys (which are all stored within the 17 | * D2SContext). 18 | */ 19 | public struct D2SNavigationLink: View 20 | where Label: View, Destination: View 21 | { 22 | @Environment(\.ruleContext) private var context 23 | @Environment(\.managedObjectContext) private var moc 24 | 25 | private let destination : Destination 26 | private let label : Label 27 | private let isActive : Binding? 28 | 29 | public init(destination: Destination, 30 | isActive: Binding? = nil, 31 | @ViewBuilder label: () -> Label) 32 | { 33 | self.destination = destination 34 | self.label = label() 35 | self.isActive = isActive 36 | } 37 | 38 | public var body: some View { 39 | Group { 40 | if isActive != nil { 41 | NavigationLink(destination: destination 42 | .environmentObject(context.object) 43 | .environment(\.managedObjectContext, moc) 44 | .ruleContext(context), 45 | isActive: isActive!) 46 | { 47 | label 48 | .environmentObject(context.object) 49 | .environment(\.managedObjectContext, moc) 50 | .ruleContext(context) 51 | } 52 | } 53 | else { 54 | NavigationLink(destination: destination 55 | .environmentObject(context.object) 56 | .environment(\.managedObjectContext, moc) 57 | .ruleContext(context)) 58 | { 59 | label 60 | .environmentObject(context.object) 61 | .environment(\.managedObjectContext, moc) 62 | .ruleContext(context) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SNilText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SNilText.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * Shows the `\.title` stored in the environment. 12 | */ 13 | public struct D2SNilText: View { 14 | 15 | @Environment(\.displayStringForNil) private var displayStringForNil 16 | 17 | public var body: some View { Text(verbatim: displayStringForNil) } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SPropertyName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SPropertyName.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * A text representing the display name of the active property. 12 | */ 13 | public struct D2SPropertyName: View { 14 | 15 | @Environment(\.displayNameForProperty) private var label 16 | 17 | public var body: some View { Text(label) } 18 | } 19 | 20 | /** 21 | * A text representing the display name of the active property. 22 | * 23 | * This one is a variant which shows the label as a subheadline above the 24 | * field. 25 | */ 26 | public struct D2SPropertyNameHeadline: View { 27 | 28 | private let isValid : Bool 29 | 30 | public init(isValid: Bool = true) { 31 | self.isValid = isValid 32 | } 33 | 34 | @Environment(\.displayNameForProperty) private var label 35 | 36 | public var body: some View { 37 | HStack { 38 | (Text(label) + Text(verbatim: ":")) 39 | .foregroundColor(isValid ? .secondary : .red) 40 | .font(.subheadline) 41 | Spacer() 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * A text representing the display name of the active property. 48 | * 49 | * This one is a variant which colors the text based on validity. 50 | */ 51 | public struct D2SEditPropertyName: View { 52 | 53 | private let isValid : Bool 54 | 55 | public init(isValid: Bool = true) { 56 | self.isValid = isValid 57 | } 58 | 59 | @Environment(\.displayNameForProperty) private var label 60 | 61 | public var body: some View { 62 | Text(label) 63 | .foregroundColor(isValid ? nil : .red) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SRowFault.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SRowFault.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * The wildcard View to use if a fault object has not been resolved yet. 12 | */ 13 | struct D2SRowFault: View { 14 | #if false 15 | var body: some View { 16 | HStack { 17 | Spacer() 18 | Text("⏳") // that is too intrusive 19 | } 20 | } 21 | #else 22 | var body: some View { 23 | // show something nicer, some nice Path with a gray fake object 24 | Spacer() 25 | .frame(minHeight: 32) 26 | } 27 | #endif 28 | } 29 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SSummaryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SSummaryView.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * Displays a comma separated list of the `displayPropertyKeys` of the current 12 | * `object`. 13 | * 14 | * Use a an empty string for the `displayNameForProperty` to just show the 15 | * value. 16 | */ 17 | public struct D2SSummaryView: View { 18 | 19 | @EnvironmentObject private var object : NSManagedObject 20 | 21 | @Environment(\.displayPropertyKeys) private var displayPropertyKeys 22 | @Environment(\.ruleContext) private var ruleContext 23 | 24 | private func title(for object: NSManagedObject) -> String { 25 | var localContext = ruleContext 26 | localContext.object = object 27 | localContext.task = "list" // TBD 28 | return localContext.title 29 | } 30 | 31 | private func summary(for object: NSManagedObject) -> String { 32 | return summary(for: object, entity: object.entity) 33 | } 34 | private func summary(for object: NSManagedObject, entity: NSEntityDescription, 35 | fieldSeparator : String = ": ", 36 | itemSeparator : String = ", ") -> String 37 | { 38 | var localContext = ruleContext 39 | var summary = "" 40 | 41 | func stringForValue(_ value: Any) -> String { 42 | // We can't use the D2SDisplay (`.component`) things here :-/ 43 | // So we essentially replicate the logic ... 44 | // We can't even reflect on `component`, because it is the `AnyView`. 45 | // TBD: use an own wrapping AnyView? Which we could then ask for it's 46 | // stringValue 47 | 48 | let attribute = localContext.optional(D2SKeys.attribute.self) 49 | 50 | if let attribute = attribute, attribute.isPassword { 51 | return "🔐" 52 | } 53 | 54 | if let s = value as? String { return s } 55 | 56 | if let date = value as? Date { 57 | if let attribute = attribute { 58 | return attribute.dateFormatter().string(from: date) 59 | } 60 | return DateFormatter().string(from: date) 61 | } 62 | 63 | if let mo = value as? NSManagedObject { 64 | // This will recurse ... 65 | #if false 66 | return "[" + self.summary(for: mo) + "]" 67 | #else 68 | return self.title(for: mo) 69 | #endif 70 | } 71 | 72 | return String(describing: value) 73 | } 74 | 75 | var isFirst = true 76 | for name in displayPropertyKeys { 77 | localContext.propertyKey = name 78 | defer { localContext.propertyKey = "" } 79 | 80 | guard let value = localContext.propertyValue else { continue } 81 | 82 | if let v = value as? String, v.isEmpty { continue } // hide empty 83 | 84 | if value is Data { continue } // No data in summary 85 | 86 | let name = localContext.displayNameForProperty 87 | let string = stringForValue(value) 88 | if string.isEmpty { continue } // hide empty 89 | 90 | if isFirst { isFirst = false } 91 | else { summary += itemSeparator } 92 | 93 | if !name.isEmpty { // do not add separator if name is empty 94 | summary += name 95 | summary += fieldSeparator 96 | } 97 | summary += string 98 | } 99 | 100 | return summary 101 | } 102 | 103 | private var objectSummary: String { 104 | let o = object // makes it work, cannot directly use `object` ... 105 | return self.summary(for: o) 106 | } 107 | 108 | public var body: some View { 109 | // The .lineLimit isn't used on macOS (stays 1 line) 110 | // .fixedSize(horizontal: false, vertical: true) 111 | // makes it wrap, but then yields other layout issues. 112 | Text(objectSummary) // doesn't wrap on macOS? 113 | .lineLimit(3) // TODO: make it a rule 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2STitleText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2STitleText.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * A `Text` which shows the title of the object which is active in the 12 | * environment. 13 | */ 14 | public struct D2STitleText: View { 15 | 16 | @EnvironmentObject private var object : NSManagedObject // to get refreshes 17 | @Environment(\.title) private var title 18 | 19 | public init() {} 20 | 21 | public var body: some View { Text(verbatim: title) } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2STitledSummaryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2STitledSummaryView.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct D2STitledSummaryView: View { 11 | 12 | let font = Font.headline 13 | 14 | public var body: some View { 15 | VStack(alignment: .leading) { 16 | D2STitleText() 17 | .font(font) 18 | .lineLimit(1) 19 | HStack { 20 | D2SSummaryView() 21 | Spacer() 22 | } 23 | } 24 | .frame(maxWidth: .infinity) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SToOneContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SToOneContainer.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * This asynchronously fetches the toOne relationship target of the 12 | * current `\.object` / `\.propertyKey`. 13 | * 14 | * While it is being fetched, the `placeholder` is shown, 15 | * once it is ready the provided `Content`. Which will be 16 | * filled w/ the proper targetObject in `\.object` and the 17 | * matching `\.entity`. 18 | */ 19 | public struct D2SToOneContainer: View { 20 | 21 | @EnvironmentObject private var object : NSManagedObject 22 | @Environment(\.propertyKey) private var propertyKey 23 | 24 | private let content : Content 25 | private let placeholder : Placeholder 26 | 27 | public init(placeholder: Placeholder, @ViewBuilder content: () -> Content) { 28 | self.content = content() 29 | self.placeholder = placeholder 30 | } 31 | 32 | public var body: some View { 33 | Bound(object: object, propertyKey: propertyKey, 34 | placeholder: placeholder, content: content) 35 | } 36 | 37 | private struct Bound: View { 38 | 39 | @ObservedObject private var fetch : D2SToOneFetch 40 | 41 | // Strong types crash swiftc https://bugs.swift.org/browse/SR-11409 42 | private let content : AnyView 43 | private let placeholder : AnyView 44 | 45 | init(object: NSManagedObject, propertyKey: String, 46 | placeholder: Placeholder, content: Content) 47 | { 48 | self.content = AnyView(content) 49 | self.placeholder = AnyView(placeholder) 50 | self.fetch = D2SToOneFetch(object: object, propertyKey: propertyKey) 51 | } 52 | 53 | private var targetObject: NSManagedObject? { 54 | fetch.destination 55 | } 56 | 57 | #if os(macOS) 58 | @Environment(\.ruleContext) private var ruleContext 59 | 60 | private func handleDoubleClick(on object: NSManagedObject) { 61 | let view = D2SPageView() 62 | .ruleObject(object) 63 | .ruleContext(ruleContext) 64 | 65 | let title = object.d2s.defaultTitle 66 | let wc = D2SInspectWindow(rootView: view) 67 | wc.window?.title = title 68 | wc.window?.setFrameAutosaveName("Inspect:\(title)") 69 | wc.showWindow(nil) 70 | } 71 | #endif 72 | 73 | public var body: some View { 74 | Group { 75 | if fetch.isReady { 76 | content 77 | .ruleObject(targetObject!) 78 | .environment(\.entity, targetObject!.entity) 79 | } 80 | else { 81 | placeholder 82 | } 83 | } 84 | .onAppear { self.fetch.resume() } 85 | } 86 | } 87 | } 88 | 89 | public extension D2SToOneContainer where Placeholder == D2SNilText { 90 | 91 | init(@ViewBuilder content: () -> Content) { 92 | self.content = content() 93 | self.placeholder = D2SNilText() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Reusable/D2SToOneLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SToOneLink.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * This asynchronously fetches the toOne relationship target of the 12 | * current `\.object` / `\.propertyKey`. 13 | * 14 | * While it is being fetched, the `placeholder` is shown, 15 | * once it is ready the provided `Content`. Which will be 16 | * filled w/ the proper targetObject in `\.object` and the 17 | * matching `\.entity`. 18 | * 19 | * If the `navigationTask` is set, the content is wrapped in a 20 | * `NavigationLink` (or tap-window on macOS). 21 | * 22 | * See: There is also D2SToOneContainer, which just fetches the destination, 23 | * but doesn't wrap in an link. 24 | */ 25 | public struct D2SToOneLink: View { 26 | // TBD: can this reuse D2SToOneContainer? 27 | // Probably doable. 28 | 29 | @EnvironmentObject private var object : NSManagedObject 30 | @Environment(\.propertyKey) private var propertyKey 31 | 32 | private let content : Content 33 | private let placeholder : Placeholder 34 | private let navigationTask : String 35 | 36 | public init(navigationTask: String = "inspect", 37 | placeholder: Placeholder, @ViewBuilder content: () -> Content) 38 | { 39 | self.content = content() 40 | self.placeholder = placeholder 41 | self.navigationTask = navigationTask 42 | } 43 | 44 | public var body: some View { 45 | Bound(object: object, propertyKey: propertyKey, 46 | navigationTask: navigationTask, 47 | placeholder: placeholder, content: content) 48 | } 49 | 50 | private struct Bound: View { 51 | 52 | @ObservedObject private var fetch : D2SToOneFetch 53 | 54 | // Strong types crash swiftc https://bugs.swift.org/browse/SR-11409 55 | private let content : AnyView 56 | private let placeholder : AnyView 57 | private let navigationTask : String 58 | 59 | init(object: NSManagedObject, propertyKey: String, 60 | navigationTask: String, placeholder: Placeholder, content: Content) 61 | { 62 | self.content = AnyView(content) 63 | self.placeholder = AnyView(placeholder) 64 | self.navigationTask = navigationTask 65 | self.fetch = D2SToOneFetch(object: object, propertyKey: propertyKey) 66 | } 67 | 68 | private var targetObject: NSManagedObject? { 69 | fetch.destination 70 | } 71 | 72 | #if os(macOS) 73 | @Environment(\.ruleContext) private var ruleContext 74 | 75 | private func handleDoubleClick(on object: NSManagedObject) { 76 | let view = D2SPageView() 77 | .task(navigationTask) 78 | .ruleObject(object) 79 | .ruleContext(ruleContext) 80 | 81 | let title = object.d2s.defaultTitle 82 | let wc = D2SInspectWindow(rootView: view) 83 | wc.window?.title = title 84 | wc.window?.setFrameAutosaveName("Inspect:\(title)") 85 | wc.showWindow(nil) 86 | } 87 | #endif 88 | 89 | public var body: some View { 90 | Group { 91 | if fetch.isReady { 92 | if navigationTask.isEmpty { 93 | content 94 | .ruleObject(targetObject!) 95 | .environment(\.entity, targetObject!.entity) 96 | } 97 | else { 98 | #if os(macOS) 99 | content 100 | .ruleObject(targetObject!) 101 | .environment(\.entity, targetObject!.entity) 102 | .onTapGesture(count: 2) { // this looses the D2SCtx! 103 | self.handleDoubleClick(on: self.targetObject!) 104 | } 105 | #else // watchOS requires EnvObj transfer 106 | D2SNavigationLink(destination: D2SPageView() 107 | .ruleObject(targetObject!)) 108 | { 109 | content 110 | } 111 | .ruleObject(targetObject!) 112 | .environment(\.entity, targetObject!.entity) 113 | #endif 114 | } 115 | } 116 | else { 117 | placeholder 118 | } 119 | } 120 | .onAppear { self.fetch.resume() } 121 | } 122 | } 123 | } 124 | 125 | public extension D2SToOneLink where Placeholder == D2SNilText { 126 | 127 | init(navigationTask: String = "inspect", @ViewBuilder content: () -> Content) 128 | { 129 | self.content = content() 130 | self.placeholder = D2SNilText() 131 | self.navigationTask = navigationTask 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Rows/NamedToManyLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SToManyLinkRow.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | import CoreData 10 | 11 | public extension BasicLook.Row { 12 | 13 | /** 14 | * This provides a row which serves as a link to follow the toMany 15 | * relationship. 16 | */ 17 | struct NamedToManyLink: View { 18 | 19 | @Environment(\.object) private var object 20 | @Environment(\.displayNameForProperty) private var label 21 | @Environment(\.relationship) private var relationship 22 | 23 | private var targetEntityName: String { 24 | relationship.destinationEntity?.name ?? "" 25 | } 26 | 27 | private var relationshipPredicate : NSPredicate? { 28 | // Note: We could also attempt to lookup the inverse relationship and use 29 | // that, but we don't really know whether the `Model` has that 30 | // defined. 31 | // TBD: why is it ambiguous w/o the `as DatabaseObject`? (Xcode 11GM2) 32 | relationship.predicateInDestinationForSource(object as NSManagedObject) 33 | } 34 | 35 | public var body: some View { 36 | D2SNavigationLink(destination: 37 | D2SPageView() 38 | .environment(\.auxiliaryPredicate, relationshipPredicate) 39 | .environment(\.entity, relationship.destinationEntity!) 40 | .task(.list) 41 | .ruleObject(D2SKeys.object.defaultValue)) 42 | { 43 | Text(label) 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Rows/PropertyNameAsTitle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyNameAsTitle.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Row { 11 | /** 12 | * A row which shows the property name as a title, 13 | * and the property value below. 14 | */ 15 | struct PropertyNameAsTitle: View { 16 | 17 | let font = Font.headline 18 | 19 | public var body: some View { 20 | VStack(alignment: .leading) { 21 | D2SPropertyName() 22 | .font(self.font) 23 | D2SComponentView() 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Rows/PropertyNameValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyNameValue.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension BasicLook.Row { 11 | 12 | /** 13 | * A row which displays the property name on the left and the value on the 14 | * right. 15 | */ 16 | struct PropertyNameValue: View { 17 | 18 | public var body: some View { 19 | HStack(alignment: .firstTextBaseline) { 20 | D2SPropertyName() 21 | Spacer() 22 | D2SComponentView() 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/BasicLook/Rows/PropertyValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SPropertyValueRow.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import protocol SwiftUI.View 9 | 10 | public extension BasicLook.Row { 11 | /** 12 | * A row which just emits the property value component. 13 | * 14 | * E.g. useful in Forms. 15 | */ 16 | struct PropertyValue: View { 17 | 18 | public var body: some View { 19 | D2SComponentView() 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Debug/D2SDebugBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDebugBox.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct D2SDebugBox: View { 11 | 12 | let content : Content 13 | 14 | init(@ViewBuilder content: () -> Content) { 15 | self.content = content() 16 | } 17 | 18 | var body: some View { 19 | ScrollView { 20 | VStack(alignment: .leading) { 21 | content 22 | } 23 | // Spacer() // consumes too much space, doesn't shrink? 24 | } 25 | .padding(8) 26 | .background(RoundedRectangle(cornerRadius: 4) 27 | .stroke() 28 | .foregroundColor(.red)) 29 | .padding() 30 | .frame(maxWidth: .infinity, maxHeight: 200) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Debug/D2SDebugEntityDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDebugEntityDetails.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct D2SDebugEntityDetails: View { 11 | 12 | @Environment(\.entity) var entity 13 | 14 | struct AttributeInfo: View { 15 | 16 | let attribute: NSAttributeDescription 17 | 18 | var body: some View { 19 | VStack(alignment: .leading) { 20 | Text(verbatim: attribute.name) 21 | VStack(alignment: .leading) { 22 | Text(verbatim: String(describing: attribute.attributeType)) 23 | } 24 | .frame(maxWidth: .infinity) 25 | .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 0)) 26 | } 27 | .frame(maxWidth: .infinity) 28 | } 29 | } 30 | 31 | struct RelationshipInfo: View { 32 | 33 | let relationship: NSRelationshipDescription 34 | 35 | var body: some View { 36 | VStack(alignment: .leading) { 37 | Text(verbatim: relationship.name) 38 | VStack(alignment: .leading) { 39 | if relationship.isOptional { Text("Optional") } 40 | if relationship.isOrdered { Text("Ordered") } 41 | Text(relationship.isToMany ? "ToMany" : "ToOne") 42 | relationship.destinationEntity.map { entity in 43 | Text(verbatim: entity.name ?? "-") 44 | } 45 | /* 46 | var entity : Entity { get } 47 | var destinationEntity : Entity? { get } 48 | 49 | var minCount : Int? { get } 50 | var maxCount : Int? { get } 51 | 52 | var joins : [ Join ] { get } 53 | var joinSemantic : Join.Semantic { get } 54 | var updateRule : ConstraintRule? { get } 55 | var deleteRule : ConstraintRule? { get } 56 | var ownsDestination : Bool { get } 57 | var constraintName : String? { get } 58 | */ 59 | } 60 | .frame(maxWidth: .infinity) 61 | .padding(EdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 0)) 62 | } 63 | .frame(maxWidth: .infinity) 64 | } 65 | 66 | } 67 | 68 | public var body: some View { 69 | D2SDebugBox { 70 | if entity.d2s.isDefault { 71 | Text("No Entity set") 72 | } 73 | else { 74 | Text(verbatim: entity.displayName) 75 | .font(.title) 76 | 77 | ForEach(Array(entity.attributes), id: \.name) { attribute in 78 | AttributeInfo(attribute: attribute) 79 | } 80 | 81 | ForEach(Array(entity.relationships), id: \.name) { relationship in 82 | RelationshipInfo(relationship: relationship) 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Debug/D2SDebugEntityInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDebugEntityInfo.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct D2SDebugEntityInfo: View { 11 | 12 | @Environment(\.entity) var entity 13 | 14 | public var body: some View { 15 | D2SDebugBox { 16 | if entity.d2s.isDefault { 17 | Text("No Entity set") 18 | } 19 | else { 20 | Text(verbatim: entity.displayName) 21 | .font(.title) 22 | Text(verbatim: "\(entity)") 23 | .lineLimit(3) 24 | 25 | Text("#\(entity.attributes.count) attributes") 26 | Text("#\(entity.relationshipsByName.count) relationships") 27 | } 28 | } 29 | .lineLimit(1) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Debug/D2SDebugFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDebugFormatter.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | fileprivate var counter = 0 11 | 12 | public class D2SDebugFormatter: Formatter { 13 | 14 | public let wrapped: Formatter 15 | 16 | override public var description: String { 17 | "" 18 | } 19 | 20 | public init(_ formatter: Formatter) { 21 | counter += 1 22 | self.wrapped = formatter 23 | globalD2SLogger.log("\(counter) wrapping:", formatter) 24 | super.init() 25 | } 26 | required init?(coder: NSCoder) { 27 | fatalError("\(#function) has not been implemented") 28 | } 29 | 30 | open override func string(for obj: Any?) -> String? { 31 | let s = wrapped.string(for: obj) 32 | if let s = s { 33 | globalD2SLogger.log("\(counter) stringForObj:", obj, "=>", s) 34 | } 35 | else { 36 | globalD2SLogger.log("\(counter) stringForObj:", obj, "=> NIL") 37 | } 38 | return s 39 | } 40 | 41 | 42 | open override 43 | func attributedString(for obj: Any, 44 | withDefaultAttributes 45 | attrs: [NSAttributedString.Key : Any]?) 46 | -> NSAttributedString? 47 | { 48 | let s = wrapped.attributedString(for: obj, withDefaultAttributes: attrs) 49 | if let s = s { 50 | globalD2SLogger.log("\(counter) asForObj:", obj, "=>", s) 51 | } 52 | else { 53 | globalD2SLogger.log("\(counter) asForObj:", obj, "=> NIL") 54 | } 55 | return s 56 | } 57 | 58 | 59 | open override func editingString(for obj: Any) -> String? { 60 | let s = wrapped.editingString(for: obj) 61 | if let s = s { 62 | globalD2SLogger.log("\(counter) edstringForObj:", obj, "=>", s) 63 | } 64 | else { 65 | globalD2SLogger.log("\(counter) edstringForObj:", obj, "=> NIL") 66 | } 67 | return s 68 | } 69 | 70 | open override 71 | func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, 72 | for s: String, 73 | errorDescription 74 | error: AutoreleasingUnsafeMutablePointer?) 75 | -> Bool 76 | { 77 | var e : NSString? 78 | var o : AnyObject? 79 | let ok = wrapped.getObjectValue(&o, for: s, errorDescription: &e) 80 | obj? .pointee = o 81 | error?.pointee = e 82 | if let o = o { 83 | globalD2SLogger.log("\(counter) valueForString:", s, "=>", o) 84 | } 85 | else if let e = e { 86 | globalD2SLogger.log("\(counter) valueForString:", s, "error:", e) 87 | } 88 | else { 89 | globalD2SLogger.log("\(counter) valueForString:", s, "=> NIL") 90 | } 91 | return ok 92 | } 93 | 94 | open override 95 | func isPartialStringValid( 96 | _ partialStringPtr: AutoreleasingUnsafeMutablePointer, 97 | proposedSelectedRange proposedSelRangePtr: NSRangePointer?, 98 | originalString origString: String, 99 | originalSelectedRange origSelRange: NSRange, 100 | errorDescription error: AutoreleasingUnsafeMutablePointer?) 101 | -> Bool 102 | { 103 | var e : NSString? 104 | let ok = wrapped.isPartialStringValid( 105 | partialStringPtr, proposedSelectedRange: proposedSelRangePtr, 106 | originalString: origString, originalSelectedRange: origSelRange, 107 | errorDescription: &e 108 | ) 109 | error?.pointee = e 110 | if ok { 111 | globalD2SLogger.log("\(counter) p-valid:", origString) 112 | } 113 | else if let e = e { 114 | globalD2SLogger.log("\(counter) p-INvalid:", origString, e) 115 | } 116 | else { 117 | globalD2SLogger.log("\(counter) p-INvalid:", origString) 118 | } 119 | return ok 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Debug/D2SDebugLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDebugLabel.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct D2SDebugLabel: View { 11 | 12 | @Environment(\.debug) private var debug 13 | 14 | let label : String 15 | let content : Content 16 | 17 | public init(_ label: String, @ViewBuilder content: () -> Content) { 18 | self.label = label 19 | self.content = content() 20 | } 21 | 22 | public var body: some View { 23 | Group { 24 | if debug { 25 | HStack { 26 | Text(verbatim: label) 27 | .font(.footnote) 28 | .foregroundColor(.gray) 29 | content 30 | } 31 | } 32 | else { 33 | content 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Debug/D2SDebugMOCInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDebugMOCInfo.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct D2SDebugMOCInfo: View { 11 | 12 | @Environment(\.ruleObjectContext) private var moc 13 | 14 | public var body: some View { 15 | D2SDebugBox { 16 | if moc.d2s.isDefault { 17 | Text("Dummy MOC!") 18 | } 19 | else { 20 | Text(verbatim: moc.d2s.defaultTitle) 21 | .font(.title) 22 | Text(verbatim: "\(moc)") 23 | (moc.persistentStoreCoordinator?.managedObjectModel).flatMap { 24 | Text("Model: #\($0.entities.count) entities") 25 | } 26 | moc.persistentStoreCoordinator.flatMap { 27 | Text(verbatim: "\($0)") 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Debug/D2SDebugObjectEditInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDebugObjectEditInfo.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | fileprivate let changeCount = 0 12 | 13 | /** 14 | * Show debug info about the editing state of an object. 15 | */ 16 | public struct D2SDebugObjectEditInfo: View { 17 | 18 | @EnvironmentObject private var object : NSManagedObject 19 | 20 | public var body: some View { 21 | D2SDebugBox { 22 | if object.d2s.isDefault { 23 | Text("No object set") 24 | } 25 | else { 26 | Text(verbatim: object.entity.displayName) 27 | .font(.title) 28 | Text(verbatim: "\(object)") 29 | .lineLimit(3) 30 | Text(verbatim: "\(changeCount)") 31 | 32 | if object.isNew { 33 | Text("Object is new") 34 | Changes(object: object) 35 | } 36 | else if object.hasChanges { 37 | Text("Object has changes") 38 | Changes(object: object) 39 | } 40 | } 41 | } 42 | .lineLimit(1) 43 | } 44 | 45 | struct Changes: View { 46 | 47 | @ObservedObject var object : NSManagedObject 48 | 49 | private var changes : [ ( key: String, value: Any? ) ] { 50 | #if true 51 | // TODO: implement me 52 | return [] 53 | #else 54 | if object.isNew { 55 | return object.values.sorted(by: { $0.key < $1.key }) 56 | } 57 | else { 58 | return object.changesFromSnapshot(object.snapshot ?? [:]) 59 | .sorted(by: { $0.key < $1.key }) 60 | } 61 | #endif 62 | } 63 | 64 | var body: some View { 65 | VStack { 66 | ForEach(changes, id: \.key) { pair in 67 | HStack { 68 | Text(pair.key) 69 | Spacer() 70 | if pair.value == nil { 71 | Text("nil") 72 | } 73 | else { 74 | Text(String(describing: pair.value!)) 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/DefaultLook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultLook.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | public typealias DefaultLook = BasicLook 9 | 10 | public typealias D2SDisplayString = DefaultLook.Property.Display.String 11 | public typealias D2SDisplayBool = DefaultLook.Property.Display.Bool 12 | public typealias D2SDisplayDate = DefaultLook.Property.Display.Date 13 | public typealias D2SDisplayEmail = DefaultLook.Property.Display.Email 14 | public typealias D2SDisplayPassword = DefaultLook.Property.Display.Password 15 | 16 | public typealias D2SEditString = DefaultLook.Property.Edit.String 17 | public typealias D2SEditLargeString = DefaultLook.Property.Edit.LargeString 18 | public typealias D2SEditNumber = DefaultLook.Property.Edit.Number 19 | public typealias D2SEditDate = DefaultLook.Property.Edit.Date 20 | public typealias D2SEditBool = DefaultLook.Property.Edit.Bool 21 | 22 | public typealias D2SDisplayToOneTitle = DefaultLook.Property.Display.ToOneTitle 23 | public typealias D2SEditToOne = DefaultLook.Property.Edit.ToOne 24 | public typealias D2SDisplayToOneSummary 25 | = DefaultLook.Property.Display.ToOneSummary 26 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Generic/D2SEntityPageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SEntityPageView.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * Show the View which the `page` key yields, but inject the `entity` for the 12 | * provided `name` first. 13 | */ 14 | public struct D2SEntityPageView: View { 15 | 16 | @Environment(\.model) private var model 17 | 18 | public let entityName : String 19 | 20 | var entity: NSEntityDescription { 21 | guard let entity = model[entity: entityName] else { 22 | fatalError("did not find entity: \(entityName) in \(model)") 23 | } 24 | return entity 25 | } 26 | 27 | public var body: some View { 28 | D2SPageView() 29 | .environment(\.entity, entity) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Generic/D2SPageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SPageView.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * Show the View which the `page` key yields. 12 | * 13 | * Also configures the navbar title on iOS. 14 | */ 15 | public struct D2SPageView: View { 16 | 17 | #if os(iOS) 18 | @Environment(\.navigationBarTitle) private var title 19 | @Environment(\.page) private var page 20 | 21 | public var body: some View { 22 | return page 23 | .navigationBarTitle(Text(title), displayMode: .inline) 24 | } 25 | #else 26 | @Environment(\.page) public var body 27 | #endif 28 | } 29 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Misc/ListEnabledDatePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // D2SDatePicker.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | * Reimplementation of Form DatePicker. Required because Form-DatePickers 12 | * do not work properly when embedded in a ForEach/List (FB7212377). 13 | */ 14 | public struct ListEnabledDatePicker: View { 15 | 16 | private static let formatter : DateFormatter = { 17 | let df = DateFormatter() 18 | df.dateStyle = .short 19 | df.timeStyle = .short 20 | df.doesRelativeDateFormatting = true // today, tomorrow 21 | return df 22 | }() 23 | 24 | private let selection : Binding 25 | private let title : String 26 | 27 | @State var isEditing : Bool = false 28 | 29 | var textDate : String { 30 | ListEnabledDatePicker.formatter.string(from: selection.wrappedValue) 31 | } 32 | 33 | init(_ title: String, selection: Binding) { 34 | self.title = title 35 | self.selection = selection 36 | } 37 | 38 | private var editColor : Color { 39 | #if os(iOS) 40 | return Color(UIColor.systemBlue) // TBD 41 | #elseif os(macOS) 42 | return Color(NSColor.systemBlue) // FIXME, use proper predefined 43 | #else 44 | return Color.blue 45 | #endif 46 | } 47 | 48 | #if os(watchOS) // no DatePicker on watchOS. TODO: implement one 49 | public var body: some View { 50 | HStack { 51 | Text(title) 52 | Spacer() // FIXME: spacer ignores taps 53 | Text(verbatim: textDate) 54 | .foregroundColor(isEditing ? editColor : .secondary) 55 | } 56 | } 57 | #else 58 | public var body: some View { 59 | VStack { 60 | if isEditing { // Hack to fix the padding shrink when expanded 61 | Spacer() 62 | .frame(height: 6) 63 | } 64 | 65 | HStack { 66 | Text(title) 67 | Spacer() 68 | Text(verbatim: textDate) 69 | .foregroundColor(isEditing ? editColor : .secondary) 70 | } 71 | .contentShape(Rectangle()) // to make the whole thing tap'able 72 | .onTapGesture { self.isEditing.toggle() } 73 | 74 | if isEditing { 75 | Divider() 76 | DatePicker(selection: selection) { EmptyView() } 77 | } 78 | } 79 | } 80 | #endif 81 | } 82 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Misc/MultilineEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultilineEditor.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if os(iOS) 11 | import UIKit 12 | 13 | struct MultilineEditor: UIViewRepresentable { 14 | 15 | @Binding var text : String 16 | 17 | func makeUIView(context: Context) -> UITextView { 18 | let view = UITextView() 19 | view.isScrollEnabled = true 20 | view.isEditable = true 21 | view.isUserInteractionEnabled = true 22 | view.allowsEditingTextAttributes = false 23 | view.font = UIFont.systemFont(ofSize: UIFont.systemFontSize) 24 | return view 25 | } 26 | 27 | func updateUIView(_ view: UITextView, context: Context) { 28 | view.text = text 29 | } 30 | } 31 | #elseif os(macOS) 32 | #if false // enable once tested 33 | import AppKit 34 | 35 | struct MultilineEditor: UIViewRepresentable { 36 | 37 | @Binding var text : String 38 | 39 | func makeNSView(context: Context) -> NSTextField { 40 | let view = NSTextField() 41 | view.allowsEditingTextAttributes = false 42 | view.importsGraphics = false 43 | view.maximumNumberOfLines = 3 44 | return view 45 | } 46 | 47 | func updateNSView(_ view: NSTextField, context: Context) { 48 | view.text = text 49 | } 50 | } 51 | #endif 52 | #endif 53 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Misc/SearchField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchField.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SearchField: View { 11 | 12 | @Binding var search: String 13 | var onSearch : () -> Void = { } 14 | 15 | #if os(iOS) 16 | var body: some View { 17 | HStack { 18 | Image(systemName: "magnifyingglass") 19 | 20 | TextField("Search", text: $search, onCommit: onSearch) 21 | .textFieldStyle(RoundedBorderTextFieldStyle()) 22 | } 23 | .padding() 24 | } 25 | #elseif os(macOS) 26 | var body: some View { 27 | TextField("Search", text: $search, onCommit: onSearch) 28 | .textFieldStyle(RoundedBorderTextFieldStyle()) 29 | .padding() 30 | } 31 | #else // watchOS 32 | var body: some View { 33 | TextField("Search", text: $search, onCommit: onSearch) 34 | .padding() 35 | } 36 | #endif 37 | } 38 | -------------------------------------------------------------------------------- /Sources/DirectToSwiftUI/Views/Misc/Spinner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Spinner.swift 3 | // Direct to SwiftUI 4 | // 5 | // Copyright © 2019 ZeeZide GmbH. All rights reserved. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Spinner: View { 11 | 12 | let isAnimating : Bool 13 | let speed : Double 14 | let size : CGFloat 15 | 16 | init(isAnimating: Bool, speed: Double = 1.8, size: CGFloat = 64) { 17 | self.isAnimating = isAnimating 18 | self.speed = speed 19 | self.size = size 20 | } 21 | 22 | #if os(iOS) 23 | var body: some View { 24 | Image(systemName: "arrow.2.circlepath.circle.fill") 25 | .resizable() 26 | .frame(width: size, height: size) 27 | .rotationEffect(.degrees(isAnimating ? 360 : 0)) 28 | .animation( 29 | Animation.linear(duration: speed) 30 | .repeatForever(autoreverses: false) 31 | ) 32 | } 33 | #else // no systemImage on macOS ... 34 | var body: some View { 35 | Text("Connecting ...") 36 | } 37 | #endif 38 | } 39 | 40 | -------------------------------------------------------------------------------- /xcconfig/Base.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (C) 2017-2019 ZeeZide GmbH, All Rights Reserved 3 | // Created by Helge Hess on 23/01/2017. 4 | // 5 | 6 | // -------------------------- Base config ----------------------------- 7 | 8 | SWIFT_VERSION = 5.1 9 | 10 | // Deployment Targets 11 | MACOSX_DEPLOYMENT_TARGET = 10.15 12 | IPHONEOS_DEPLOYMENT_TARGET = 13.0 13 | WATCHOS_DEPLOYMENT_TARGET = 6.0 14 | TVOS_DEPLOYMENT_TARGET = 13.0 15 | SUPPORTS_MACCATALYST = NO 16 | 17 | // Signing 18 | CODE_SIGN_IDENTITY = - 19 | DEVELOPMENT_TEAM = 20 | 21 | // Include 22 | ALWAYS_SEARCH_USER_PATHS = NO 23 | 24 | // Language 25 | GCC_C_LANGUAGE_STANDARD = gnu11 26 | GCC_NO_COMMON_BLOCKS = YES 27 | CLANG_ENABLE_MODULES = YES 28 | CLANG_ENABLE_OBJC_ARC = YES 29 | CLANG_ENABLE_OBJC_WEAK = YES 30 | ENABLE_STRICT_OBJC_MSGSEND = YES 31 | 32 | // Warnings 33 | CLANG_WARN_BOOL_CONVERSION = YES 34 | CLANG_WARN_CONSTANT_CONVERSION = YES 35 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR 36 | CLANG_WARN_EMPTY_BODY = YES 37 | CLANG_WARN_ENUM_CONVERSION = YES 38 | CLANG_WARN_INT_CONVERSION = YES 39 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR 40 | CLANG_WARN_UNREACHABLE_CODE = YES 41 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 42 | CLANG_WARN_INFINITE_RECURSION = YES 43 | CLANG_WARN_SUSPICIOUS_MOVE = YES 44 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES 45 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES 46 | CLANG_WARN_COMMA = YES 47 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES 48 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES 49 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES 50 | CLANG_WARN_STRICT_PROTOTYPES = YES 51 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 52 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES 53 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 54 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES 55 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR 56 | GCC_WARN_UNDECLARED_SELECTOR = YES 57 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE 58 | GCC_WARN_UNUSED_FUNCTION = YES 59 | GCC_WARN_UNUSED_VARIABLE = YES 60 | 61 | // Analyzer 62 | CLANG_ANALYZER_NONNULL = YES 63 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES 64 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE 65 | 66 | // Debug 67 | DEBUG_INFORMATION_FORMAT = dwarf 68 | DEBUG_INFORMATION_FORMAT = dwarf-with-dsym 69 | 70 | // Swift Optimization 71 | SWIFT_OPTIMIZATION_LEVEL_Release = -Owholemodule 72 | SWIFT_OPTIMIZATION_LEVEL_Debug = -Onone 73 | SWIFT_OPTIMIZATION_LEVEL = $(SWIFT_OPTIMIZATION_LEVEL_$(CONFIGURATION)) 74 | 75 | SWIFT_ACTIVE_COMPILATION_CONDITIONS_Release = 76 | SWIFT_ACTIVE_COMPILATION_CONDITIONS_Debug = DEBUG 77 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(SWIFT_ACTIVE_COMPILATION_CONDITIONS_$(CONFIGURATION)) 78 | 79 | MTL_FAST_MATH = YES 80 | --------------------------------------------------------------------------------