diff --git a/Sources/SpeziViews/ViewModifier/ConditionalModifier.swift b/Sources/SpeziViews/ViewModifier/ConditionalModifier.swift new file mode 100644 index 0000000..82d0b07 --- /dev/null +++ b/Sources/SpeziViews/ViewModifier/ConditionalModifier.swift @@ -0,0 +1,86 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +extension View { + /// Applies the given transform if the given condition evaluates to `true`. + /// + /// ### Usage + /// + /// ```swift + /// struct ConditionalModifierTestView: View { + /// @State var condition = false + /// + /// var body: some View { + /// VStack { + /// Text("Condition present") + /// .if(condition) { view in + /// view + /// .hidden() + /// } + /// + /// Button("Toggle Condition") { + /// condition.toggle() + /// } + /// } + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - condition: The condition to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. + @ViewBuilder public func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } + + /// Applies the given transform if the given condition closure evaluates to `true`. + /// + /// ### Usage + /// + /// ```swift + /// struct ConditionalModifierTestView: View { + /// @State var closureCondition = false + /// + /// var body: some View { + /// VStack { + /// Text("Closure Condition present") + /// .if(condition: { + /// closureCondition + /// }, transform: { view in + /// view + /// .hidden() + /// }) + /// + /// Button("Toggle Closure Condition") { + /// closureCondition.toggle() + /// } + /// } + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - condition: The condition closure to evaluate. + /// - transform: The transform to apply to the source `View`. + /// - Returns: Either the original `View` or the modified `View` if the condition closure is `true`. + @ViewBuilder public func `if`(condition: () -> Bool, transform: (Self) -> Content) -> some View { + if condition() { + transform(self) + } else { + self + } + } +} diff --git a/Tests/UITests/TestApp/Localizable.xcstrings b/Tests/UITests/TestApp/Localizable.xcstrings index 1ed07ae..339edc0 100644 --- a/Tests/UITests/TestApp/Localizable.xcstrings +++ b/Tests/UITests/TestApp/Localizable.xcstrings @@ -25,6 +25,12 @@ }, "CanvasTest" : { + }, + "Closure Condition present" : { + + }, + "Condition present" : { + }, "Credentials" : { @@ -153,6 +159,12 @@ }, "This is a label ...\nAn other text. This is longer and we can check if the justified text works as expected. This is a very long text." : { + }, + "Toggle Closure Condition" : { + + }, + "Toggle Condition" : { + }, "Username" : { diff --git a/Tests/UITests/TestApp/ViewsTests/ConditionalModifierTestView.swift b/Tests/UITests/TestApp/ViewsTests/ConditionalModifierTestView.swift new file mode 100644 index 0000000..06c5cc0 --- /dev/null +++ b/Tests/UITests/TestApp/ViewsTests/ConditionalModifierTestView.swift @@ -0,0 +1,54 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziViews +import SwiftUI + + +struct ConditionalModifierTestView: View { + @State var condition = false + @State var closureCondition = false + + + var body: some View { + VStack { + Text("Condition present") + .if(condition) { view in + view + .hidden() + } + + Button("Toggle Condition") { + condition.toggle() + } + .buttonStyle(.borderedProminent) + .padding(.bottom, 20) + + Divider() + + Text("Closure Condition present") + .if(condition: { + closureCondition + }, transform: { view in + view + .hidden() + }) + .padding(.top, 20) + + Button("Toggle Closure Condition") { + condition.toggle() + } + .buttonStyle(.borderedProminent) + } + } +} + + +#Preview { + ConditionalModifierTestView() +} diff --git a/Tests/UITests/TestApp/ViewsTests/SpeziViewsTests.swift b/Tests/UITests/TestApp/ViewsTests/SpeziViewsTests.swift index 661b460..eae3e48 100644 --- a/Tests/UITests/TestApp/ViewsTests/SpeziViewsTests.swift +++ b/Tests/UITests/TestApp/ViewsTests/SpeziViewsTests.swift @@ -24,6 +24,7 @@ enum SpeziViewsTests: String, TestAppTests { case viewState = "View State" case operationState = "Operation State" case viewStateMapper = "View State Mapper" + case conditionalModifier = "Conditional Modifier" case defaultErrorOnly = "Default Error Only" case defaultErrorDescription = "Default Error Description" case asyncButton = "Async Button" @@ -98,6 +99,11 @@ enum SpeziViewsTests: String, TestAppTests { private var viewStateMapper: some View { ViewStateMapperTestView() } + + @ViewBuilder + private var conditionalModifier: some View { + ConditionalModifierTestView() + } @ViewBuilder private var defaultErrorOnly: some View { @@ -146,6 +152,8 @@ enum SpeziViewsTests: String, TestAppTests { operationState case .viewStateMapper: viewStateMapper + case .conditionalModifier: + conditionalModifier case .defaultErrorOnly: defaultErrorOnly case .defaultErrorDescription: diff --git a/Tests/UITests/TestAppUITests/SpeziViews/ModelTests.swift b/Tests/UITests/TestAppUITests/SpeziViews/ModelTests.swift index f7da3df..a0e1393 100644 --- a/Tests/UITests/TestAppUITests/SpeziViews/ModelTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziViews/ModelTests.swift @@ -116,6 +116,38 @@ final class ModelTests: XCTestCase { #endif XCTAssert(content.contains("Operation State: error")) } + + func testConditionalModifier() throws { + let app = XCUIApplication() + + XCTAssert(app.buttons["Conditional Modifier"].waitForExistence(timeout: 2)) + app.buttons["Conditional Modifier"].tap() + + XCTAssert(app.staticTexts["Condition present"].waitForExistence(timeout: 1)) + XCTAssert(app.staticTexts["Closure Condition present"].waitForExistence(timeout: 1)) + + // Check regular condition + XCTAssert(app.buttons["Toggle Condition"].waitForExistence(timeout: 1)) + app.buttons["Toggle Condition"].tap() + + XCTAssert(!app.staticTexts["Condition present"].waitForExistence(timeout: 2)) + + XCTAssert(app.buttons["Toggle Condition"].waitForExistence(timeout: 1)) + app.buttons["Toggle Condition"].tap() + + XCTAssert(app.staticTexts["Condition present"].waitForExistence(timeout: 2)) + + // Check closure condition + XCTAssert(app.buttons["Toggle Closure Condition"].waitForExistence(timeout: 1)) + app.buttons["Toggle Closure Condition"].tap() + + XCTAssert(!app.staticTexts["Closure Condition present"].waitForExistence(timeout: 2)) + + XCTAssert(app.buttons["Toggle Closure Condition"].waitForExistence(timeout: 1)) + app.buttons["Toggle Closure Condition"].tap() + + XCTAssert(app.staticTexts["Closure Condition present"].waitForExistence(timeout: 2)) + } func testDefaultErrorDescription() throws { let app = XCUIApplication() diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 3943370..909b2e9 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 2FB099B82A8AD25300B20952 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2FB099B72A8AD25100B20952 /* Localizable.xcstrings */; }; 9731B58F2B167053007676C0 /* ViewStateMapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9731B58E2B167053007676C0 /* ViewStateMapperView.swift */; }; 977CF55C2AD2B92C006D9B54 /* XCTestApp in Frameworks */ = {isa = PBXBuildFile; productRef = 977CF55B2AD2B92C006D9B54 /* XCTestApp */; }; + 97A0A5102B8D7FD7006102EF /* ConditionalModifierTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97A0A50F2B8D7FD7006102EF /* ConditionalModifierTestView.swift */; }; 97EE16AC2B16D5AB004D25A3 /* OperationStateTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97EE16AB2B16D5AB004D25A3 /* OperationStateTestView.swift */; }; A95B6E652AF4298500919504 /* SpeziPersonalInfo in Frameworks */ = {isa = PBXBuildFile; productRef = A95B6E642AF4298500919504 /* SpeziPersonalInfo */; }; A963ACAC2AF4683A00D745F2 /* SpeziValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A963ACAB2AF4683A00D745F2 /* SpeziValidationTests.swift */; }; @@ -67,6 +68,7 @@ 2FB0758A299DDB9000C0B37F /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestApp.xctestplan; sourceTree = ""; }; 2FB099B72A8AD25100B20952 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 9731B58E2B167053007676C0 /* ViewStateMapperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewStateMapperView.swift; sourceTree = ""; }; + 97A0A50F2B8D7FD7006102EF /* ConditionalModifierTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalModifierTestView.swift; sourceTree = ""; }; 97EE16AB2B16D5AB004D25A3 /* OperationStateTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationStateTestView.swift; sourceTree = ""; }; A963ACAB2AF4683A00D745F2 /* SpeziValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeziValidationTests.swift; sourceTree = ""; }; A963ACB12AF4709400D745F2 /* XCUIApplication+Targets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+Targets.swift"; sourceTree = ""; }; @@ -169,6 +171,7 @@ 2FA9485E29DE90720081C086 /* ViewStateTestView.swift */, 9731B58E2B167053007676C0 /* ViewStateMapperView.swift */, 97EE16AB2B16D5AB004D25A3 /* OperationStateTestView.swift */, + 97A0A50F2B8D7FD7006102EF /* ConditionalModifierTestView.swift */, 2FA9485F29DE90720081C086 /* MarkdownViewTestView.swift */, 2FA9486029DE90720081C086 /* CanvasTestView.swift */, 2FA9486329DE90720081C086 /* GeometryReaderTestView.swift */, @@ -357,6 +360,7 @@ 2FA9486A29DE90720081C086 /* GeometryReaderTestView.swift in Sources */, A9F85B722B32A052005F16E6 /* NameFieldsExample.swift in Sources */, 2FA9486729DE90720081C086 /* CanvasTestView.swift in Sources */, + 97A0A5102B8D7FD7006102EF /* ConditionalModifierTestView.swift in Sources */, A963ACAC2AF4683A00D745F2 /* SpeziValidationTests.swift in Sources */, A9FBAE982AF446F3001E4AF1 /* SpeziPersonalInfoTests.swift in Sources */, 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */,