Skip to content
Open
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
14 changes: 8 additions & 6 deletions Example/Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@
3D5D898A281D511A00DD9301 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D5D8989281D511A00DD9301 /* Assets.xcassets */; };
3D7BB5D827A46B8C009D4145 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7BB5D727A46B8C009D4145 /* App.swift */; };
3D7BB5DA27A46B8C009D4145 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7BB5D927A46B8C009D4145 /* ContentView.swift */; };
3D7BB5E827A46BA4009D4145 /* ValidatedPropertyKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3D7BB5E727A46BA4009D4145 /* ValidatedPropertyKit */; };
EDE7521229D7099D00FDB406 /* ValidatedPropertyKit in Frameworks */ = {isa = PBXBuildFile; productRef = EDE7521129D7099D00FDB406 /* ValidatedPropertyKit */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
3D5D8989281D511A00DD9301 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
3D7BB5D427A46B8C009D4145 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
3D7BB5D727A46B8C009D4145 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
3D7BB5D927A46B8C009D4145 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
3D7BB5E527A46B9F009D4145 /* ValidatedPropertyKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ValidatedPropertyKit; path = ..; sourceTree = "<group>"; };
EDE7521029D7097B00FDB406 /* ValidatedPropertyKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ValidatedPropertyKit; path = ..; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
3D7BB5D127A46B8C009D4145 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3D7BB5E827A46BA4009D4145 /* ValidatedPropertyKit in Frameworks */,
EDE7521229D7099D00FDB406 /* ValidatedPropertyKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -36,10 +36,10 @@
3D7BB5CB27A46B8C009D4145 = {
isa = PBXGroup;
children = (
EDE7521029D7097B00FDB406 /* ValidatedPropertyKit */,
3D7BB5D627A46B8C009D4145 /* Example */,
3D7BB5D527A46B8C009D4145 /* Products */,
3D7BB5E627A46BA4009D4145 /* Frameworks */,
3D7BB5E527A46B9F009D4145 /* ValidatedPropertyKit */,
);
sourceTree = "<group>";
};
Expand Down Expand Up @@ -85,7 +85,7 @@
);
name = Example;
packageProductDependencies = (
3D7BB5E727A46BA4009D4145 /* ValidatedPropertyKit */,
EDE7521129D7099D00FDB406 /* ValidatedPropertyKit */,
);
productName = Example;
productReference = 3D7BB5D427A46B8C009D4145 /* Example.app */;
Expand Down Expand Up @@ -279,6 +279,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -307,6 +308,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -344,7 +346,7 @@
/* End XCConfigurationList section */

