Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 13 additions & 27 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,28 @@ on:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:

jobs:
test:
name: Test SwiftUI Query
runs-on: macos-14
runs-on: macos-15

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Swift
uses: swift-actions/setup-swift@v2
with:
swift-version: "6.2"


- name: Show Swift version
run: swift --version

- name: Run Tests
run: swift test --parallel


- name: Install SwiftLint
run: brew install swiftlint

- name: Lint Code
run: make lint

- name: Run Tests with Coverage
run: swift test --enable-code-coverage
run: swift test --parallel --enable-code-coverage

- name: Generate Coverage Report
run: |
Expand All @@ -43,17 +42,4 @@ jobs:
with:
file: ./coverage.lcov
fail_ci_if_error: false
continue-on-error: true

lint:
name: Swift Format
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Swift Format Lint
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --strict
continue-on-error: true
11 changes: 4 additions & 7 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
included:
- Sources
- Tests
- Example

excluded:
- Documentation
- Example
- .build
- .swiftpm
- Package.swift
Expand All @@ -23,7 +23,8 @@ disabled_rules:
- function_parameter_count # Allow functions with more than 6 parameters
- redundant_string_enum_value # Allow explicit raw values for consistency
- raw_value_for_camel_cased_codable_enum # Disable conflicting camel case enum rule

- closure_parameter_position
- optional_data_string_conversion
# Per-path rule customization

opt_in_rules:
Expand Down Expand Up @@ -92,6 +93,7 @@ opt_in_rules:
- vertical_whitespace_opening_braces
- weak_delegate
- yoda_condition
- non_optional_string_data_conversion

# Rule configuration
analyzer_rules:
Expand Down Expand Up @@ -184,11 +186,6 @@ custom_rules:
severity: warning


observable_usage:
name: "Observable Usage"
regex: 'ObservableObject'
message: "Consider using @Observable instead of ObservableObject for Swift 6"
severity: warning

# Reporter type (xcode, json, csv, checkstyle, junit, html, emoji)
reporter: "xcode"
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ install: ## Install dependencies (SwiftLint and SwiftFormat)

.PHONY: lint
lint: ## Run SwiftLint
swiftlint
swiftlint --strict

.PHONY: lint-fix
lint-fix: ## Run SwiftLint with auto-fix
Expand Down Expand Up @@ -40,7 +40,7 @@ fix: ## Fix all issues (lint auto-fix + format)

.PHONY: build
build: ## Build the package
swift build -Xswiftc -strict-concurrency=complete
swift build

.PHONY: test
test: ## Run tests
Expand Down
6 changes: 0 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,12 @@ let package = Package(
name: "SwiftUIQuery",
dependencies: [
.product(name: "Perception", package: "swift-perception")
],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency")
]
),
.testTarget(
name: "SwiftUIQueryTests",
dependencies: [
"SwiftUIQuery",
],
swiftSettings: [
.enableUpcomingFeature("StrictConcurrency")
]
),
]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SwiftUI Query

