diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Republished.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Republished.xcscheme index af7ab88..6aaa0c6 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Republished.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Republished.xcscheme @@ -29,7 +29,7 @@ shouldUseLaunchSchemeArgsEnv = "YES"> diff --git a/Package.swift b/Package.swift index 049addd..951ee16 100644 --- a/Package.swift +++ b/Package.swift @@ -11,7 +11,7 @@ let package = Package( .library( name: "Republished", targets: ["Republished"] - ) + ), ], dependencies: [ // Dependencies declare other packages that this package depends on. @@ -27,6 +27,6 @@ let package = Package( .testTarget( name: "RepublishedTests", dependencies: ["Republished"] - ) + ), ] ) diff --git a/README.md b/README.md index 3f3593e..3cae04a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ from inner `ObservableObjects` that it actually accesses. You can use this library via Swift Package Manger by adding a dependency in your Package.swift. ```swift -.package(url: "https://github.com/adam-zethraeus/Republished", from: "0.1.0") +.package(url: "https://github.com/adam-zethraeus/Republished", from: "1.0.1") ``` ## Example App diff --git a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/CachedManifest.plist b/RepublishTestApp.swiftpm/.swiftpm/playgrounds/CachedManifest.plist deleted file mode 100644 index ec45f98..0000000 --- a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/CachedManifest.plist +++ /dev/null @@ -1,41 +0,0 @@ - - - - - CachedManifest - - manifestData - - eyJkZXBlbmRlbmNpZXMiOltdLCJkaXNwbGF5TmFtZSI6IlJlcHVibGlzaFRl - c3RBcHAiLCJwYWNrYWdlS2luZCI6eyJyb290Ijp7fX0sInBsYXRmb3JtcyI6 - W3sib3B0aW9ucyI6W10sInBsYXRmb3JtTmFtZSI6ImlvcyIsInZlcnNpb24i - OiIxNS4yIn1dLCJwcm9kdWN0cyI6W3sibmFtZSI6IlJlcHVibGlzaFRlc3RB - cHAiLCJzZXR0aW5ncyI6W3siZGlzcGxheVZlcnNpb24iOlsiMS4wIl19LHsi - YnVuZGxlVmVyc2lvbiI6WyIxIl19LHsiaU9TQXBwSW5mbyI6W3siYWNjZW50 - Q29sb3IiOnsicHJlc2V0Q29sb3IiOnsicHJlc2V0Q29sb3IiOnsicmF3VmFs - dWUiOiJwaW5rIn19fSwiYXBwSWNvbiI6eyJwbGFjZWhvbGRlciI6eyJpY29u - Ijp7InJhd1ZhbHVlIjoiY2FsZW5kYXIifX19LCJjYXBhYmlsaXRpZXMiOltd - LCJzdXBwb3J0ZWREZXZpY2VGYW1pbGllcyI6WyJwYWQiLCJwaG9uZSJdLCJz - dXBwb3J0ZWRJbnRlcmZhY2VPcmllbnRhdGlvbnMiOlt7InBvcnRyYWl0Ijp7 - fX0seyJsYW5kc2NhcGVSaWdodCI6e319LHsibGFuZHNjYXBlTGVmdCI6e319 - LHsicG9ydHJhaXRVcHNpZGVEb3duIjp7ImNvbmRpdGlvbiI6eyJkZXZpY2VG - YW1pbGllcyI6WyJwYWQiXX19fV19XX1dLCJ0YXJnZXRzIjpbIkFwcCJdLCJ0 - eXBlIjp7ImV4ZWN1dGFibGUiOm51bGx9fV0sInRhcmdldE1hcCI6eyJBcHAi - OnsiZGVwZW5kZW5jaWVzIjpbXSwiZXhjbHVkZSI6W10sIm5hbWUiOiJBcHAi - LCJwYXRoIjoiQXBwIiwicmVzb3VyY2VzIjpbXSwic2V0dGluZ3MiOltdLCJ0 - eXBlIjoiZXhlY3V0YWJsZSJ9fSwidGFyZ2V0cyI6W3siZGVwZW5kZW5jaWVz - IjpbXSwiZXhjbHVkZSI6W10sIm5hbWUiOiJBcHAiLCJwYXRoIjoiQXBwIiwi - cmVzb3VyY2VzIjpbXSwic2V0dGluZ3MiOltdLCJ0eXBlIjoiZXhlY3V0YWJs - ZSJ9XSwidG9vbHNWZXJzaW9uIjp7Il92ZXJzaW9uIjoiNS42LjAifX0= - - manifestHash - - 67WQxc7+xhzNa6Ms8hXit4D9TlQCnVRJTuqNMN96+Yw= - - schemaVersion - 4 - swiftPMVersionString - 5.6.0-dev - - - diff --git a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.plist b/RepublishTestApp.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.plist deleted file mode 100644 index e13743f..0000000 --- a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.plist +++ /dev/null @@ -1,19 +0,0 @@ - - - - - DocumentThumbnailConfiguration - - accentColorHash - - pnpByLx51dqRe1BR8fDT9a60tjuiRrNUapYe96PH2TE= - - appIconHash - - UVJ5DieOuJA5+L+qNUuUTsG0TF0/wUTtxXIMPtwEXHM= - - thumbnailIsPrerendered - - - - diff --git a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.png b/RepublishTestApp.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.png deleted file mode 100644 index 3e29a79..0000000 Binary files a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.png and /dev/null differ diff --git a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/Workspace.plist b/RepublishTestApp.swiftpm/.swiftpm/playgrounds/Workspace.plist deleted file mode 100644 index e713dbb..0000000 --- a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/Workspace.plist +++ /dev/null @@ -1,13 +0,0 @@ - - - - - AppSettings - - appIconPlaceholderGlyphName - calendar - appSettingsVersion - 1 - - - diff --git a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/contentInfo.plist b/RepublishTestApp.swiftpm/.swiftpm/playgrounds/contentInfo.plist deleted file mode 100644 index 4ffd5fe..0000000 --- a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/contentInfo.plist +++ /dev/null @@ -1,12 +0,0 @@ - - - - - contentVersion - 1.0.1 - contentIdentifier - com.apple.playgrounds.recognizinggestures - guideVersion - 1.0.0 - - diff --git a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/version.plist b/RepublishTestApp.swiftpm/.swiftpm/playgrounds/version.plist deleted file mode 100644 index 090c480..0000000 --- a/RepublishTestApp.swiftpm/.swiftpm/playgrounds/version.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - BuildVersion - 4 - ProductBuildVersion - 5E95 - - diff --git a/RepublishTestApp.swiftpm/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/RepublishTestApp.swiftpm/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/RepublishTestApp.swiftpm/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/RepublishTestApp.swiftpm/App/App.swift b/RepublishTestApp.swiftpm/App/App.swift deleted file mode 100644 index 0a48b60..0000000 --- a/RepublishTestApp.swiftpm/App/App.swift +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 0d31e61..0000000 --- a/RepublishTestApp.swiftpm/App/DomainModel.swift +++ /dev/null @@ -1,62 +0,0 @@ -import SwiftUI - -final class DomainModel: 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 - // isn't actually changing. - - @Published private(set) var count = 0 - - var isEven: Bool { - count % 2 == 0 - } - - var isZero: Bool { - count == 0 - } - - var isNegative: Bool { - count < 0 - } - - var isPositive: Bool { - count > 0 - } - - var isMax: Bool { - count == Int.max - } - - var isMin: Bool { - count == Int.min - } - - var isPrime: Bool { - switch true { - case count < 2: return false - case count < 4: return true - default: - return (2...Int(Double(count).squareRoot())) - .lazy - .filter { [count] div in - count % div == 0 - } - .first == nil - } - } - - func set(count: Int) { - self.count = count - } -} diff --git a/RepublishTestApp.swiftpm/App/ViewModel.swift b/RepublishTestApp.swiftpm/App/ViewModel.swift deleted file mode 100644 index cc6047f..0000000 --- a/RepublishTestApp.swiftpm/App/ViewModel.swift +++ /dev/null @@ -1,61 +0,0 @@ -import Republished -import SwiftUI - -@MainActor -final class ViewModel: ObservableObject { - - init(model: DomainModel) { - _model = .init(wrappedValue: model) - } - - var info: String { - [ - model.isEven ? "even" : nil, - model.isZero ? "zero" : nil, - model.isNegative ? "negative" : nil, - model.isPositive ? "positive" : nil, - model.isMax ? "MAXINT" : nil, - model.isMin ? "MININT" : nil, - model.isPrime ? "prime" : nil - ] - .compactMap { $0 } - .sorted() - .joined(separator: ", ") - } - - var countString: String { - "\(model.count)" - } - - func increment() { - model.set(count: model.count + 1) - } - - func decrement() { - model.set(count: model.count - 1) - } - - func rand() { - model.set(count: Int.random(in: Int.min...Int.max)) - } - - 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 deleted file mode 100644 index 297f74e..0000000 --- a/RepublishTestApp.swiftpm/App/Views/CapsuleButton.swift +++ /dev/null @@ -1,34 +0,0 @@ -import SwiftUI - -struct CapsuleButton: View { - - let bg: (c: CGFloat, m: CGFloat, y: CGFloat, k: CGFloat) - let fg: CGFloat - let text: String - let action: () -> Void - - var body: some View { - Button(text) { action() } - .padding(16) - .background( - Color( - CGColor( - genericCMYKCyan: bg.c, - magenta: bg.m, - yellow: bg.y, - black: bg.k, - alpha: 1 - ) - ) - ) - .foregroundColor( - Color( - hue: 0, - saturation: 0, - brightness: fg - ) - ) - .clipShape(Capsule()) - } - -} diff --git a/RepublishTestApp.swiftpm/App/Views/ContentView.swift b/RepublishTestApp.swiftpm/App/Views/ContentView.swift deleted file mode 100644 index bff9d68..0000000 --- a/RepublishTestApp.swiftpm/App/Views/ContentView.swift +++ /dev/null @@ -1,67 +0,0 @@ -import SwiftUI - -// MARK: - ContentView - -struct ContentView: View { - - // Regular direct use of outer ObservableObject - - @StateObject var viewModel: ViewModel - - var body: some View { - ScrollView { - VStack(alignment: .center, spacing: 24) { - Spacer() - Text(viewModel.countString) - .font(.title) - .fontWeight(.bold) - .scaledToFit() - Text(viewModel.info) - .font(.body.monospaced()) - Spacer() - VStack(alignment: .center, spacing: 24) { - Spacer() - CapsuleButton( - bg: (1, 0, 0, 0), - fg: 1, - text: "count += 1" - ) { - viewModel.increment() - } - CapsuleButton( - bg: (0, 1, 0, 0), - fg: 1, - text: "count -= 1" - ) { - viewModel.decrement() - } - CapsuleButton( - bg: (0, 0, 1, 0), - fg: 0, - text: "count = rand()" - ) { - viewModel.rand() - } - CapsuleButton( - bg: (0, 0, 0, 1), - fg: 1, - text: "count = 0" - ) { - viewModel.zero() - } - Spacer() - } - } - .frame(maxWidth: .infinity) - } - .background(.gray.opacity(0.6)) - } -} - -// MARK: - ContentView_Previews - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView(viewModel: ViewModel(model: DomainModel())) - } -} diff --git a/RepublishTestApp.swiftpm/Package.swift b/RepublishTestApp.swiftpm/Package.swift deleted file mode 100644 index 94c38cd..0000000 --- a/RepublishTestApp.swiftpm/Package.swift +++ /dev/null @@ -1,45 +0,0 @@ -// swift-tools-version: 5.6 - -// WARNING: -// This file is automatically generated. -// Do not edit it by hand because the contents will be replaced. - -import AppleProductTypes -import PackageDescription - -let package = Package( - name: "RepublishTestApp", - platforms: [ - .iOS("15.2") - ], - products: [ - .iOSApplication( - name: "RepublishTestApp", - targets: ["App"], - displayVersion: "1.0", - bundleVersion: "1", - appIcon: .placeholder(icon: .calendar), - accentColor: .presetColor(.pink), - supportedDeviceFamilies: [ - .pad, - .phone - ], - supportedInterfaceOrientations: [ - .portrait, - .landscapeRight, - .landscapeLeft, - .portraitUpsideDown(.when(deviceFamilies: [.pad])) - ] - ) - ], - dependencies: [ - .package(path: "..") - ], - targets: [ - .executableTarget( - name: "App", - dependencies: ["Republished"], - path: "App" - ) - ] -) diff --git a/RepublishedExampleApp/RepublishedExampleApp/DomainModel.swift b/RepublishedExampleApp/RepublishedExampleApp/DomainModel.swift index 0d31e61..aa86139 100644 --- a/RepublishedExampleApp/RepublishedExampleApp/DomainModel.swift +++ b/RepublishedExampleApp/RepublishedExampleApp/DomainModel.swift @@ -5,16 +5,16 @@ final class DomainModel: 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. + // consumers can listen to to know when to read — and that + // SwiftUI listens to by default. - // However, if you nest this in another ObservableObject, there's + // 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 - // isn't actually changing. + // An ObservableObject is a reference type, so an @Published field + // on the outer object (which contains this object) the field does + // not actually change. @Published private(set) var count = 0 diff --git a/RepublishedExampleApp/RepublishedExampleApp/ViewModel.swift b/RepublishedExampleApp/RepublishedExampleApp/ViewModel.swift index cc6047f..c42a9a9 100644 --- a/RepublishedExampleApp/RepublishedExampleApp/ViewModel.swift +++ b/RepublishedExampleApp/RepublishedExampleApp/ViewModel.swift @@ -4,6 +4,21 @@ import SwiftUI @MainActor final class ViewModel: ObservableObject { + // Here the @Republished property wrapper is used *instead* of + // an @Published property wrapper and hold the nested ObservableObject. + // (Note that 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 here derived from the inner object + // just as 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) } @@ -16,7 +31,7 @@ final class ViewModel: ObservableObject { model.isPositive ? "positive" : nil, model.isMax ? "MAXINT" : nil, model.isMin ? "MININT" : nil, - model.isPrime ? "prime" : nil + model.isPrime ? "prime" : nil, ] .compactMap { $0 } .sorted() @@ -43,19 +58,4 @@ final class ViewModel: ObservableObject { 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/Sources/Republished/Republished.swift b/Sources/Republished/Republished.swift index 1ae5c17..67ad5f4 100644 --- a/Sources/Republished/Republished.swift +++ b/Sources/Republished/Republished.swift @@ -24,7 +24,7 @@ import SwiftUI /// > of inner `ObservableObjects` that it actually accesses. @MainActor @propertyWrapper -public class Republished +public final class Republished where Republishing.ObjectWillChangePublisher == ObservableObjectPublisher { public init(wrappedValue republished: Republishing) { @@ -52,10 +52,9 @@ public class Republished ) -> Republishing where Instance.ObjectWillChangePublisher == ObservableObjectPublisher { let storage = instance[keyPath: storageKeyPath] - let wrapped = storage.republishedSelf - if storage.republishedSelf.cancellable == nil { - storage.republishedSelf.cancellable = wrapped + if storage.cancellable == nil { + storage.cancellable = storage .wrappedValue .objectWillChange .sink { [objectWillChange = instance.objectWillChange] in @@ -63,11 +62,7 @@ public class Republished } } - return wrapped.wrappedValue - } - - var republishedSelf: Republished { - self + return storage.wrappedValue } private var republished: Republishing