Skip to content

Commit

Permalink
Improve the API Surface and Documentation Generation (#12)
Browse files Browse the repository at this point in the history
# Improve the API Surface and Documentation Generation

## ⚙️ Release Notes 
- Exposes a public API to reset a code, check the setup state of a code,
and lock a view
- Hides private elements behind a private access modifier
- Triggers Observable Object changes on locks


## 📚 Documentation
- Improves documentation generation using `@_documentation(visibility:
internal)`


## ✅ Testing
- Improves the UI tests


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [ ] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
PSchmiedmayer committed Sep 12, 2023
1 parent 742355c commit 14dc31a
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 21 deletions.
37 changes: 33 additions & 4 deletions Sources/SpeziAccessGuard/AccessGuard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,44 +39,73 @@ import SwiftUI
///
/// You can use the ``AccessGuarded`` SwiftUI [`View`](https://developer.apple.com/documentation/swiftui/view) in your SwiftUI application to
/// enforce a code or biometrics-based access guard to SwiftUI views.
public final class AccessGuard: Module {
@Dependency var secureStorage: SecureStorage
@Published var inTheBackground = true
@Published var lastEnteredBackground: Date = .now
public final class AccessGuard: Module, DefaultInitializable {
@Dependency private var secureStorage: SecureStorage
@Published private(set) var inTheBackground = true
@Published private(set) var lastEnteredBackground: Date = .now
private let configurations: [AccessGuardConfiguration]
private var viewModels: [String: AccessGuardViewModel] = [:]
private var cancellables: Set<AnyCancellable> = []


public convenience init() {
self.init([])
}

public init(_ configurations: [AccessGuardConfiguration]) {
self.configurations = configurations
}


@_documentation(visibility: internal)
public func sceneDidEnterBackground(_ scene: UIScene) {
Task { @MainActor in
inTheBackground = true
lastEnteredBackground = .now

for viewModel in viewModels.values {
viewModel.didEnterBackground()
}
}
}

@_documentation(visibility: internal)
public func sceneWillEnterForeground(_ scene: UIScene) {
Task { @MainActor in
inTheBackground = false

for viewModel in viewModels.values {
viewModel.willEnterForeground(lastEnteredBackground: lastEnteredBackground)
}
}
}


/// Resets the access guard for an identifier.
///
/// The function removes the code and all stored information.
/// - Parameter identifier: The identifier of the access guard.
@MainActor
public func resetAccessCode(for identifier: AccessGuardConfiguration.Identifier) throws {
try viewModel(for: identifier).resetAccessCode()
}

/// Determine the setup state of an access lock.
///
/// Use the ``SetAccessGuard`` view to setup an access guard.
/// - Parameter identifier: The identifier of the access guard.
/// - Returns: Returns `true` of the access guard is successfully setup. False if no access guard is setup.
@MainActor
public func setupComplete(for identifier: AccessGuardConfiguration.Identifier) -> Bool {
viewModel(for: identifier).setup
}

/// Locks an access guard.
/// - Parameter identifier: The identifier of the access guard that should be locked.
@MainActor
public func lock(identifier: AccessGuardConfiguration.Identifier) async {
await viewModel(for: identifier).lock()
}

@MainActor
func viewModel(for identifier: AccessGuardConfiguration.Identifier) -> AccessGuardViewModel {
Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziAccessGuard/AccessGuardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import Spezi
import SwiftUI


public struct AccessGuardView<GuardedView: View>: View {
struct AccessGuardView<GuardedView: View>: View {
private let guardedView: GuardedView
@StateObject private var viewModel: AccessGuardViewModel


public var body: some View {
var body: some View {
guardedView
.overlay {
if viewModel.locked {
Expand Down
30 changes: 18 additions & 12 deletions Sources/SpeziAccessGuard/AccessGuardViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,22 @@ final class AccessGuardViewModel: ObservableObject {

self.locked = setup

accessGuard.objectWillChange

self.objectWillChange
.sink {
self.lockAfterInactivity()
self.objectWillChange.send()
accessGuard.objectWillChange.send()
}
.store(in: &cancellables)
}


private func lockAfterInactivity() {
Task { @MainActor in
if let lastEnteredBackground = accessGuard?.lastEnteredBackground,
lastEnteredBackground.addingTimeInterval(configuration.timeout) < .now {
locked = true
}
@MainActor
func didEnterBackground() {}

@MainActor
func willEnterForeground(lastEnteredBackground: Date) {
if lastEnteredBackground.addingTimeInterval(configuration.timeout) < .now {
locked = true
}
}

Expand Down Expand Up @@ -105,6 +106,12 @@ final class AccessGuardViewModel: ObservableObject {
}
}

func lock() async {
await MainActor.run {
locked = true
}
}

func setAccessCode(_ code: String, codeOption: CodeOptions) async throws {
guard configuration.fixedCode == nil else {
throw AccessGuardError.storeCodeError
Expand All @@ -118,8 +125,7 @@ final class AccessGuardViewModel: ObservableObject {

try secureStorage.store(credentials: Credentials(username: configuration.identifier, password: accessCodeData))

await MainActor.run {
locked = true
}
// Ensure that the model is in a state as if the user has just entered the access code.
try await checkAccessCode(code)
}
}
7 changes: 4 additions & 3 deletions Sources/SpeziAccessGuard/CodeViews/CodeOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public struct CodeOptions: OptionSet, Codable, CaseIterable, Identifiable {
public static let all: CodeOptions = [.fourDigitNumeric, .sixDigitNumeric, .customNumeric, .customAlphanumeric]


public var id: Int {
@_documentation(visibility: internal) public var id: Int {
rawValue
}

Expand All @@ -63,7 +63,7 @@ public struct CodeOptions: OptionSet, Codable, CaseIterable, Identifiable {
return Int.max
}

public var description: LocalizedStringResource {
var description: LocalizedStringResource {
switch self {
case .fourDigitNumeric:
return LocalizedStringResource("CODE_OPTIONS_FOUR_DIGIT", bundle: .atURL(from: .module))
Expand All @@ -86,11 +86,12 @@ public struct CodeOptions: OptionSet, Codable, CaseIterable, Identifiable {
}
}

public let rawValue: Int
@_documentation(visibility: internal) public let rawValue: Int


/// Raw initializer for the ``CodeOptions`` option set. Do not use this initializer.
/// - Parameter rawValue: The raw option set value.
@_documentation(visibility: internal)
public init(rawValue: Int) {
self.rawValue = rawValue
}
Expand Down
8 changes: 8 additions & 0 deletions Tests/UITests/TestAppUITests/TestAppUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,15 @@ class TestAppUITests: XCTestCase {
XCTAssert(app.images["Passcode set was successful"].waitForExistence(timeout: 2.0))
app.buttons["Back"].tap()

// View should be unlocked as we just set the passcode ...
XCTAssert(app.buttons["Access Guarded"].waitForExistence(timeout: 2.0))
app.buttons["Access Guarded"].tap()
XCTAssert(app.staticTexts["Secured ..."].waitForExistence(timeout: 2.0))
XCTAssert(app.staticTexts["Secured ..."].isHittable)

// Try the new passcode
app.terminate()
app.launch()
XCTAssert(app.buttons["Access Guarded"].waitForExistence(timeout: 2.0))
app.buttons["Access Guarded"].tap()

Expand Down

0 comments on commit 14dc31a

Please sign in to comment.