Skip to content

Commit e86d0bc

Browse files
committed
initial open source
0 parents  commit e86d0bc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+5300
-0
lines changed

.editorconfig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
root = true
2+
3+
[*]
4+
indent_size = 2
5+
indent_style = space
6+
trim_trailing_whitespace = true
7+
8+
[Makefile]
9+
indent_size = 4
10+
indent_style = tab
11+
trim_trailing_whitespace = true

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.DS_Store
2+
.vscode/
3+
/.build
4+
/Packages
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm
8+
.netrc
9+
.index-build/
10+
.build/

.spi.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version: 1
2+
builder:
3+
configs:
4+
- documentation_targets:
5+
- SharingFirestore
6+
swift_version: 6.0

.swift-format

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"fileScopedDeclarationPrivacy" : {
3+
"accessLevel" : "private"
4+
},
5+
"indentConditionalCompilationBlocks" : true,
6+
"indentSwitchCaseLabels" : false,
7+
"indentation" : {
8+
"spaces" : 2
9+
},
10+
"lineBreakAroundMultilineExpressionChainComponents" : false,
11+
"lineBreakBeforeControlFlowKeywords" : false,
12+
"lineBreakBeforeEachArgument" : false,
13+
"lineBreakBeforeEachGenericRequirement" : false,
14+
"lineLength" : 100,
15+
"maximumBlankLines" : 1,
16+
"multiElementCollectionTrailingCommas" : true,
17+
"noAssignmentInExpressions" : {
18+
"allowedFunctions" : [
19+
"XCTAssertNoThrow"
20+
]
21+
},
22+
"prioritizeKeepingFunctionOutputTogether" : false,
23+
"respectsExistingLineBreaks" : true,
24+
"rules" : {
25+
"AllPublicDeclarationsHaveDocumentation" : false,
26+
"AlwaysUseLiteralForEmptyCollectionInit" : false,
27+
"AlwaysUseLowerCamelCase" : true,
28+
"AmbiguousTrailingClosureOverload" : true,
29+
"BeginDocumentationCommentWithOneLineSummary" : false,
30+
"DoNotUseSemicolons" : true,
31+
"DontRepeatTypeInStaticProperties" : true,
32+
"FileScopedDeclarationPrivacy" : true,
33+
"FullyIndirectEnum" : true,
34+
"GroupNumericLiterals" : true,
35+
"IdentifiersMustBeASCII" : true,
36+
"NeverForceUnwrap" : false,
37+
"NeverUseForceTry" : false,
38+
"NeverUseImplicitlyUnwrappedOptionals" : false,
39+
"NoAccessLevelOnExtensionDeclaration" : true,
40+
"NoAssignmentInExpressions" : true,
41+
"NoBlockComments" : true,
42+
"NoCasesWithOnlyFallthrough" : true,
43+
"NoEmptyTrailingClosureParentheses" : true,
44+
"NoLabelsInCasePatterns" : true,
45+
"NoLeadingUnderscores" : false,
46+
"NoParensAroundConditions" : true,
47+
"NoPlaygroundLiterals" : true,
48+
"NoVoidReturnOnFunctionSignature" : true,
49+
"OmitExplicitReturns" : false,
50+
"OneCasePerLine" : true,
51+
"OneVariableDeclarationPerLine" : true,
52+
"OnlyOneTrailingClosureArgument" : true,
53+
"OrderedImports" : true,
54+
"ReplaceForEachWithForLoop" : true,
55+
"ReturnVoidInsteadOfEmptyTuple" : true,
56+
"TypeNamesShouldBeCapitalized" : true,
57+
"UseEarlyExits" : false,
58+
"UseExplicitNilCheckInConditions" : true,
59+
"UseLetInEveryBoundCaseVariable" : true,
60+
"UseShorthandTypeNames" : true,
61+
"UseSingleLinePropertyGetter" : true,
62+
"UseSynthesizedInitializer" : true,
63+
"UseTripleSlashForDocumentationComments" : true,
64+
"UseWhereClausesInForLoops" : false,
65+
"ValidateDocumentationComments" : false
66+
},
67+
"spacesAroundRangeFormationOperators" : false,
68+
"tabWidth" : 8,
69+
"version" : 1
70+
}

