(_ 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 | 
7 | 
8 | 
9 | 
10 | 
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 |
--------------------------------------------------------------------------------