diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..2e07d6a --- /dev/null +++ b/.swiftformat @@ -0,0 +1,81 @@ +# options +--swiftversion 5.6 +--self remove # redundantSelf +--importgrouping testable-bottom # sortedImports +--commas always # trailingCommas +--trimwhitespace always # trailingSpace +--indent 4 #indent +--ifdef no-indent #indent +--indentstrings true #indent +--wraparguments before-first # wrapArguments +--wrapparameters before-first # wrapArguments +--wrapcollections before-first # wrapArguments +--wrapconditions before-first # wrapArguments +--wrapreturntype if-multiline #wrapArguments +--closingparen balanced # wrapArguments +--wraptypealiases before-first # wrapArguments +--funcattributes prev-line # wrapAttributes +--typeattributes prev-line # wrapAttributes +--wrapternary before-operators # wrap +--structthreshold 20 # organizeDeclarations +--enumthreshold 20 # organizeDeclarations +--organizetypes class,struct,enum,extension,actor # organizeDeclarations +--markcategories false #organizeDeclarations +--extensionacl on-declarations # extensionAccessControl +--patternlet inline # hoistPatternLet +--redundanttype inferred # redundantType +--typeblanklines preserve # blankLinesAtStartOfScope, blankLinesAtEndOfScope +--emptybraces no-space # emptyBraces +--maxwidth 120 # wrap + +# rules +--rules andOperator +--rules anyObjectProtocol +--rules blankLinesAtEndOfScope +--rules blankLinesAtStartOfScope +--rules blankLinesBetweenScopes +--rules blockComments +--rules braces +--rules consecutiveSpaces +--rules duplicateImports +--rules emptyBraces +--rules enumNamespaces +--rules extensionAccessControl +--rules hoistPatternLet +--rules indent +--rules markTypes +--rules organizeDeclarations +--rules redundantClosure +--rules redundantFileprivate +--rules redundantGet +--rules redundantInit +--rules redundantLet +--rules redundantParens +--rules redundantPattern +--rules redundantRawValues +--rules redundantReturn +--rules redundantSelf +--rules redundantType +--rules redundantVoidReturnType +--rules sortDeclarations +--rules sortedImports +--rules spaceAroundBraces +--rules spaceAroundComments +--rules spaceAroundParens +--rules spaceInsideBraces +--rules spaceInsideBrackets +--rules spaceInsideComments +--rules spaceInsideParens +--rules strongifiedSelf +--rules todos +--rules trailingCommas +--rules trailingSpace +--rules typeSugar +--rules unusedArguments +--rules wrap +--rules wrapArguments +--rules wrapAttributes +--rules wrapConditionalBodies +--rules wrapEnumCases +--rules wrapMultilineStatementBraces +--rules wrapSwitchCases diff --git a/.swiftlint b/.swiftlint new file mode 100644 index 0000000..fe767fb --- /dev/null +++ b/.swiftlint @@ -0,0 +1,32 @@ +only_rules: + - colon + - fatal_error_message + - implicitly_unwrapped_optional + - legacy_cggeometry_functions + - legacy_constant + - legacy_constructor + - legacy_nsgeometry_functions + - operator_usage_whitespace + - return_arrow_whitespace + - trailing_newline + - unused_optional_binding + - vertical_whitespace + - void_return + - custom_rules + +excluded: + - Carthage + - Pods + - ./**/.build + +colon: + apply_to_dictionaries: false + +indentation: 2 + +custom_rules: + no_objcMembers: + name: "@objcMembers" + regex: "@objcMembers" + message: "Explicitly use @objc on each member you want to expose to Objective-C" + severity: error diff --git a/Package.swift b/Package.swift index 9f9dbb0..049addd 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,8 @@ let package = Package( // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "Republished", - targets: ["Republished"]), + targets: ["Republished"] + ) ], dependencies: [ // Dependencies declare other packages that this package depends on. @@ -21,9 +22,11 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "Republished", - dependencies: []), + dependencies: [] + ), .testTarget( name: "RepublishedTests", - dependencies: ["Republished"]), + dependencies: ["Republished"] + ) ] ) diff --git a/RepublishTestApp.swiftpm/App/App.swift b/RepublishTestApp.swiftpm/App/App.swift index afae4b4..0a48b60 100644 --- a/RepublishTestApp.swiftpm/App/App.swift +++ b/RepublishTestApp.swiftpm/App/App.swift @@ -2,7 +2,7 @@ import SwiftUI @main struct RepublishTestApp: App { - + var body: some Scene { WindowGroup { ContentView(viewModel: ViewModel(model: DomainModel())) diff --git a/RepublishTestApp.swiftpm/App/DomainModel.swift b/RepublishTestApp.swiftpm/App/DomainModel.swift index 3423030..0d31e61 100644 --- a/RepublishTestApp.swiftpm/App/DomainModel.swift +++ b/RepublishTestApp.swiftpm/App/DomainModel.swift @@ -2,21 +2,21 @@ import SwiftUI final class DomainModel: ObservableObject { - // A standard ObservableObject. + // A standard ObservableObject. // Updates to `count` makes the object fire a signal that // consumers can listen to to know when to read — and SwiftUI // does this by default. - + // However, if you nest this in another ObservableObject, there's // no inbuilt functionality to make the outer one fire for updates // in response to this inner one firing. - + // (An ObservableObject is a reference type, so an @Published field - // on the outer object containing this object as an inner one + // on the outer object containing this object as an inner one // isn't actually changing. - - @Published private(set) var count: Int = 0 + + @Published private(set) var count = 0 var isEven: Bool { count % 2 == 0 diff --git a/RepublishTestApp.swiftpm/App/ViewModel.swift b/RepublishTestApp.swiftpm/App/ViewModel.swift index d224deb..cc6047f 100644 --- a/RepublishTestApp.swiftpm/App/ViewModel.swift +++ b/RepublishTestApp.swiftpm/App/ViewModel.swift @@ -4,21 +4,6 @@ import SwiftUI @MainActor final class ViewModel: ObservableObject { - // Here the @Republished property wrapper is used to hold - // the nested object *instead* of an @Published property wrapper. - // (There are *no* @Published wrappers in this file.) - - // @Republished listens to the inner ObservableObject's - // change notifications and propagates them to the outer one. - - // SwiftUI views can use properties derived from the inner object - // normally — just like how they would use an @Published field. - // - // This outer object could also provide @Binding surfaces into - // the inner object's data. - - @Republished private var model: DomainModel - init(model: DomainModel) { _model = .init(wrappedValue: model) } @@ -33,9 +18,9 @@ final class ViewModel: ObservableObject { model.isMin ? "MININT" : nil, model.isPrime ? "prime" : nil ] - .compactMap { $0 } - .sorted() - .joined(separator: ", ") + .compactMap { $0 } + .sorted() + .joined(separator: ", ") } var countString: String { @@ -57,4 +42,20 @@ final class ViewModel: ObservableObject { func zero() { model.set(count: 0) } + + // Here the @Republished property wrapper is used to hold + // the nested object *instead* of an @Published property wrapper. + // (There are *no* @Published wrappers in this file.) + + // @Republished listens to the inner ObservableObject's + // change notifications and propagates them to the outer one. + + // SwiftUI views can use properties derived from the inner object + // normally — just like how they would use an @Published field. + // + // This outer object could also provide @Binding surfaces into + // the inner object's data. + + @Republished private var model: DomainModel + } diff --git a/RepublishTestApp.swiftpm/App/Views/CapsuleButton.swift b/RepublishTestApp.swiftpm/App/Views/CapsuleButton.swift index 6150f60..297f74e 100644 --- a/RepublishTestApp.swiftpm/App/Views/CapsuleButton.swift +++ b/RepublishTestApp.swiftpm/App/Views/CapsuleButton.swift @@ -5,8 +5,8 @@ struct CapsuleButton: View { let bg: (c: CGFloat, m: CGFloat, y: CGFloat, k: CGFloat) let fg: CGFloat let text: String - let action: () -> () - + let action: () -> Void + var body: some View { Button(text) { action() } .padding(16) diff --git a/RepublishTestApp.swiftpm/App/Views/ContentView.swift b/RepublishTestApp.swiftpm/App/Views/ContentView.swift index 048b207..bff9d68 100644 --- a/RepublishTestApp.swiftpm/App/Views/ContentView.swift +++ b/RepublishTestApp.swiftpm/App/Views/ContentView.swift @@ -1,9 +1,11 @@ import SwiftUI +// MARK: - ContentView + struct ContentView: View { // Regular direct use of outer ObservableObject - + @StateObject var viewModel: ViewModel var body: some View { @@ -20,28 +22,28 @@ struct ContentView: View { VStack(alignment: .center, spacing: 24) { Spacer() CapsuleButton( - bg: (1,0,0,0), + bg: (1, 0, 0, 0), fg: 1, text: "count += 1" ) { viewModel.increment() } CapsuleButton( - bg: (0,1,0,0), + bg: (0, 1, 0, 0), fg: 1, text: "count -= 1" ) { viewModel.decrement() } CapsuleButton( - bg: (0,0,1,0), + bg: (0, 0, 1, 0), fg: 0, text: "count = rand()" ) { viewModel.rand() } CapsuleButton( - bg: (0,0,0,1), + bg: (0, 0, 0, 1), fg: 1, text: "count = 0" ) { @@ -56,6 +58,7 @@ struct ContentView: View { } } +// MARK: - ContentView_Previews struct ContentView_Previews: PreviewProvider { static var previews: some View { diff --git a/RepublishTestApp.swiftpm/Package.swift b/RepublishTestApp.swiftpm/Package.swift index 36c933e..94c38cd 100644 --- a/RepublishTestApp.swiftpm/Package.swift +++ b/RepublishTestApp.swiftpm/Package.swift @@ -4,8 +4,8 @@ // This file is automatically generated. // Do not edit it by hand because the contents will be replaced. -import PackageDescription import AppleProductTypes +import PackageDescription let package = Package( name: "RepublishTestApp", diff --git a/RepublishedExampleApp/RepublishedExampleApp/App.swift b/RepublishedExampleApp/RepublishedExampleApp/App.swift index afae4b4..0a48b60 100644 --- a/RepublishedExampleApp/RepublishedExampleApp/App.swift +++ b/RepublishedExampleApp/RepublishedExampleApp/App.swift @@ -2,7 +2,7 @@ import SwiftUI @main struct RepublishTestApp: App { - + var body: some Scene { WindowGroup { ContentView(viewModel: ViewModel(model: DomainModel())) diff --git a/RepublishedExampleApp/RepublishedExampleApp/DomainModel.swift b/RepublishedExampleApp/RepublishedExampleApp/DomainModel.swift index 3423030..0d31e61 100644 --- a/RepublishedExampleApp/RepublishedExampleApp/DomainModel.swift +++ b/RepublishedExampleApp/RepublishedExampleApp/DomainModel.swift @@ -2,21 +2,21 @@ import SwiftUI final class DomainModel: ObservableObject { - // A standard ObservableObject. + // A standard ObservableObject. // Updates to `count` makes the object fire a signal that // consumers can listen to to know when to read — and SwiftUI // does this by default. - + // However, if you nest this in another ObservableObject, there's // no inbuilt functionality to make the outer one fire for updates // in response to this inner one firing. - + // (An ObservableObject is a reference type, so an @Published field - // on the outer object containing this object as an inner one + // on the outer object containing this object as an inner one // isn't actually changing. - - @Published private(set) var count: Int = 0 + + @Published private(set) var count = 0 var isEven: Bool { count % 2 == 0 diff --git a/RepublishedExampleApp/RepublishedExampleApp/ViewModel.swift b/RepublishedExampleApp/RepublishedExampleApp/ViewModel.swift index d224deb..cc6047f 100644 --- a/RepublishedExampleApp/RepublishedExampleApp/ViewModel.swift +++ b/RepublishedExampleApp/RepublishedExampleApp/ViewModel.swift @@ -4,21 +4,6 @@ import SwiftUI @MainActor final class ViewModel: ObservableObject { - // Here the @Republished property wrapper is used to hold - // the nested object *instead* of an @Published property wrapper. - // (There are *no* @Published wrappers in this file.) - - // @Republished listens to the inner ObservableObject's - // change notifications and propagates them to the outer one. - - // SwiftUI views can use properties derived from the inner object - // normally — just like how they would use an @Published field. - // - // This outer object could also provide @Binding surfaces into - // the inner object's data. - - @Republished private var model: DomainModel - init(model: DomainModel) { _model = .init(wrappedValue: model) } @@ -33,9 +18,9 @@ final class ViewModel: ObservableObject { model.isMin ? "MININT" : nil, model.isPrime ? "prime" : nil ] - .compactMap { $0 } - .sorted() - .joined(separator: ", ") + .compactMap { $0 } + .sorted() + .joined(separator: ", ") } var countString: String { @@ -57,4 +42,20 @@ final class ViewModel: ObservableObject { func zero() { model.set(count: 0) } + + // Here the @Republished property wrapper is used to hold + // the nested object *instead* of an @Published property wrapper. + // (There are *no* @Published wrappers in this file.) + + // @Republished listens to the inner ObservableObject's + // change notifications and propagates them to the outer one. + + // SwiftUI views can use properties derived from the inner object + // normally — just like how they would use an @Published field. + // + // This outer object could also provide @Binding surfaces into + // the inner object's data. + + @Republished private var model: DomainModel + } diff --git a/RepublishedExampleApp/RepublishedExampleApp/Views/CapsuleButton.swift b/RepublishedExampleApp/RepublishedExampleApp/Views/CapsuleButton.swift index 6150f60..297f74e 100644 --- a/RepublishedExampleApp/RepublishedExampleApp/Views/CapsuleButton.swift +++ b/RepublishedExampleApp/RepublishedExampleApp/Views/CapsuleButton.swift @@ -5,8 +5,8 @@ struct CapsuleButton: View { let bg: (c: CGFloat, m: CGFloat, y: CGFloat, k: CGFloat) let fg: CGFloat let text: String - let action: () -> () - + let action: () -> Void + var body: some View { Button(text) { action() } .padding(16) diff --git a/RepublishedExampleApp/RepublishedExampleApp/Views/ContentView.swift b/RepublishedExampleApp/RepublishedExampleApp/Views/ContentView.swift index 048b207..bff9d68 100644 --- a/RepublishedExampleApp/RepublishedExampleApp/Views/ContentView.swift +++ b/RepublishedExampleApp/RepublishedExampleApp/Views/ContentView.swift @@ -1,9 +1,11 @@ import SwiftUI +// MARK: - ContentView + struct ContentView: View { // Regular direct use of outer ObservableObject - + @StateObject var viewModel: ViewModel var body: some View { @@ -20,28 +22,28 @@ struct ContentView: View { VStack(alignment: .center, spacing: 24) { Spacer() CapsuleButton( - bg: (1,0,0,0), + bg: (1, 0, 0, 0), fg: 1, text: "count += 1" ) { viewModel.increment() } CapsuleButton( - bg: (0,1,0,0), + bg: (0, 1, 0, 0), fg: 1, text: "count -= 1" ) { viewModel.decrement() } CapsuleButton( - bg: (0,0,1,0), + bg: (0, 0, 1, 0), fg: 0, text: "count = rand()" ) { viewModel.rand() } CapsuleButton( - bg: (0,0,0,1), + bg: (0, 0, 0, 1), fg: 1, text: "count = 0" ) { @@ -56,6 +58,7 @@ struct ContentView: View { } } +// MARK: - ContentView_Previews struct ContentView_Previews: PreviewProvider { static var previews: some View { diff --git a/Sources/Republished/Republished.swift b/Sources/Republished/Republished.swift index 56308ab..1ae5c17 100644 --- a/Sources/Republished/Republished.swift +++ b/Sources/Republished/Republished.swift @@ -11,8 +11,8 @@ import SwiftUI /// ```swift /// @Republished private var inner: InnerObservableObject /// ``` -/// -/// The inner `ObservableObject's` `objectWillChange` notifications will be +/// +/// The inner `ObservableObject's` `objectWillChange` notifications will be /// re-emitted by the outer `ObservableObject` allowing it to provide accessors /// derived from the inner one's values. /// @@ -27,10 +27,7 @@ import SwiftUI public class Republished where Republishing.ObjectWillChangePublisher == ObservableObjectPublisher { - private var republished: Republishing - private var cancellable: AnyCancellable? - - public init(wrappedValue republished: Republishing) { + public init(wrappedValue republished: Republishing) { self.republished = republished } @@ -38,31 +35,25 @@ public class Republished republished } - var republishedSelf: Republished { - self - } - public var projectedValue: Binding { Binding { self.republished } set: { newValue in self.republished = newValue } - } public static subscript< Instance: ObservableObject >( _enclosingInstance instance: Instance, - wrapped wrappedKeyPath: KeyPath, + wrapped _: KeyPath, storage storageKeyPath: KeyPath - ) -> Republishing where Instance.ObjectWillChangePublisher == ObservableObjectPublisher { - + ) + -> Republishing where Instance.ObjectWillChangePublisher == ObservableObjectPublisher { let storage = instance[keyPath: storageKeyPath] let wrapped = storage.republishedSelf - if storage.republishedSelf.cancellable == nil { storage.republishedSelf.cancellable = wrapped .wrappedValue @@ -74,4 +65,12 @@ public class Republished return wrapped.wrappedValue } + + var republishedSelf: Republished { + self + } + + private var republished: Republishing + private var cancellable: AnyCancellable? + } diff --git a/Tests/RepublishedTests/RepublishedTests.swift b/Tests/RepublishedTests/RepublishedTests.swift index 8bf5e3e..1105e1f 100644 --- a/Tests/RepublishedTests/RepublishedTests.swift +++ b/Tests/RepublishedTests/RepublishedTests.swift @@ -3,6 +3,8 @@ import SwiftUI import XCTest @testable import Republished +// MARK: - RepublishedTests + @MainActor final class RepublishedTests: XCTestCase { @@ -48,10 +50,14 @@ final class RepublishedTests: XCTestCase { } +// MARK: - OuterObject + final class OuterObject: ObservableObject { @Republished var object = RepublishedObject() } +// MARK: - RepublishedObject + final class RepublishedObject: ObservableObject { @Published var x = 10 @Published private(set) var y = "Hello"