Examples/CaseStudies/App.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import SharingFirestore
2+
import SwiftUI
3+
import Dependencies
4+
5+
func prepareFirestore(_ values: inout DependencyValues) {
6+
let options = FirebaseOptions(googleAppID: "1:123:ios:123abc", gcmSenderID: "123")
7+
options.projectID = "demo-project"
8+
FirebaseApp.configure(options: options)
9+
let settings = Firestore.firestore().settings
10+
settings.cacheSettings = MemoryCacheSettings()
11+
settings.host = "127.0.0.1:8080"
12+
settings.isSSLEnabled = false
13+
Firestore.firestore().settings = settings
14+
values.defaultFirestore = Firestore.firestore()
15+
}
16+
17+
@main
18+
struct CaseStudiesApp: App {
19+
20+
init() {
21+
prepareDependencies(prepareFirestore(_:))
22+
}
23+
24+
var body: some Scene {
25+
WindowGroup {
26+
NavigationStack {
27+
Form {
28+
NavigationLink("Query Demo") {
29+
CaseStudyView {
30+
SwiftUIQueryDemo()
31+
}
32+
}
33+
NavigationLink("Sync Demo") {
34+
CaseStudyView {
35+
SwiftUISyncDemo()
36+
}
37+
}
38+
NavigationLink("Observable Demo") {
39+
CaseStudyView {
40+
ObservableModelDemo()
41+
}
42+
}
43+
NavigationLink("Dynamic Query Demo") {
44+
CaseStudyView {
45+
DynamicQueryDemo()
46+
}
47+
}
48+
}
49+
}
50+
}
51+
}
52+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"colors" : [
3+
{
4+
"idiom" : "universal"
5+
}
6+
],
7+
"info" : {
8+
"author" : "xcode",
9+
"version" : 1
10+
}
11+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "universal",
5+
"platform" : "ios",
6+
"size" : "1024x1024"
7+
},
8+
{
9+
"appearances" : [
10+
{
11+
"appearance" : "luminosity",
12+
"value" : "dark"
13+
}
14+
],
15+
"idiom" : "universal",
16+
"platform" : "ios",
17+
"size" : "1024x1024"
18+
},
19+
{
20+
"appearances" : [
21+
{
22+
"appearance" : "luminosity",
23+
"value" : "tinted"
24+
}
25+
],
26+
"idiom" : "universal",
27+
"platform" : "ios",
28+
"size" : "1024x1024"
29+
}
30+
],
31+
"info" : {
32+
"author" : "xcode",
33+
"version" : 1
34+
}
35+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import Dependencies
2+
@preconcurrency import SharingFirestore
3+
import SwiftUI
4+
5+
private let collectionPath = "dynamic-facts"
6+
7+
struct DynamicQueryDemo: SwiftUICaseStudy {
8+
let readMe = """
9+
This demo shows how to perform a dynamic query using the tools provided by the library. Every 3 seconds, \
10+
a fact about a number is loaded from the network and saved to Firestore. You can search the \
11+
facts for specific text, and the list will stay in sync so that if a new fact is added to the database \
12+
that satisfies your search criteria, it will immediately appear.
13+
14+
To achieve this, you can call the `load` method defined on the `@SharedReader` projected \
15+
value to set a new query with dynamic parameters. You can also swipe to delete items.
16+
"""
17+
let caseStudyTitle = "Dynamic Query"
18+
19+
@State.SharedReader(value: []) private var facts: [Fact]
20+
@State var query = ""
21+
@State var totalCount = 0
22+
@State var searchCount = 0
23+
24+
@Dependency(\.defaultFirestore) var database
25+
26+
var body: some View {
27+
List {
28+
Section {
29+
if query.isEmpty {
30+
Text("Facts: \(totalCount)")
31+
.contentTransition(.numericText(value: Double(totalCount)))
32+
.font(.largeTitle)
33+
.bold()
34+
} else {
35+
Text("Search: \(facts.count)")
36+
.contentTransition(.numericText(value: Double(facts.count)))
37+
Text("Facts: \(totalCount)")
38+
.contentTransition(.numericText(value: Double(totalCount)))
39+
}
40+
}
41+
Section {
42+
ForEach(facts) { fact in
43+
Text(fact.body)
44+
}
45+
.onDelete { indexSet in
46+
Task {
47+
do {
48+
let factsToDelete = indexSet.map { facts[$0] }
49+
let batch = database.batch()
50+
for fact in factsToDelete {
51+
if let documentId = fact.id {
52+
let ref = database.collection(collectionPath).document(documentId)
53+
batch.deleteDocument(ref)
54+
}
55+
}
56+
try await batch.commit()
57+
} catch {
58+
print("Error deleting facts: \(error)")
59+
}
60+
}
61+
}
62+
}
63+
}
64+
.searchable(text: $query)
65+
.task(id: query) {
66+
await withErrorReporting {
67+
try await $facts.load(.query(FactsQuery(searchText: query)))
68+
}
69+
}
70+
.task {
71+
let listener: LockIsolated<(any ListenerRegistration)?> = .init(nil)
72+
do {
73+
try await withTaskCancellationHandler {
74+
// Monitor the total count of facts
75+
let reg = database
76+
.collection(collectionPath)
77+
.addSnapshotListener { snapshot, error in
78+
if let error = error {
79+
print("Error fetching total count: \(error)")
80+
return
81+
}
82+
self.totalCount = snapshot?.documents.count ?? 0
83+
}
84+
listener.setValue(reg)
85+
86+
// Add facts every second
87+
var number = 0
88+
while true {
89+
try await Task.sleep(for: .seconds(3))
90+
number += 1
91+
let fact = try await String(
92+
decoding: URLSession.shared
93+
.data(from: URL(string: "http://numberapi.com/\(number)")!).0,
94+
as: UTF8.self
95+
)
96+
97+
let newFact = Fact(
98+
body: fact,
99+
createdAt: Date()
100+
)
101+
102+
try database.collection(collectionPath).addDocument(from: newFact)
103+
}
104+
} onCancel: {
105+
listener.withValue {
106+
$0?.remove()
107+
$0 = nil
108+
}
109+
}
110+
} catch {}
111+
}
112+
}
113+
114+
private struct FactsQuery: SharingFirestoreQuery.KeyRequest {
115+
var searchText: String
116+
117+
var configuration: SharingFirestoreQuery.Configuration<Fact> {
118+
.init(
119+
path: collectionPath,
120+
predicates: [.order(by: "createdAt", descending: true)],
121+
animation: .default
122+
)
123+
}
124+
125+
func query(_ db: Firestore) throws -> Query {
126+
let query = db.collection(configuration.path)
127+
128+
// Apply base predicates
129+
var resultQuery = applingPredicated(query)
130+
131+
// Apply search filter if needed
132+
if !searchText.isEmpty {
133+
// In a real app, you might use a specialized solution for text search
134+
// Here we're doing a simple filter for demonstration
135+
resultQuery = resultQuery.whereField("body", isGreaterThanOrEqualTo: searchText)
136+
.whereField("body", isLessThanOrEqualTo: searchText + "\u{f8ff}")
137+
}
138+
139+
return resultQuery
140+
}
141+
}
142+
}
143+
144+
private struct Fact: Codable, Sendable, Identifiable {
145+
@DocumentID var id: String?
146+
var body: String
147+
var createdAt: Date
148+
}
149+
150+
#Preview {
151+
let _ = prepareDependencies(prepareFirestore(_:))
152+
NavigationStack {
153+
CaseStudyView {
154+
DynamicQueryDemo()
155+
}
156+
}
157+
}

Examples/CaseStudies/Info.plist

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>NSAppTransportSecurity</key>
6+
<dict>
7+
<key>NSAllowsArbitraryLoads</key>
8+
<true/>
9+
</dict>
10+
</dict>
11+
</plist>

0 commit comments

Comments
 (0)