├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Images ├── migration_fixit_code_1.png ├── migration_fixit_code_2.png ├── migration_fixit_code_applied_1.png └── migration_fixit_code_applied_2.png ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Invalidating │ ├── AnyViewInvalidating.swift │ ├── Imports.swift │ ├── InvalidatingStaticMember+Extensions.swift │ ├── InvalidatingStaticMember.swift │ ├── Invalidations.swift │ ├── Tuple.swift │ ├── ViewInvalidating+Extensions.swift │ └── ViewInvalidating.swift └── Tests └── InvalidatingTests └── InvalidatingTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Images/migration_fixit_code_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theblixguy/Invalidating/2e4c4d234e9dd7a0aeb3770fd0f441e78bac25bc/Images/migration_fixit_code_1.png -------------------------------------------------------------------------------- /Images/migration_fixit_code_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theblixguy/Invalidating/2e4c4d234e9dd7a0aeb3770fd0f441e78bac25bc/Images/migration_fixit_code_2.png -------------------------------------------------------------------------------- /Images/migration_fixit_code_applied_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theblixguy/Invalidating/2e4c4d234e9dd7a0aeb3770fd0f441e78bac25bc/Images/migration_fixit_code_applied_1.png -------------------------------------------------------------------------------- /Images/migration_fixit_code_applied_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theblixguy/Invalidating/2e4c4d234e9dd7a0aeb3770fd0f441e78bac25bc/Images/migration_fixit_code_applied_2.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Suyash Srijan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Invalidating", 8 | platforms: [ 9 | .iOS(.v11), 10 | .tvOS(.v11), 11 | .macOS(.v10_11), 12 | ], 13 | products: [ 14 | .library( 15 | name: "Invalidating", 16 | targets: ["Invalidating"]), 17 | ], 18 | dependencies: [], 19 | targets: [ 20 | .target( 21 | name: "Invalidating", 22 | dependencies: []), 23 | .testTarget( 24 | name: "InvalidatingTests", 25 | dependencies: ["Invalidating"]), 26 | ], 27 | swiftLanguageVersions: [.v5] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ViewInvalidating 2 | 3 | A property wrapper that backports the new `@Invalidating` property wrapper to older versions of iOS/tvOS/macOS. For more information on this new property wrapper, see the WWDC 2021 talk ["What's new in AppKit"](https://developer.apple.com/wwdc21/10054) for a brief introduction. 4 | 5 | The syntax and types closely follows what Apple is doing, so when it's time to finally update your project's deployment target to iOS 15+/tvOS 15+/macOS 12+, you can easily migrate to using Apple's version by making very minimal changes. See the [migration section](#migration-to-ios-15+/tvos-15+/macos-12+-deployment-target) for more info! 6 | 7 | ## Usage 8 | 9 | Annotate your `Equatable` properties with `@ViewInvalidating` and provide options that will be used to invalidate the view whenever the property's value changes: 10 | 11 | ```swift 12 | final class MyView: UIView { 13 | // Calls setNeedsLayout() 14 | @ViewInvalidating(.layout) var cornerRadius: CGFloat = 12.0 15 | 16 | // Calls setNeedsLayout() then setNeedsUpdateConstraints() 17 | @ViewInvalidating(.layout, .constraints) var heightConstraintValue: CFloat = 200.0 18 | 19 | // Calls setNeedsLayout() then setNeedsUpdateConstraints() then invalidateIntrinsicContentSize() 20 | @ViewInvalidating(.layout, .constraints, .intrinsicContentSize) var magicProperty: CGFloat = 1234.0 21 | ``` 22 | 23 | You can initialize the property wrapper with up to 10 options. You can of course add extensions to support more options though, but realistically speaking you'll likely never have a need to pass more than a few of them! 24 | 25 | By default, there is support for a total of 5 invalidation options per platform: 26 | 27 | #### Common 28 | - Layout 29 | - Display 30 | - Constraints 31 | - Intrinsic Size 32 | 33 | #### macOS only 34 | - Restorable State 35 | 36 | #### iOS 14+ only 37 | - Configuration 38 | 39 | ### Adding custom invalidators 40 | 41 | You can add custom invalidators by creating a type that conforms to `UIViewInvalidatingType` or `NSViewInvalidatingType` protocol (depending on the target platform) and implementing the `invalidate` method requirement: 42 | 43 | ```swift 44 | extension Invalidations { 45 | struct Focus: UIViewInvalidatingType { 46 | static let focus: Self = .init() 47 | 48 | func invalidate(view: UIView) { 49 | view.setNeedsFocusUpdate() 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | You can then expose it to the property wrapper by extending the `InvalidatingStaticMember` type: 56 | 57 | ```swift 58 | extension InvalidatingStaticMember where Base: UIViewInvalidatingType { 59 | static var focus: InvalidatingStaticMember { .init(.focus) } 60 | } 61 | ``` 62 | 63 | > #### Note: 64 | > 65 | > If you're using Xcode 13, you should do this instead: 66 | > 67 | > ```swift 68 | > extension UIViewInvalidatingType where Self == Invalidations.Focus { 69 | > static var focus: Self { .focus } 70 | > } 71 | > ``` 72 | 73 | > The `InvalidatingStaticMember` type only exists to workaround some language limitations which have been addressed in Swift 5.5, which ships with Xcode 13. So if you're on the latest Xcode, you do not need to use the workaround. The `InvalidatingStaticMember` will also be unavailable. 74 | 75 | Then you can use your new invalidator on `@ViewInvalidating`: 76 | 77 | ```swift 78 | final class MyView: UIView { 79 | 80 | // Calls setNeedsLayout() and Focus.invalidate(self) 81 | @ViewInvalidating(.layout, .focus) var customProperty: CGFloat = 1.0 82 | } 83 | ``` 84 | 85 | ## Requirements 86 | 87 | - Deployment target of iOS 11+, tvOS 11+ or macOS 10.11+ 88 | - Xcode 11 or above 89 | 90 | ## Installation 91 | 92 | Add the following to your project's `Package.swift` file: 93 | 94 | ```swift 95 | .package(url: "https://github.com/theblixguy/Invalidating", from: "0.1.0") 96 | ``` 97 | 98 | or add this package via the Xcode UI by going to File > Swift Packages > Add Package Dependency. 99 | 100 | ## Migration to iOS 15+/tvOS 15+/macOS 12+ deployment target 101 | 102 | When it's time to update your project's deployment target to iOS 15+/tvOS 15+/macOS 12+, you will need to make some very minor changes to your code to make it compatible with Apple's `@Invalidating` and related types. 103 | 104 | The types that ship with this package have been annotated with `@available` and contain the right mappings to Apple's types on its `renamed` argument to make it super easy for you to update your code. Once you have update the deployment target, you will see some errors: 105 | 106 | ![Migration Fix-it](Images/migration_fixit_code_1.png) 107 | ![Migration Fix-it](Images/migration_fixit_code_2.png) 108 | 109 | As you can see, they all offer a fix-it to automatically change the type names. With a click of a button, the errors disappears without you even having to manually rename them: 110 | 111 | ![Migration Fix-it](Images/migration_fixit_code_applied_1.png) 112 | ![Migration Fix-it](Images/migration_fixit_code_applied_2.png) 113 | 114 | ✨ 115 | 116 | ## License 117 | 118 | ``` 119 | MIT License 120 | 121 | Copyright (c) 2021 Suyash Srijan 122 | 123 | Permission is hereby granted, free of charge, to any person obtaining a copy 124 | of this software and associated documentation files (the "Software"), to deal 125 | in the Software without restriction, including without limitation the rights 126 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 127 | copies of the Software, and to permit persons to whom the Software is 128 | furnished to do so, subject to the following conditions: 129 | 130 | The above copyright notice and this permission notice shall be included in all 131 | copies or substantial portions of the Software. 132 | 133 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 134 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 135 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 136 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 137 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 138 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 139 | SOFTWARE. 140 | ``` 141 | -------------------------------------------------------------------------------- /Sources/Invalidating/AnyViewInvalidating.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyInvalidation.swift 3 | // 4 | // 5 | // Created by Suyash Srijan on 28/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | #if compiler(<5.5) 11 | internal struct AnyViewInvalidating: InvalidatingViewProtocol { 12 | let base: InvalidatingViewProtocol 13 | 14 | init(base: InvalidatingViewProtocol) { 15 | self.base = base 16 | } 17 | 18 | func invalidate(view: InvalidatingViewType) { 19 | base.invalidate(view: view) 20 | } 21 | } 22 | #endif 23 | -------------------------------------------------------------------------------- /Sources/Invalidating/Imports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Imports.swift 3 | // 4 | // 5 | // Created by Suyash Srijan on 28/06/2021. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import UIKit 10 | 11 | @available(iOS, introduced: 11, obsoleted: 15, renamed: "UIView") 12 | public typealias InvalidatingViewType = UIView 13 | 14 | @available(iOS, introduced: 11, obsoleted: 15, renamed: "UIViewInvalidating") 15 | public typealias InvalidatingViewProtocol = UIViewInvalidatingType 16 | 17 | @available(iOS, introduced: 11, obsoleted: 15, renamed: "UIViewInvalidating") 18 | public protocol UIViewInvalidatingType { 19 | #if compiler(<5.5) 20 | typealias Member = InvalidatingStaticMember 21 | #endif 22 | func invalidate(view: InvalidatingViewType) 23 | } 24 | #elseif os(macOS) 25 | import AppKit 26 | 27 | @available(macOS, introduced: 10.11, obsoleted: 12, renamed: "NSView") 28 | public typealias InvalidatingViewType = NSView 29 | 30 | @available(macOS, introduced: 10.11, obsoleted: 12, renamed: "NSViewInvalidating") 31 | public typealias InvalidatingViewProtocol = NSViewInvalidatingType 32 | 33 | @available(macOS, introduced: 10.11, obsoleted: 12, renamed: "NSViewInvalidating") 34 | public protocol NSViewInvalidatingType { 35 | #if compiler(<5.5) 36 | typealias Member = InvalidatingStaticMember 37 | #endif 38 | func invalidate(view: InvalidatingViewType) 39 | } 40 | #else 41 | #error("Unsupported platform") 42 | #endif 43 | -------------------------------------------------------------------------------- /Sources/Invalidating/InvalidatingStaticMember+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvalidatingStaticMember+Extensions.swift 3 | // 4 | // 5 | // Created by Suyash Srijan on 28/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | #if compiler(>=5.5) 11 | @available(iOS, introduced: 11, obsoleted: 15) 12 | public extension InvalidatingViewProtocol where Self == Invalidations.Layout { 13 | static var layout: Self { .layout } 14 | } 15 | 16 | @available(iOS, introduced: 11, obsoleted: 15) 17 | public extension InvalidatingViewProtocol where Self == Invalidations.Display { 18 | static var display: Self { .display } 19 | } 20 | 21 | @available(iOS, introduced: 11, obsoleted: 15) 22 | public extension InvalidatingViewProtocol where Self == Invalidations.Constraints { 23 | static var constraints: Self { .constraints } 24 | } 25 | 26 | @available(iOS, introduced: 11, obsoleted: 15) 27 | public extension InvalidatingViewProtocol where Self == Invalidations.IntrinsicContentSize { 28 | static var intrinsicContentSize: Self { .intrinsicContentSize } 29 | } 30 | 31 | #if os(macOS) 32 | @available(macOS, introduced: 10.11, obsoleted: 12) 33 | public extension InvalidatingViewProtocol where Self == Invalidations.RestorableState { 34 | static var restorableState: Self { .restorableState } 35 | } 36 | #endif 37 | 38 | #if os(iOS) || os(tvOS) 39 | @available(iOS, introduced: 11, obsoleted: 15) 40 | public extension InvalidatingViewProtocol where Self == Invalidations.Configuration { 41 | static var configuration: Self { .configuration } 42 | } 43 | #endif 44 | 45 | #else 46 | public extension InvalidatingStaticMember where Base: InvalidatingViewProtocol { 47 | static var layout: InvalidatingStaticMember { .init(.layout) } 48 | 49 | static var display: InvalidatingStaticMember { .init(.display) } 50 | 51 | static var constraints: InvalidatingStaticMember { .init(.constraints) } 52 | 53 | static var intrinsicContentSize: InvalidatingStaticMember { .init(.intrinsicContentSize) } 54 | 55 | #if os(macOS) 56 | static var restorableState: InvalidatingStaticMember { .init(.restorableState) } 57 | #endif 58 | 59 | #if os(iOS) || os(tvOS) 60 | static var configuration: InvalidatingStaticMember { .init(.configuration) } 61 | #endif 62 | } 63 | #endif 64 | -------------------------------------------------------------------------------- /Sources/Invalidating/InvalidatingStaticMember.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvalidatingStaticMember.swift 3 | // 4 | // 5 | // Created by Suyash Srijan on 28/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | #if compiler(<5.5) 11 | public struct InvalidatingStaticMember { 12 | public let base: Base 13 | public init(_ base: Base) { 14 | self.base = base 15 | } 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /Sources/Invalidating/Invalidations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Invalidations.swift 3 | // 4 | // 5 | // Created by Suyash Srijan on 28/06/2021. 6 | // 7 | 8 | import Foundation 9 | #if os(iOS) || os(tvOS) 10 | import UIKit 11 | #endif 12 | 13 | @available(iOS, introduced: 11, obsoleted: 15, renamed: "UIView.Invalidations") 14 | @available(tvOS, introduced: 11, obsoleted: 15, renamed: "UIView.Invalidations") 15 | @available(macOS, introduced: 10.11, obsoleted: 12, renamed: "NSView.Invalidations") 16 | public enum Invalidations { 17 | public struct Layout: InvalidatingViewProtocol { 18 | public static let layout: Self = .init() 19 | 20 | public func invalidate(view: InvalidatingViewType) { 21 | #if os(iOS) || os(tvOS) 22 | view.setNeedsLayout() 23 | #elseif os(macOS) 24 | view.needsLayout = true 25 | #endif 26 | } 27 | } 28 | 29 | public struct Display: InvalidatingViewProtocol { 30 | public static let display: Self = .init() 31 | 32 | public func invalidate(view: InvalidatingViewType) { 33 | #if os(iOS) || os(tvOS) 34 | view.setNeedsDisplay() 35 | #elseif os(macOS) 36 | view.setNeedsDisplay(view.bounds) 37 | #endif 38 | } 39 | } 40 | 41 | public struct Constraints: InvalidatingViewProtocol { 42 | public static let constraints: Self = .init() 43 | 44 | public func invalidate(view: InvalidatingViewType) { 45 | #if os(iOS) || os(tvOS) 46 | view.setNeedsUpdateConstraints() 47 | #elseif os(macOS) 48 | view.needsUpdateConstraints = true 49 | #endif 50 | } 51 | } 52 | 53 | public struct IntrinsicContentSize: InvalidatingViewProtocol { 54 | public static let intrinsicContentSize: Self = .init() 55 | 56 | public func invalidate(view: InvalidatingViewType) { 57 | view.invalidateIntrinsicContentSize() 58 | } 59 | } 60 | 61 | #if os(macOS) 62 | public struct RestorableState: InvalidatingViewProtocol { 63 | public static let restorableState: Self = .init() 64 | 65 | public func invalidate(view: InvalidatingViewType) { 66 | view.invalidateRestorableState() 67 | } 68 | } 69 | #endif 70 | 71 | #if os(iOS) || os(tvOS) 72 | public struct Configuration: InvalidatingViewProtocol { 73 | public static let configuration: Self = .init() 74 | 75 | public func invalidate(view: InvalidatingViewType) { 76 | if #available(iOS 14, *) { 77 | switch view { 78 | case let view as UITableViewCell: 79 | view.setNeedsUpdateConfiguration() 80 | case let view as UICollectionViewCell: 81 | view.setNeedsUpdateConfiguration() 82 | case let view as UITableViewHeaderFooterView: 83 | view.setNeedsUpdateConfiguration() 84 | default: 85 | assertionFailure("View '\(String(describing: view))' does not support configuration updates!") 86 | } 87 | } 88 | } 89 | } 90 | #endif 91 | } 92 | -------------------------------------------------------------------------------- /Sources/Invalidating/Tuple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tuple.swift 3 | // 4 | // 5 | // Created by Suyash Srijan on 28/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Invalidations { 11 | 12 | @available(iOS, introduced: 11, obsoleted: 15) 13 | @available(tvOS, introduced: 11, obsoleted: 15) 14 | @available(macOS, introduced: 10.11, obsoleted: 12) 15 | struct Tuple: InvalidatingViewProtocol { 16 | private let tuple: (Invalidation1, Invalidation2) 17 | 18 | public init(invalidation1: Invalidation1, invalidation2: Invalidation2) { 19 | self.tuple = (invalidation1, invalidation2) 20 | } 21 | 22 | public func invalidate(view: InvalidatingViewType) { 23 | tuple.0.invalidate(view: view) 24 | tuple.1.invalidate(view: view) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/Invalidating/ViewInvalidating+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Invalidating+Extensions.swift 3 | // 4 | // 5 | // Created by Suyash Srijan on 29/06/2021. 6 | // 7 | 8 | import Foundation 9 | 10 | #if compiler(>=5.5) 11 | public extension ViewInvalidating { 12 | typealias Tuple = Invalidations.Tuple 13 | 14 | init(wrappedValue: Value, _ invalidation1: InvalidationType1, _ invalidation2: InvalidationType2) where InvalidationType == Tuple { 15 | self.init(wrappedValue: wrappedValue, .init(invalidation1: invalidation1, invalidation2: invalidation2)) 16 | } 17 | 18 | init(wrappedValue: Value, _ invalidation1: InvalidationType1, _ invalidation2: InvalidationType2, _ invalidation3: InvalidationType3) where InvalidationType == Tuple, InvalidationType3> { 19 | self.init(wrappedValue: wrappedValue, .init(invalidation1: .init(invalidation1: invalidation1, invalidation2: invalidation2), invalidation2: invalidation3)) 20 | } 21 | 22 | init(wrappedValue: Value, _ invalidation1: InvalidationType1, _ invalidation2: InvalidationType2, _ invalidation3: InvalidationType3, _ invalidation4: InvalidationType4) where InvalidationType == Tuple, Tuple> { 23 | self.init(wrappedValue: wrappedValue, .init(invalidation1: .init(invalidation1: invalidation1, invalidation2: invalidation2), invalidation2: .init(invalidation1: invalidation3, invalidation2: invalidation4))) 24 | } 25 | 26 | init(wrappedValue: Value, _ invalidation1: InvalidationType1, _ invalidation2: InvalidationType2, _ invalidation3: InvalidationType3, _ invalidation4: InvalidationType4, _ invalidation5: InvalidationType5) where InvalidationType == Tuple, Tuple>, InvalidationType5> { 27 | self.init(wrappedValue: wrappedValue, .init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1, invalidation2: invalidation2), invalidation2: .init(invalidation1: invalidation3, invalidation2: invalidation4)), invalidation2: invalidation5)) 28 | } 29 | 30 | init(wrappedValue: Value, _ invalidation1: InvalidationType1, _ invalidation2: InvalidationType2, _ invalidation3: InvalidationType3, _ invalidation4: InvalidationType4, _ invalidation5: InvalidationType5, _ invalidation6: InvalidationType6) where InvalidationType == Tuple, Tuple>, Tuple> { 31 | self.init(wrappedValue: wrappedValue, .init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1, invalidation2: invalidation2), invalidation2: .init(invalidation1: invalidation3, invalidation2: invalidation4)), invalidation2: .init(invalidation1: invalidation5, invalidation2: invalidation6))) 32 | } 33 | 34 | init(wrappedValue: Value, _ invalidation1: InvalidationType1, _ invalidation2: InvalidationType2, _ invalidation3: InvalidationType3, _ invalidation4: InvalidationType4, _ invalidation5: InvalidationType5, _ invalidation6: InvalidationType6, _ invalidation7: InvalidationType7) where InvalidationType == Tuple, Tuple>, Tuple, InvalidationType7>> { 35 | self.init(wrappedValue: wrappedValue, .init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1, invalidation2: invalidation2), invalidation2: .init(invalidation1: invalidation3, invalidation2: invalidation4)), invalidation2: .init(invalidation1: .init(invalidation1: invalidation5, invalidation2: invalidation6), invalidation2: invalidation7))) 36 | } 37 | 38 | init(wrappedValue: Value, _ invalidation1: InvalidationType1, _ invalidation2: InvalidationType2, _ invalidation3: InvalidationType3, _ invalidation4: InvalidationType4, _ invalidation5: InvalidationType5, _ invalidation6: InvalidationType6, _ invalidation7: InvalidationType7, _ invalidation8: InvalidationType8) where InvalidationType == Tuple, Tuple>, Tuple, Tuple>> { 39 | self.init(wrappedValue: wrappedValue, .init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1, invalidation2: invalidation2), invalidation2: .init(invalidation1: invalidation3, invalidation2: invalidation4)), invalidation2: .init(invalidation1: .init(invalidation1: invalidation5, invalidation2: invalidation6), invalidation2: .init(invalidation1: invalidation7, invalidation2: invalidation8)))) 40 | } 41 | 42 | init(wrappedValue: Value, _ invalidation1: InvalidationType1, _ invalidation2: InvalidationType2, _ invalidation3: InvalidationType3, _ invalidation4: InvalidationType4, _ invalidation5: InvalidationType5, _ invalidation6: InvalidationType6, _ invalidation7: InvalidationType7, _ invalidation8: InvalidationType8, _ invalidation9: InvalidationType9) where InvalidationType == Tuple, Tuple>, Tuple, Tuple>>, InvalidationType9> { 43 | self.init(wrappedValue: wrappedValue, .init(invalidation1: .init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1, invalidation2: invalidation2), invalidation2: .init(invalidation1: invalidation3, invalidation2: invalidation4)), invalidation2: .init(invalidation1: .init(invalidation1: invalidation5, invalidation2: invalidation6), invalidation2: .init(invalidation1: invalidation7, invalidation2: invalidation8))), invalidation2: invalidation9)) 44 | } 45 | 46 | init(wrappedValue: Value, _ invalidation1: InvalidationType1, _ invalidation2: InvalidationType2, _ invalidation3: InvalidationType3, _ invalidation4: InvalidationType4, _ invalidation5: InvalidationType5, _ invalidation6: InvalidationType6, _ invalidation7: InvalidationType7, _ invalidation8: InvalidationType8, _ invalidation9: InvalidationType9, _ invalidation10: InvalidationType10) where InvalidationType == Tuple, Tuple>, Tuple, Tuple>>, Tuple> { 47 | self.init(wrappedValue: wrappedValue, .init(invalidation1: .init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1, invalidation2: invalidation2), invalidation2: .init(invalidation1: invalidation3, invalidation2: invalidation4)), invalidation2: .init(invalidation1: .init(invalidation1: invalidation5, invalidation2: invalidation6), invalidation2: .init(invalidation1: invalidation7, invalidation2: invalidation8))), invalidation2: .init(invalidation1: invalidation9, invalidation2: invalidation10))) 48 | } 49 | } 50 | #else 51 | public extension ViewInvalidating { 52 | typealias Tuple = Invalidations.Tuple 53 | 54 | init(wrappedValue: Value, _ invalidation1: InvalidationType1.Member, _ invalidation2: InvalidationType2.Member, _ invalidation3: InvalidationType3.Member) where InvalidationType == Tuple, InvalidationType3> { 55 | self.init(wrappedValue: wrappedValue, .init(.init(invalidation1: .init(invalidation1: invalidation1.base, invalidation2: invalidation2.base), invalidation2: invalidation3.base))) 56 | } 57 | 58 | init(wrappedValue: Value, _ invalidation1: InvalidationType1.Member, _ invalidation2: InvalidationType2.Member, _ invalidation3: InvalidationType3.Member, _ invalidation4: InvalidationType4.Member) where InvalidationType == Tuple, Tuple> { 59 | self.init(wrappedValue: wrappedValue, .init(.init(invalidation1: .init(invalidation1: invalidation1.base, invalidation2: invalidation2.base), invalidation2: .init(invalidation1: invalidation3.base, invalidation2: invalidation4.base)))) 60 | } 61 | 62 | init(wrappedValue: Value, _ invalidation1: InvalidationType1.Member, _ invalidation2: InvalidationType2.Member, _ invalidation3: InvalidationType3.Member, _ invalidation4: InvalidationType4.Member, _ invalidation5: InvalidationType5.Member) where InvalidationType == Tuple, Tuple>, InvalidationType5> { 63 | self.init(wrappedValue: wrappedValue, .init(.init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1.base, invalidation2: invalidation2.base), invalidation2: .init(invalidation1: invalidation3.base, invalidation2: invalidation4.base)), invalidation2: invalidation5.base))) 64 | } 65 | 66 | init(wrappedValue: Value, _ invalidation1: InvalidationType1.Member, _ invalidation2: InvalidationType2.Member, _ invalidation3: InvalidationType3.Member, _ invalidation4: InvalidationType4.Member, _ invalidation5: InvalidationType5.Member, _ invalidation6: InvalidationType6.Member) where InvalidationType == Tuple, Tuple>, Tuple> { 67 | self.init(wrappedValue: wrappedValue, .init(.init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1.base, invalidation2: invalidation2.base), invalidation2: .init(invalidation1: invalidation3.base, invalidation2: invalidation4.base)), invalidation2: .init(invalidation1: invalidation5.base, invalidation2: invalidation6.base)))) 68 | } 69 | 70 | init(wrappedValue: Value, _ invalidation1: InvalidationType1.Member, _ invalidation2: InvalidationType2.Member, _ invalidation3: InvalidationType3.Member, _ invalidation4: InvalidationType4.Member, _ invalidation5: InvalidationType5.Member, _ invalidation6: InvalidationType6.Member, _ invalidation7: InvalidationType7.Member) where InvalidationType == Tuple, Tuple>, Tuple, InvalidationType7>> { 71 | self.init(wrappedValue: wrappedValue, .init(.init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1.base, invalidation2: invalidation2.base), invalidation2: .init(invalidation1: invalidation3.base, invalidation2: invalidation4.base)), invalidation2: .init(invalidation1: .init(invalidation1: invalidation5.base, invalidation2: invalidation6.base), invalidation2: invalidation7.base)))) 72 | } 73 | 74 | init(wrappedValue: Value, _ invalidation1: InvalidationType1.Member, _ invalidation2: InvalidationType2.Member, _ invalidation3: InvalidationType3.Member, _ invalidation4: InvalidationType4.Member, _ invalidation5: InvalidationType5.Member, _ invalidation6: InvalidationType6.Member, _ invalidation7: InvalidationType7.Member, _ invalidation8: InvalidationType8.Member) where InvalidationType == Tuple, Tuple>, Tuple, Tuple>> { 75 | self.init(wrappedValue: wrappedValue, .init(.init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1.base, invalidation2: invalidation2.base), invalidation2: .init(invalidation1: invalidation3.base, invalidation2: invalidation4.base)), invalidation2: .init(invalidation1: .init(invalidation1: invalidation5.base, invalidation2: invalidation6.base), invalidation2: .init(invalidation1: invalidation7.base, invalidation2: invalidation8.base))))) 76 | } 77 | 78 | init(wrappedValue: Value, _ invalidation1: InvalidationType1.Member, _ invalidation2: InvalidationType2.Member, _ invalidation3: InvalidationType3.Member, _ invalidation4: InvalidationType4.Member, _ invalidation5: InvalidationType5.Member, _ invalidation6: InvalidationType6.Member, _ invalidation7: InvalidationType7.Member, _ invalidation8: InvalidationType8.Member, _ invalidation9: InvalidationType9.Member) where InvalidationType == Tuple, Tuple>, Tuple, Tuple>>, InvalidationType9> { 79 | self.init(wrappedValue: wrappedValue, .init(.init(invalidation1: .init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1.base, invalidation2: invalidation2.base), invalidation2: .init(invalidation1: invalidation3.base, invalidation2: invalidation4.base)), invalidation2: .init(invalidation1: .init(invalidation1: invalidation5.base, invalidation2: invalidation6.base), invalidation2: .init(invalidation1: invalidation7.base, invalidation2: invalidation8.base))), invalidation2: invalidation9.base))) 80 | } 81 | 82 | init(wrappedValue: Value, _ invalidation1: InvalidationType1.Member, _ invalidation2: InvalidationType2.Member, _ invalidation3: InvalidationType3.Member, _ invalidation4: InvalidationType4.Member, _ invalidation5: InvalidationType5.Member, _ invalidation6: InvalidationType6.Member, _ invalidation7: InvalidationType7.Member, _ invalidation8: InvalidationType8.Member, _ invalidation9: InvalidationType9.Member, _ invalidation10: InvalidationType10.Member) where InvalidationType == Tuple, Tuple>, Tuple, Tuple>>, Tuple> { 83 | self.init(wrappedValue: wrappedValue, .init(.init(invalidation1: .init(invalidation1: .init(invalidation1: .init(invalidation1: invalidation1.base, invalidation2: invalidation2.base), invalidation2: .init(invalidation1: invalidation3.base, invalidation2: invalidation4.base)), invalidation2: .init(invalidation1: .init(invalidation1: invalidation5.base, invalidation2: invalidation6.base), invalidation2: .init(invalidation1: invalidation7.base, invalidation2: invalidation8.base))), invalidation2: .init(invalidation1: invalidation9.base, invalidation2: invalidation10.base)))) 84 | } 85 | } 86 | #endif 87 | -------------------------------------------------------------------------------- /Sources/Invalidating/ViewInvalidating.swift: -------------------------------------------------------------------------------- 1 | @available(iOS, introduced: 11, obsoleted: 15, renamed: "Invalidating", message: "Use Apple's @Invalidating property wrapper") 2 | @available(tvOS, introduced: 11, obsoleted: 15, renamed: "Invalidating", message: "Use Apple's @Invalidating property wrapper") 3 | @available(macOS, introduced: 10.11, obsoleted: 12, renamed: "Invalidating", message: "Use Apple's @Invalidating property wrapper") 4 | @available(swift, introduced: 5.1) 5 | @propertyWrapper 6 | public struct ViewInvalidating { 7 | 8 | #if compiler(<5.5) 9 | private enum Storage { 10 | case single(InvalidationType.Member) 11 | case multiple(Invalidations.Tuple) 12 | } 13 | #endif 14 | 15 | @available(*, unavailable) 16 | public var wrappedValue: Value { 17 | get { fatalError() } 18 | set { fatalError() } 19 | } 20 | 21 | private var _wrappedValue: Value 22 | 23 | #if compiler(>=5.5) 24 | private let storage: InvalidationType 25 | #else 26 | private let storage: Storage 27 | #endif 28 | 29 | #if compiler(>=5.5) 30 | public init(wrappedValue: Value, _ invalidation: InvalidationType) { 31 | self._wrappedValue = wrappedValue 32 | self.storage = invalidation 33 | } 34 | #else 35 | public init(wrappedValue: Value, _ invalidation: InvalidationType.Member) { 36 | self._wrappedValue = wrappedValue 37 | self.storage = .single(invalidation) 38 | } 39 | #endif 40 | 41 | #if compiler(<5.5) 42 | public init(wrappedValue: Value, _ invalidation1: InvalidationType1.Member, _ invalidation2: InvalidationType2.Member) where InvalidationType == Tuple { 43 | self._wrappedValue = wrappedValue 44 | self.storage = .multiple(.init(invalidation1: .init(base: invalidation1.base), invalidation2: .init(base: invalidation2.base))) 45 | } 46 | #endif 47 | 48 | public static subscript( 49 | _enclosingInstance observed: EnclosingSelf, 50 | wrapped wrappedKeyPath: ReferenceWritableKeyPath, 51 | storage storageKeyPath: ReferenceWritableKeyPath 52 | ) -> Value where EnclosingSelf: InvalidatingViewType { 53 | 54 | get { 55 | return observed[keyPath: storageKeyPath]._wrappedValue 56 | } 57 | 58 | set { 59 | guard observed[keyPath: storageKeyPath]._wrappedValue != newValue else { return } 60 | observed[keyPath: storageKeyPath]._wrappedValue = newValue 61 | #if compiler(>=5.5) 62 | observed[keyPath: storageKeyPath].storage.invalidate(view: observed) 63 | #else 64 | switch observed[keyPath: storageKeyPath].storage { 65 | case let .single(value): 66 | value.base.invalidate(view: observed) 67 | case let .multiple(tuple): 68 | tuple.invalidate(view: observed) 69 | } 70 | #endif 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/InvalidatingTests/InvalidatingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Invalidating 3 | 4 | final class InvalidatingBackportTests: XCTestCase { 5 | #if os(iOS) || os(tvOS) 6 | final class UIKitTestView: UIView { 7 | @ViewInvalidating(.layout) var invalidatingLayoutValue: Int = 0 8 | @ViewInvalidating(.display) var invalidatingDisplayValue: Int = 0 9 | @ViewInvalidating(.constraints) var invalidatingConstraintsValue: Int = 0 10 | @ViewInvalidating(.intrinsicContentSize) var invalidatingIntrinsicContentSizeValue: Int = 0 11 | @ViewInvalidating(.layout, .constraints) var invalidatingLayoutAndConstraints: Int = 0 12 | @ViewInvalidating(.layout, .constraints, .intrinsicContentSize) var invalidatingLayoutAndConstraintsAndIntrinsicContentSize: Int = 0 13 | 14 | private(set) var didCallSetNeedsLayout = false 15 | private(set) var didCallSetNeedsDisplay = false 16 | private(set) var didCallSetNeedsUpdateConstraints = false 17 | private(set) var didCallInvalidateIntrinsicContentSize = false 18 | 19 | override func setNeedsLayout() { 20 | super.setNeedsLayout() 21 | 22 | didCallSetNeedsLayout = true 23 | } 24 | 25 | override func setNeedsDisplay() { 26 | super.setNeedsDisplay() 27 | 28 | didCallSetNeedsDisplay = true 29 | } 30 | 31 | override func setNeedsUpdateConstraints() { 32 | super.setNeedsUpdateConstraints() 33 | 34 | didCallSetNeedsUpdateConstraints = true 35 | } 36 | 37 | override func invalidateIntrinsicContentSize() { 38 | super.invalidateIntrinsicContentSize() 39 | 40 | didCallInvalidateIntrinsicContentSize = true 41 | } 42 | 43 | func reset() { 44 | didCallSetNeedsLayout = false 45 | didCallSetNeedsDisplay = false 46 | didCallSetNeedsUpdateConstraints = false 47 | didCallInvalidateIntrinsicContentSize = false 48 | } 49 | } 50 | 51 | @available(iOS 14, *) 52 | final class UIKitTableViewCell: UITableViewCell { 53 | @ViewInvalidating(.configuration) var invalidatingConfiguration: Int = 0 54 | 55 | private(set) var didCallNeedsUpdateConfiguration = false 56 | 57 | override func setNeedsUpdateConfiguration() { 58 | super.setNeedsUpdateConfiguration() 59 | 60 | didCallNeedsUpdateConfiguration = true 61 | } 62 | } 63 | 64 | @available(iOS 14, *) 65 | final class UIKitCollectionViewCell: UICollectionViewCell { 66 | @ViewInvalidating(.configuration) var invalidatingConfiguration: Int = 0 67 | 68 | private(set) var didCallNeedsUpdateConfiguration = false 69 | 70 | override func setNeedsUpdateConfiguration() { 71 | super.setNeedsUpdateConfiguration() 72 | 73 | didCallNeedsUpdateConfiguration = true 74 | } 75 | } 76 | 77 | @available(iOS 14, *) 78 | final class UIKitTableViewHeaderFooterView: UITableViewHeaderFooterView { 79 | @ViewInvalidating(.configuration) var invalidatingConfiguration: Int = 0 80 | 81 | private(set) var didCallNeedsUpdateConfiguration = false 82 | 83 | override func setNeedsUpdateConfiguration() { 84 | super.setNeedsUpdateConfiguration() 85 | 86 | didCallNeedsUpdateConfiguration = true 87 | } 88 | } 89 | 90 | func testSingleInvalidation() { 91 | let view = UIKitTestView() 92 | 93 | XCTAssertFalse(view.didCallSetNeedsLayout) 94 | view.invalidatingLayoutValue = 1 95 | XCTAssertTrue(view.didCallSetNeedsLayout) 96 | 97 | view.reset() 98 | 99 | XCTAssertFalse(view.didCallSetNeedsDisplay) 100 | view.invalidatingDisplayValue = 2 101 | XCTAssertTrue(view.didCallSetNeedsDisplay) 102 | 103 | view.reset() 104 | 105 | XCTAssertFalse(view.didCallSetNeedsUpdateConstraints) 106 | view.invalidatingConstraintsValue = 3 107 | XCTAssertTrue(view.didCallSetNeedsUpdateConstraints) 108 | 109 | view.reset() 110 | 111 | XCTAssertFalse(view.didCallInvalidateIntrinsicContentSize) 112 | view.invalidatingIntrinsicContentSizeValue = 4 113 | XCTAssertTrue(view.didCallInvalidateIntrinsicContentSize) 114 | } 115 | 116 | func testSequenceInvalidations() { 117 | let view = UIKitTestView() 118 | 119 | XCTAssertFalse(view.didCallSetNeedsLayout) 120 | XCTAssertFalse(view.didCallSetNeedsUpdateConstraints) 121 | 122 | view.invalidatingLayoutAndConstraints = 1 123 | 124 | XCTAssertTrue(view.didCallSetNeedsLayout) 125 | XCTAssertTrue(view.didCallSetNeedsUpdateConstraints) 126 | } 127 | 128 | func testThreeSequenceInvalidations() { 129 | let view = UIKitTestView() 130 | 131 | XCTAssertFalse(view.didCallSetNeedsLayout) 132 | XCTAssertFalse(view.didCallSetNeedsUpdateConstraints) 133 | XCTAssertFalse(view.didCallInvalidateIntrinsicContentSize) 134 | 135 | view.invalidatingLayoutAndConstraintsAndIntrinsicContentSize = 1 136 | 137 | XCTAssertTrue(view.didCallSetNeedsLayout) 138 | XCTAssertTrue(view.didCallSetNeedsUpdateConstraints) 139 | XCTAssertTrue(view.didCallInvalidateIntrinsicContentSize) 140 | } 141 | 142 | func testConfigurationInvalidation() { 143 | if #available(iOS 14, *) { 144 | let tableViewCell = UIKitTableViewCell() 145 | let collectionViewCell = UIKitCollectionViewCell() 146 | let headerFooterView = UIKitTableViewHeaderFooterView() 147 | 148 | XCTAssertFalse(tableViewCell.didCallNeedsUpdateConfiguration) 149 | tableViewCell.invalidatingConfiguration = 1 150 | XCTAssertTrue(tableViewCell.didCallNeedsUpdateConfiguration) 151 | 152 | XCTAssertFalse(collectionViewCell.didCallNeedsUpdateConfiguration) 153 | collectionViewCell.invalidatingConfiguration = 1 154 | XCTAssertTrue(collectionViewCell.didCallNeedsUpdateConfiguration) 155 | 156 | XCTAssertFalse(headerFooterView.didCallNeedsUpdateConfiguration) 157 | headerFooterView.invalidatingConfiguration = 1 158 | XCTAssertTrue(headerFooterView.didCallNeedsUpdateConfiguration) 159 | } else { 160 | XCTAssertTrue(true) 161 | } 162 | } 163 | 164 | #elseif os(macOS) 165 | final class AppKitTestView: NSView { 166 | @ViewInvalidating(.layout) var invalidatingLayoutValue: Int = 0 167 | @ViewInvalidating(.display) var invalidatingDisplayValue: Int = 0 168 | @ViewInvalidating(.constraints) var invalidatingConstraintsValue: Int = 0 169 | @ViewInvalidating(.intrinsicContentSize) var invalidatingIntrinsicContentSizeValue: Int = 0 170 | @ViewInvalidating(.restorableState) var invalidatingRestorableState: Int = 0 171 | @ViewInvalidating(.layout, .constraints) var invalidatingLayoutAndConstraints: Int = 0 172 | @ViewInvalidating(.layout, .constraints, .intrinsicContentSize) var invalidatingLayoutAndConstraintsAndIntrinsicContentSize: Int = 0 173 | 174 | private(set) var didCallSetNeedsLayout = false 175 | private(set) var didCallSetNeedsDisplay = false 176 | private(set) var didCallSetNeedsUpdateConstraints = false 177 | private(set) var didCallInvalidateIntrinsicContentSize = false 178 | private(set) var didCallInvalidateRestorableState = false 179 | 180 | override var needsLayout: Bool { 181 | willSet { 182 | if newValue { 183 | self.didCallSetNeedsLayout = true 184 | } 185 | } 186 | } 187 | 188 | override var needsUpdateConstraints: Bool { 189 | willSet { 190 | if newValue { 191 | self.didCallSetNeedsUpdateConstraints = true 192 | } 193 | } 194 | } 195 | 196 | override func setNeedsDisplay(_ invalidRect: NSRect) { 197 | super.setNeedsDisplay(invalidRect) 198 | 199 | didCallSetNeedsDisplay = true 200 | } 201 | 202 | override func invalidateIntrinsicContentSize() { 203 | super.invalidateIntrinsicContentSize() 204 | 205 | didCallInvalidateIntrinsicContentSize = true 206 | } 207 | 208 | override func invalidateRestorableState() { 209 | super.invalidateRestorableState() 210 | 211 | didCallInvalidateRestorableState = true 212 | } 213 | 214 | func reset() { 215 | didCallSetNeedsLayout = false 216 | didCallSetNeedsDisplay = false 217 | didCallSetNeedsUpdateConstraints = false 218 | didCallInvalidateIntrinsicContentSize = false 219 | didCallInvalidateRestorableState = false 220 | } 221 | } 222 | 223 | func testSingleInvalidation() { 224 | let view = AppKitTestView() 225 | 226 | XCTAssertFalse(view.didCallSetNeedsLayout) 227 | view.invalidatingLayoutValue = 1 228 | XCTAssertTrue(view.didCallSetNeedsLayout) 229 | 230 | view.reset() 231 | 232 | XCTAssertFalse(view.didCallSetNeedsDisplay) 233 | view.invalidatingDisplayValue = 2 234 | XCTAssertTrue(view.didCallSetNeedsDisplay) 235 | 236 | view.reset() 237 | 238 | XCTAssertFalse(view.didCallSetNeedsUpdateConstraints) 239 | view.invalidatingConstraintsValue = 3 240 | XCTAssertTrue(view.didCallSetNeedsUpdateConstraints) 241 | 242 | view.reset() 243 | 244 | XCTAssertFalse(view.didCallInvalidateIntrinsicContentSize) 245 | view.invalidatingIntrinsicContentSizeValue = 4 246 | XCTAssertTrue(view.didCallInvalidateIntrinsicContentSize) 247 | 248 | view.reset() 249 | 250 | XCTAssertFalse(view.didCallInvalidateRestorableState) 251 | view.invalidatingRestorableState = 5 252 | XCTAssertTrue(view.didCallInvalidateRestorableState) 253 | } 254 | 255 | func testTwoSequenceInvalidations() { 256 | let view = AppKitTestView() 257 | 258 | XCTAssertFalse(view.didCallSetNeedsLayout) 259 | XCTAssertFalse(view.didCallSetNeedsUpdateConstraints) 260 | 261 | view.invalidatingLayoutAndConstraints = 1 262 | 263 | XCTAssertTrue(view.didCallSetNeedsLayout) 264 | XCTAssertTrue(view.didCallSetNeedsUpdateConstraints) 265 | } 266 | 267 | func testThreeSequenceInvalidations() { 268 | let view = AppKitTestView() 269 | 270 | XCTAssertFalse(view.didCallSetNeedsLayout) 271 | XCTAssertFalse(view.didCallSetNeedsUpdateConstraints) 272 | XCTAssertFalse(view.didCallInvalidateIntrinsicContentSize) 273 | 274 | view.invalidatingLayoutAndConstraintsAndIntrinsicContentSize = 1 275 | 276 | XCTAssertTrue(view.didCallSetNeedsLayout) 277 | XCTAssertTrue(view.didCallSetNeedsUpdateConstraints) 278 | XCTAssertTrue(view.didCallInvalidateIntrinsicContentSize) 279 | } 280 | #endif 281 | 282 | } 283 | --------------------------------------------------------------------------------