[![Tests](https://github.com/muzix/swiftui-query/actions/workflows/tests.yml/badge.svg)](https://github.com/muzix/swiftui-query/actions/workflows/tests.yml)
[![Tests](https://github.com/muzix/SwiftUIQuery/actions/workflows/tests.yml/badge.svg)](https://github.com/muzix/SwiftUIQuery/actions/workflows/tests.yml)
[![Swift 6.0](https://img.shields.io/badge/Swift-6.0-orange.svg)](https://swift.org)
[![Platforms](https://img.shields.io/badge/Platforms-iOS%20|%20macOS%20|%20tvOS%20|%20watchOS-blue.svg)](https://swift.org)
[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
Expand Down
24 changes: 7 additions & 17 deletions Sources/SwiftUIQuery/Core/GarbageCollector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ public final class GarbageCollector {

isRunning = true

#if DEBUG
print("🗑️ SwiftUI Query: Starting GarbageCollector with \(interval)s interval")
#endif
QueryLogger.shared.logGCStart(interval: interval)

timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
Task { @MainActor [weak self] in
Expand All @@ -68,9 +66,7 @@ public final class GarbageCollector {
timer?.invalidate()
timer = nil

#if DEBUG
print("🗑️ SwiftUI Query: Stopping GarbageCollector")
#endif
QueryLogger.shared.logGCStop()
}

/// Register a query cache for garbage collection monitoring
Expand Down Expand Up @@ -132,22 +128,16 @@ public final class GarbageCollector {
for query in inactiveQueries {
cache.remove(query)
removedQueries += 1

#if DEBUG
print("🗑️ SwiftUI Query: GC removed inactive query \(query.queryHash)")
#endif
QueryLogger.shared.logGCRemoving(hash: query.queryHash, reason: "inactive query")
}
}

let duration = Date().timeIntervalSince(startTime)

#if DEBUG
if removedQueries > 0 {
print(
"🗑️ SwiftUI Query: GC completed - removed \(removedQueries)/\(totalQueries) queries in \(String(format: "%.2f", duration * 1000))ms"
)
}
#endif
if removedQueries > 0 {
let reason = "removed \(removedQueries)/\(totalQueries) queries in \(String(format: "%.2f", duration * 1000))ms"
QueryLogger.shared.logGCRemoving(hash: "GC-Complete", reason: reason)
}
}

/// Check if a query is eligible for garbage collection
Expand Down
8 changes: 2 additions & 6 deletions Sources/SwiftUIQuery/InfiniteQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -867,15 +867,11 @@ public final class InfiniteQuery<
public func optionalRemove() {
// Only remove if query has no observers and is eligible for GC
guard observers.isEmpty, state.fetchStatus == .idle, isEligibleForGC else {
#if DEBUG
print("🗑️ SwiftUI Query: GC cancelled for \(queryHash) - Query is active or not eligible")
#endif
QueryLogger.shared.logGCCancelled(hash: queryHash)
return
}

#if DEBUG
print("🗑️ SwiftUI Query: Executing GC for \(queryHash)")
#endif
QueryLogger.shared.logGCExecuting(hash: queryHash)

// Remove from cache (let cache handle the cleanup)
cache?.remove(self)
Expand Down
8 changes: 2 additions & 6 deletions Sources/SwiftUIQuery/Query.swift
Original file line number Diff line number Diff line change
Expand Up @@ -709,15 +709,11 @@ public final class Query<TData: Sendable, TKey: QueryKey>: AnyQuery {
public func optionalRemove() {
// Only remove if query has no observers and is eligible for GC
guard observers.isEmpty, state.fetchStatus == .idle, isEligibleForGC else {
#if DEBUG
print("🗑️ SwiftUI Query: GC cancelled for \(queryHash) - Query is active or not eligible")
#endif
QueryLogger.shared.logGCCancelled(hash: queryHash)
return
}

#if DEBUG
print("🗑️ SwiftUI Query: Executing GC for \(queryHash)")
#endif
QueryLogger.shared.logGCExecuting(hash: queryHash)

// Remove from cache (let cache handle the cleanup)
cache?.remove(self)
Expand Down
38 changes: 38 additions & 0 deletions Sources/SwiftUIQuery/QueryLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public final class QueryLogger {
/// Enable/disable QueryObserver cache interaction logging
public var logQueryObserver = true

/// Enable/disable GarbageCollector logging
public var logGarbageCollector = true

private init() {}

// MARK: - Logging Methods
Expand Down Expand Up @@ -115,6 +118,38 @@ public final class QueryLogger {
guard isEnabled, logQueryObserver else { return }
print("📊 QueryObserver reading query state for hash: \(hash)")
}

// MARK: - Garbage Collector Logging

/// Log GarbageCollector starting
func logGCStart(interval: TimeInterval) {
guard isEnabled, logGarbageCollector else { return }
print("🗑️ SwiftUI Query: Starting GarbageCollector with \(interval)s interval")
}

/// Log GarbageCollector stopping
func logGCStop() {
guard isEnabled, logGarbageCollector else { return }
print("🗑️ SwiftUI Query: Stopping GarbageCollector")
}

/// Log GC cancelled for active query
func logGCCancelled(hash: String) {
guard isEnabled, logGarbageCollector else { return }
print("🗑️ SwiftUI Query: GC cancelled for \(hash) - Query is active or not eligible")
}

/// Log GC executing for query
func logGCExecuting(hash: String) {
guard isEnabled, logGarbageCollector else { return }
print("🗑️ SwiftUI Query: Executing GC for \(hash)")
}

/// Log GC removing query
func logGCRemoving(hash: String, reason: String) {
guard isEnabled, logGarbageCollector else { return }
print("🗑️ SwiftUI Query: GC removing \(hash) - \(reason)")
}
}

// MARK: - Public API Extensions
Expand All @@ -126,6 +161,7 @@ extension QueryLogger {
logQueryClient = true
logQuery = true
logQueryObserver = true
logGarbageCollector = true
}

/// Disable all cache logging (convenience method)
Expand Down Expand Up @@ -157,3 +193,5 @@ extension QueryLogger {
logQueryObserver = true
}
}

// swiftlint:enable no_print_statements
Loading
Loading