Skip to content

Commit 164da34

Browse files
committed
Merge branch 'develop' into master
2 parents 6c7fb60 + 3e845c2 commit 164da34

20 files changed

+141
-28
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,5 @@ fastlane/test_output
7171
.DS_Store
7272
Package.resolved
7373
.docker.build
74+
.swiftpm
7475

.travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ script:
2020
- export PATH="$HOME/usr/bin:$PATH"
2121
- swift build -c release
2222
- swift build -c debug
23+
- set -o pipefail
2324
- xcodebuild -scheme DirectToSwiftUI-All -configuration Debug -target DirectToSwiftUI-All | xcpretty
2425
- xcodebuild -scheme DirectToSwiftUI-All -configuration Release -target DirectToSwiftUI-All | xcpretty

DirectToSwiftUI.xcodeproj/project.pbxproj

+4-1
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,7 @@
945945
E8F4DF332332510C0077B171 /* Project object */ = {
946946
isa = PBXProject;
947947
attributes = {
948-
LastUpgradeCheck = 1100;
948+
LastUpgradeCheck = 1200;
949949
ORGANIZATIONNAME = "ZeeZide GmbH";
950950
TargetAttributes = {
951951
E8F4DF3A2332510C0077B171 = {
@@ -1346,6 +1346,7 @@
13461346
buildSettings = {
13471347
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
13481348
CLANG_CXX_LIBRARY = "libc++";
1349+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
13491350
COPY_PHASE_STRIP = NO;
13501351
ENABLE_TESTABILITY = YES;
13511352
GCC_DYNAMIC_NO_PIC = NO;
@@ -1366,10 +1367,12 @@
13661367
buildSettings = {
13671368
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
13681369
CLANG_CXX_LIBRARY = "libc++";
1370+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
13691371
COPY_PHASE_STRIP = NO;
13701372
ENABLE_NS_ASSERTIONS = NO;
13711373
MTL_ENABLE_DEBUG_INFO = NO;
13721374
SDKROOT = "";
1375+
SWIFT_COMPILATION_MODE = wholemodule;
13731376
};
13741377
name = Release;
13751378
};

DirectToSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

DirectToSwiftUI.xcodeproj/xcshareddata/xcschemes/DirectToSwiftUI-Mac.xcscheme

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1100"
3+
LastUpgradeVersion = "1200"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

DirectToSwiftUI.xcodeproj/xcshareddata/xcschemes/DirectToSwiftUI-Mobile.xcscheme

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1100"
3+
LastUpgradeVersion = "1200"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

DirectToSwiftUI.xcodeproj/xcshareddata/xcschemes/DirectToSwiftUI-Watch.xcscheme

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1100"
3+
LastUpgradeVersion = "1200"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

README.md

+47
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,29 @@
1111

1212
_Going fully declarative_: Direct to SwiftUI.
1313

14+
**Direct to SwiftUI**
15+
is an adaption of an old
16+
[WebObjects](https://en.wikipedia.org/wiki/WebObjects)
17+
technology called
18+
[Direct to Web](https://developer.apple.com/library/archive/documentation/WebObjects/Developing_With_D2W/WalkThrough/WalkThrough.html#//apple_ref/doc/uid/TP30001015-DontLinkChapterID_5-TPXREF101).
19+
This time for Apple's new framework:
20+
[SwiftUI](https://developer.apple.com/xcode/swiftui/).
21+
Instant
22+
[CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete)
23+
apps, configurable using
24+
[a declarative rule system](http://www.alwaysrightinstitute.com/swiftuirules/),
25+
yet fully integrated with SwiftUI.
26+
27+
There is a blog entry explaining how to use this:
28+
[Introducing Direct to SwiftUI](http://www.alwaysrightinstitute.com/directtoswiftui/).
29+
30+
A Direct to SwiftUI variant using
31+
[CoreData](https://developer.apple.com/documentation/coredata)
32+
instead of
33+
[ZeeQL](http://zeeql.io)
34+
can be found over here:
35+
[CoreDataToSwiftUI](https://github.com/DirectToSwift/CoreDataToSwiftUI).
36+
1437
## Requirements
1538

1639
Direct to SwiftUI requires an environment capable to run SwiftUI.
@@ -37,6 +60,30 @@ The package URL is:
3760
- [Views](Sources/DirectToSwiftUI/Views/README.md)
3861
- [Database Setup](Sources/DirectToSwiftUI/DatabaseSetup.md)
3962

63+
## What it looks like
64+
65+
A demo application using the Sakila database is provided:
66+
[DVDRental](https://github.com/DirectToSwift/DVDRental).
67+
68+
### Watch
69+
70+
<p float="left" valign="top">
71+
<img width="200" src="http://www.alwaysrightinstitute.com/images/d2s/watchos-screenshots/01-homepage.png?v=2">
72+
<img width="200" src="http://www.alwaysrightinstitute.com/images/d2s/watchos-screenshots/02-customers.png?v=2">
73+
<img width="200" src="http://www.alwaysrightinstitute.com/images/d2s/watchos-screenshots/03-customer.png?v=2">
74+
<img width="200" src="http://www.alwaysrightinstitute.com/images/d2s/watchos-screenshots/04-movies.png?v=2">
75+
</p>
76+
77+
### Phone
78+
79+
<p float="left" valign="top">
80+
<img width="320" src="http://www.alwaysrightinstitute.com/images/d2s/limited-entities.png">
81+
<img width="320" src="http://www.alwaysrightinstitute.com/images/d2s/list-customer-default.png">
82+
</p>
83+
84+
### macOS
85+
86+
Still too ugly to show, but works in a very restricted way ;-)
4087

4188
## Who
4289

Sources/DirectToSwiftUI/Environment/EnvironmentKeys.swift

+14-8
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ public extension D2SKeys {
2727

2828
struct debug: DynamicEnvironmentKey {
2929
#if DEBUG
30-
public static var defaultValue = true
30+
public static let defaultValue = true
3131
#else
32-
public static var defaultValue = false
32+
public static let defaultValue = false
3333
#endif
3434
}
3535

3636
struct database: DynamicEnvironmentKey {
37-
public static var defaultValue : Database = D2SDummyDatabase()
37+
public static let defaultValue : Database = D2SDummyDatabase()
3838
}
3939

4040
struct firstTask: DynamicEnvironmentKey {
@@ -51,7 +51,7 @@ public extension D2SKeys {
5151
struct object: DynamicEnvironmentKey {
5252
// TBD: This one should really be an EnvironmentObject, but how
5353
// would we do this? More in the keypath \.object.
54-
public static var defaultValue : OActiveRecord = .init()
54+
public static let defaultValue : OActiveRecord = .init()
5555
}
5656

5757
struct propertyKey: DynamicEnvironmentKey {
@@ -65,11 +65,11 @@ public extension D2SKeys {
6565
// MARK: - Model
6666

6767
struct model: DynamicEnvironmentKey {
68-
public static var defaultValue : Model = D2SDefaultModel()
68+
public static let defaultValue : Model = D2SDefaultModel()
6969
}
7070

7171
struct entity: DynamicEnvironmentKey {
72-
public static var defaultValue : Entity = D2SDefaultEntity.shared
72+
public static let defaultValue : Entity = D2SDefaultEntity.shared
7373
}
7474

7575
/**
@@ -80,7 +80,7 @@ public extension D2SKeys {
8080
* If that also fails, the default dummy attribute is returned.
8181
*/
8282
struct attribute: DynamicEnvironmentKey {
83-
public static var defaultValue : Attribute = D2SDefaultAttribute()
83+
public static let defaultValue : Attribute = D2SDefaultAttribute()
8484
}
8585

8686
/**
@@ -91,7 +91,7 @@ public extension D2SKeys {
9191
* If that also fails, the default dummy relationship is returned.
9292
*/
9393
struct relationship: DynamicEnvironmentKey {
94-
public static var defaultValue : Relationship = D2SDefaultRelationship()
94+
public static let defaultValue : Relationship = D2SDefaultRelationship()
9595
}
9696

9797
// MARK: - Derived
@@ -143,6 +143,12 @@ public extension D2SKeys {
143143
struct initialPropertyValues: DynamicEnvironmentKey {
144144
public static let defaultValue : [ String : Any? ] = [:]
145145
}
146+
struct creationTimestampPropertyKey: DynamicEnvironmentKey {
147+
public static let defaultValue : String? = nil
148+
}
149+
struct updateTimestampPropertyKey: DynamicEnvironmentKey {
150+
public static let defaultValue : String? = nil
151+
}
146152

147153
/**
148154
* The entities which are being displayed on a page.

Sources/DirectToSwiftUI/Environment/EnvironmentPathes.swift

+10-1
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,16 @@ public extension DynamicEnvironmentPathes {
414414
set { self[dynamic: D2SKeys.initialPropertyValues.self] = newValue }
415415
get { self[dynamic: D2SKeys.initialPropertyValues.self] }
416416
}
417-
417+
418+
var creationTimestampPropertyKey : String? {
419+
set { self[dynamic: D2SKeys.creationTimestampPropertyKey.self] = newValue }
420+
get { self[dynamic: D2SKeys.creationTimestampPropertyKey.self] }
421+
}
422+
var updateTimestampPropertyKey : String? {
423+
set { self[dynamic: D2SKeys.updateTimestampPropertyKey.self] = newValue }
424+
get { self[dynamic: D2SKeys.updateTimestampPropertyKey.self] }
425+
}
426+
418427
/**
419428
* If you implement a login page, you can assign a value to the `user`
420429
* environment. And then adjust other environments based on that,

Sources/DirectToSwiftUI/Environment/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ D2S has quiet a set of builtin environment keys, including:
6363
- `platform`
6464
- `debug`
6565
- `initialPropertyValues`
66+
- `creationTimestampPropertyKey`
67+
- `updateTimestampPropertyKey`
6668

6769
Checkout the `D2SKeys` for the full set.
6870

Sources/DirectToSwiftUI/Support/FoundationExtras.swift

-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ extension String {
8686
return needle.range(of: needle)
8787
}
8888

89-
let len = count
9089
let slen = needle.count
9190
let sc = needle.first!
9291

Sources/DirectToSwiftUI/Support/SwiftUI/FormatterBinding.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import struct SwiftUI.Binding
1212
public extension Binding {
1313

1414
/**
15-
* Creates a String binding form an arbitrary value binding which pipes the
15+
* Creates a String binding from an arbitrary value binding which pipes the
1616
* value binding through a formatter.
1717
*
1818
* This is a workaround to fix the `TextField` not doing the same when a

Sources/DirectToSwiftUI/ViewModel/D2SDisplayGroup.swift

+20-5
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ public final class D2SDisplayGroup<Object: OActiveRecord>
120120

121121
private func integrateCount(_ count: Int) {
122122
assert(_dispatchPreconditionTest(.onQueue(.main)))
123-
123+
self.countFetchToken = nil
124+
124125
#if false // nope, a fetch count means we rebuild!
125126
if count == results.count { return } // all good already
126127
#endif
@@ -138,11 +139,14 @@ public final class D2SDisplayGroup<Object: OActiveRecord>
138139
results.clearOrderAndApplyNewCount(count)
139140
}
140141

142+
private var countFetchToken : AnyCancellable?
143+
141144
private func fetchCount(_ fetchSpecification: FetchSpecification) {
142145
let fs = fetchSpecification // has to be done, can't use inside fetchCount?
143-
_ = dataSource.fetchCount(fs, on: D2SFetchQueue)
146+
countFetchToken = dataSource.fetchCount(fs, on: D2SFetchQueue)
144147
.receive(on: RunLoop.main)
145148
.catch { ( error : Swift.Error ) -> Just<Int> in
149+
self.countFetchToken = nil
146150
self.handleError(error)
147151
return Just(0)
148152
}
@@ -196,8 +200,13 @@ public final class D2SDisplayGroup<Object: OActiveRecord>
196200
self.results = newResults
197201
}
198202

199-
private struct Query: Equatable {
203+
private final class Query: Equatable {
200204
let range : Range<Int>
205+
var token : AnyCancellable?
206+
init(range: Range<Int>) { self.range = range }
207+
static func ==(lhs: Query, rhs: Query) -> Bool {
208+
return lhs.range == rhs.range
209+
}
201210
}
202211
private var activeQueries = [ Query ]()
203212

@@ -242,7 +251,7 @@ public final class D2SDisplayGroup<Object: OActiveRecord>
242251
let query = Query(range: fetchRange)
243252
activeQueries.append(query) // keep it alive
244253

245-
_ = dataSource.fetchGlobalIDs(fs, on: D2SFetchQueue)
254+
query.token = dataSource.fetchGlobalIDs(fs, on: D2SFetchQueue)
246255
.receive(on: RunLoop.main)
247256
.flatMap { ( globalIDs ) -> AnyPublisher<[ Object ], Error> in
248257
var missingGIDs = Set<GlobalID>()
@@ -317,7 +326,13 @@ fileprivate func buildInitialFetchSpec<Object: ActiveRecordType>
317326
if (fs.sortOrderings?.count ?? 0) == 0 {
318327
fs.sortOrderings = dataSource.entity?.d2s.defaultSortOrderings ?? []
319328
}
320-
assert(fs.sortOrderings != nil && !(fs.sortOrderings?.isEmpty ?? true))
329+
#if os(macOS)
330+
if !(fs.sortOrderings != nil && !(fs.sortOrderings?.isEmpty ?? true)) {
331+
globalD2SLogger.error("got no sort orderings for fetchspec:", fs)
332+
}
333+
#else
334+
assert(fs.sortOrderings != nil && !(fs.sortOrderings?.isEmpty ?? true))
335+
#endif
321336

322337
if let aux = auxiliaryQualifier {
323338
fs.qualifier = aux.and(fs.qualifier)

Sources/DirectToSwiftUI/ViewModel/D2SRuleEnvironment.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,21 @@ public final class D2SRuleEnvironment: ObservableObject {
4343
ruleContext[D2SKeys.model] = model
4444
}
4545

46+
private var modelFetch : AnyCancellable?
47+
4648
public func resume() {
4749
guard databaseModel == nil else { return }
4850

4951
// TODO: setup timer to refetch and compare model tag
50-
_ = adaptor.fetchModel(on: D2SFetchQueue)
52+
modelFetch = adaptor.fetchModel(on: D2SFetchQueue)
5153
.map { ( model, tag ) in
5254
FancyModelMaker(model: model).fancyfyModel()
5355
}
5456
.receive(on: RunLoop.main)
5557
.catch { ( error : Swift.Error ) -> Just<Model> in
5658
self.error = error
5759
globalD2SLogger.error("failed to fetch model:", error)
60+
self.modelFetch = nil
5861
return Just(Model(entities: [
5962
ModelEntity(name: "Could not load model.")
6063
]))
@@ -64,6 +67,7 @@ public final class D2SRuleEnvironment: ObservableObject {
6467
self.adaptor.model = model
6568
}
6669
self.setupWithModel(model)
70+
self.modelFetch = nil
6771
}
6872
}
6973
}

Sources/DirectToSwiftUI/Views/BasicLook/PageWrapper/EntityMasterDetailPage.swift

+9-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ public extension BasicLook.PageWrapper.MasterDetail {
2323
private var selectedEntity: Entity? {
2424
guard let entityName = selectedEntityName else { return nil }
2525
guard let entity = model[entity: entityName] else {
26-
fatalError("did not find entity: \(entityName) in \(model)")
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
2733
}
2834
return entity
2935
}
@@ -71,7 +77,8 @@ public extension BasicLook.PageWrapper.MasterDetail {
7177
}
7278
else {
7379
EntityContent()
74-
.environment(\.entity, selectedEntity!)
80+
.environment(\.entity,
81+
selectedEntity ?? D2SKeys.entity.defaultValue)
7582
}
7683
}
7784
.frame(minWidth: 400 as CGFloat, idealWidth: 600 as CGFloat,

Sources/DirectToSwiftUI/Views/BasicLook/PageWrapper/EntitySidebar.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ public extension BasicLook.PageWrapper.MasterDetail.EntityMasterDetailPage {
6868
EntityName()
6969
Spacer()
7070
}
71-
.environment(\.entity, { self.model[entity: name]! }())
71+
.environment(\.entity, {
72+
self.model[entity: name] ?? D2SKeys.entity.defaultValue
73+
}())
7274
.frame(maxWidth: .infinity, maxHeight: .infinity)
7375
.foregroundColor(self.colorForEntityName(name))
7476
.onTapGesture {

0 commit comments

Comments
 (0)