/* Begin XCSwiftPackageProductDependency section */
3D7BB5E727A46BA4009D4145 /* ValidatedPropertyKit */ = {
EDE7521129D7099D00FDB406 /* ValidatedPropertyKit */ = {
isa = XCSwiftPackageProductDependency;
productName = ValidatedPropertyKit;
};
Expand Down
78 changes: 66 additions & 12 deletions Example/Example/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,34 +1,56 @@
import Combine
import RegexBuilder
import SwiftUI
import ValidatedPropertyKit

final class ContentModel: ObservableObject {
@PublishedValidated(
.range(3..., error: "Username is too short")
&& .regex(Regex { OneOrMore(.digit) }, error: "Requires 1+ digits")
)
var username = "" {
didSet {
usernameInvalid = !_username.isValid
usernameError = _username.errorAfterChanges
}
}

@Published
var usernameInvalid = true

@Published
var usernameError: String? = nil
}

struct ContentView {

@Validated(!.isEmpty)
@Validated(!.isEmpty(error: "Username is not valid"))
var username = String()


@ObservedObject
var model = ContentModel()

@FocusState var focus
}

extension ContentView: View {

var body: some View {
NavigationView {
List {
Section(
header: Text(verbatim: "Username"),
footer: Group {
if self._username.isInvalidAfterChanges {
Text(
verbatim: "Username is not valid"
)
.foregroundColor(.red)
}
Text(
verbatim: _username.isInvalidAfterChanges ? "Username is not valid" : ""
)
.foregroundColor(.red)
}
) {
TextField(
"John Doe",
text: self.$username
)
}

Section(
footer: Button(
action: {
Expand All @@ -39,12 +61,44 @@ extension ContentView: View {
}
.buttonStyle(.borderedProminent)
.validated(self._username)
) {}

Section(
header: Text(verbatim: "View Model Username"),
footer: Group {
Text(
verbatim: model.usernameError ?? ""
)
.foregroundColor(.red)
}
) {

TextField(
"John Doe",
text: $model.username
)
}

Section(
footer: Button(
action: {
print("Login")
}
) {
Text(verbatim: "Login")
}
.buttonStyle(.borderedProminent)
.disabled(model.usernameInvalid)
) {}
}
.navigationTitle("ValidatedPropertyKit")
}
}

}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
95 changes: 95 additions & 0 deletions Sources/PublishedValidated.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// PublishedValidated.swift
//
//
// Created by Jan Švec on 31.03.2023.
//

import Combine
import SwiftUI

@available(iOS 14.0, *)
@propertyWrapper
public final class PublishedValidated<Value: Equatable>: ObservableObject, Validatable {
public var validation: Validation<Value> {
didSet {
validate()
}
}

@Published
public private(set) var value: Value

@Published
public private(set) var hasChanges = false

@Published
public private(set) var isValid: Bool

@Published
public private(set) var error: String?

public var wrappedValue: Value {
get {
return value
}
set {
if newValue == value { return }

value = newValue
validate()

if !hasChanges {
hasChanges.toggle()
}
}
}

public var projectedValue: Published<Value>.Publisher {
get {
return $value
}
set {
$value = newValue
validate()

if !hasChanges {
hasChanges.toggle()
}
}
}

public var isInvalidAfterChanges: Bool {
hasChanges && !isValid
}

public var errorAfterChanges: String? {
isInvalidAfterChanges ? error : nil
}

public func validate() {
do {
try validation.validate(value)
isValid = true
error = nil
} catch {
self.error = error as? String
isValid = false
}
}

public init(
wrappedValue: Value,
_ validation: Validation<Value>
) {
self.validation = validation

_value = .init(
initialValue: wrappedValue
)

_isValid = .init(
initialValue: validation.validateCatched(wrappedValue)
)
}
}
41 changes: 24 additions & 17 deletions Sources/Validated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ import SwiftUI
/// A Validated PropertyWrapper
@propertyWrapper
public struct Validated<Value>: Validatable, DynamicProperty {

// MARK: Properties

/// The Validation
public var validation: Validation<Value> {
didSet {
// Re-Validate
self.isValid = self.validation.validate(self.value)
self.revalidate()
}
}

Expand Down Expand Up @@ -51,7 +49,8 @@ public struct Validated<Value>: Validatable, DynamicProperty {
if !self.hasChanges {
self.hasChanges.toggle()
}
self.isValid = self.validation.validate(newValue)

revalidate()
}
)
}
Expand All @@ -67,13 +66,15 @@ public struct Validated<Value>: Validatable, DynamicProperty {
_ validation: Validation<Value>
) {
self.validation = validation
self._value = .init(
initialValue: wrappedValue
)
self._isValid = .init(
initialValue: validation
.validate(wrappedValue)
)
self._value = .init(initialValue: wrappedValue)

var isValid = false
do {
try validation.validate(wrappedValue)
isValid = true
} catch {}

self._isValid = .init(initialValue: isValid)
}

/// Creates a new instance of `Validated`
Expand All @@ -95,37 +96,43 @@ public struct Validated<Value>: Validatable, DynamicProperty {
)
}

func revalidate() {
self.isValid = self.validate()
}

func validate() -> Bool {
do {
try self.validation.validate(self.value)
return true
} catch {
return false
}
}
}

// MARK: - Validated+validatedValue

public extension Validated {

/// The value if is valid otherwise returns `nil`
var validatedValue: Value? {
self.isValid ? self.value : nil
}

}

// MARK: - Validated+isInvalid

public extension Validated {

/// A Boolean value if the value is invalid
var isInvalid: Bool {
!self.isValid
}

}

// MARK: - Validated+isInvalidAfterChanges

public extension Validated {

/// A Boolean value if the value is invalid and has been previously modified
var isInvalidAfterChanges: Bool {
self.hasChanges && !self.isValid
}

}
Loading