├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── main.yml ├── .gitignore ├── .spi.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ConfigurePodspec.rb ├── Epoxy.podspec ├── Epoxy.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── EpoxyBars.podspec ├── EpoxyCollectionView.podspec ├── EpoxyCore.podspec ├── EpoxyLayoutGroups.podspec ├── EpoxyNavigationController.podspec ├── EpoxyPresentations.podspec ├── Example ├── EpoxyExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── EpoxyExample.xcscheme └── EpoxyExample │ ├── Application │ └── AppDelegate.swift │ ├── Assets │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── App_Store_1024x1024@1x.png │ │ │ ├── Contents.json │ │ │ ├── iPad_App_76_1x.png │ │ │ ├── iPad_App_76_2x.png │ │ │ ├── iPad_Notifications_20_1x.png │ │ │ ├── iPad_Notifications_20_2x.png │ │ │ ├── iPad_Pro_App_83.5_2x.png │ │ │ ├── iPad_Pro_Spotlight_40_2x.png │ │ │ ├── iPad_Settings_29_1x.png │ │ │ ├── iPad_Settings_29_2x.png │ │ │ ├── iPad_Spotlight_40_1x.png │ │ │ ├── iPhone_App_60_2x.png │ │ │ ├── iPhone_App_60_3x.png │ │ │ ├── iPhone_Notifications_20_2x.png │ │ │ ├── iPhone_Notifications_20_3x.png │ │ │ ├── iPhone_Settings_29_2x.png │ │ │ ├── iPhone_Settings_29_3x.png │ │ │ ├── iPhone_Spotlight_40_2x.png │ │ │ └── iPhone_Spotlight_40_3x.png │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ └── Info.plist │ ├── Data │ ├── BeloIpsum.swift │ ├── Example.swift │ ├── LayoutGroupsExample.swift │ └── ReadmeExample.swift │ ├── Extensions │ ├── UICollectionViewCompositionalLayout+List.swift │ └── UIImageView+RemoteImage.swift │ ├── ViewControllers │ ├── CollectionView │ │ ├── CardStackViewController.swift │ │ ├── CompositionalLayoutViewController.swift │ │ ├── CustomSelfSizingContentViewController.swift │ │ ├── FlowLayoutViewController.swift │ │ ├── ShuffleViewController.swift │ │ └── TextFieldViewController.swift │ ├── Helpers │ │ └── NavigationWrapperViewController.swift │ ├── LayoutGroups │ │ ├── ColorsViewController.swift │ │ ├── ComplexDeclarativeViewController.swift │ │ ├── DynamicLayoutGroupsViewController.swift │ │ ├── EntirelyInlineViewController.swift │ │ ├── LayoutGroupsReadmeExamplesViewController.swift │ │ ├── MessagesUIStackViewViewController.swift │ │ ├── MessagesViewController.swift │ │ ├── TextRowExampleViewController.swift │ │ ├── TodoListViewController.swift │ │ └── UIViewController+LayoutGroupsExample.swift │ ├── MainViewController.swift │ ├── ProductViewController.swift │ ├── Readme │ │ ├── BottomButtonViewController.swift │ │ ├── CounterViewController.swift │ │ ├── FormNavigationController.swift │ │ ├── LayoutGroupsExampleViewController.swift │ │ ├── PresentationViewController.swift │ │ ├── ReadmeExamplesViewController.swift │ │ ├── TapMeViewController.swift │ │ └── UIViewController+ReadmeExample.swift │ └── SwiftUI │ │ ├── EpoxyInSwiftUISizingStrategiesViewController.swift │ │ ├── EpoxyInSwiftUIViewController.swift │ │ ├── SwiftUIInEpoxyResizingViewController.swift │ │ └── SwiftUIInEpoxyViewController.swift │ └── Views │ ├── ButtonRow.swift │ ├── CardContainer.swift │ ├── ColorView.swift │ ├── CustomSizingView.swift │ ├── ImageMarquee.swift │ ├── ImageRow.swift │ ├── LayoutGroups │ ├── ActionButtonRow.swift │ ├── AlignableTextRow.swift │ ├── CheckboxRow.swift │ ├── ColorsRow.swift │ ├── DynamicRow.swift │ ├── Elements │ │ ├── BaseRow.swift │ │ ├── Button.swift │ │ ├── ImageView.swift │ │ └── Label.swift │ ├── IconRow.swift │ ├── MessageRow.swift │ └── MessageRowStackView.swift │ ├── TextFieldRow.swift │ └── TextRow.swift ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Rakefile ├── Sources ├── Epoxy │ └── Exports.swift ├── EpoxyBars │ ├── BarCoordinator │ │ ├── BarCoordinating.swift │ │ ├── BarCoordinatorProperty.swift │ │ └── CoordinatedBarModel.swift │ ├── BarInstaller │ │ ├── BarContainer.swift │ │ ├── BarInstaller.swift │ │ ├── BarInstallerConfiguration.swift │ │ ├── BarStackView.swift │ │ ├── BarWrapperView.swift │ │ ├── UIScrollView+ContentOffset.swift │ │ ├── UIView+HasHierarchyScaleTransform.swift │ │ └── UIViewController+OriginalSafeAreaInsets.swift │ ├── BarModel │ │ ├── AnyBarModel.swift │ │ ├── BarModel.swift │ │ ├── BarModelBuilder.swift │ │ ├── BarModeling.swift │ │ ├── EpoxyableView+BarModel.swift │ │ ├── InternalBarModeling.swift │ │ └── SwiftUI.View+BarModel.swift │ ├── BarView │ │ ├── HeightInvalidatingBarView.swift │ │ └── SafeAreaLayoutMarginsBarView.swift │ ├── BottomBarInstaller │ │ ├── BottomBarContainer.swift │ │ ├── BottomBarInstaller.swift │ │ └── BottomBarsProviding.swift │ ├── Keyboard │ │ ├── InputAccessoryBarStackView.swift │ │ └── KeyboardPositionWatcher.swift │ └── TopBarInstaller │ │ ├── TopBarContainer.swift │ │ ├── TopBarInstaller.swift │ │ └── TopBarsProviding.swift ├── EpoxyCollectionView │ ├── CollectionView │ │ ├── CollectionView.swift │ │ ├── CollectionViewConfiguration.swift │ │ ├── CollectionViewVisibilityMetadata.swift │ │ ├── Delegates │ │ │ ├── CollectionViewAccessibilityDelegate.swift │ │ │ ├── CollectionViewDisplayDelegate.swift │ │ │ ├── CollectionViewPrefetchingDelegate.swift │ │ │ ├── CollectionViewReorderingDelegate.swift │ │ │ └── CollectionViewTransitionLayoutDelegate.swift │ │ ├── Internal │ │ │ ├── CollectionViewChangeset.swift │ │ │ ├── CollectionViewData.swift │ │ │ ├── CollectionViewDataSource.swift │ │ │ ├── CollectionViewDataSourceReorderingDelegate.swift │ │ │ ├── CollectionViewScrollToItemHelper.swift │ │ │ └── ReuseIDStore.swift │ │ ├── ItemPath.swift │ │ ├── ItemSectionPath.swift │ │ ├── ReusableViews │ │ │ ├── CollectionViewCell.swift │ │ │ ├── CollectionViewReusableView.swift │ │ │ ├── FittingPrioritiesProvidingLayoutAttributes.swift │ │ │ ├── ItemCellView.swift │ │ │ └── ItemSelectionStyle.swift │ │ └── SupplementaryItemPath.swift │ ├── Layouts │ │ ├── CompositionalLayout │ │ │ ├── SectionModel+CompositionalLayout.swift │ │ │ └── UICollectionViewCompositionalLayout+Epoxy.swift │ │ └── FlowLayout │ │ │ ├── CollectionView+UICollectionViewFlowLayoutDelegate.swift │ │ │ └── EpoxyModeled+UICollectionViewFlowLayout.swift │ ├── Models │ │ ├── ItemModel │ │ │ ├── AnyItemModel.swift │ │ │ ├── EpoxyableView+ItemModel.swift │ │ │ ├── ItemCellMetadata.swift │ │ │ ├── ItemCellState.swift │ │ │ ├── ItemModel.swift │ │ │ ├── ItemModelBuilder.swift │ │ │ ├── ItemModeling.swift │ │ │ └── SwiftUI.View+ItemModel.swift │ │ ├── Providers │ │ │ ├── DidChangeStateProviding.swift │ │ │ ├── IsMoveableProviding.swift │ │ │ ├── ItemsProviding.swift │ │ │ ├── SelectionStyleProviding.swift │ │ │ └── SupplementaryItemsProviding.swift │ │ ├── SectionModel │ │ │ ├── SectionModel+ReuseIDs.swift │ │ │ ├── SectionModel.swift │ │ │ └── SectionModelBuilder.swift │ │ └── SupplementaryItemModel │ │ │ ├── AnySupplementaryItemModel.swift │ │ │ ├── EpoxyableView+SupplementaryItemModel.swift │ │ │ ├── SupplementaryItemModel.swift │ │ │ ├── SupplementaryItemModeling.swift │ │ │ └── SwiftUI.View+SupplementaryItemModel.swift │ ├── ViewControllers │ │ └── CollectionViewController.swift │ └── Views │ │ ├── AccessibilityCustomizedView.swift │ │ ├── DisplayRespondingView.swift │ │ ├── EphemeralCachedStateView.swift │ │ ├── HighlightableView.swift │ │ └── SelectableView.swift ├── EpoxyCore │ ├── Diffing │ │ ├── Collection+Diff.swift │ │ ├── Diffable.swift │ │ ├── DiffableSection.swift │ │ ├── IndexChangeset.swift │ │ └── SectionedChangeset.swift │ ├── EpoxyCore.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── EpoxyCore.xcscheme │ ├── Logging │ │ └── EpoxyLogger.swift │ ├── Model │ │ ├── CallbackContextEpoxyModeled.swift │ │ ├── EpoxyModelArrayBuilder.swift │ │ ├── EpoxyModelProperty.swift │ │ ├── EpoxyModelStorage.swift │ │ ├── EpoxyModeled.swift │ │ ├── Internal │ │ │ ├── AnyEpoxyModelProperty.swift │ │ │ └── ClassReference.swift │ │ ├── Providers │ │ │ ├── AnimatedProviding.swift │ │ │ ├── DataIDProviding.swift │ │ │ ├── DidDisplayProviding.swift │ │ │ ├── DidEndDisplayingProviding.swift │ │ │ ├── DidSelectProviding.swift │ │ │ ├── ErasedContentProviding.swift │ │ │ ├── MakeViewProviding.swift │ │ │ ├── SetBehaviorsProviding.swift │ │ │ ├── SetContentProviding.swift │ │ │ ├── StyleIDProviding.swift │ │ │ ├── TraitCollectionProviding.swift │ │ │ ├── ViewDifferentiatorProviding.swift │ │ │ ├── ViewProviding.swift │ │ │ └── WillDisplayProviding.swift │ │ └── ViewEpoxyModeled.swift │ ├── SwiftUI │ │ ├── EpoxySwiftUIHostingController.swift │ │ ├── EpoxySwiftUIHostingView.swift │ │ ├── EpoxySwiftUIIntrinsicContentSizeInvalidator.swift │ │ ├── EpoxySwiftUILayoutMargins.swift │ │ ├── EpoxyableView+SwiftUIView.swift │ │ ├── LayoutUtilities │ │ │ ├── MeasuringViewRepresentable.swift │ │ │ └── SwiftUIMeasurementContainer.swift │ │ ├── SwiftUIView.swift │ │ ├── UIView+SwiftUIView.swift │ │ └── UIViewConfiguringSwiftUIView.swift │ └── Views │ │ ├── BehaviorsConfigurableView.swift │ │ ├── ContentConfigurableView.swift │ │ ├── EpoxyableView.swift │ │ ├── StyledView.swift │ │ └── ViewType.swift ├── EpoxyLayoutGroups │ ├── Constrainable │ │ ├── AnchoringContainer.swift │ │ ├── Constrainable.swift │ │ └── ConstrainableContainer.swift │ ├── Constraints │ │ ├── GroupConstraints.swift │ │ ├── HGroupConstraints.swift │ │ └── VGroupConstraints.swift │ ├── Extensions │ │ ├── EpoxyableView+GroupItem.swift │ │ └── NSLayoutConstraint+Optional.swift │ ├── Groups │ │ ├── Group.swift │ │ ├── HGroup.swift │ │ ├── HGroupItemAlignment.swift │ │ ├── LayoutSpacer.swift │ │ ├── VGroup.swift │ │ └── VGroupItemAlignment.swift │ ├── Models │ │ ├── AnyGroupItem.swift │ │ ├── GroupItem.swift │ │ ├── GroupItemModeling.swift │ │ ├── GroupModelBuilder.swift │ │ ├── HGroupItem.swift │ │ ├── InternalGroupItemModeling.swift │ │ ├── SpacerItem.swift │ │ ├── StaticGroupItem.swift │ │ └── VGroupItem.swift │ ├── Providers │ │ ├── AccessibilityAlignmentProviding.swift │ │ ├── GroupItemsProviding.swift │ │ ├── HorizontalAlignmentProviding.swift │ │ ├── MakeConstrainableProviding.swift │ │ ├── PaddingProviding.swift │ │ ├── ReflowsForAccessibilityTypeSizeProviding.swift │ │ └── VerticalAlignmentProviding.swift │ ├── Types │ │ ├── GroupEdgeInsets.swift │ │ └── LayoutGroupUpdateAnimation.swift │ └── Views │ │ ├── HGroupView.swift │ │ └── VGroupView.swift ├── EpoxyNavigationController │ ├── NavigationController.swift │ ├── NavigationModel.swift │ ├── NavigationModelBuilder.swift │ ├── NavigationQueue.swift │ └── StackProviding.swift └── EpoxyPresentations │ ├── PresentationModel.swift │ ├── PresentationModelBuilder.swift │ ├── PresentationModelProviding.swift │ ├── PresentationQueue.swift │ └── UIViewController+PresentationModel.swift ├── Tests ├── EpoxyTests │ ├── BarsTests │ │ ├── BarStackViewSpec.swift │ │ ├── BaseBarInstallerSpec.swift │ │ ├── BottomBarInstallerSpec.swift │ │ ├── SafeAreaWindow.swift │ │ ├── StaticHeightBar.swift │ │ └── TopBarInstallerSpec.swift │ ├── CollectionViewTests │ │ ├── CollectionViewSpec.swift │ │ ├── FlowLayoutTests.swift │ │ ├── ProxyDelegate.swift │ │ └── ReuseIDStoreTests.swift │ ├── CoreTests │ │ ├── CollectionDiffSpec.swift │ │ ├── EpoxyModelBuilderArraySpec.swift │ │ └── EpoxyModeledSpec.swift │ ├── LayoutGroupsTests │ │ ├── ConstrainableContainerSpec.swift │ │ ├── GroupItemSpec.swift │ │ ├── GroupPerformanceTests.swift │ │ ├── HGroupItemSpec.swift │ │ ├── HGroupSpec.swift │ │ ├── TestHelpers.swift │ │ ├── VGroupItemSpec.swift │ │ └── VGroupSpec.swift │ ├── NavigationControllerTests │ │ ├── NavigationQueueSpec.swift │ │ └── StubTransitionCoordinator.swift │ └── PresentationsTests │ │ ├── PresentationModelBuilderSpec.swift │ │ └── PresentationQueueSpec.swift └── PerformanceTests │ └── CollectionDiffPerformanceTestCase.swift └── docs ├── images ├── ActionRow.png ├── CheckboxRow.png ├── IconRow.png ├── MessageRow.png ├── MessageRowHierarchy.png ├── MessageRowReflow.png ├── bottom_button_example.png ├── checkbox_row_bottom.png ├── checkbox_row_center.png ├── checkbox_row_centered_to_subtitle.png ├── checkbox_row_custom_subtitle_first_baseline.png ├── counter_example.gif ├── form_navigation_example.gif ├── home_details.png ├── home_photos.png ├── layout_groups_examples.png ├── logo.svg ├── messaging.png ├── modal_example.gif ├── registration.png └── tap_me_example.png └── pull_request_template.md /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup the Epoxy iOS CI Environment 3 | inputs: 4 | xcode: 5 | description: The version of Xcode to select 6 | runs: 7 | using: composite 8 | steps: 9 | - name: Select Xcode ${{ inputs.xcode }} 10 | run: sudo xcode-select --switch /Applications/Xcode_${{ inputs.xcode }}.app 11 | if: ${{ inputs.xcode }} 12 | shell: bash 13 | 14 | - name: Install Mint via Homebrew 15 | run: brew install mint 16 | shell: bash 17 | 18 | - name: Install Ruby Gems 19 | run: bundle install 20 | shell: bash 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build-package: 11 | runs-on: macos-latest 12 | strategy: 13 | matrix: 14 | xcode: 15 | - '14.3.1' # Swift 5.8 (lowest) 16 | - '15.4' # Swift 5.10 (highest) 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: ./.github/actions/setup 20 | with: 21 | xcode: ${{ matrix.xcode }} 22 | - name: Build Package 23 | run: bundle exec rake build:package 24 | 25 | build-epoxy-core: 26 | runs-on: macos-latest 27 | strategy: 28 | matrix: 29 | xcode: 30 | - '15.4' # Swift 5.10 (highest) 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: ./.github/actions/setup 34 | with: 35 | xcode: ${{ matrix.xcode }} 36 | - name: Build EpoxyCore 37 | run: bundle exec rake build:EpoxyCore 38 | 39 | build-example: 40 | runs-on: macos-latest 41 | strategy: 42 | matrix: 43 | xcode: 44 | - '14.3.1' # Swift 5.8 (lowest) 45 | - '15.4' # Swift 5.10 (highest) 46 | steps: 47 | - uses: actions/checkout@v2 48 | - uses: ./.github/actions/setup 49 | with: 50 | xcode: ${{ matrix.xcode }} 51 | - name: Build Example 52 | run: bundle exec rake build:example 53 | 54 | test-package: 55 | runs-on: macos-latest 56 | strategy: 57 | matrix: 58 | xcode: 59 | - '14.3.1' # Swift 5.8 (lowest) 60 | - '15.4' # Swift 5.10 (highest) 61 | steps: 62 | - uses: actions/checkout@v2 63 | - uses: ./.github/actions/setup 64 | with: 65 | xcode: ${{ matrix.xcode }} 66 | - name: Test Package 67 | run: bundle exec rake test:package 68 | 69 | lint-swift: 70 | runs-on: macos-latest 71 | steps: 72 | - uses: actions/checkout@v2 73 | - uses: ./.github/actions/setup 74 | - name: Lint Swift 75 | run: bundle exec rake lint:swift 76 | 77 | lint-podspec: 78 | runs-on: macos-latest 79 | steps: 80 | - uses: actions/checkout@v2 81 | - uses: ./.github/actions/setup 82 | - name: Lint Podspec 83 | run: bundle exec rake lint:podspec 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | *~.nib 4 | 5 | *.xcodeproj/project.xcworkspace/ 6 | xcuserdata 7 | *.xccheckout 8 | *.xcscmblueprint 9 | 10 | xcuserdata/ 11 | *.xcuserstate 12 | 13 | .swiftpm 14 | .build/ 15 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - platform: ios 5 | documentation_targets: 6 | - EpoxyCore 7 | - EpoxyCollectionView 8 | - EpoxyBars 9 | - EpoxyNavigationController 10 | - EpoxyPresentations 11 | - EpoxyLayoutGroups 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Airbnb has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full Code of Conduct text](https://airbnb.io/codeofconduct/) so that you can understand what actions will and will not be tolerated. Report violations to the maintainers of this project or to [opensource-conduct@airbnb.com](mailto:opensource-conduct@airbnb.com). 2 | 3 | Reports sent to [opensource-conduct@airbnb.com](mailto:opensource-conduct@airbnb.com) are received by Airbnb's open source code of conduct moderation team, which is composed of Airbnb employees. All communications are private and confidential. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Pull requests are welcome! We'd love help improving this library. To contribute first fork this repository and create your branch from `master`. Then open a pull request when you're ready to submit your change for review. 4 | 5 | If you have a feature request or bug, please open a new [issue](https://github.com/airbnb/epoxy-ios/issues) so we can track it. 6 | 7 | Contributors are expected to follow the [Code of Conduct](https://github.com/airbnb/epoxy-ios/blob/master/CODE_OF_CONDUCT.md). 8 | -------------------------------------------------------------------------------- /ConfigurePodspec.rb: -------------------------------------------------------------------------------- 1 | # Configures the given Podspec with shared constants for all Epoxy podspecs. 2 | def configure(spec:, name:, summary:, local_deps: []) 3 | # The shared CocoaPods version number for Epoxy. 4 | # 5 | # Change this constant to increment the Podspec version for all Epoxy Podspecs from a single place. 6 | version = '0.10.0' 7 | 8 | spec.name = name 9 | spec.summary = summary 10 | spec.version = version 11 | spec.license = 'Apache License, Version 2.0' 12 | spec.homepage = 'https://github.com/airbnb/epoxy-ios' 13 | spec.authors = 'Airbnb' 14 | spec.source = { git: 'https://github.com/airbnb/epoxy-ios.git', tag: version } 15 | spec.source_files = "Sources/#{name}/**/*.swift" 16 | spec.ios.deployment_target = '13.0' 17 | spec.swift_versions = ['5.5'] 18 | 19 | local_deps.each do |dep| 20 | spec.dependency dep, version 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /Epoxy.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | # Update ConfigurePodspec.rb to increment the version number. 3 | require_relative 'ConfigurePodspec' 4 | configure( 5 | spec: spec, 6 | name: 'Epoxy', 7 | summary: 'Declarative UI APIs for UIKit', 8 | local_deps: ['EpoxyCore', 'EpoxyLayoutGroups', 'EpoxyCollectionView', 'EpoxyBars', 'EpoxyNavigationController', 'EpoxyPresentations']) 9 | end 10 | -------------------------------------------------------------------------------- /Epoxy.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Epoxy.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Epoxy.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CwlCatchException", 6 | "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "f809deb30dc5c9d9b78c872e553261a61177721a", 10 | "version": "2.0.0" 11 | } 12 | }, 13 | { 14 | "package": "CwlPreconditionTesting", 15 | "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "02b7a39a99c4da27abe03cab2053a9034379639f", 19 | "version": "2.0.0" 20 | } 21 | }, 22 | { 23 | "package": "Nimble", 24 | "repositoryURL": "https://github.com/Quick/Nimble.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "e491a6731307bb23783bf664d003be9b2fa59ab5", 28 | "version": "9.0.0" 29 | } 30 | }, 31 | { 32 | "package": "Quick", 33 | "repositoryURL": "https://github.com/Quick/Quick.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "bd86ca0141e3cfb333546de5a11ede63f0c4a0e6", 37 | "version": "4.0.0" 38 | } 39 | }, 40 | { 41 | "package": "AirbnbSwift", 42 | "repositoryURL": "https://github.com/airbnb/swift", 43 | "state": { 44 | "branch": null, 45 | "revision": "07bb2e0822ca6e464bf3610ed452568931fdbf65", 46 | "version": "1.0.3" 47 | } 48 | }, 49 | { 50 | "package": "swift-argument-parser", 51 | "repositoryURL": "https://github.com/apple/swift-argument-parser.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "e394bf350e38cb100b6bc4172834770ede1b7232", 55 | "version": "1.0.3" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /EpoxyBars.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | # Update ConfigurePodspec.rb to increment the version number. 3 | require_relative 'ConfigurePodspec' 4 | configure( 5 | spec: spec, 6 | name: 'EpoxyBars', 7 | summary: 'Declarative API for adding fixed top/bottom bar stacks to a UIViewController', 8 | local_deps: ['EpoxyCore']) 9 | end 10 | -------------------------------------------------------------------------------- /EpoxyCollectionView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | # Update ConfigurePodspec.rb to increment the version number. 3 | require_relative 'ConfigurePodspec' 4 | configure( 5 | spec: spec, 6 | name: 'EpoxyCollectionView', 7 | summary: 'Declarative API for driving the content of a UICollectionView', 8 | local_deps: ['EpoxyCore']) 9 | end 10 | -------------------------------------------------------------------------------- /EpoxyCore.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | # Update ConfigurePodspec.rb to increment the version number. 3 | require_relative 'ConfigurePodspec' 4 | configure( 5 | spec: spec, 6 | name: 'EpoxyCore', 7 | summary: 'Foundational APIs that are used to build all Epoxy declarative UI APIs') 8 | end 9 | -------------------------------------------------------------------------------- /EpoxyLayoutGroups.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | # Update ConfigurePodspec.rb to increment the version number. 3 | require_relative 'ConfigurePodspec' 4 | configure( 5 | spec: spec, 6 | name: 'EpoxyLayoutGroups', 7 | summary: 'Declarative API for building composable layouts in UIKit with a syntax similar to SwiftUI stack APIs', 8 | local_deps: ['EpoxyCore']) 9 | end 10 | -------------------------------------------------------------------------------- /EpoxyNavigationController.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | # Update ConfigurePodspec.rb to increment the version number. 3 | require_relative 'ConfigurePodspec' 4 | configure( 5 | spec: spec, 6 | name: 'EpoxyNavigationController', 7 | summary: 'Declarative API for driving the navigation stack of a UINavigationController', 8 | local_deps: ['EpoxyCore']) 9 | end 10 | -------------------------------------------------------------------------------- /EpoxyPresentations.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | # Update ConfigurePodspec.rb to increment the version number. 3 | require_relative 'ConfigurePodspec' 4 | configure( 5 | spec: spec, 6 | name: 'EpoxyPresentations', 7 | summary: 'Declarative API for driving the modal presentations of a UIViewController', 8 | local_deps: ['EpoxyCore']) 9 | end 10 | -------------------------------------------------------------------------------- /Example/EpoxyExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/EpoxyExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Application/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 9/12/19. 2 | // Copyright © 2019 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | @UIApplicationMain 7 | class AppDelegate: UIResponder, UIApplicationDelegate { 8 | 9 | var window: UIWindow? 10 | 11 | func application( 12 | _: UIApplication, 13 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) 14 | -> Bool 15 | { 16 | window = UIWindow(frame: UIScreen.main.bounds) 17 | window?.rootViewController = MainViewController() 18 | window?.makeKeyAndVisible() 19 | return true 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/App_Store_1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/App_Store_1024x1024@1x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_App_76_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_App_76_1x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_App_76_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_App_76_2x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Notifications_20_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Notifications_20_1x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Notifications_20_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Notifications_20_2x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Pro_App_83.5_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Pro_App_83.5_2x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Pro_Spotlight_40_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Pro_Spotlight_40_2x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Settings_29_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Settings_29_1x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Settings_29_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Settings_29_2x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Spotlight_40_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPad_Spotlight_40_1x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_2x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_App_60_3x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_2x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Notifications_20_3x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_2x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Settings_29_3x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_2x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/Example/EpoxyExample/Assets/Assets.xcassets/AppIcon.appiconset/iPhone_Spotlight_40_3x.png -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Assets/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Epoxy 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Data/LayoutGroupsExample.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 4/14/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | /// All examples of Epoxy layout groups. 5 | enum LayoutGroupsExample: CaseIterable { 6 | case readmeExamples 7 | case textRowExample 8 | case colors 9 | case messages 10 | case messagesUIStackView 11 | case todoList 12 | case entirelyInline 13 | case complex 14 | case dynamic 15 | 16 | // MARK: Internal 17 | 18 | var title: String { 19 | switch self { 20 | case .readmeExamples: 21 | return "Readme examples" 22 | case .textRowExample: 23 | return "Text rows" 24 | case .colors: 25 | return "Group alignments" 26 | case .messages: 27 | return "Message list" 28 | case .messagesUIStackView: 29 | return "Message list (UIStackView)" 30 | case .todoList: 31 | return "Todo List" 32 | case .entirelyInline: 33 | return "Inline components" 34 | case .complex: 35 | return "Shuffle" 36 | case .dynamic: 37 | return "Dynamic" 38 | } 39 | } 40 | 41 | var body: String { 42 | switch self { 43 | case .readmeExamples: 44 | return "All of the examples from the EpoxyLayoutGroups readme" 45 | case .textRowExample: 46 | return "Text rows with various alignments used in the titles" 47 | case .colors: 48 | return "A set of examples that show how group alignments affect subviews" 49 | case .messages: 50 | return "A list of message thread rows created using EpoxyLayoutGroups" 51 | case .messagesUIStackView: 52 | return "A list of message thread rows created using UIStackView to showcase the difference in API" 53 | case .todoList: 54 | return "A sample todo list" 55 | case .entirelyInline: 56 | return "An example showcasing creating components inline in an EpoxyCollectionView ItemModel" 57 | case .complex: 58 | return "An example showing how groups handle updates to the contained items" 59 | case .dynamic: 60 | return "An example showcasing layout groups with dynamic subviews in a CollectionView" 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Data/ReadmeExample.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/17/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | /// All examples from the README of this project. 5 | enum ReadmeExample: CaseIterable { 6 | case tapMe 7 | case counter 8 | case bottomButton 9 | case formNavigation 10 | case modalPresentation 11 | 12 | // MARK: Internal 13 | 14 | var title: String { 15 | switch self { 16 | case .tapMe: 17 | return "Tap Me" 18 | case .counter: 19 | return "Counter" 20 | case .bottomButton: 21 | return "Bottom button" 22 | case .formNavigation: 23 | return "Form Navigation" 24 | case .modalPresentation: 25 | return "Modal Presentation" 26 | } 27 | } 28 | 29 | var body: String { 30 | switch self { 31 | case .tapMe, .counter: 32 | return "EpoxyCollectionView" 33 | case .bottomButton: 34 | return "EpoxyBars" 35 | case .formNavigation: 36 | return "EpoxyNavigationController" 37 | case .modalPresentation: 38 | return "EpoxyPresentations" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Extensions/UIImageView+RemoteImage.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/12/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Foundation 5 | import ObjectiveC 6 | import UIKit 7 | 8 | /// Note that this is a quick and dirty solution for downloading images and 9 | /// should by no means be used in a production app. 10 | extension UIImageView { 11 | 12 | // MARK: Internal 13 | 14 | func setURL(_ url: URL?) { 15 | // Currently loading an image, URL is updated to nil: 16 | guard let url = url else { 17 | if let storage = storage { 18 | self.storage = nil 19 | storage.dataTask.cancel() 20 | image = nil 21 | } 22 | return 23 | } 24 | 25 | // We're already actively loading an image with this URL: 26 | if let storage = storage, storage.url == url { 27 | return 28 | } 29 | 30 | // We need to load the image at the URL: 31 | image = nil 32 | let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in 33 | guard 34 | self?.storage?.url == url, 35 | let data = data, 36 | error == nil, 37 | let image = UIImage(data: data) 38 | else { 39 | return 40 | } 41 | 42 | DispatchQueue.main.async { [weak self] in 43 | // If the image changed, don't replace it. 44 | guard self?.storage?.url == url else { return } 45 | self?.image = image 46 | } 47 | } 48 | storage = .init(url: url, dataTask: task) 49 | task.resume() 50 | } 51 | 52 | // MARK: Private 53 | 54 | private struct Storage { 55 | var url: URL 56 | var dataTask: URLSessionDataTask 57 | static var key = 0 58 | } 59 | 60 | @nonobjc 61 | private var storage: Storage? { 62 | get { 63 | objc_getAssociatedObject(self, &Storage.key) as? Storage 64 | } 65 | set { 66 | // Atomic since we access this property from the URLSession background thread. 67 | objc_setAssociatedObject(self, &Storage.key, newValue, .OBJC_ASSOCIATION_COPY) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/CollectionView/CardStackViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 2/9/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | final class CardStackViewController: CollectionViewController { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(layout: UICollectionViewCompositionalLayout.listNoDividers) 13 | setSections(sections, animated: false) 14 | } 15 | 16 | // MARK: Private 17 | 18 | private var sections: [SectionModel] { 19 | (0..<10).map { (dataID: Int) in 20 | SectionModel(dataID: dataID) { 21 | (0..<10).map { (dataID: Int) in 22 | // Inlining this URL initializer causes SwiftLint to error for some reason 23 | let imageURL = URL(string: "https://picsum.photos/id/\(dataID + 310)/600/300")! 24 | return CardContainer.itemModel( 25 | dataID: dataID, 26 | content: .init( 27 | models: [ 28 | ImageMarquee.barModel( 29 | // swiftlint:disable:next force_unwrapping 30 | content: .init(imageURL: imageURL), 31 | style: .init(height: 150, contentMode: .scaleAspectFill)) 32 | .didSelect { _ in 33 | // swiftlint:disable:next no_direct_standard_out_logs 34 | print("Selected Image Marquee \(dataID)") 35 | }, 36 | TextRow.barModel( 37 | content: .init(title: "Row \(dataID)", body: BeloIpsum.paragraph(count: 1, seed: dataID)), 38 | style: .small) 39 | .didSelect { _ in 40 | // swiftlint:disable:next no_direct_standard_out_logs 41 | print("Selected Text Row \(dataID)") 42 | }, 43 | ], 44 | selectedBackgroundColor: .secondarySystemBackground), 45 | style: .init(card: .init())) 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/CollectionView/CompositionalLayoutViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 9/12/19. 2 | // Copyright © 2019 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | final class CompositionalLayoutViewController: CollectionViewController { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(layout: UICollectionViewCompositionalLayout.epoxy) 13 | setSections(sections, animated: false) 14 | } 15 | 16 | // MARK: Private 17 | 18 | private enum SectionID { 19 | case carousel, list 20 | } 21 | 22 | @SectionModelBuilder private var sections: [SectionModel] { 23 | SectionModel(dataID: SectionID.carousel) { 24 | (0..<10).map { (dataID: Int) in 25 | TextRow.itemModel( 26 | dataID: dataID, 27 | content: .init(title: "Page \(dataID)"), 28 | style: .small) 29 | .didSelect { _ in 30 | // swiftlint:disable:next no_direct_standard_out_logs 31 | print("Carousel page \(dataID) did select") 32 | } 33 | .willDisplay { _ in 34 | // swiftlint:disable:next no_direct_standard_out_logs 35 | print("Carousel page \(dataID) will display") 36 | } 37 | } 38 | } 39 | .supplementaryItems(ofKind: UICollectionView.elementKindSectionHeader, [ 40 | TextRow.supplementaryItemModel(dataID: 0, content: .init(title: "Carousel section"), style: .large), 41 | ]) 42 | .compositionalLayoutSection(.carouselWithHeader) 43 | 44 | SectionModel(dataID: SectionID.list) { 45 | (0..<10).map { (dataID: Int) in 46 | TextRow.itemModel( 47 | dataID: dataID, 48 | content: .init(title: "Row \(dataID)", body: BeloIpsum.paragraph(count: 1, seed: dataID)), 49 | style: .small) 50 | .didSelect { _ in 51 | // swiftlint:disable:next no_direct_standard_out_logs 52 | print("List row \(dataID) selected") 53 | } 54 | .willDisplay { _ in 55 | // swiftlint:disable:next no_direct_standard_out_logs 56 | print("List row \(dataID) will display") 57 | } 58 | } 59 | } 60 | .supplementaryItems(ofKind: UICollectionView.elementKindSectionHeader, [ 61 | TextRow.supplementaryItemModel(dataID: 0, content: .init(title: "List section"), style: .large), 62 | ]) 63 | .compositionalLayoutSectionProvider(NSCollectionLayoutSection.listWithHeader) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/CollectionView/CustomSelfSizingContentViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Cal Stephens on 2/12/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | final class CustomSelfSizingContentViewController: CollectionViewController { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(layout: UICollectionViewCompositionalLayout.listNoDividers) 13 | setItems(items, animated: false) 14 | } 15 | 16 | // MARK: Private 17 | 18 | private var items: [ItemModeling] { 19 | (0..<10).map { (dataID: Int) in 20 | CustomSizingView.itemModel(dataID: dataID) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/CollectionView/ShuffleViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Logan Shire on 1/24/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | final class ShuffleViewController: CollectionViewController { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(layout: UICollectionViewCompositionalLayout.listNoDividers) 13 | setSections(sections, animated: false) 14 | } 15 | 16 | // MARK: Internal 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | addTimer() 22 | } 23 | 24 | // MARK: Private 25 | 26 | private struct State { 27 | init() { 28 | randomize() 29 | } 30 | 31 | var sections = [(id: Int, itemIDs: [Int])]() 32 | 33 | mutating func randomize() { 34 | sections = (0..<3).shuffled().filter { _ in Int.random(in: 0..<3) != 0 }.map { id in 35 | let itemIDs = (0..<10).shuffled().filter { _ in Int.random(in: 0..<3) != 0 } 36 | return (id: id, itemIDs: itemIDs) 37 | } 38 | } 39 | } 40 | 41 | private var state = State() { 42 | didSet { setSections(sections, animated: true) } 43 | } 44 | 45 | private var sections: [SectionModel] { 46 | state.sections.map { section in 47 | SectionModel( 48 | dataID: section.id, 49 | items: section.itemIDs.map { itemID in 50 | TextRow.itemModel( 51 | dataID: itemID, 52 | content: .init( 53 | title: "Section \(section.id), Row \(itemID)", 54 | body: BeloIpsum.paragraph(count: 1, seed: itemID)), 55 | style: .small) 56 | .didSelect { _ in 57 | // swiftlint:disable:next no_direct_standard_out_logs 58 | print("Selected section \(section.id), Row \(itemID)") 59 | } 60 | }) 61 | } 62 | } 63 | 64 | private func addTimer() { 65 | Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in 66 | guard let self = self else { 67 | timer.invalidate() 68 | return 69 | } 70 | self.state.randomize() 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/CollectionView/TextFieldViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by oleksandr_zarochintsev on 4/26/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | final class TextFieldViewController: CollectionViewController { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(layout: UICollectionViewCompositionalLayout.listNoDividers) 13 | setItems([textFieldRowItem], animated: false) 14 | } 15 | 16 | // MARK: Internal 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | bottomBarInstaller.install() 21 | } 22 | 23 | // MARK: Private 24 | 25 | private enum DataID { 26 | case username 27 | case button 28 | } 29 | 30 | private lazy var bottomBarInstaller = BottomBarInstaller( 31 | viewController: self, 32 | avoidsKeyboard: true, 33 | bars: [buttonRowBar]) 34 | 35 | private var username = "@airbnb" { 36 | didSet { setItems([textFieldRowItem], animated: true) } 37 | } 38 | 39 | private var textFieldRowItem: ItemModeling { 40 | TextFieldRow.itemModel( 41 | dataID: DataID.username, 42 | content: .init(text: username, placeholder: "Username"), 43 | behaviors: .init(didUpdateText: { [weak self] username in 44 | self?.username = username ?? "" 45 | }), 46 | style: .base) 47 | } 48 | 49 | private var buttonRowBar: BarModeling { 50 | ButtonRow.barModel( 51 | dataID: DataID.button, 52 | content: .init(text: "Submit"), 53 | behaviors: .init(didTap: { [weak self] in 54 | guard let self = self else { return } 55 | self.view.endEditing(true) 56 | // swiftlint:disable:next no_direct_standard_out_logs 57 | print("Submitted '\(self.username)'") 58 | })) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/Helpers/NavigationWrapperViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/17/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// A naive implementation a Navigation wrapper so we can nest the `FormNavigationController` 7 | /// without a UIKit crash. 8 | /// 9 | /// You probably want a custom wrapper for your use cases. 10 | final class NavigationWrapperViewController: UIViewController { 11 | init(navigationController: UINavigationController) { 12 | // A naive implementation of `wrapNavigation` so we can nest the `FormNavigationController`. 13 | navigationController.setNavigationBarHidden(true, animated: false) 14 | 15 | super.init(nibName: nil, bundle: nil) 16 | 17 | addChild(navigationController) 18 | view.addSubview(navigationController.view) 19 | navigationController.view.frame = view.bounds 20 | navigationController.view.translatesAutoresizingMaskIntoConstraints = true 21 | navigationController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth] 22 | navigationController.didMove(toParent: self) 23 | } 24 | 25 | required init?(coder _: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/LayoutGroups/ColorsViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/27/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCollectionView 5 | import UIKit 6 | 7 | class ColorsViewController: CollectionViewController { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(layout: UICollectionViewCompositionalLayout.list) 13 | setItems(items, animated: false) 14 | } 15 | 16 | // MARK: Internal 17 | 18 | enum DataID { 19 | case hGroupFill 20 | case hGroupTop 21 | case hGroupCenter 22 | case hGroupBottom 23 | case vGroupFill 24 | case vGroupLeading 25 | case vGroupCenter 26 | case vGroupTrailing 27 | } 28 | 29 | @ItemModelBuilder 30 | var items: [ItemModeling] { 31 | ColorsRow.itemModel(dataID: DataID.hGroupFill, style: .init(variant: .hGroup(.fill))) 32 | ColorsRow.itemModel(dataID: DataID.hGroupTop, style: .init(variant: .hGroup(.top))) 33 | ColorsRow.itemModel(dataID: DataID.hGroupCenter, style: .init(variant: .hGroup(.center))) 34 | ColorsRow.itemModel(dataID: DataID.hGroupBottom, style: .init(variant: .hGroup(.bottom))) 35 | ColorsRow.itemModel(dataID: DataID.vGroupFill, style: .init(variant: .vGroup(.fill))) 36 | ColorsRow.itemModel(dataID: DataID.vGroupLeading, style: .init(variant: .vGroup(.leading))) 37 | ColorsRow.itemModel(dataID: DataID.vGroupCenter, style: .init(variant: .vGroup(.center))) 38 | ColorsRow.itemModel(dataID: DataID.vGroupTrailing, style: .init(variant: .vGroup(.trailing))) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/LayoutGroups/DynamicLayoutGroupsViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 11/11/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | final class DynamicLayoutGroupsViewController: CollectionViewController { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(layout: UICollectionViewCompositionalLayout.list) 13 | setItems(items, animated: false) 14 | } 15 | 16 | // MARK: Private 17 | 18 | private enum DataID: CaseIterable { 19 | case row1 20 | case row2 21 | case row3 22 | } 23 | 24 | private var openOptions: [AnyHashable: Bool] = [:] 25 | 26 | private var items: [ItemModeling] { 27 | DataID.allCases.map { id in 28 | DynamicRow.itemModel( 29 | dataID: id, 30 | content: .init( 31 | title: "Want to know more?", 32 | subtitle: "Tap below to reveal a set of options you can choose from", 33 | revealOptionsButton: openOptions(id) ? nil : "Reveal options", 34 | options: options(for: id), 35 | footer: "Thank you"), 36 | behaviors: .init(didTapRevealOptions: { [weak self] in 37 | self?.openOptions[id] = true 38 | self?.updateData() 39 | }, didTapOption: { [weak self] option in 40 | // swiftlint:disable:next no_direct_standard_out_logs 41 | print("Selected option \(option)") 42 | self?.openOptions[id] = false 43 | self?.updateData() 44 | })) 45 | } 46 | } 47 | 48 | private func updateData() { 49 | setItems(items, animated: true) 50 | } 51 | 52 | private func openOptions(_ dataID: AnyHashable) -> Bool { 53 | openOptions[dataID] ?? false 54 | } 55 | 56 | private func options(for dataID: AnyHashable) -> [String]? { 57 | if openOptions(dataID) { 58 | return [ 59 | "Option 1", 60 | "Option 2", 61 | "Option 3", 62 | "Option 4", 63 | "Option 5", 64 | ] 65 | } 66 | return nil 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/LayoutGroups/LayoutGroupsReadmeExamplesViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 2/5/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCollectionView 5 | import UIKit 6 | 7 | class LayoutGroupsReadmeExamplesViewController: CollectionViewController { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(layout: UICollectionViewCompositionalLayout.list) 13 | setItems(items, animated: false) 14 | } 15 | 16 | // MARK: Internal 17 | 18 | @ItemModelBuilder 19 | var items: [ItemModeling] { 20 | ActionButtonRow.itemModel( 21 | dataID: DataID.actionButtonRow, 22 | content: .init( 23 | title: "Title text", 24 | subtitle: "Subtitle text", 25 | actionText: "Perform action")) 26 | IconRow.itemModel( 27 | dataID: DataID.iconRow, 28 | content: .init( 29 | title: "This is an IconRow", 30 | // swiftlint:disable:next force_unwrapping 31 | icon: UIImage(systemName: "person.fill")!)) 32 | } 33 | 34 | // MARK: Private 35 | 36 | private enum DataID { 37 | case actionButtonRow 38 | case iconRow 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/LayoutGroups/MessagesUIStackViewViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 2/2/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCollectionView 5 | import UIKit 6 | 7 | class MessagesUIStackViewViewController: CollectionViewController { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(layout: UICollectionViewCompositionalLayout.list) 13 | setItems(items, animated: false) 14 | } 15 | 16 | // MARK: Internal 17 | 18 | var items: [ItemModeling] { 19 | let sampleRows = [ 20 | MessageRowStackView.itemModel( 21 | dataID: DataID.sara, 22 | content: .init( 23 | name: "Sara Bareilles", 24 | date: "Jan 25, 2021", 25 | messagePreview: BeloIpsum.sentence(count: 5), 26 | seenText: "Seen"), 27 | style: .init(showUnread: true)), 28 | MessageRowStackView.itemModel( 29 | dataID: DataID.beyonce, 30 | content: .init( 31 | name: "Beyoncé Knowles", 32 | date: "Jan 22, 2021", 33 | messagePreview: BeloIpsum.sentence(count: 2), 34 | seenText: "Unread"), 35 | style: .init(showUnread: false)), 36 | MessageRowStackView.itemModel( 37 | dataID: DataID.taylor, 38 | content: .init( 39 | name: "Taylor Swift", 40 | date: "Dec 21, 2020", 41 | messagePreview: BeloIpsum.sentence(count: 1), 42 | seenText: "Seen"), 43 | style: .init(showUnread: false)), 44 | ] 45 | return Array(repeating: sampleRows, count: 100).flatMap { $0 } 46 | } 47 | 48 | // MARK: Private 49 | 50 | private enum DataID { 51 | case sara 52 | case beyonce 53 | case taylor 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/LayoutGroups/MessagesViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/27/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCollectionView 5 | import UIKit 6 | 7 | class MessagesViewController: CollectionViewController { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(layout: UICollectionViewCompositionalLayout.list) 13 | setItems(items, animated: false) 14 | } 15 | 16 | // MARK: Internal 17 | 18 | var items: [ItemModeling] { 19 | let sampleRows = [ 20 | MessageRow.itemModel( 21 | dataID: DataID.sara, 22 | content: .init( 23 | name: "Sara Bareilles", 24 | date: "Jan 25, 2021", 25 | messagePreview: BeloIpsum.sentence(count: 5), 26 | seenText: "Seen"), 27 | style: .init(showUnread: true)), 28 | MessageRow.itemModel( 29 | dataID: DataID.beyonce, 30 | content: .init( 31 | name: "Beyoncé Knowles", 32 | date: "Jan 22, 2021", 33 | messagePreview: BeloIpsum.sentence(count: 2), 34 | seenText: "Unread"), 35 | style: .init(showUnread: false)), 36 | MessageRow.itemModel( 37 | dataID: DataID.taylor, 38 | content: .init( 39 | name: "Taylor Swift", 40 | date: "Dec 21, 2020", 41 | messagePreview: BeloIpsum.sentence(count: 1), 42 | seenText: "Seen"), 43 | style: .init(showUnread: false)), 44 | ] 45 | return Array(repeating: sampleRows, count: 100).flatMap { $0 } 46 | } 47 | 48 | // MARK: Private 49 | 50 | private enum DataID { 51 | case sara 52 | case beyonce 53 | case taylor 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/LayoutGroups/TextRowExampleViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/26/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCollectionView 5 | import UIKit 6 | 7 | class TextRowExampleViewController: CollectionViewController { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(layout: UICollectionViewCompositionalLayout.list) 13 | setItems(items, animated: false) 14 | } 15 | 16 | // MARK: Internal 17 | 18 | @ItemModelBuilder 19 | var items: [ItemModeling] { 20 | AlignableTextRow.itemModel( 21 | dataID: DataID.leading, 22 | content: .init(title: "Title Text", subtitle: "The title in this row uses .horizontalAlignment(.leading)"), 23 | style: .leadingTitle) 24 | AlignableTextRow.itemModel( 25 | dataID: DataID.center, 26 | content: .init(title: "Title Text", subtitle: "The title in this row uses .horizontalAlignment(.center)"), 27 | style: .centerTitle) 28 | AlignableTextRow.itemModel( 29 | dataID: DataID.trailing, 30 | content: .init(title: "Title Text", subtitle: "The title in this row uses .horizontalAlignment(.trailing)"), 31 | style: .trailingTitle) 32 | } 33 | 34 | // MARK: Private 35 | 36 | private enum DataID { 37 | case leading 38 | case center 39 | case trailing 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/LayoutGroups/TodoListViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/28/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCollectionView 5 | import UIKit 6 | 7 | // MARK: - TodoItem 8 | 9 | struct TodoItem { 10 | let id = UUID() 11 | let title: String 12 | let notes: String 13 | var isComplete: Bool 14 | } 15 | 16 | // MARK: - TodoListViewController 17 | 18 | class TodoListViewController: CollectionViewController { 19 | 20 | // MARK: Lifecycle 21 | 22 | init() { 23 | super.init(layout: UICollectionViewCompositionalLayout.list) 24 | setItems(items, animated: false) 25 | } 26 | 27 | // MARK: Internal 28 | 29 | var items: [ItemModeling] { 30 | demoItems.map { item in 31 | CheckboxRow.itemModel( 32 | dataID: item.id, 33 | content: .with(todoItem: item)) 34 | } 35 | } 36 | 37 | // MARK: Private 38 | 39 | private var demoItems: [TodoItem] { 40 | [ 41 | .init(title: "Laundry", notes: "Make sure the laundry is washed and folded", isComplete: false), 42 | .init(title: "Make the bed", notes: "Do this first thing after waking up!", isComplete: false), 43 | .init(title: "Buy Groceries", notes: "Eggs, milk, cheese, bread", isComplete: false), 44 | .init(title: "Build iOS App", notes: "Using LayoutGroups to make my layout fun and easy!", isComplete: true), 45 | ] 46 | } 47 | } 48 | 49 | extension CheckboxRow.Content { 50 | static func with(todoItem: TodoItem) -> CheckboxRow.Content { 51 | .init( 52 | title: todoItem.title, 53 | subtitle: todoItem.notes, 54 | isChecked: todoItem.isComplete) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/LayoutGroups/UIViewController+LayoutGroupsExample.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 10/14/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | extension UIViewController { 7 | static func makeLayoutGroupsExample(_ example: LayoutGroupsExample) -> UIViewController { 8 | let viewController: UIViewController 9 | switch example { 10 | case .readmeExamples: 11 | viewController = LayoutGroupsReadmeExamplesViewController() 12 | case .textRowExample: 13 | viewController = TextRowExampleViewController() 14 | case .colors: 15 | viewController = ColorsViewController() 16 | case .messages: 17 | viewController = MessagesViewController() 18 | case .messagesUIStackView: 19 | viewController = MessagesUIStackViewViewController() 20 | case .todoList: 21 | viewController = TodoListViewController() 22 | case .entirelyInline: 23 | viewController = EntirelyInlineViewController() 24 | case .complex: 25 | viewController = ComplexDeclarativeViewController() 26 | case .dynamic: 27 | viewController = DynamicLayoutGroupsViewController() 28 | } 29 | viewController.title = example.title 30 | return viewController 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/Readme/BottomButtonViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 1/21/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | /// Source code for `EpoxyBars` "Bottom Button" example from `README.md`: 8 | class BottomButtonViewController: UIViewController { 9 | 10 | // MARK: Internal 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | view.backgroundColor = .systemBackground 15 | bottomBarInstaller.install() 16 | } 17 | 18 | // MARK: Private 19 | 20 | private lazy var bottomBarInstaller = BottomBarInstaller( 21 | viewController: self, 22 | bars: bars) 23 | 24 | @BarModelBuilder private var bars: [BarModeling] { 25 | ButtonRow.barModel( 26 | content: .init(text: "Click me!"), 27 | behaviors: .init(didTap: { 28 | // Handle button selection 29 | })) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/Readme/CounterViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 1/21/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | /// Source code for `EpoxyCollectionView` "Counter" example from `README.md`: 8 | class CounterViewController: CollectionViewController { 9 | 10 | // MARK: Lifecycle 11 | 12 | init() { 13 | super.init(layout: UICollectionViewCompositionalLayout.list) 14 | setItems(items, animated: false) 15 | } 16 | 17 | // MARK: Private 18 | 19 | private enum DataID { 20 | case row 21 | } 22 | 23 | private var count = 0 { 24 | didSet { setItems(items, animated: true) } 25 | } 26 | 27 | @ItemModelBuilder private var items: [ItemModeling] { 28 | TextRow.itemModel( 29 | dataID: DataID.row, 30 | content: .init( 31 | title: "Count \(count)", 32 | body: "Tap to increment"), 33 | style: .large) 34 | .didSelect { [weak self] _ in 35 | self?.count += 1 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/Readme/LayoutGroupsExampleViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 4/14/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | extension CollectionViewController { 8 | static func layoutGroupsExampleViewController( 9 | didSelect: @escaping (LayoutGroupsExample) -> Void) 10 | -> CollectionViewController 11 | { 12 | CollectionViewController(layout: UICollectionViewCompositionalLayout.list, items: { 13 | LayoutGroupsExample.allCases.map { example in 14 | TextRow.itemModel( 15 | dataID: example, 16 | content: .init(title: example.title, body: example.body), 17 | style: .small) 18 | .didSelect { _ in 19 | didSelect(example) 20 | } 21 | } 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/Readme/PresentationViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 1/29/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | // MARK: - PresentationViewController 8 | 9 | /// Source code for `EpoxyPresentations` example from `README.md`: 10 | final class PresentationViewController: UIViewController { 11 | 12 | // MARK: Internal 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | view.backgroundColor = .systemBackground 17 | } 18 | 19 | override func viewDidAppear(_ animated: Bool) { 20 | super.viewDidAppear(animated) 21 | setPresentation(presentation, animated: true) 22 | } 23 | 24 | // MARK: Private 25 | 26 | private enum DataID { 27 | case detail 28 | } 29 | 30 | private var showDetail = true { 31 | didSet { setPresentation(presentation, animated: true) } 32 | } 33 | 34 | @PresentationModelBuilder private var presentation: PresentationModel? { 35 | if showDetail { 36 | PresentationModel( 37 | dataID: DataID.detail, 38 | presentation: .system, 39 | makeViewController: { [weak self] in 40 | DetailViewController(didTapDismiss: { 41 | self?.showDetail = false 42 | }) 43 | }, 44 | dismiss: { [weak self] in 45 | self?.showDetail = false 46 | }) 47 | } 48 | } 49 | 50 | } 51 | 52 | // MARK: - DetailViewController 53 | 54 | final class DetailViewController: CollectionViewController { 55 | 56 | // MARK: Lifecycle 57 | 58 | init(didTapDismiss: @escaping () -> Void) { 59 | super.init(layout: UICollectionViewCompositionalLayout.list) 60 | topBarInstaller.setBars([ 61 | ButtonRow.barModel(content: .init(text: "Dismiss"), behaviors: .init(didTap: didTapDismiss)), 62 | ], animated: false) 63 | } 64 | 65 | // MARK: Internal 66 | 67 | override func viewDidLoad() { 68 | super.viewDidLoad() 69 | topBarInstaller.install() 70 | } 71 | 72 | // MARK: Private 73 | 74 | private lazy var topBarInstaller = TopBarInstaller(viewController: self) 75 | } 76 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/Readme/ReadmeExamplesViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 1/21/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | extension CollectionViewController { 8 | static func readmeExamplesViewController( 9 | didSelect: @escaping (ReadmeExample) -> Void) 10 | -> CollectionViewController 11 | { 12 | CollectionViewController(layout: UICollectionViewCompositionalLayout.list, items: { 13 | ReadmeExample.allCases.map { example in 14 | TextRow.itemModel( 15 | dataID: example, 16 | content: .init(title: example.title, body: example.body), 17 | style: .small) 18 | .didSelect { _ in 19 | didSelect(example) 20 | } 21 | } 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/Readme/TapMeViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/17/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | /// Source code for `EpoxyCollectionView` "Tap me" example from `README.md`: 8 | extension CollectionViewController { 9 | static func makeTapMeViewController() -> CollectionViewController { 10 | enum DataID { 11 | case row 12 | } 13 | 14 | return CollectionViewController( 15 | layout: UICollectionViewCompositionalLayout.list, 16 | items: { 17 | TextRow.itemModel( 18 | dataID: DataID.row, 19 | content: .init(title: "Tap me!"), 20 | style: .small) 21 | .didSelect { _ in 22 | // Handle selection 23 | } 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/Readme/UIViewController+ReadmeExample.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 10/14/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | extension UIViewController { 8 | static func makeReadmeExample(_ example: ReadmeExample) -> UIViewController { 9 | switch example { 10 | case .tapMe: 11 | return CollectionViewController.makeTapMeViewController() 12 | case .counter: 13 | return CounterViewController() 14 | case .bottomButton: 15 | return BottomButtonViewController() 16 | case .formNavigation: 17 | return FormNavigationController() 18 | case .modalPresentation: 19 | return PresentationViewController() 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUIViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 9/13/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import SwiftUI 6 | import UIKit 7 | 8 | // MARK: - EpoxyInSwiftUIViewController 9 | 10 | final class EpoxyInSwiftUIViewController: UIHostingController { 11 | init() { 12 | super.init(rootView: EpoxyInSwiftUIView()) 13 | } 14 | 15 | required init?(coder _: NSCoder) { 16 | fatalError("init(coder:) has not been implemented") 17 | } 18 | } 19 | 20 | // MARK: - EpoxyInSwiftUIView 21 | 22 | struct EpoxyInSwiftUIView: View { 23 | var body: some View { 24 | ScrollView { 25 | LazyVStack(spacing: 0) { 26 | ForEach(1...100, id: \.self) { index in 27 | TextRow.swiftUIView( 28 | content: .init(title: "Row \(index)", body: BeloIpsum.sentence(count: 1, wordCount: index)), 29 | style: .small) 30 | .configure { context in 31 | // swiftlint:disable:next no_direct_standard_out_logs 32 | print("Configuring \(context.view)") 33 | } 34 | .onTapGesture { 35 | // swiftlint:disable:next no_direct_standard_out_logs 36 | print("Row \(index) tapped!") 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/EpoxyExample/ViewControllers/SwiftUI/SwiftUIInEpoxyResizingViewController.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 7/5/22. 2 | // Copyright © 2022 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import SwiftUI 6 | import UIKit 7 | 8 | // MARK: - SwiftUIInEpoxyResizingViewController 9 | 10 | /// An example view controller that renders an scrollable list of SwiftUI text rows in an Epoxy 11 | /// container that can expand on tap. 12 | final class SwiftUIInEpoxyResizingViewController: CollectionViewController { 13 | 14 | // MARK: Lifecycle 15 | 16 | init() { 17 | super.init(layout: UICollectionViewCompositionalLayout.listNoDividers) 18 | setItems(items, animated: false) 19 | } 20 | 21 | // MARK: Private 22 | 23 | private var items: [ItemModeling] { 24 | (1...100).map { (index: Int) in 25 | SwiftUIExpandableRow(title: "Row \(index)", subtitle: BeloIpsum.sentence(count: 1, wordCount: index)) 26 | // Since each view has its own state, it needs its own reuse ID. 27 | .itemModel(dataID: index, reuseBehavior: .unique(reuseID: index)) 28 | } 29 | } 30 | } 31 | 32 | // MARK: - SwiftUIExpandableRow 33 | 34 | /// An implementation of an expandable row in SwiftUI, which has a local isExpanded state. 35 | struct SwiftUIExpandableRow: View { 36 | var title: String 37 | var subtitle: String 38 | 39 | var body: some View { 40 | VStack(alignment: .leading, spacing: 8) { 41 | Text(title) 42 | .font(Font.body) 43 | .foregroundColor(Color(.label)) 44 | if isExpanded { 45 | Text(subtitle) 46 | .font(Font.caption) 47 | .foregroundColor(Color(.secondaryLabel)) 48 | .transition(.opacity.combined(with: .move(edge: .top))) 49 | } 50 | } 51 | .animation(.default, value: isExpanded) 52 | .padding(EdgeInsets(top: 16, leading: 24, bottom: 16, trailing: 24)) 53 | // Ensure that the text is aligned to the leading edge of the container when it expands beyond 54 | // its ideal width, instead of the center (the default). 55 | .frame(maxWidth: .infinity, alignment: .leading) 56 | .contentShape(Rectangle()) 57 | .onTapGesture { 58 | isExpanded.toggle() 59 | invalidateIntrinsicContentSize() 60 | } 61 | } 62 | 63 | @State private var isExpanded = false 64 | @Environment(\.epoxyIntrinsicContentSizeInvalidator) var invalidateIntrinsicContentSize 65 | } 66 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Views/ButtonRow.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/13/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | final class ButtonRow: UIView, EpoxyableView { 8 | 9 | // MARK: Lifecycle 10 | 11 | init() { 12 | super.init(frame: .zero) 13 | setUp() 14 | } 15 | 16 | required init?(coder _: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | // MARK: Internal 21 | 22 | struct Behaviors { 23 | var didTap: (() -> Void)? 24 | } 25 | 26 | struct Content: Equatable { 27 | var text: String? 28 | } 29 | 30 | func setContent(_ content: Content, animated _: Bool) { 31 | text = content.text 32 | } 33 | 34 | func setBehaviors(_ behaviors: Behaviors?) { 35 | didTap = behaviors?.didTap 36 | } 37 | 38 | // MARK: Private 39 | 40 | private let button = UIButton(type: .system) 41 | private var didTap: (() -> Void)? 42 | 43 | private var text: String? { 44 | get { button.title(for: .normal) } 45 | set { button.setTitle(newValue, for: .normal) } 46 | } 47 | 48 | private func setUp() { 49 | translatesAutoresizingMaskIntoConstraints = false 50 | layoutMargins = UIEdgeInsets(top: 20, left: 24, bottom: 20, right: 24) 51 | backgroundColor = .quaternarySystemFill 52 | 53 | button.tintColor = .systemBlue 54 | button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .title3) 55 | button.translatesAutoresizingMaskIntoConstraints = false 56 | 57 | addSubview(button) 58 | NSLayoutConstraint.activate([ 59 | button.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), 60 | button.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), 61 | button.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), 62 | button.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor), 63 | ]) 64 | 65 | button.addTarget(self, action: #selector(handleTap), for: .touchUpInside) 66 | } 67 | 68 | @objc 69 | private func handleTap() { 70 | didTap?() 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Views/ColorView.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/20/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import UIKit 6 | 7 | // MARK: - ColorView 8 | 9 | final class ColorView: UIView, EpoxyableView { 10 | 11 | // MARK: Lifecycle 12 | 13 | init(style: Style) { 14 | size = style.size 15 | super.init(frame: .zero) 16 | translatesAutoresizingMaskIntoConstraints = false 17 | backgroundColor = style.color 18 | } 19 | 20 | required init?(coder _: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | // MARK: Internal 25 | 26 | struct Style: Hashable { 27 | var size: CGSize? = nil 28 | var color = UIColor.red 29 | } 30 | 31 | override var intrinsicContentSize: CGSize { 32 | size ?? super.intrinsicContentSize 33 | } 34 | 35 | // MARK: Private 36 | 37 | private let size: CGSize? 38 | 39 | } 40 | 41 | // MARK: - ColorView.Style 42 | 43 | extension ColorView.Style { 44 | static var red: ColorView.Style = .init(color: .systemRed) 45 | static var orange: ColorView.Style = .init(color: .systemOrange) 46 | static var yellow: ColorView.Style = .init(color: .systemYellow) 47 | static var green: ColorView.Style = .init(color: .systemGreen) 48 | static var blue: ColorView.Style = .init(color: .systemBlue) 49 | static var purple: ColorView.Style = .init(color: .systemPurple) 50 | } 51 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Views/ImageMarquee.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 1/18/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | final class ImageMarquee: UIView, EpoxyableView { 8 | 9 | // MARK: Lifecycle 10 | 11 | init(style: Style) { 12 | self.style = style 13 | super.init(frame: .zero) 14 | contentMode = style.contentMode 15 | clipsToBounds = true 16 | addSubviews() 17 | constrainSubviews() 18 | } 19 | 20 | required init?(coder _: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | // MARK: Internal 25 | 26 | struct Style: Hashable { 27 | var height: CGFloat 28 | var contentMode: UIView.ContentMode 29 | } 30 | 31 | struct Content: Equatable { 32 | var imageURL: URL? 33 | } 34 | 35 | func setContent(_ content: Content, animated _: Bool) { 36 | imageView.setURL(content.imageURL) 37 | } 38 | 39 | // MARK: Private 40 | 41 | private let style: Style 42 | private let imageView = UIImageView() 43 | 44 | private func addSubviews() { 45 | addSubview(imageView) 46 | } 47 | 48 | private func constrainSubviews() { 49 | imageView.translatesAutoresizingMaskIntoConstraints = false 50 | let heightAnchor = imageView.heightAnchor.constraint(equalToConstant: style.height) 51 | heightAnchor.priority = .defaultHigh 52 | NSLayoutConstraint.activate([ 53 | heightAnchor, 54 | imageView.leadingAnchor.constraint(equalTo: leadingAnchor), 55 | imageView.topAnchor.constraint(equalTo: topAnchor), 56 | imageView.trailingAnchor.constraint(equalTo: trailingAnchor), 57 | imageView.bottomAnchor.constraint(equalTo: bottomAnchor), 58 | ]) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Views/LayoutGroups/ActionButtonRow.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 2/5/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import EpoxyLayoutGroups 6 | import UIKit 7 | 8 | final class ActionButtonRow: BaseRow, EpoxyableView { 9 | 10 | // MARK: Lifecycle 11 | 12 | override init() { 13 | super.init() 14 | setUp() 15 | } 16 | 17 | required init?(coder _: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | // MARK: Internal 22 | 23 | struct Content: Equatable { 24 | let title: String 25 | let subtitle: String 26 | let actionText: String 27 | } 28 | 29 | enum DataID { 30 | case title 31 | case subtitle 32 | case action 33 | } 34 | 35 | func setContent(_ content: Content, animated _: Bool) { 36 | group.setItems { 37 | Label.groupItem( 38 | dataID: DataID.title, 39 | content: content.title, 40 | style: .style(with: .title2)) 41 | Label.groupItem( 42 | dataID: DataID.subtitle, 43 | content: content.subtitle, 44 | style: .style(with: .body)) 45 | Button.groupItem( 46 | dataID: DataID.action, 47 | content: .init(title: content.actionText), 48 | behaviors: .init { button in 49 | // swiftlint:disable:next no_direct_standard_out_logs 50 | print("Tapped the button \(button)") 51 | }, 52 | style: .init()) 53 | } 54 | } 55 | 56 | // MARK: Private 57 | 58 | private let group = VGroup(alignment: .leading, spacing: 8) 59 | 60 | private func setUp() { 61 | group.install(in: self) 62 | group.constrainToMarginsWithHighPriorityBottom() 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Views/LayoutGroups/CheckboxRow.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/28/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import EpoxyLayoutGroups 6 | import UIKit 7 | 8 | final class CheckboxRow: BaseRow, EpoxyableView { 9 | 10 | // MARK: Lifecycle 11 | 12 | override init() { 13 | super.init() 14 | setUp() 15 | } 16 | 17 | required init?(coder _: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | // MARK: Internal 22 | 23 | struct Content: Equatable { 24 | let title: String 25 | let subtitle: String 26 | let isChecked: Bool 27 | } 28 | 29 | enum DataID { 30 | case checkbox 31 | case verticalGroup 32 | case title 33 | case subtitle 34 | } 35 | 36 | func setContent(_ content: Content, animated _: Bool) { 37 | group.setItems { 38 | IconView.groupItem( 39 | dataID: DataID.checkbox, 40 | content: UIImage(systemName: content.isChecked ? "checkmark.square.fill" : "checkmark.square"), 41 | style: .init( 42 | size: .init(width: 24, height: 24), 43 | tintColor: content.isChecked ? .systemGreen : .systemGray)) 44 | VGroupItem( 45 | dataID: DataID.verticalGroup, 46 | style: .init(spacing: 4)) 47 | { 48 | Label.groupItem( 49 | dataID: DataID.title, 50 | content: content.title, 51 | style: .style(with: .title2)) 52 | Label.groupItem( 53 | dataID: DataID.subtitle, 54 | content: content.subtitle, 55 | style: .style(with: .body)) 56 | } 57 | } 58 | } 59 | 60 | // MARK: Private 61 | 62 | private let group = HGroup(alignment: .top, spacing: 8) 63 | 64 | private func setUp() { 65 | group.install(in: self) 66 | group.constrainToMarginsWithHighPriorityBottom() 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Views/LayoutGroups/Elements/BaseRow.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/28/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | class BaseRow: UIView { 7 | init() { 8 | super.init(frame: .zero) 9 | translatesAutoresizingMaskIntoConstraints = false 10 | layoutMargins = UIEdgeInsets(top: 16, left: 24, bottom: 16, right: 24) 11 | } 12 | 13 | required init?(coder _: NSCoder) { 14 | fatalError("init(coder:) has not been implemented") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Views/LayoutGroups/Elements/Button.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 4/5/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import UIKit 6 | 7 | final class Button: UIButton, EpoxyableView { 8 | 9 | // MARK: Lifecycle 10 | 11 | init(style: Style) { 12 | super.init(frame: .zero) 13 | translatesAutoresizingMaskIntoConstraints = false 14 | setTitleColor(style.color, for: .normal) 15 | titleLabel?.font = UIFont.preferredFont(forTextStyle: .title2) 16 | addTarget( 17 | self, 18 | action: #selector(handleButtonTapped(_:)), 19 | for: .touchUpInside) 20 | } 21 | 22 | required init?(coder _: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | // MARK: Internal 27 | 28 | // MARK: EpoxyableView 29 | 30 | struct Style: Hashable { 31 | let color = UIColor.systemGreen 32 | } 33 | 34 | struct Content: Equatable { 35 | let title: String 36 | } 37 | 38 | struct Behaviors { 39 | let didTap: (UIButton) -> Void 40 | } 41 | 42 | func setContent(_ content: Content, animated _: Bool) { 43 | setTitle(content.title, for: .normal) 44 | } 45 | 46 | func setBehaviors(_ behaviors: Behaviors?) { 47 | didTap = behaviors?.didTap 48 | } 49 | 50 | @objc 51 | func handleButtonTapped(_ sender: UIButton) { 52 | didTap?(sender) 53 | } 54 | 55 | // MARK: Private 56 | 57 | private var didTap: ((UIButton) -> Void)? 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Views/LayoutGroups/Elements/ImageView.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/27/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import UIKit 6 | 7 | final class IconView: UIImageView, EpoxyableView { 8 | 9 | // MARK: Lifecycle 10 | 11 | init(style: Style) { 12 | size = style.size 13 | super.init(image: nil) 14 | translatesAutoresizingMaskIntoConstraints = false 15 | tintColor = style.tintColor 16 | setContentHuggingPriority(.required, for: .vertical) 17 | setContentHuggingPriority(.required, for: .horizontal) 18 | setContentCompressionResistancePriority(.required, for: .horizontal) 19 | setContentCompressionResistancePriority(.required, for: .vertical) 20 | } 21 | 22 | convenience init(image: UIImage?, size: CGSize) { 23 | self.init(style: .init(size: size, tintColor: .systemBlue)) 24 | setContent(image, animated: false) 25 | } 26 | 27 | required init?(coder _: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | // MARK: Internal 32 | 33 | struct Style: Hashable { 34 | var size: CGSize 35 | var tintColor: UIColor = .systemBlue 36 | 37 | func hash(into hasher: inout Hasher) { 38 | hasher.combine(size.width) 39 | hasher.combine(size.height) 40 | hasher.combine(tintColor) 41 | } 42 | } 43 | 44 | let size: CGSize 45 | 46 | override var intrinsicContentSize: CGSize { 47 | size 48 | } 49 | 50 | func setContent(_ content: UIImage?, animated _: Bool) { 51 | image = content 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Views/LayoutGroups/Elements/Label.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/22/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import UIKit 6 | 7 | // MARK: - Label 8 | 9 | final class Label: UILabel, EpoxyableView { 10 | 11 | // MARK: Lifecycle 12 | 13 | init(style: Style) { 14 | super.init(frame: .zero) 15 | translatesAutoresizingMaskIntoConstraints = false 16 | font = style.font 17 | numberOfLines = style.numberOfLines 18 | if style.showLabelBackground { 19 | backgroundColor = .secondarySystemBackground 20 | } 21 | } 22 | 23 | required init?(coder _: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | // MARK: Internal 28 | 29 | // MARK: StyledView 30 | 31 | struct Style: Hashable { 32 | let font: UIFont 33 | let showLabelBackground: Bool 34 | var numberOfLines = 0 35 | } 36 | 37 | // MARK: ContentConfigurableView 38 | 39 | typealias Content = String 40 | 41 | func setContent(_ content: String, animated _: Bool) { 42 | text = content 43 | } 44 | 45 | } 46 | 47 | extension Label.Style { 48 | static func style( 49 | with textStyle: UIFont.TextStyle, 50 | showBackground: Bool = false) 51 | -> Label.Style 52 | { 53 | .init( 54 | font: UIFont.preferredFont(forTextStyle: textStyle), 55 | showLabelBackground: showBackground) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Example/EpoxyExample/Views/LayoutGroups/IconRow.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 2/5/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import EpoxyLayoutGroups 6 | import UIKit 7 | 8 | final class IconRow: BaseRow, EpoxyableView { 9 | 10 | // MARK: Lifecycle 11 | 12 | override init() { 13 | super.init() 14 | setUp() 15 | } 16 | 17 | required init?(coder _: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | // MARK: Internal 22 | 23 | struct Content: Equatable { 24 | let title: String 25 | let icon: UIImage 26 | } 27 | 28 | func setContent(_ content: Content, animated _: Bool) { 29 | imageView.image = content.icon 30 | titleLabel.text = content.title 31 | } 32 | 33 | // MARK: Private 34 | 35 | private let imageView = IconView( 36 | image: nil, 37 | size: .init(width: 24, height: 24)) 38 | private let titleLabel = Label(style: .style(with: .title2)) 39 | 40 | private lazy var group = HGroup(spacing: 8) { 41 | StaticGroupItem(imageView) 42 | StaticGroupItem(titleLabel) 43 | } 44 | 45 | private func setUp() { 46 | imageView.tintColor = .black 47 | group.install(in: self) 48 | group.constrainToMarginsWithHighPriorityBottom() 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'cocoapods', '~> 1.11.0' 3 | gem 'rake', "~> 13.0.0" 4 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CwlCatchException", 6 | "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "35f9e770f54ce62dd8526470f14c6e137cef3eea", 10 | "version": "2.1.1" 11 | } 12 | }, 13 | { 14 | "package": "CwlPreconditionTesting", 15 | "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", 19 | "version": "2.1.0" 20 | } 21 | }, 22 | { 23 | "package": "Nimble", 24 | "repositoryURL": "https://github.com/Quick/Nimble.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "c93f16c25af5770f0d3e6af27c9634640946b068", 28 | "version": "9.2.1" 29 | } 30 | }, 31 | { 32 | "package": "Quick", 33 | "repositoryURL": "https://github.com/Quick/Quick.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "bd86ca0141e3cfb333546de5a11ede63f0c4a0e6", 37 | "version": "4.0.0" 38 | } 39 | }, 40 | { 41 | "package": "AirbnbSwift", 42 | "repositoryURL": "https://github.com/airbnb/swift", 43 | "state": { 44 | "branch": null, 45 | "revision": "07bb2e0822ca6e464bf3610ed452568931fdbf65", 46 | "version": "1.0.3" 47 | } 48 | }, 49 | { 50 | "package": "swift-argument-parser", 51 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 52 | "state": { 53 | "branch": null, 54 | "revision": "fddd1c00396eed152c45a46bea9f47b98e59301d", 55 | "version": "1.2.0" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 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: "Epoxy", 8 | platforms: [.iOS(.v13)], 9 | products: [ 10 | .library(name: "Epoxy", targets: ["Epoxy"]), 11 | .library(name: "EpoxyCore", targets: ["EpoxyCore"]), 12 | .library(name: "EpoxyCollectionView", targets: ["EpoxyCollectionView"]), 13 | .library(name: "EpoxyBars", targets: ["EpoxyBars"]), 14 | .library(name: "EpoxyNavigationController", targets: ["EpoxyNavigationController"]), 15 | .library(name: "EpoxyPresentations", targets: ["EpoxyPresentations"]), 16 | .library(name: "EpoxyLayoutGroups", targets: ["EpoxyLayoutGroups"]), 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/Quick/Quick.git", .upToNextMajor(from: "4.0.0")), 20 | .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "9.0.0")), 21 | ], 22 | targets: [ 23 | .target( 24 | name: "Epoxy", 25 | dependencies: [ 26 | "EpoxyCore", 27 | "EpoxyCollectionView", 28 | "EpoxyBars", 29 | "EpoxyNavigationController", 30 | "EpoxyPresentations", 31 | "EpoxyLayoutGroups", 32 | ]), 33 | .target(name: "EpoxyCore"), 34 | .target(name: "EpoxyCollectionView", dependencies: ["EpoxyCore"]), 35 | .target(name: "EpoxyBars", dependencies: ["EpoxyCore"]), 36 | .target(name: "EpoxyNavigationController", dependencies: ["EpoxyCore"]), 37 | .target(name: "EpoxyPresentations", dependencies: ["EpoxyCore"]), 38 | .target(name: "EpoxyLayoutGroups", dependencies: ["EpoxyCore"]), 39 | .testTarget(name: "EpoxyTests", dependencies: ["Epoxy", "Quick", "Nimble"]), 40 | .testTarget(name: "PerformanceTests", dependencies: ["EpoxyCore"]), 41 | ]) 42 | 43 | #if swift(>=5.6) 44 | // Add the Airbnb Swift formatting plugin if possible 45 | package.dependencies.append(.package(url: "https://github.com/airbnb/swift", .upToNextMajor(from: "1.0.1"))) 46 | #endif 47 | -------------------------------------------------------------------------------- /Sources/Epoxy/Exports.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 11/16/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // Make the symbols from each module listed importable with just `import Epoxy`. 5 | @_exported import EpoxyBars 6 | @_exported import EpoxyCollectionView 7 | @_exported import EpoxyCore 8 | @_exported import EpoxyLayoutGroups 9 | @_exported import EpoxyNavigationController 10 | @_exported import EpoxyPresentations 11 | -------------------------------------------------------------------------------- /Sources/EpoxyBars/BarInstaller/BarInstallerConfiguration.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 9/1/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | /// A singleton that enables consumers to control how bar installers' internal implementations 5 | /// behave across the entire app, without needing to update every place that uses it. 6 | /// 7 | /// Can additionally be provided when initializing a bar installer to customize the behavior of 8 | /// that specific instance. 9 | public struct BarInstallerConfiguration { 10 | 11 | // MARK: Lifecycle 12 | 13 | public init( 14 | applyBars: ((_ container: BarContainer, _ bars: [BarModeling], _ animated: Bool) -> Void)? = nil) 15 | { 16 | self.applyBars = applyBars 17 | } 18 | 19 | // MARK: Public 20 | 21 | /// The default configuration instance used if none is provided when initializing a bar installer. 22 | /// 23 | /// Set this to a new instance to override the default configuration. 24 | public static var shared = BarInstallerConfiguration() 25 | 26 | /// A closure that's invoked whenever new bar models are set on a `BarInstaller` following its 27 | /// initial configuration to customize _when_ those same bars are applied to the underlying 28 | /// `BarContainer`. 29 | /// 30 | /// For example, if the bar installer is actively participating in a shared element transition, 31 | /// this property can be used to defer bar updates until the transition is over to ensure that the 32 | /// shared bar elements remain constant over the course of the transition. 33 | /// 34 | /// Defaults to `nil`, resulting in any new bars being immediately applied to the underlying 35 | /// `BarContainer`. 36 | /// 37 | /// Not calling `setBars` on the given `BarContainer` with the provided bars will result in 38 | /// skipped bar model updates. 39 | public var applyBars: ((_ container: BarContainer, _ bars: [BarModeling], _ animated: Bool) -> Void)? 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Sources/EpoxyBars/BarInstaller/UIScrollView+ContentOffset.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 2/23/23. 2 | // Copyright © 2023 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | // MARK: - UIScrollView 7 | 8 | extension UIScrollView { 9 | /// The content offset at which this scroll view is scrolled to its top. 10 | @nonobjc 11 | var topContentOffset: CGFloat { 12 | -adjustedContentInset.top 13 | } 14 | 15 | /// The content offset at which this scroll view is scrolled to its bottom. 16 | @nonobjc 17 | var bottomContentOffset: CGFloat { 18 | max(contentSize.height - bounds.height + adjustedContentInset.bottom, topContentOffset) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/EpoxyBars/BarInstaller/UIView+HasHierarchyScaleTransform.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 2/23/23. 2 | // Copyright © 2023 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | // MARK: - UIView 7 | 8 | extension UIView { 9 | /// Whether this view has a scale in its view hierarchy at any point in the 10 | /// given number of ancestor views. 11 | @nonobjc 12 | func hasHierarchyScaleTransform(below ancestor: Int = 10) -> Bool { 13 | guard ancestor > 0 else { 14 | return false 15 | } 16 | 17 | guard CATransform3DEqualToTransform(transform3D, CATransform3DIdentity) else { 18 | // m11, m22, and m33 correspond to x, y, and z scale respectively. 19 | return transform3D.m11 != 1.0 || transform3D.m22 != 1.0 || transform3D.m33 != 1.0 20 | } 21 | 22 | return superview?.hasHierarchyScaleTransform(below: ancestor - 1) ?? false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/EpoxyBars/BarInstaller/UIViewController+OriginalSafeAreaInsets.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 2/23/23. 2 | // Copyright © 2023 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | extension UIViewController { 7 | /// The original safe area inset top before the additional safe area insets are applied. 8 | @nonobjc 9 | var originalSafeAreaInsetTop: CGFloat { 10 | view.safeAreaInsets.top - additionalSafeAreaInsets.top 11 | } 12 | 13 | /// The original safe area inset bottom before the additional safe area insets are applied. 14 | @nonobjc 15 | var originalSafeAreaInsetBottom: CGFloat { 16 | view.safeAreaInsets.bottom - additionalSafeAreaInsets.bottom 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/EpoxyBars/BarModel/BarModelBuilder.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/15/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | /// A result builder that enables a DSL for building arrays of bar models. 7 | public typealias BarModelBuilder = EpoxyModelArrayBuilder 8 | -------------------------------------------------------------------------------- /Sources/EpoxyBars/BarModel/BarModeling.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 4/15/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | // MARK: - BarModeling 7 | 8 | /// A model that can provide an bar view to an `BarStackView`. 9 | public protocol BarModeling { 10 | /// Returns this bar model with its type erased to the `AnyItemModel` type. 11 | func eraseToAnyBarModel() -> AnyBarModel 12 | } 13 | 14 | // MARK: Defaults 15 | 16 | extension BarModeling { 17 | /// The internal wrapped bar model. 18 | var internalBarModel: InternalBarCoordinating { 19 | eraseToAnyBarModel().model 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/EpoxyBars/BarModel/SwiftUI.View+BarModel.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 9/16/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import SwiftUI 6 | 7 | extension View { 8 | /// Vends a `BarModel` representing this SwiftUI `View`. 9 | /// 10 | /// - Parameters: 11 | /// - dataID: An ID that uniquely identifies this item relative to other items in the 12 | /// same collection. 13 | /// - reuseBehavior: The reuse behavior of the `EpoxySwiftUIHostingView`. 14 | public func barModel( 15 | dataID: AnyHashable? = nil, 16 | reuseBehavior: SwiftUIHostingViewReuseBehavior = .reusable) 17 | -> BarModel> 18 | { 19 | EpoxySwiftUIHostingView.barModel( 20 | dataID: dataID, 21 | content: .init(rootView: self, dataID: dataID), 22 | style: .init( 23 | reuseBehavior: reuseBehavior, 24 | forceLayoutOnLayoutMarginsChange: true, 25 | initialContent: .init(rootView: self, dataID: dataID))) 26 | .linkDisplayLifecycle() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/EpoxyBars/BarView/HeightInvalidatingBarView.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 4/6/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | // MARK: - HeightInvalidatingBarView 7 | 8 | /// A bar view that's able to animatedly invalidate its height within an enclosing `BarStackView`. 9 | public protocol HeightInvalidatingBarView: UIView { 10 | /// The height invalidation context that can be used to animatedly resize the `BarStackView` that 11 | /// the bar is enclosed within. 12 | var heightInvalidationContext: BarHeightInvalidationContext? { get set } 13 | } 14 | 15 | // MARK: Defaults 16 | 17 | extension HeightInvalidatingBarView { 18 | @nonobjc 19 | public var heightInvalidationContext: BarHeightInvalidationContext? { 20 | get { objc_getAssociatedObject(self, &Keys.context) as? BarHeightInvalidationContext } 21 | set { objc_setAssociatedObject(self, &Keys.context, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 22 | } 23 | } 24 | 25 | // MARK: Height Invalidation 26 | 27 | extension HeightInvalidatingBarView { 28 | /// Should be called prior to height invalidation to ensure that other changes are not batched 29 | /// within the animation transaction. 30 | public func prepareHeightBarHeightInvalidation() { 31 | heightInvalidationContext?.barStackSuperview()?.layoutIfNeeded() 32 | } 33 | 34 | /// Should be called within an animation transaction following a 35 | /// `prepareHeightBarHeightInvalidation` once the constraints affecting the height have been 36 | /// updated. 37 | public func invalidateBarHeight() { 38 | heightInvalidationContext?.barStackSuperview()?.layoutIfNeeded() 39 | } 40 | } 41 | 42 | // MARK: - BarHeightInvalidationContext 43 | 44 | /// A context that's used to animatedly resize an enclosing bar stack view. 45 | public struct BarHeightInvalidationContext { 46 | /// A closure that returns the enclosing `BarStackView`'s superview. 47 | let barStackSuperview: () -> UIView? 48 | } 49 | 50 | // MARK: - Keys 51 | 52 | /// Associated object keys. 53 | private enum Keys { 54 | static var context = 0 55 | } 56 | -------------------------------------------------------------------------------- /Sources/EpoxyBars/BarView/SafeAreaLayoutMarginsBarView.swift: -------------------------------------------------------------------------------- 1 | // Created by Benjamin Scazzero on 7/17/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | // MARK: - SafeAreaLayoutMarginsBarView 7 | 8 | /// Describes how the view's original layout margins and safe area should interact. 9 | public protocol SafeAreaLayoutMarginsBarView: UIView { 10 | var preferredSafeAreaLayoutMarginsBehavior: SafeAreaLayoutMarginsBehavior { get } 11 | } 12 | 13 | // MARK: - SafeAreaLayoutMarginsBehavior 14 | 15 | public enum SafeAreaLayoutMarginsBehavior { 16 | /// The bar's layout margins are set to the max of the safe area and its original layout margins. 17 | case max 18 | /// The bar's layout margins are set to the sum of the safe area and its original layout margins. 19 | case sum 20 | } 21 | -------------------------------------------------------------------------------- /Sources/EpoxyBars/BottomBarInstaller/BottomBarsProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/23/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - BottomBarsProviding 5 | 6 | /// The ability to provide models representing a stack of bar views at the bottom of a screen. 7 | /// 8 | /// Generally conformed to by view controller content. 9 | public protocol BottomBarsProviding { 10 | /// The stack of bars displayed at the bottom of the screen ordered from top to bottom, else an 11 | /// empty array if there should be none. 12 | /// 13 | /// - SeeAlso: BarModel 14 | /// - SeeAlso: AlertBar 15 | /// - SeeAlso: BottomBarInstaller 16 | var bottomBars: [BarModeling] { get } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/EpoxyBars/TopBarInstaller/TopBarsProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/23/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - TopBarsProviding 5 | 6 | /// The ability to provide models representing a stack of bar views at the top of a screen. 7 | /// 8 | /// Generally conformed to by view controller content. 9 | public protocol TopBarsProviding { 10 | /// The stack of bars displayed at the top of the screen ordered from top to bottom, else an empty 11 | /// array if there should be none. 12 | /// 13 | /// - SeeAlso: BasicNavigationBarModel 14 | /// - SeeAlso: OverlayNavigationBarModel 15 | /// - SeeAlso: BarModel 16 | /// - SeeAlso: TopBarInstaller 17 | var topBars: [BarModeling] { get } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/Delegates/CollectionViewAccessibilityDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by nick_miller on 7/16/19. 2 | // Copyright © 2019 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// A delegate that's invoked as item gain and lose accessibility focus on a `CollectionView`. 7 | public protocol CollectionViewAccessibilityDelegate: AnyObject { 8 | /// Called when an item gains accessibility focus. 9 | /// 10 | /// Corresponds to `UICollectionViewCell.accessibilityElementDidBecomeFocused()` 11 | func collectionView( 12 | _ collectionView: CollectionView, 13 | itemDidBecomeFocused item: AnyItemModel, 14 | with view: UIView?, 15 | in section: SectionModel) 16 | 17 | /// Called when an item loses accessibility focus. 18 | /// 19 | /// Corresponds to `UICollectionViewCell.accessibilityElementDidLoseFocus()` 20 | func collectionView( 21 | _ collectionView: CollectionView, 22 | itemDidLoseFocus item: AnyItemModel, 23 | with view: UIView?, 24 | in section: SectionModel) 25 | } 26 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/Delegates/CollectionViewDisplayDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by bryn_bodayle on 2/9/17. 2 | // Copyright © 2017 Airbnb. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// A delegate that's invoked as the items in a `CollectionView` appear and disappear. 7 | /// 8 | /// - SeeAlso: `WillDisplayProviding` 9 | /// - SeeAlso: `DidEndDisplayingProviding` 10 | public protocol CollectionViewDisplayDelegate: AnyObject { 11 | /// Called when an item is about to be displayed. 12 | /// 13 | /// Corresponds to `UICollectionViewDelegate.collectionView(_:willDisplay:forItemAt:)`. 14 | func collectionView( 15 | _ collectionView: CollectionView, 16 | willDisplayItem item: AnyItemModel, 17 | with view: UIView?, 18 | in section: SectionModel) 19 | 20 | /// Called after an item ends displaying. 21 | /// 22 | /// Corresponds to `UICollectionViewDelegate.collectionView(_:didEndDisplaying:forItemAt:)`. 23 | func collectionView( 24 | _ collectionView: CollectionView, 25 | didEndDisplayingItem item: AnyItemModel, 26 | with view: UIView?, 27 | in section: SectionModel) 28 | 29 | /// Called when a supplementary item is about to be displayed. 30 | /// 31 | /// Corresponds to 32 | /// `UICollectionViewDelegate.collectionView(_:willDisplaySupplementaryView:forElementKind:at:)`. 33 | func collectionView( 34 | _ collectionView: CollectionView, 35 | willDisplaySupplementaryItem item: AnySupplementaryItemModel, 36 | forElementKind elementKind: String, 37 | with view: UIView?, 38 | in section: SectionModel) 39 | 40 | /// Called after a supplementary item ends displaying. 41 | /// 42 | /// Corresponds to 43 | /// `UICollectionViewDelegate.collectionView(_:didEndDisplayingSupplementaryView:forElementOfKind:at:)`. 44 | func collectionView( 45 | _ collectionView: CollectionView, 46 | didEndDisplayingSupplementaryItem item: AnySupplementaryItemModel, 47 | forElementKind elementKind: String, 48 | with view: UIView?, 49 | in section: SectionModel) 50 | } 51 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/Delegates/CollectionViewPrefetchingDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 4/2/18. 2 | // Copyright © 2018 Airbnb. All rights reserved. 3 | 4 | /// A delegate that's invoked as the content of items in a `CollectionView` flows through the 5 | /// lifecycle of prefetching. 6 | /// 7 | /// - Note: prefetching must be enabled on the `CollectionViewConfiguration` via 8 | /// `usesCellPrefetching` for these methods to be called. 9 | public protocol CollectionViewPrefetchingDelegate: AnyObject { 10 | /// Invoked when the given items should be prefetched. 11 | /// 12 | /// Corresponds to `UICollectionViewDataSourcePrefetching.collectionView(_:prefetchItemsAt:)`. 13 | func collectionView( 14 | _ collectionView: CollectionView, 15 | prefetch items: [AnyItemModel]) 16 | 17 | /// Invoked when the prefetching for the given items should be cancelled. 18 | /// 19 | /// Corresponds to 20 | /// `UICollectionViewDataSourcePrefetching.collectionView(_:cancelPrefetchingForItemsAt:)`. 21 | func collectionView( 22 | _ collectionView: CollectionView, 23 | cancelPrefetchingOf items: [AnyItemModel]) 24 | } 25 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/Delegates/CollectionViewReorderingDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by shunji_li on 10/9/17. 2 | // Copyright © 2017 Airbnb. All rights reserved. 3 | 4 | import UIKit 5 | 6 | // MARK: - CollectionViewReorderingDelegate 7 | 8 | /// A delegate that's invoked when the items in a `CollectionView` are moved using the legacy 9 | /// drag/drop system. 10 | /// 11 | /// - Note: Corresponds to the legacy `UICollectionViewDataSource.collectionView(_:moveItemAt:to:)` 12 | /// drag/drop system, not the modern `UICollectionViewDragDelegate`/`UICollectionViewDropDelegate` 13 | /// system. 14 | /// 15 | /// - SeeAlso: `IsMovableProviding` 16 | public protocol CollectionViewReorderingDelegate: AnyObject { 17 | 18 | /// Returns whether the source item is allowed to move to the proposed destination. 19 | /// 20 | /// If `false`, the destination item will be pinned and the interactive item cannot be moved to 21 | /// the destination position. Defaults to `true`. 22 | /// 23 | /// Corresponds to 24 | /// `UICollectionViewDelegate.collectionView(_:targetIndexPathForMoveFromItemAt:toProposedIndexPath:)` 25 | func collectionView( 26 | _ collectionView: CollectionView, 27 | shouldMoveItem sourceItem: AnyItemModel, 28 | inSection sourceSection: SectionModel, 29 | toDestinationItem destinationItem: AnyItemModel, 30 | inSection destinationSection: SectionModel) -> Bool 31 | 32 | /// Move the specified item to the given new location. 33 | /// 34 | /// Corresponds to `UICollectionViewDataSource.collectionView(_:moveItemAt:to:)`. 35 | func collectionView( 36 | _ collectionView: CollectionView, 37 | moveItem sourceItem: AnyItemModel, 38 | inSection sourceSection: SectionModel, 39 | toDestinationItem destinationItem: AnyItemModel, 40 | inSection destinationSection: SectionModel) 41 | } 42 | 43 | extension CollectionViewReorderingDelegate { 44 | 45 | public func collectionView( 46 | _: CollectionView, 47 | shouldMoveItem _: AnyItemModel, 48 | inSection _: SectionModel, 49 | toDestinationItem _: AnyItemModel, 50 | inSection _: SectionModel) 51 | -> Bool 52 | { 53 | true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/Delegates/CollectionViewTransitionLayoutDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 9/18/17. 2 | // Copyright © 2017 Airbnb. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// A delegate that's invoked when to transition between `UICollectionViewLayout` in a 7 | /// `CollectionView`. 8 | public protocol CollectionViewTransitionLayoutDelegate: AnyObject { 9 | /// Asks for the custom transition layout to use when moving between the specified layouts. 10 | /// 11 | /// Corresponds to 12 | /// `UICollectionViewDelegate.collectionView(_:transitionLayoutForOldLayout:newLayout:)`. 13 | func collectionView( 14 | _ collectionView: CollectionView, 15 | transitionLayoutForOldLayout fromLayout: UICollectionViewLayout, 16 | newLayout toLayout: UICollectionViewLayout) 17 | -> UICollectionViewTransitionLayout 18 | } 19 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/Internal/CollectionViewChangeset.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/14/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | // MARK: - CollectionViewChangeset 7 | 8 | /// A set of the minimum changes to get from one array of `SectionModel`s to another, used for 9 | /// diffing. 10 | struct CollectionViewChangeset { 11 | /// A set of the minimum changes to get from one set of sections to another. 12 | var sectionChangeset: IndexSetChangeset 13 | 14 | /// A set of the minimum changes to get from one set of items to another, aggregated across all 15 | /// sections. 16 | var itemChangeset: IndexPathChangeset 17 | 18 | /// A set of the minimum changes to get from one set of supplementary items to another, aggregated 19 | /// across all sections, keyed by supplementary element kind. 20 | var supplementaryItemChangeset: [String: IndexPathChangeset] 21 | } 22 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/Internal/CollectionViewDataSourceReorderingDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 1/19/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - CollectionViewDataSourceReorderingDelegate 5 | 6 | protocol CollectionViewDataSourceReorderingDelegate: AnyObject { 7 | func dataSource( 8 | _ dataSource: CollectionViewDataSource, 9 | moveItem sourceItem: AnyItemModel, 10 | inSection sourceSection: SectionModel, 11 | toDestinationItem destinationItem: AnyItemModel, 12 | inSection destinationSection: SectionModel) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/ItemPath.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/8/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import Foundation 5 | 6 | /// A path to a specific item within a section in a `CollectionView`. 7 | public struct ItemPath: Hashable { 8 | 9 | // MARK: Lifecycle 10 | 11 | public init(itemDataID: AnyHashable, section: ItemSectionPath) { 12 | self.itemDataID = itemDataID 13 | self.section = section 14 | } 15 | 16 | // MARK: Public 17 | 18 | /// The item identified by the `dataID` on its corresponding `ItemModel`. 19 | public var itemDataID: AnyHashable 20 | 21 | /// The section in which the item referenced by this path located. 22 | public var section: ItemSectionPath 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/ItemSectionPath.swift: -------------------------------------------------------------------------------- 1 | // Created by Bryn Bodayle on 8/15/23. 2 | // Copyright © 2023 Airbnb Inc. All rights reserved. 3 | 4 | import Foundation 5 | 6 | /// The section in which an item referenced by an `ItemPath` or `SupplementaryItemPath` is located. 7 | public enum ItemSectionPath: Hashable { 8 | /// The section identified by the `dataID` on its corresponding `SectionModel`. 9 | case dataID(AnyHashable) 10 | 11 | /// The last section that contains an item with `itemDataID` as its `dataID`. 12 | /// 13 | /// If there are multiple sections with items that have the same `dataID`, it is not 14 | /// recommended to use this case, as the located item may be unstable over time. 15 | case lastWithItemDataID 16 | } 17 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/ReusableViews/FittingPrioritiesProvidingLayoutAttributes.swift: -------------------------------------------------------------------------------- 1 | // Created by bryan_keller on 12/4/18. 2 | // Copyright © 2018 Airbnb. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// A protocol that provides additional sizing mode information for a `CollectionViewCell`, 7 | /// via the layout attributes passed in to 8 | /// `preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes)`. 9 | public protocol FittingPrioritiesProvidingLayoutAttributes: UICollectionViewLayoutAttributes { 10 | var horizontalFittingPriority: UILayoutPriority { get } 11 | var verticalFittingPriority: UILayoutPriority { get } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/ReusableViews/ItemCellView.swift: -------------------------------------------------------------------------------- 1 | // Created by Laura Skelton on 1/12/17. 2 | // Copyright © 2017 Airbnb. All rights reserved. 3 | 4 | import UIKit 5 | 6 | // MARK: - ItemWrapperView 7 | 8 | /// A reusable view that contains an item view. 9 | public protocol ItemWrapperView: UIView { 10 | /// The view if it has been set via `setViewIfNeeded(view:)`, else `nil`. 11 | var view: UIView? { get } 12 | 13 | /// Updates the `view` of this wrapper to the given view if it has not been set yet. 14 | func setViewIfNeeded(view: UIView) 15 | } 16 | 17 | // MARK: - ItemCellView 18 | 19 | /// A reusable cell that contains an item view. 20 | public protocol ItemCellView: ItemWrapperView { 21 | /// Whether this cell is highlighted. 22 | var isHighlighted: Bool { get } 23 | 24 | /// Whether this cell is selected. 25 | var isSelected: Bool { get } 26 | } 27 | 28 | // MARK: Extensions 29 | 30 | extension ItemCellView { 31 | /// The state of this cell view. 32 | public var state: ItemCellState { 33 | var state: ItemCellState = .normal 34 | if isHighlighted { 35 | state = .highlighted 36 | } 37 | if isSelected { 38 | state = .selected 39 | } 40 | return state 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/ReusableViews/ItemSelectionStyle.swift: -------------------------------------------------------------------------------- 1 | // Created by bryan_keller on 10/10/17. 2 | // Copyright © 2017 Airbnb. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// The style applied to selected items in a `CollectionView`. 7 | /// 8 | /// - SeeAlso: `SelectionStyleProviding` 9 | public enum ItemSelectionStyle: Hashable { 10 | /// No background is drawn behind selected items. 11 | /// 12 | /// This case can't be labeled "none" as that is misinterpreted by the compiler as Optional.none 13 | /// when checking against .none in other files. https://forums.swift.org/t/optional-enum-with-case-none/19126 14 | case noBackground 15 | /// The associated color is drawn behind selected items. 16 | case color(UIColor) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/CollectionView/SupplementaryItemPath.swift: -------------------------------------------------------------------------------- 1 | // Created by Bryn Bodayle on 8/15/23. 2 | // Copyright © 2023 Airbnb Inc. All rights reserved. 3 | 4 | import Foundation 5 | 6 | /// A path to a specific supplementary item within a section in a `CollectionView`. 7 | public struct SupplementaryItemPath: Hashable { 8 | 9 | // MARK: Lifecycle 10 | 11 | public init(elementKind: String, itemDataID: AnyHashable, section: ItemSectionPath) { 12 | self.elementKind = elementKind 13 | self.itemDataID = itemDataID 14 | self.section = section 15 | } 16 | 17 | // MARK: Public 18 | 19 | /// The type of supplementary view 20 | public var elementKind: String 21 | 22 | /// The supplementary item identified by the `dataID` on its corresponding `ItemModel`. 23 | public var itemDataID: AnyHashable 24 | 25 | /// The section in which the supplementary item referenced by this path located. 26 | public var section: ItemSectionPath 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Layouts/CompositionalLayout/UICollectionViewCompositionalLayout+Epoxy.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 1/7/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import UIKit 6 | 7 | // MARK: - UICollectionViewCompositionalLayout 8 | 9 | extension UICollectionViewCompositionalLayout { 10 | 11 | // MARK: Public 12 | 13 | /// Vends a compositional layout that has its section layout determined by invoking the 14 | /// `layoutSectionProvider` of its `CollectionView`'s corresponding `SectionModel` for each 15 | /// section. 16 | public static var epoxy: UICollectionViewCompositionalLayout { 17 | epoxy(UICollectionViewCompositionalLayout.init(sectionProvider:)) 18 | } 19 | 20 | /// Vends a compositional layout that has its section layout determined by invoking the 21 | /// `layoutSectionProvider` of its `CollectionView`'s corresponding `SectionModel` for each 22 | /// section. 23 | public static func epoxy( 24 | configuration: UICollectionViewCompositionalLayoutConfiguration) 25 | -> UICollectionViewCompositionalLayout 26 | { 27 | epoxy { provider in 28 | UICollectionViewCompositionalLayout(sectionProvider: provider, configuration: configuration) 29 | } 30 | } 31 | 32 | // MARK: Private 33 | 34 | private typealias MakeLayout = (@escaping UICollectionViewCompositionalLayoutSectionProvider) 35 | -> UICollectionViewCompositionalLayout 36 | 37 | private static func epoxy( 38 | _ makeLayout: MakeLayout) 39 | -> UICollectionViewCompositionalLayout 40 | { 41 | weak var layoutReference: UICollectionViewCompositionalLayout? 42 | 43 | let provider: UICollectionViewCompositionalLayoutSectionProvider = { index, environment in 44 | guard let collectionView = layoutReference?.collectionView as? CollectionView else { 45 | EpoxyLogger.shared.assertionFailure( 46 | """ 47 | Epoxy compositional layout does not have a corresponding CollectionView. This is \ 48 | programmer error. 49 | """) 50 | return nil 51 | } 52 | 53 | return collectionView.section(at: index)?.compositionalLayoutSectionProvider?(environment) 54 | } 55 | 56 | let layout = makeLayout(provider) 57 | 58 | layoutReference = layout 59 | 60 | return layout 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/ItemModel/ItemCellMetadata.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 9/27/19. 2 | // Copyright © 2019 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// The metadata describing an item view within a cell. 7 | public struct ItemCellMetadata { 8 | 9 | // MARK: Lifecycle 10 | 11 | public init(traitCollection: UITraitCollection, state: ItemCellState, animated: Bool) { 12 | self.traitCollection = traitCollection 13 | self.state = state 14 | self.animated = animated 15 | } 16 | 17 | // MARK: Public 18 | 19 | public var traitCollection: UITraitCollection 20 | public var state: ItemCellState 21 | public var animated: Bool 22 | } 23 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/ItemModel/ItemCellState.swift: -------------------------------------------------------------------------------- 1 | // Created by Laura Skelton on 5/14/17. 2 | // Copyright © 2017 Airbnb. All rights reserved. 3 | 4 | // MARK: - ItemCellState 5 | 6 | /// The state of an cell that contains an item view. 7 | public enum ItemCellState { 8 | /// The item cell is neither selected nor highlighted. 9 | case normal 10 | /// The item cell is highlighted. 11 | case highlighted 12 | /// The item cell is selected. 13 | case selected 14 | } 15 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/ItemModel/ItemModelBuilder.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/15/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | /// A result builder that enables a DSL for building arrays of item models. 7 | /// 8 | /// For example: 9 | /// ``` 10 | /// @ItemModelBuilder var items: [ItemModeling] { 11 | /// MyView.itemModel(…) 12 | /// MyOtherView.itemModel(…) 13 | /// } 14 | /// ``` 15 | /// 16 | /// Will return an array containing two item models. 17 | public typealias ItemModelBuilder = EpoxyModelArrayBuilder 18 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/ItemModel/ItemModeling.swift: -------------------------------------------------------------------------------- 1 | // Created by Laura Skelton on 1/12/17. 2 | // Copyright © 2017 Airbnb. All rights reserved. 3 | 4 | import EpoxyCore 5 | import Foundation 6 | import UIKit 7 | 8 | // MARK: - ItemModeling 9 | 10 | public protocol ItemModeling: ViewDifferentiatorProviding, DataIDProviding, Diffable { 11 | /// Returns this item model with its type erased to the `AnyItemModel` type. 12 | func eraseToAnyItemModel() -> AnyItemModel 13 | } 14 | 15 | // MARK: - InternalItemModeling 16 | 17 | public protocol InternalItemModeling: ItemModeling, 18 | EpoxyModeled, 19 | SelectionStyleProviding, 20 | IsMovableProviding 21 | { 22 | /// Configures the cell for presentation. 23 | func configure(cell: ItemWrapperView, with metadata: ItemCellMetadata) 24 | 25 | /// Set behaviors needed by the view. 26 | /// 27 | /// Called before presentation and when cells are reordered. 28 | func setBehavior(cell: ItemWrapperView, with metadata: ItemCellMetadata) 29 | 30 | /// Updates the cell based on a state change. 31 | func configureStateChange(in cell: ItemWrapperView, with metadata: ItemCellMetadata) 32 | 33 | /// Handles the cell being selected. 34 | func handleDidSelect(_ cell: ItemWrapperView, with metadata: ItemCellMetadata) 35 | 36 | /// Informs consumers that this item is about to be displayed. 37 | func handleWillDisplay(_ cell: ItemWrapperView, with metadata: ItemCellMetadata) 38 | 39 | /// Informs consumers that this item is no longer displayed. 40 | func handleDidEndDisplaying(_ cell: ItemWrapperView, with metadata: ItemCellMetadata) 41 | 42 | /// Whether the cell should be selectable. 43 | var isSelectable: Bool { get } 44 | 45 | /// Creates view for this item. This should only be used to create a view outside of a collection 46 | /// view. 47 | /// 48 | /// - Parameter traitCollection: The trait collection to create the view for 49 | /// - Returns: The configured view for this item model. 50 | func configuredView(traitCollection: UITraitCollection) -> UIView 51 | } 52 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/ItemModel/SwiftUI.View+ItemModel.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 9/9/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import SwiftUI 6 | 7 | extension View { 8 | /// Vends an `ItemModel` representing this SwiftUI `View`. 9 | /// 10 | /// - Parameters: 11 | /// - dataID: An ID that uniquely identifies this item relative to other items in the 12 | /// same collection. 13 | /// - reuseBehavior: The reuse behavior of the `EpoxySwiftUIHostingView`. 14 | public func itemModel( 15 | dataID: AnyHashable, 16 | reuseBehavior: SwiftUIHostingViewReuseBehavior = .reusable) 17 | -> ItemModel> 18 | { 19 | EpoxySwiftUIHostingView.itemModel( 20 | dataID: dataID, 21 | content: .init(rootView: self, dataID: dataID), 22 | style: .init( 23 | reuseBehavior: reuseBehavior, 24 | forceLayoutOnLayoutMarginsChange: false, 25 | initialContent: .init(rootView: self, dataID: dataID))) 26 | .linkDisplayLifecycle() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/Providers/DidChangeStateProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/2/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import UIKit 6 | 7 | // MARK: - DidChangeStateProviding 8 | 9 | /// A sentinel protocol for enabling a `CallbackContextEpoxyModeled` to provide a `didChangeState` 10 | /// closure property. 11 | public protocol DidChangeStateProviding { } 12 | 13 | // MARK: - CallbackContextEpoxyModeled 14 | 15 | extension CallbackContextEpoxyModeled where Self: DidChangeStateProviding { 16 | 17 | // MARK: Public 18 | 19 | /// A closure that's called to configure the state of this model's view when it changes. 20 | public typealias DidChangeState = (CallbackContext) -> Void 21 | 22 | /// A closure that's called to configure the state of this model's view when it changes. 23 | public var didChangeState: DidChangeState? { 24 | get { self[didChangeStateProperty] } 25 | set { self[didChangeStateProperty] = newValue } 26 | } 27 | 28 | /// Returns a copy of this model with the given did change state closure called after the current 29 | /// did change state closure of this model, if there is one. 30 | public func didChangeState(_ value: DidChangeState?) -> Self { 31 | copy(updating: didChangeStateProperty, to: value) 32 | } 33 | 34 | // MARK: Private 35 | 36 | private var didChangeStateProperty: EpoxyModelProperty { 37 | .init(keyPath: \Self.didChangeState, defaultValue: nil, updateStrategy: .chain()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/Providers/IsMoveableProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/2/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | // MARK: - IsMovableProviding 7 | 8 | /// The capability of an item being movable within a `CollectionView` using the legacy drag/drop 9 | /// system. 10 | /// 11 | /// - Note: Corresponds to the legacy `UICollectionViewDataSource.collectionView(_:canMoveItemAt:)` 12 | /// drag/drop system, not the modern `UICollectionViewDragDelegate`/`UICollectionViewDropDelegate` 13 | /// system. 14 | /// 15 | /// - SeeAlso: `CollectionViewReorderingDelegate` 16 | public protocol IsMovableProviding { 17 | /// A legacy property to allow interactive reordering of items within a collection view, 18 | /// defaults to `false`, but you can configure it to be `true` to enable reordering. 19 | var isMovable: Bool { get } 20 | } 21 | 22 | // MARK: - EpoxyModeled 23 | 24 | extension EpoxyModeled where Self: IsMovableProviding { 25 | 26 | // MARK: Public 27 | 28 | public var isMovable: Bool { 29 | get { self[isMovableProperty] } 30 | set { self[isMovableProperty] = newValue } 31 | } 32 | 33 | /// Returns a copy of this model with the current `isMovable` value replaced with the provided 34 | /// `value`. 35 | public func isMovable(_ value: Bool) -> Self { 36 | copy(updating: isMovableProperty, to: value) 37 | } 38 | 39 | // MARK: Private 40 | 41 | private var isMovableProperty: EpoxyModelProperty { 42 | .init(keyPath: \IsMovableProviding.isMovable, defaultValue: false, updateStrategy: .replace) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/Providers/ItemsProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/1/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | // MARK: - ItemsProviding 7 | 8 | public protocol ItemsProviding { 9 | /// The array of items in a section, typically within the context of a `CollectionView`. 10 | var items: [ItemModeling] { get } 11 | } 12 | 13 | // MARK: - EpoxyModeled 14 | 15 | extension EpoxyModeled where Self: ItemsProviding { 16 | 17 | // MARK: Public 18 | 19 | public var items: [ItemModeling] { 20 | get { self[itemsProperty] } 21 | set { self[itemsProperty] = newValue } 22 | } 23 | 24 | /// Returns a copy of this model with the current `items` value replaced with the provided 25 | /// `value`. 26 | public func items(_ value: [ItemModeling]) -> Self { 27 | copy(updating: itemsProperty, to: value) 28 | } 29 | 30 | // MARK: Private 31 | 32 | private var itemsProperty: EpoxyModelProperty<[ItemModeling]> { 33 | .init(keyPath: \ItemsProviding.items, defaultValue: [], updateStrategy: .replace) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/Providers/SelectionStyleProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/2/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | // MARK: - SelectionStyleProviding 7 | 8 | public protocol SelectionStyleProviding { 9 | /// The selection style of the cell. 10 | /// 11 | /// If `nil`, defaults to the `selectionStyle` set of the `CollectionView`. 12 | var selectionStyle: ItemSelectionStyle? { get } 13 | } 14 | 15 | // MARK: - EpoxyModeled 16 | 17 | extension EpoxyModeled where Self: SelectionStyleProviding { 18 | 19 | // MARK: Public 20 | 21 | public var selectionStyle: ItemSelectionStyle? { 22 | get { self[selectionStyleProperty] } 23 | set { self[selectionStyleProperty] = newValue } 24 | } 25 | 26 | /// Returns a copy of this model with the selection style replaced with the provided `value`. 27 | public func selectionStyle(_ value: ItemSelectionStyle?) -> Self { 28 | copy(updating: selectionStyleProperty, to: value) 29 | } 30 | 31 | // MARK: Private 32 | 33 | private var selectionStyleProperty: EpoxyModelProperty { 34 | .init( 35 | keyPath: \SelectionStyleProviding.selectionStyle, 36 | defaultValue: nil, 37 | updateStrategy: .replace) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/Providers/SupplementaryItemsProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/1/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | // MARK: - SupplementaryItemsProviding 7 | 8 | public protocol SupplementaryItemsProviding { 9 | /// The supplementary items with in a collection view, with a key of the element kind and a value 10 | /// of the models of that specific kind. 11 | typealias SupplementaryItems = [String: [SupplementaryItemModeling]] 12 | 13 | /// The supplementary items with in a collection view. 14 | var supplementaryItems: SupplementaryItems { get } 15 | } 16 | 17 | // MARK: - EpoxyModeled 18 | 19 | extension EpoxyModeled where Self: SupplementaryItemsProviding { 20 | 21 | // MARK: Public 22 | 23 | public var supplementaryItems: SupplementaryItems { 24 | get { self[supplementaryItemsProperty] } 25 | set { self[supplementaryItemsProperty] = newValue } 26 | } 27 | 28 | /// Returns a copy of this model with the current `supplementaryItems` value replaced with the 29 | /// provided `value`. 30 | public func supplementaryItems(_ value: SupplementaryItems) -> Self { 31 | copy(updating: supplementaryItemsProperty, to: value) 32 | } 33 | 34 | /// Returns a copy of this model with the `supplementaryItems` of the given `elementKind` replaced 35 | /// with the provided `value`. 36 | public func supplementaryItems( 37 | ofKind elementKind: String, 38 | _ value: [SupplementaryItemModeling]?) 39 | -> Self 40 | { 41 | var copy = self 42 | copy.supplementaryItems[elementKind] = value 43 | return copy 44 | } 45 | 46 | // MARK: Private 47 | 48 | private var supplementaryItemsProperty: EpoxyModelProperty { 49 | .init( 50 | keyPath: \SupplementaryItemsProviding.supplementaryItems, 51 | defaultValue: [:], 52 | updateStrategy: .replace) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/SectionModel/SectionModelBuilder.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/15/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | /// A result builder that enables a DSL for building arrays of section models. 7 | /// 8 | /// For example: 9 | /// ``` 10 | /// @SectionModelBuilder var sections: [SectionModel] { 11 | /// SectionModel(…) { … } 12 | /// SectionModel(…) { … } 13 | /// } 14 | /// ``` 15 | /// 16 | /// Will return an array containing two section models. 17 | public typealias SectionModelBuilder = EpoxyModelArrayBuilder 18 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/SupplementaryItemModel/SupplementaryItemModeling.swift: -------------------------------------------------------------------------------- 1 | // Created by Laura Skelton on 9/8/17. 2 | // Copyright © 2017 Airbnb. All rights reserved. 3 | 4 | import EpoxyCore 5 | import UIKit 6 | 7 | // MARK: - SupplementaryItemModeling 8 | 9 | /// Contains the reference data id for the model backing an item, as well as the reuse id for the 10 | /// item's type. 11 | public protocol SupplementaryItemModeling { 12 | /// Returns this item model with its type erased to the `AnySupplementaryItemModel` type. 13 | func eraseToAnySupplementaryItemModel() -> AnySupplementaryItemModel 14 | } 15 | 16 | // MARK: Extensions 17 | 18 | extension SupplementaryItemModeling { 19 | /// The internal wrapped item model. 20 | var internalItemModel: InternalSupplementaryItemModeling { 21 | eraseToAnySupplementaryItemModel().model 22 | } 23 | } 24 | 25 | // MARK: - InternalSupplementaryItemModeling 26 | 27 | protocol InternalSupplementaryItemModeling: SupplementaryItemModeling, 28 | DataIDProviding, 29 | ViewDifferentiatorProviding, 30 | Diffable, 31 | EpoxyModeled 32 | { 33 | /// Configures the cell for presentation. 34 | func configure( 35 | reusableView: CollectionViewReusableView, 36 | traitCollection: UITraitCollection, 37 | animated: Bool) 38 | 39 | /// Creates view for this supplementary item. This should only be used to create a view outside of a collection 40 | /// view. 41 | /// 42 | /// - Parameter traitCollection: The trait collection to create the view for 43 | /// - Returns: The configured view for this supplementary item model. 44 | func configuredView(traitCollection: UITraitCollection) -> UIView 45 | 46 | /// Set behaviors needed by the view. 47 | /// 48 | /// Called before presentation and when cells are reordered. 49 | func setBehavior( 50 | reusableView: CollectionViewReusableView, 51 | traitCollection: UITraitCollection, 52 | animated: Bool) 53 | 54 | /// Informs consumers that this item is about to be displayed. 55 | func handleWillDisplay( 56 | _ view: CollectionViewReusableView, 57 | traitCollection: UITraitCollection, 58 | animated: Bool) 59 | 60 | /// Informs consumers that this item is no longer displayed. 61 | func handleDidEndDisplaying( 62 | _ view: CollectionViewReusableView, 63 | traitCollection: UITraitCollection, 64 | animated: Bool) 65 | } 66 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Models/SupplementaryItemModel/SwiftUI.View+SupplementaryItemModel.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 9/16/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import SwiftUI 6 | 7 | extension View { 8 | /// Vends a `SupplementaryItemModel` representing this SwiftUI `View`. 9 | /// 10 | /// - Parameters: 11 | /// - dataID: An ID that uniquely identifies this item relative to other items in the 12 | /// same collection. 13 | /// - reuseBehavior: The reuse behavior of the `EpoxySwiftUIHostingView`. 14 | public func supplementaryItemModel( 15 | dataID: AnyHashable, 16 | reuseBehavior: SwiftUIHostingViewReuseBehavior = .reusable) 17 | -> SupplementaryItemModel> 18 | { 19 | EpoxySwiftUIHostingView.supplementaryItemModel( 20 | dataID: dataID, 21 | content: .init(rootView: self, dataID: dataID), 22 | style: .init( 23 | reuseBehavior: reuseBehavior, 24 | forceLayoutOnLayoutMarginsChange: false, 25 | initialContent: .init(rootView: self, dataID: dataID))) 26 | .linkDisplayLifecycle() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Views/AccessibilityCustomizedView.swift: -------------------------------------------------------------------------------- 1 | // Created by tyler_hedrick on 9/26/18. 2 | // Copyright © 2018 Airbnb. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// A view with customized accessibility behavior within a `CollectionView`. 7 | /// 8 | /// If a view conforms to this protocol, its `UIAccessibility` values (e.g. 9 | /// `accessibilityElementsHidden`) override the `CollectionView` default. 10 | /// 11 | /// For now, this just supports ``accessibilityElementsHidden`. 12 | public protocol AccessibilityCustomizedView: UIView { } 13 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Views/DisplayRespondingView.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 12/5/19. 2 | // Copyright © 2019 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// A view that responds to being displayed within a `CollectionView`. 7 | public protocol DisplayRespondingView: UIView { 8 | /// Implement this method on your view to react to display events, not to manage internal state 9 | /// for your view. 10 | /// 11 | /// Example use case: override to start / stop animations when the view is displayed / ends 12 | /// displaying respectively. 13 | func didDisplay(_ isDisplayed: Bool) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Views/EphemeralCachedStateView.swift: -------------------------------------------------------------------------------- 1 | // Created by Kieraj Mumick on 1/16/19. 2 | // Copyright © 2019 Airbnb. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// A protocol for views that have ephemeral state that (e.g. expansion state) within a 7 | /// `CollectionView`. 8 | /// 9 | /// Epoxy uses the cached ephemeral state to automatically restore state when cells are recycled. 10 | public protocol EphemeralCachedStateView: UIView { 11 | /// The cached ephemeral state that is automatically restored after this cell is recycled. 12 | var cachedEphemeralState: Any? { get set } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Views/HighlightableView.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 9/12/19. 2 | // Copyright © 2019 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// A view that responds to being highlighted within a `CollectionView`. 7 | public protocol HighlightableView: UIView { 8 | /// Implement this method on your view to react to highlight events. Do NOT use this method to 9 | /// manage internal state for your view. 10 | /// 11 | /// Example use case: override to animate a shrink / grow effect when the user highlights a cell. 12 | func didHighlight(_ highlighted: Bool) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/EpoxyCollectionView/Views/SelectableView.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 9/12/19. 2 | // Copyright © 2019 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// A view that responds to being selected within a `CollectionView`. 7 | public protocol SelectableView: UIView { 8 | /// Implement this method on your view to react to selection events from user interaction. Do NOT 9 | /// use this method to manage internal state for your view. 10 | /// 11 | /// Example use case: override to fire haptics when a user taps the view 12 | func didSelect() 13 | } 14 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Diffing/Diffable.swift: -------------------------------------------------------------------------------- 1 | // Created by Laura Skelton on 5/11/17. 2 | // Copyright © 2017 Airbnb. All rights reserved. 3 | 4 | // MARK: - Diffable 5 | 6 | /// A protocol that allows us to check identity and equality between items for the purposes of 7 | /// diffing. 8 | public protocol Diffable { 9 | 10 | /// Checks for equality between items when diffing. 11 | /// 12 | /// - Parameters: 13 | /// - otherDiffableItem: The other item to check equality against while diffing. 14 | func isDiffableItemEqual(to otherDiffableItem: Diffable) -> Bool 15 | 16 | /// The identifier to use when checking identity while diffing. 17 | var diffIdentifier: AnyHashable { get } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Diffing/DiffableSection.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/9/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - DiffableSection 5 | 6 | /// A protocol that allows us to check identity and equality between sections of `Diffable` items 7 | /// for the purposes of diffing. 8 | public protocol DiffableSection: Diffable { 9 | /// The diffable items in this section. 10 | associatedtype DiffableItems: Collection where 11 | DiffableItems.Index == Int, 12 | DiffableItems.Element: Diffable 13 | 14 | /// The diffable items in this section. 15 | var diffableItems: DiffableItems { get } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Diffing/SectionedChangeset.swift: -------------------------------------------------------------------------------- 1 | // Created by Laura Skelton on 5/11/17. 2 | // Copyright © 2017 Airbnb. All rights reserved. 3 | 4 | /// A set of the minimum changes to get from one array of `DiffableSection`s to another, used for 5 | /// diffing. 6 | public struct SectionedChangeset { 7 | 8 | // MARK: Lifecycle 9 | 10 | public init( 11 | sectionChangeset: IndexSetChangeset, 12 | itemChangeset: IndexPathChangeset) 13 | { 14 | self.sectionChangeset = sectionChangeset 15 | self.itemChangeset = itemChangeset 16 | } 17 | 18 | // MARK: Public 19 | 20 | /// A set of the minimum changes to get from one set of sections to another. 21 | public var sectionChangeset: IndexSetChangeset 22 | 23 | /// A set of the minimum changes to get from one set of items to another, aggregated across all 24 | /// sections. 25 | public var itemChangeset: IndexPathChangeset 26 | 27 | /// Whether there are any inserts, deletes, moves, or updates in this changeset. 28 | public var isEmpty: Bool { 29 | sectionChangeset.isEmpty && itemChangeset.isEmpty 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/EpoxyCore.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/CallbackContextEpoxyModeled.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/15/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | /// An Epoxy model with an associated context type that's passed into callback closures. 5 | public protocol CallbackContextEpoxyModeled: EpoxyModeled { 6 | /// A context type that's passed into callback closures. 7 | associatedtype CallbackContext 8 | } 9 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/EpoxyModelArrayBuilder.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/15/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | /// A generic result builder that enables a DSL for building arrays of Epoxy models. 5 | @resultBuilder 6 | public enum EpoxyModelArrayBuilder { 7 | public typealias Expression = Model 8 | public typealias Component = [Model] 9 | 10 | public static func buildExpression(_ expression: Expression) -> Component { 11 | [expression] 12 | } 13 | 14 | public static func buildExpression(_ expression: Component) -> Component { 15 | expression 16 | } 17 | 18 | public static func buildExpression(_ expression: Expression?) -> Component { 19 | if let expression = expression { 20 | return [expression] 21 | } 22 | return [] 23 | } 24 | 25 | public static func buildBlock(_ children: Component...) -> Component { 26 | children.flatMap { $0 } 27 | } 28 | 29 | public static func buildBlock(_ component: Component) -> Component { 30 | component 31 | } 32 | 33 | public static func buildOptional(_ children: Component?) -> Component { 34 | children ?? [] 35 | } 36 | 37 | public static func buildEither(first child: Component) -> Component { 38 | child 39 | } 40 | 41 | public static func buildEither(second child: Component) -> Component { 42 | child 43 | } 44 | 45 | public static func buildArray(_ components: [Component]) -> Component { 46 | components.flatMap { $0 } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/EpoxyModeled.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 11/18/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - EpoxyModeled 5 | 6 | /// A protocol that all concrete Epoxy declarative UI model types conform to. 7 | /// 8 | /// This protocol should be conditionally extended to fulfill provider protocols and with chainable 9 | /// setters for those providers that concrete model types can receive by declaring conformance to 10 | /// provider protocols. 11 | public protocol EpoxyModeled { 12 | /// The underlying storage of this model that stores the current property values. 13 | var storage: EpoxyModelStorage { get set } 14 | } 15 | 16 | // MARK: Extensions 17 | 18 | extension EpoxyModeled { 19 | /// Stores or retrieves a value of the specified property in `storage`. 20 | /// 21 | /// If the value was set previously for the given `property`, the conflict is resolved using the 22 | /// `EpoxyModelProperty.UpdateStrategy` of the `property`. 23 | public subscript(property: EpoxyModelProperty) -> Property { 24 | get { storage[property] } 25 | set { storage[property] = newValue } 26 | } 27 | 28 | /// Returns a copy of this model with the given property updated to the provided value. 29 | /// 30 | /// Typically called from within the context of a chainable setter to allow fluent setting of a 31 | /// property, e.g.: 32 | /// 33 | /// ```` 34 | /// public func title(_ value: String?) -> Self { 35 | /// copy(updating: titleProperty, to: value) 36 | /// } 37 | /// ```` 38 | /// 39 | /// If a `value` was set previously for the given `property`, the conflict is resolved using the 40 | /// `EpoxyModelProperty.UpdateStrategy` of the `property`. 41 | public func copy(updating property: EpoxyModelProperty, to value: Value) -> Self { 42 | var copy = self 43 | copy.storage[property] = value 44 | return copy 45 | } 46 | 47 | /// Returns a copy of this model produced by merging the given `other` model's storage into this 48 | /// model's storage. 49 | public func merging(_ other: EpoxyModeled) -> Self { 50 | var copy = self 51 | copy.storage.merge(other.storage) 52 | return copy 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Internal/AnyEpoxyModelProperty.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/1/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - AnyEpoxyModelProperty 5 | 6 | /// An erased `EpoxyModelProperty`, with the ability to call the `UpdateStrategy` even when the type 7 | /// has been erased. 8 | protocol AnyEpoxyModelProperty { 9 | /// Returns the updated property from updating from given old to new property. 10 | func update(old: Any, new: Any) -> Any 11 | } 12 | 13 | // MARK: - EpoxyModelProperty + AnyEpoxyModelProperty 14 | 15 | extension EpoxyModelProperty: AnyEpoxyModelProperty { 16 | func update(old: Any, new: Any) -> Any { 17 | guard let typedOld = old as? Value else { 18 | EpoxyLogger.shared.assertionFailure( 19 | "Expected old to be of type \(Value.self), instead found \(old). This is programmer error.") 20 | return defaultValue() 21 | } 22 | guard let typedNew = new as? Value else { 23 | EpoxyLogger.shared.assertionFailure( 24 | "Expected new to be of type \(Value.self), instead found \(old). This is programmer error.") 25 | return defaultValue() 26 | } 27 | return updateStrategy.update(typedOld, typedNew) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Internal/ClassReference.swift: -------------------------------------------------------------------------------- 1 | // Created by Cal Stephens on 10/15/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - ClassReference 5 | 6 | /// A `Hashable` value wrapper around an `AnyClass` value 7 | /// - Unlike `ObjectIdentifier(class)`, `ClassReference(class)` 8 | /// preserves the `AnyClass` value and is more human-readable. 9 | public struct ClassReference { 10 | public init(_ class: AnyClass) { 11 | self.class = `class` 12 | } 13 | 14 | public let `class`: AnyClass 15 | } 16 | 17 | // MARK: Equatable 18 | 19 | extension ClassReference: Equatable { 20 | public static func ==(_ lhs: Self, _ rhs: Self) -> Bool { 21 | ObjectIdentifier(lhs.class) == ObjectIdentifier(rhs.class) 22 | } 23 | } 24 | 25 | // MARK: Hashable 26 | 27 | extension ClassReference: Hashable { 28 | public func hash(into hasher: inout Hasher) { 29 | hasher.combine(ObjectIdentifier(`class`)) 30 | } 31 | } 32 | 33 | // MARK: CustomStringConvertible 34 | 35 | extension ClassReference: CustomStringConvertible { 36 | public var description: String { 37 | String(describing: `class`) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/AnimatedProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/16/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | /// The capability of providing a flag indicating whether an operation should be animated. 5 | /// 6 | /// Typically conformed to by the `CallbackContext` of a `CallbackContextEpoxyModeled`. 7 | public protocol AnimatedProviding { 8 | /// Whether this operation should be animated. 9 | var animated: Bool { get } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/DataIDProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/1/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - DataIDProviding 5 | 6 | /// The capability of providing a stable data identifier with an erased type. 7 | /// 8 | /// While it has similar semantics, this type cannot inherit from `Identifiable` as this would give 9 | /// it an associated type, which would cause the `keyPath` used in its `EpoxyModelProperty` to not 10 | /// be stable across types if written as `\Self.dataID` since the `KeyPath` `Root` would be 11 | /// different for each type. 12 | /// 13 | /// - SeeAlso: `Identifiable`. 14 | public protocol DataIDProviding { 15 | /// A stable identifier that uniquely identifies this instance, with its typed erased. 16 | /// 17 | /// Defaults to `DefaultDataID.noneProvided` if no data ID is provided. 18 | var dataID: AnyHashable { get } 19 | } 20 | 21 | // MARK: - EpoxyModeled 22 | 23 | extension EpoxyModeled where Self: DataIDProviding { 24 | 25 | // MARK: Public 26 | 27 | /// A stable identifier that uniquely identifies this model, with its typed erased. 28 | public var dataID: AnyHashable { 29 | get { self[dataIDProperty] } 30 | set { self[dataIDProperty] = newValue } 31 | } 32 | 33 | /// Returns a copy of this model with the ID replaced with the provided ID. 34 | public func dataID(_ value: AnyHashable) -> Self { 35 | copy(updating: dataIDProperty, to: value) 36 | } 37 | 38 | // MARK: Private 39 | 40 | private var dataIDProperty: EpoxyModelProperty { 41 | EpoxyModelProperty( 42 | keyPath: \DataIDProviding.dataID, 43 | defaultValue: DefaultDataID.noneProvided, 44 | updateStrategy: .replace) 45 | } 46 | } 47 | 48 | // MARK: - DefaultDataID 49 | 50 | /// The default data ID when none is provided. 51 | public enum DefaultDataID: Hashable, CustomDebugStringConvertible { 52 | case noneProvided 53 | 54 | public var debugDescription: String { 55 | "DefaultDataID.noneProvided" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/DidDisplayProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 1/6/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - DidDisplayProviding 5 | 6 | /// A sentinel protocol for enabling an `CallbackContextEpoxyModeled` to provide a `didDisplay` 7 | /// closure property. 8 | /// 9 | /// - SeeAlso: `WillDisplayProviding` 10 | /// - SeeAlso: `DidEndDisplayingProviding` 11 | public protocol DidDisplayProviding { } 12 | 13 | // MARK: - CallbackContextEpoxyModeled 14 | 15 | extension CallbackContextEpoxyModeled where Self: DidDisplayProviding { 16 | 17 | // MARK: Public 18 | 19 | /// A closure that's called after a view has been added to the view hierarchy following any 20 | /// appearance animations. 21 | public typealias DidDisplay = (_ context: CallbackContext) -> Void 22 | 23 | /// A closure that's called after the view has been added to the view hierarchy following any 24 | /// appearance animations. 25 | public var didDisplay: DidDisplay? { 26 | get { self[didDisplayProperty] } 27 | set { self[didDisplayProperty] = newValue } 28 | } 29 | 30 | /// Returns a copy of this model with the given did display closure called after the current did 31 | /// display closure of this model, if there is one. 32 | public func didDisplay(_ value: DidDisplay?) -> Self { 33 | copy(updating: didDisplayProperty, to: value) 34 | } 35 | 36 | // MARK: Private 37 | 38 | private var didDisplayProperty: EpoxyModelProperty { 39 | .init(keyPath: \Self.didDisplay, defaultValue: nil, updateStrategy: .chain()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/DidEndDisplayingProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/15/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - DidEndDisplayingProviding 5 | 6 | /// A sentinel protocol for enabling an `CallbackContextEpoxyModeled` to provide a 7 | /// `didEndDisplaying` closure property. 8 | public protocol DidEndDisplayingProviding { } 9 | 10 | // MARK: - CallbackContextEpoxyModeled 11 | 12 | extension CallbackContextEpoxyModeled where Self: DidEndDisplayingProviding { 13 | 14 | // MARK: Public 15 | 16 | /// A closure that's called when a view is no longer displayed following any disappearance 17 | /// animations and when it has been removed from the view hierarchy. 18 | public typealias DidEndDisplaying = (_ context: CallbackContext) -> Void 19 | 20 | /// A closure that's called when the view is no longer displayed following any disappearance 21 | /// animations and when it has been removed from the view hierarchy. 22 | public var didEndDisplaying: DidEndDisplaying? { 23 | get { self[didEndDisplayingProperty] } 24 | set { self[didEndDisplayingProperty] = newValue } 25 | } 26 | 27 | /// Returns a copy of this model with the given did end displaying closure called after the 28 | /// current did end displaying closure of this model, if there is one. 29 | public func didEndDisplaying(_ value: DidEndDisplaying?) -> Self { 30 | copy(updating: didEndDisplayingProperty, to: value) 31 | } 32 | 33 | // MARK: Private 34 | 35 | private var didEndDisplayingProperty: EpoxyModelProperty { 36 | .init( 37 | keyPath: \Self.didEndDisplaying, 38 | defaultValue: nil, 39 | updateStrategy: .chain()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/DidSelectProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/2/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - DidSelectProviding 5 | 6 | /// A sentinel protocol for enabling an `CallbackContextEpoxyModeled` to provide a `didSelect` 7 | /// closure property. 8 | public protocol DidSelectProviding { } 9 | 10 | // MARK: - CallbackContextEpoxyModeled 11 | 12 | extension CallbackContextEpoxyModeled where Self: DidSelectProviding { 13 | 14 | // MARK: Public 15 | 16 | /// A closure that's called to handle this model's view being selected. 17 | public typealias DidSelect = (CallbackContext) -> Void 18 | 19 | /// A closure that's called to handle this model's view being selected. 20 | public var didSelect: DidSelect? { 21 | get { self[didSelectProperty] } 22 | set { self[didSelectProperty] = newValue } 23 | } 24 | 25 | /// Returns a copy of this model with the given did select closure called after the current did 26 | /// select closure of this model, if there is one. 27 | public func didSelect(_ value: DidSelect?) -> Self { 28 | copy(updating: didSelectProperty, to: value) 29 | } 30 | 31 | // MARK: Private 32 | 33 | private var didSelectProperty: EpoxyModelProperty { 34 | .init(keyPath: \Self.didSelect, defaultValue: nil, updateStrategy: .chain()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/ErasedContentProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/2/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - ErasedContentProviding 5 | 6 | /// The capability of providing an type-erased `Equatable` content instance. 7 | public protocol ErasedContentProviding { 8 | /// The type-erased content instance of this model, else `nil` if there is no content. 9 | /// 10 | /// If there was an `AnyEquatable` type, we could store this property using it. Instead we need 11 | /// need to store `isErasedContentEqual` to determine equality. 12 | var erasedContent: Any? { get } 13 | 14 | /// A closure that can be called to determine whether the given `model`'s `erasedContent` is equal 15 | /// to this model's `erasedContent`, else `nil` if there is no content or the content is always 16 | /// equal. 17 | var isErasedContentEqual: ((ErasedContentProviding) -> Bool)? { get } 18 | } 19 | 20 | // MARK: - EpoxyModeled 21 | 22 | extension EpoxyModeled where Self: ErasedContentProviding { 23 | 24 | // MARK: Public 25 | 26 | /// The type-erased content instance of this model, else `nil` if there is no content. 27 | public var erasedContent: Any? { 28 | get { self[contentProperty] } 29 | set { self[contentProperty] = newValue } 30 | } 31 | 32 | /// A closure that can be called to determine whether the given `model`'s `erasedContent` is equal 33 | /// to this model's `erasedContent`, else `nil` if there is no content or the content is always 34 | /// equal. 35 | public var isErasedContentEqual: ((ErasedContentProviding) -> Bool)? { 36 | get { self[isContentEqualProperty] } 37 | set { self[isContentEqualProperty] = newValue } 38 | } 39 | 40 | // MARK: Private 41 | 42 | private var contentProperty: EpoxyModelProperty { 43 | .init(keyPath: \ErasedContentProviding.erasedContent, defaultValue: nil, updateStrategy: .replace) 44 | } 45 | 46 | private var isContentEqualProperty: EpoxyModelProperty<((ErasedContentProviding) -> Bool)?> { 47 | .init(keyPath: \ErasedContentProviding.isErasedContentEqual, defaultValue: nil, updateStrategy: .replace) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/MakeViewProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/1/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - MakeViewProviding 5 | 6 | /// The capability of constructing a `UIView`. 7 | public protocol MakeViewProviding { 8 | /// The view constructed when the `MakeView` closure is called. 9 | associatedtype View: ViewType 10 | 11 | /// A closure that's called to construct an instance of `View`. 12 | typealias MakeView = () -> View 13 | 14 | /// A closure that's called to construct an instance of `View`. 15 | var makeView: MakeView { get } 16 | } 17 | 18 | // MARK: - ViewEpoxyModeled 19 | 20 | extension ViewEpoxyModeled where Self: MakeViewProviding { 21 | 22 | // MARK: Public 23 | 24 | /// A closure that's called to construct an instance of `View` represented by this model. 25 | public var makeView: MakeView { 26 | get { self[makeViewProperty] } 27 | set { self[makeViewProperty] = newValue } 28 | } 29 | 30 | /// Replaces the default closure to construct the view with the given closure. 31 | public func makeView(_ value: @escaping MakeView) -> Self { 32 | copy(updating: makeViewProperty, to: value) 33 | } 34 | 35 | // MARK: Private 36 | 37 | private var makeViewProperty: EpoxyModelProperty { 38 | // If you're getting a `EXC_BAD_INSTRUCTION` crash with this property in your stack trace, you 39 | // probably either: 40 | // - Conformed a view to `EpoxyableView` / `StyledView` with a custom initializer that 41 | // takes parameters, or: 42 | // - Used the `EpoxyModeled.init(dataID:)` initializer on a view has required initializer 43 | // parameters. 44 | // If you have parameters to view initialization, they should either be passed to `init(style:)` 45 | // or you should provide a `makeView` closure when constructing your view's corresponding model, 46 | // e.g: 47 | // ``` 48 | // MyView.itemModel(…) 49 | // .makeView { MyView(customParameter: …) } 50 | // .styleID(…) 51 | // ``` 52 | // Note that with the above approach that you must supply an `styleID` with the same identity as 53 | // your view parameters to ensure that views with different parameters are not reused in place 54 | // of one another. 55 | .init( 56 | keyPath: \Self.makeView, 57 | defaultValue: View.init, 58 | updateStrategy: .replace) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/SetBehaviorsProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/2/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - SetBehaviorsProviding 5 | 6 | /// A sentinel protocol for enabling an `CallbackContextEpoxyModeled` to provide a `setBehaviors` 7 | /// closure property. 8 | public protocol SetBehaviorsProviding { } 9 | 10 | // MARK: - CallbackContextEpoxyModeled 11 | 12 | extension CallbackContextEpoxyModeled where Self: SetBehaviorsProviding { 13 | 14 | // MARK: Public 15 | 16 | /// A closure that's called to set the content on this model's view with behaviors (e.g. tap handler 17 | /// closures) whenever this model is updated. 18 | public typealias SetBehaviors = (CallbackContext) -> Void 19 | 20 | /// A closure that's called to set the content on this model's view with behaviors (e.g. tap handler 21 | /// closures) whenever this model is updated. 22 | public var setBehaviors: SetBehaviors? { 23 | get { self[setBehaviorsProperty] } 24 | set { self[setBehaviorsProperty] = newValue } 25 | } 26 | 27 | /// Returns a copy of this model with the set behaviors closure called after the current set 28 | /// behaviors closure of this model, if there is one. 29 | public func setBehaviors(_ value: SetBehaviors?) -> Self { 30 | copy(updating: setBehaviorsProperty, to: value) 31 | } 32 | 33 | // MARK: Private 34 | 35 | private var setBehaviorsProperty: EpoxyModelProperty { 36 | .init(keyPath: \Self.setBehaviors, defaultValue: nil, updateStrategy: .chain()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/SetContentProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/1/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - SetContentProviding 5 | 6 | /// A sentinel protocol for enabling an `CallbackContextEpoxyModeled` to provide a `setContent` 7 | /// closure property. 8 | public protocol SetContentProviding { } 9 | 10 | // MARK: - CallbackContextEpoxyModeled 11 | 12 | extension CallbackContextEpoxyModeled where Self: SetContentProviding { 13 | 14 | // MARK: Public 15 | 16 | /// A closure that's called to set the content on this model's view when it is first created and 17 | /// subsequently when the content changes. 18 | public typealias SetContent = (CallbackContext) -> Void 19 | 20 | /// A closure that's called to set the content on this model's view when it is first created and 21 | /// subsequently when the content changes. 22 | public var setContent: SetContent? { 23 | get { self[setContentProperty] } 24 | set { self[setContentProperty] = newValue } 25 | } 26 | 27 | /// Returns a copy of this model with the given setContent view closure called after the current 28 | /// setContent view closure of this model, if there is one. 29 | public func setContent(_ value: SetContent?) -> Self { 30 | copy(updating: setContentProperty, to: value) 31 | } 32 | 33 | // MARK: Private 34 | 35 | private var setContentProperty: EpoxyModelProperty { 36 | .init(keyPath: \Self.setContent, defaultValue: nil, updateStrategy: .chain()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/StyleIDProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/1/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - StyleIDProviding 5 | 6 | public protocol StyleIDProviding { 7 | /// An optional ID for a style type to use for reuse of a view. 8 | /// 9 | /// Use this to differentiate between different styling configurations. 10 | var styleID: AnyHashable? { get } 11 | } 12 | 13 | // MARK: - EpoxyModeled 14 | 15 | extension EpoxyModeled where Self: StyleIDProviding { 16 | 17 | // MARK: Public 18 | 19 | public var styleID: AnyHashable? { 20 | get { self[styleIDProperty] } 21 | set { self[styleIDProperty] = newValue } 22 | } 23 | 24 | /// Returns a copy of this model with the `styleID` replaced with the provided `value`. 25 | public func styleID(_ value: AnyHashable?) -> Self { 26 | copy(updating: styleIDProperty, to: value) 27 | } 28 | 29 | // MARK: Private 30 | 31 | private var styleIDProperty: EpoxyModelProperty { 32 | .init( 33 | keyPath: \StyleIDProviding.styleID, 34 | defaultValue: nil, 35 | updateStrategy: .replace) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/TraitCollectionProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/16/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | #if !os(macOS) 5 | import UIKit 6 | 7 | /// The capability of providing a `UITraitCollection` instance. 8 | /// 9 | /// Typically conformed to by the `CallbackContext` of a `CallbackContextEpoxyModeled`. 10 | public protocol TraitCollectionProviding { 11 | /// The `UITraitCollection` instance provided by this type. 12 | var traitCollection: UITraitCollection { get } 13 | } 14 | #endif 15 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/ViewDifferentiatorProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by Bryan Keller on 12/17/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - ViewDifferentiatorProviding 5 | 6 | /// The capability of providing a view differentiator that facilitates generating collection view 7 | /// cell reuse identifiers. 8 | public protocol ViewDifferentiatorProviding { 9 | /// The view differentiator for the item model. 10 | var viewDifferentiator: ViewDifferentiator { get } 11 | } 12 | 13 | // MARK: - ViewDifferentiator 14 | 15 | /// Facilitates differentiating between two models' views, based on their view type, optional style 16 | /// identifier, and optional element kind for supplementary view models. If two models have the same 17 | /// view differentiator, then they're compatible with one another for element reuse. If two models 18 | /// have different view differentiators, then they're incompatible with one another for element 19 | /// reuse. 20 | public struct ViewDifferentiator: Hashable { 21 | 22 | // MARK: Lifecycle 23 | 24 | public init(viewType: AnyClass, styleID: AnyHashable?) { 25 | viewTypeDescription = "\(type(of: viewType.self))" 26 | self.styleID = styleID 27 | } 28 | 29 | // MARK: Public 30 | 31 | public var viewTypeDescription: String 32 | public var styleID: AnyHashable? 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/ViewProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/16/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | /// The capability of providing an `View` instance 5 | /// 6 | /// Typically conformed to by the `CallbackContext` of a `CallbackContextEpoxyModeled`. 7 | public protocol ViewProviding { 8 | /// The `UIView` view of this type. 9 | associatedtype View: ViewType 10 | 11 | /// The `UIView` view instance provided by this type. 12 | var view: View { get } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/Providers/WillDisplayProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/15/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - WillDisplayProviding 5 | 6 | /// A sentinel protocol for enabling an `CallbackContextEpoxyModeled` to provide a `willDisplay` 7 | /// closure property. 8 | /// 9 | /// - SeeAlso: `DidDisplayProviding` 10 | /// - SeeAlso: `DidEndDisplayingProviding` 11 | public protocol WillDisplayProviding { } 12 | 13 | // MARK: - CallbackContextEpoxyModeled 14 | 15 | extension CallbackContextEpoxyModeled where Self: WillDisplayProviding { 16 | 17 | // MARK: Public 18 | 19 | /// A closure that's called when a view is about to be displayed, before it has been added to the 20 | /// view hierarchy. 21 | public typealias WillDisplay = (_ context: CallbackContext) -> Void 22 | 23 | /// A closure that's called when the view is about to be displayed, before it has been added to 24 | /// the view hierarchy. 25 | public var willDisplay: WillDisplay? { 26 | get { self[willDisplayProperty] } 27 | set { self[willDisplayProperty] = newValue } 28 | } 29 | 30 | /// Returns a copy of this model with the given will display closure called after the current will 31 | /// display closure of this model, if there is one. 32 | public func willDisplay(_ value: WillDisplay?) -> Self { 33 | copy(updating: willDisplayProperty, to: value) 34 | } 35 | 36 | // MARK: Private 37 | 38 | private var willDisplayProperty: EpoxyModelProperty { 39 | .init(keyPath: \Self.willDisplay, defaultValue: nil, updateStrategy: .chain()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Model/ViewEpoxyModeled.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 12/4/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | /// An Epoxy model with an associated `UIView` type. 5 | public protocol ViewEpoxyModeled: EpoxyModeled { 6 | /// The view type associated with this model. 7 | /// 8 | /// An instance of this view is typically configured by this model. 9 | associatedtype View: ViewType 10 | } 11 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/SwiftUI/EpoxySwiftUIIntrinsicContentSizeInvalidator.swift: -------------------------------------------------------------------------------- 1 | // Created by matthew_cheok on 11/19/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import SwiftUI 5 | 6 | // MARK: - EpoxyIntrinsicContentSizeInvalidator 7 | 8 | /// Allows the SwiftUI view contained in an Epoxy model to request the invalidation of 9 | /// the container's intrinsic content size. 10 | /// 11 | /// ``` 12 | /// @Environment(\.epoxyIntrinsicContentSizeInvalidator) var invalidateIntrinsicContentSize 13 | /// 14 | /// var body: some View { 15 | /// ... 16 | /// .onChange(of: size) { 17 | /// invalidateIntrinsicContentSize() 18 | /// } 19 | /// } 20 | /// ``` 21 | public struct EpoxyIntrinsicContentSizeInvalidator { 22 | let invalidate: () -> Void 23 | 24 | public func callAsFunction() { 25 | invalidate() 26 | } 27 | } 28 | 29 | // MARK: - EnvironmentValues 30 | 31 | extension EnvironmentValues { 32 | /// A means of invalidating the intrinsic content size of the parent `EpoxySwiftUIHostingView`. 33 | public var epoxyIntrinsicContentSizeInvalidator: EpoxyIntrinsicContentSizeInvalidator { 34 | get { self[EpoxyIntrinsicContentSizeInvalidatorKey.self] } 35 | set { self[EpoxyIntrinsicContentSizeInvalidatorKey.self] = newValue } 36 | } 37 | } 38 | 39 | // MARK: - EpoxyIntrinsicContentSizeInvalidatorKey 40 | 41 | private struct EpoxyIntrinsicContentSizeInvalidatorKey: EnvironmentKey { 42 | static let defaultValue = EpoxyIntrinsicContentSizeInvalidator(invalidate: { }) 43 | } 44 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/SwiftUI/EpoxySwiftUILayoutMargins.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 10/8/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import SwiftUI 5 | 6 | // MARK: - View 7 | 8 | extension View { 9 | /// Applies the layout margins from the parent `EpoxySwiftUIHostingView` to this `View`, if there 10 | /// are any. 11 | /// 12 | /// Can be used to have a background in SwiftUI underlap the safe area within a bar installer, for 13 | /// example. 14 | /// 15 | /// These margins are propagated via the `EnvironmentValues.epoxyLayoutMargins`. 16 | public func epoxyLayoutMargins() -> some View { 17 | modifier(EpoxyLayoutMarginsPadding()) 18 | } 19 | } 20 | 21 | // MARK: - EnvironmentValues 22 | 23 | extension EnvironmentValues { 24 | /// The layout margins of the parent `EpoxySwiftUIHostingView`, else zero if there is none. 25 | public var epoxyLayoutMargins: EdgeInsets { 26 | get { self[EpoxyLayoutMarginsKey.self] } 27 | set { self[EpoxyLayoutMarginsKey.self] = newValue } 28 | } 29 | } 30 | 31 | // MARK: - EpoxyLayoutMarginsKey 32 | 33 | private struct EpoxyLayoutMarginsKey: EnvironmentKey { 34 | static let defaultValue = EdgeInsets() 35 | } 36 | 37 | // MARK: - EpoxyLayoutMarginsPadding 38 | 39 | /// A view modifier that applies the layout margins from an enclosing `EpoxySwiftUIHostingView` to 40 | /// the modified `View`. 41 | private struct EpoxyLayoutMarginsPadding: ViewModifier { 42 | @Environment(\.epoxyLayoutMargins) var epoxyLayoutMargins 43 | 44 | func body(content: Content) -> some View { 45 | content.padding(epoxyLayoutMargins) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/SwiftUI/UIView+SwiftUIView.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/3/22. 2 | // Copyright © 2022 Airbnb Inc. All rights reserved. 3 | 4 | import SwiftUI 5 | 6 | // MARK: - ViewTypeProtocol + swiftUIView 7 | 8 | extension ViewTypeProtocol { 9 | /// Returns a SwiftUI `View` representing this `UIView`, constructed with the given `makeView` 10 | /// closure and sized with the given sizing configuration. 11 | /// 12 | /// To perform additional configuration of the `UIView` instance, call `configure` on the 13 | /// returned SwiftUI `View`: 14 | /// ``` 15 | /// MyUIView.swiftUIView(…) 16 | /// .configure { context in 17 | /// context.view.doSomething() 18 | /// } 19 | /// ``` 20 | /// 21 | /// To configure the sizing behavior of the `UIView` instance, call `sizing` on the returned 22 | /// SwiftUI `View`: 23 | /// ``` 24 | /// MyView.swiftUIView(…).sizing(.intrinsicSize) 25 | /// ``` 26 | /// The sizing defaults to `.automatic`. 27 | public static func swiftUIView(makeView: @escaping () -> Self) -> SwiftUIView { 28 | SwiftUIView(makeContent: makeView) 29 | } 30 | } 31 | 32 | // MARK: - ViewTypeProtocol 33 | 34 | /// A protocol that all `UIView`s conform to, enabling extensions that have a `Self` reference. 35 | public protocol ViewTypeProtocol: ViewType { } 36 | 37 | // MARK: - ViewType + ViewTypeProtocol 38 | 39 | extension ViewType: ViewTypeProtocol { } 40 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/SwiftUI/UIViewConfiguringSwiftUIView.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/4/22. 2 | // Copyright © 2022 Airbnb Inc. All rights reserved. 3 | 4 | import SwiftUI 5 | 6 | // MARK: - UIViewConfiguringSwiftUIView 7 | 8 | /// A protocol describing a SwiftUI `View` that can configure its `UIView` content via an array of 9 | /// `configuration` closures. 10 | public protocol UIViewConfiguringSwiftUIView: View { 11 | /// The context available to this configuration, which provides the `UIView` instance at a minimum 12 | /// but can include additional context as needed. 13 | associatedtype ConfigurationContext: ViewProviding 14 | 15 | /// A closure that is invoked to configure the represented content view. 16 | typealias Configuration = (ConfigurationContext) -> Void 17 | 18 | /// A mutable array of configuration closures that should each be invoked with the 19 | /// `ConfigurationContext` whenever `updateUIView` is called in a `UIViewRepresentable`. 20 | var configurations: [Configuration] { get set } 21 | } 22 | 23 | // MARK: Extensions 24 | 25 | extension UIViewConfiguringSwiftUIView { 26 | /// Returns a copy of this view updated to have the given closure applied to its represented view 27 | /// whenever it is updated via the `updateUIView(…)` method. 28 | public func configure(_ configure: @escaping Configuration) -> Self { 29 | var copy = self 30 | copy.configurations.append(configure) 31 | return copy 32 | } 33 | 34 | /// Returns a copy of this view updated to have the given closures applied to its represented view 35 | /// whenever it is updated via the `updateUIView(…)` method. 36 | public func configurations(_ configurations: [Configuration]) -> Self { 37 | var copy = self 38 | copy.configurations.append(contentsOf: configurations) 39 | return copy 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Views/BehaviorsConfigurableView.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 5/26/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - BehaviorsConfigurableView 5 | 6 | /// A view that can be configured with a `Behaviors` instance that contains the view's non- 7 | /// `Equatable` properties that can be updated on view instances after initialization, e.g. callback 8 | /// closures or delegates. 9 | /// 10 | /// Since it is not possible to establish the equality of two `Behaviors` instances, `Behaviors` 11 | /// will be set more often than `ContentConfigurableView.Content`, needing to be updated every time 12 | /// the view's corresponding `EpoxyModeled` instance is updated. As such, setting behaviors should 13 | /// be as lightweight as possible. 14 | /// 15 | /// Properties of `Behaviors` should be mutually exclusive with the properties in the 16 | /// `StyledView.Style` and `ContentConfigurableView.Content`. 17 | /// 18 | /// - SeeAlso: `ContentConfigurableView` 19 | /// - SeeAlso: `StyledView` 20 | /// - SeeAlso: `EpoxyableView` 21 | public protocol BehaviorsConfigurableView: ViewType { 22 | /// The non-`Equatable` properties that can be changed over of the lifecycle this View's 23 | /// instances, e.g. callback closures or delegates. 24 | /// 25 | /// Defaults to `Never` for views that do not have `Behaviors`. 26 | associatedtype Behaviors = Never 27 | 28 | /// Updates the behaviors of this view to those in the given `behaviors`, else resets the 29 | /// behaviors if `nil`. 30 | /// 31 | /// Behaviors are optional as they must be "resettable" in order for Epoxy to reset the behaviors 32 | /// on your view when no behaviors are provided. 33 | func setBehaviors(_ behaviors: Self.Behaviors?) 34 | } 35 | 36 | // MARK: Defaults 37 | 38 | extension BehaviorsConfigurableView where Behaviors == Never { 39 | public func setBehaviors(_ behaviors: Never?) { 40 | switch behaviors { 41 | case nil: 42 | break 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Views/ContentConfigurableView.swift: -------------------------------------------------------------------------------- 1 | // Created by Laura Skelton on 5/30/17. 2 | // Copyright © 2017 Airbnb. All rights reserved. 3 | 4 | // MARK: - ContentConfigurableView 5 | 6 | /// A view that can be configured with a `Content` instance that contains the view's `Equatable` 7 | /// properties that can be updated on existing view instances, e.g. text `String`s or image `URL`s. 8 | /// 9 | /// For performance, it is generally expected that `Content` is only set when it is not equal to the 10 | /// previous `Content` instance that has been set on a view instance. As a further optimization, 11 | /// this view can guard updates on the equality of each property of the `Content` against the 12 | /// current property value when set. 13 | /// 14 | /// Properties of `Content` should be mutually exclusive with the properties of the 15 | /// `StyledView.Style` and `BehaviorsConfigurableView.Behaviors`. 16 | /// 17 | /// - SeeAlso: `BehaviorsConfigurableView` 18 | /// - SeeAlso: `StyledView` 19 | /// - SeeAlso: `EpoxyableView` 20 | public protocol ContentConfigurableView: ViewType { 21 | /// The `Equatable` properties that can be updated on instances of this view, e.g. text `String`s 22 | /// or image `URL`s. 23 | /// 24 | /// Defaults to `Never` for views that do not have `Content`. 25 | associatedtype Content: Equatable = Never 26 | 27 | /// Updates the content of this view to the properties of the given `content`, optionally 28 | /// animating the updates. 29 | func setContent(_ content: Self.Content, animated: Bool) 30 | } 31 | 32 | // MARK: Defaults 33 | 34 | extension ContentConfigurableView where Content == Never { 35 | public func setContent(_: Never, animated _: Bool) { } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Views/EpoxyableView.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 1/13/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | /// A `UIView` that can be declaratively configured via a concrete `EpoxyableModel` instance. 5 | public typealias EpoxyableView = StyledView & ContentConfigurableView & BehaviorsConfigurableView 6 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Views/StyledView.swift: -------------------------------------------------------------------------------- 1 | // Created by Laura Skelton on 4/14/16. 2 | // Copyright © 2016 Airbnb. All rights reserved. 3 | 4 | // MARK: - StyledView 5 | 6 | /// A view that can be initialized with a `Style` instance that contains the view's invariant 7 | /// configuration parameters, e.g. the `UIButton.ButtonType` of a `UIButton`. 8 | /// 9 | /// A `Style` is expected to be invariant over the lifecycle of the view; it should not possible to 10 | /// change the `Style` of a view after it is created. All variant properties of the view should 11 | /// either be included in the `ContentConfigurableView.Content` if they are `Equatable` (e.g. a 12 | /// title `String`) or the `BehaviorsConfigurableView.Behaviors` if they are not (e.g. a callback 13 | /// closure). 14 | /// 15 | /// A `Style` is `Hashable` to allow views of the same type with equal `Style`s to be reused by 16 | /// establishing whether their invariant `Style` instances are equal. 17 | /// 18 | /// Properties of `Style` should be mutually exclusive with the properties of the 19 | /// `ContentConfigurableView.Content` and `BehaviorsConfigurableView.Behaviors`. 20 | /// 21 | /// - SeeAlso: `ContentConfigurableView` 22 | /// - SeeAlso: `BehaviorsConfigurableView` 23 | /// - SeeAlso: `EpoxyableView` 24 | public protocol StyledView: ViewType { 25 | /// The style type of this view, passed into its initializer to configure the resulting instance. 26 | /// 27 | /// Defaults to `Never` for views that do not have a `Style`. 28 | associatedtype Style: Hashable = Never 29 | 30 | /// Creates an instance of this view configured with the given `Style` instance. 31 | init(style: Style) 32 | } 33 | 34 | // MARK: Defaults 35 | 36 | extension StyledView where Style == Never { 37 | public init(style: Never) { 38 | // An empty switch is required to silence the "'self.init' isn't called on all paths before 39 | // returning from initializer" error. 40 | switch style { } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/EpoxyCore/Views/ViewType.swift: -------------------------------------------------------------------------------- 1 | // Created by Cal Stephens on 6/26/23. 2 | // Copyright © 2023 Airbnb Inc. All rights reserved. 3 | 4 | import SwiftUI 5 | 6 | #if os(iOS) || os(tvOS) 7 | import UIKit 8 | 9 | /// The platform's main view type. 10 | /// Either `UIView` on iOS/tvOS or `NSView` on macOS. 11 | public typealias ViewType = UIView 12 | 13 | /// The platform's SwiftUI view representable type. 14 | /// Either `UIViewRepresentable` on iOS/tvOS or `NSViewRepresentable` on macOS. 15 | public typealias ViewRepresentableType = UIViewRepresentable 16 | 17 | /// The platform's layout constraint priority type. 18 | /// Either `UILayoutPriority` on iOS/tvOS or `NSLayoutConstraint.Priority` on macOS. 19 | public typealias LayoutPriorityType = UILayoutPriority 20 | 21 | extension ViewRepresentableType { 22 | /// The platform's view type for `ViewRepresentableType`. 23 | /// Either `UIViewType` on iOS/tvOS or `NSViewType` on macOS. 24 | public typealias RepresentableViewType = UIViewType 25 | } 26 | 27 | #elseif os(macOS) 28 | import AppKit 29 | 30 | /// The platform's main view type. 31 | /// Either `UIView` on iOS/tvOS, or `NSView` on macOS. 32 | public typealias ViewType = NSView 33 | 34 | /// The platform's SwiftUI view representable type. 35 | /// Either `UIViewRepresentable` on iOS/tvOS, or `NSViewRepresentable` on macOS. 36 | public typealias ViewRepresentableType = NSViewRepresentable 37 | 38 | /// The platform's layout constraint priority type. 39 | /// Either `UILayoutPriority` on iOS/tvOS, or `NSLayoutConstraint.Priority` on macOS. 40 | public typealias LayoutPriorityType = NSLayoutConstraint.Priority 41 | 42 | extension ViewRepresentableType { 43 | /// The platform's view type for `ViewRepresentableType`. 44 | /// Either `UIViewType` on iOS/tvOS or `NSViewType` on macOS. 45 | public typealias RepresentableViewType = NSViewType 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Constrainable/AnchoringContainer.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 6/11/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | // MARK: - AnchoringContainer 7 | 8 | /// Defines a container that has an anchoring constrainable 9 | /// Conforming to this protocol automatically implements the anchor 10 | /// requirements for Constrainable 11 | public protocol AnchoringContainer { 12 | var anchor: Constrainable { get } 13 | } 14 | 15 | extension Constrainable where Self: AnchoringContainer { 16 | public var leadingAnchor: NSLayoutXAxisAnchor { anchor.leadingAnchor } 17 | public var trailingAnchor: NSLayoutXAxisAnchor { anchor.trailingAnchor } 18 | public var leftAnchor: NSLayoutXAxisAnchor { anchor.leftAnchor } 19 | public var rightAnchor: NSLayoutXAxisAnchor { anchor.rightAnchor } 20 | public var topAnchor: NSLayoutYAxisAnchor { anchor.topAnchor } 21 | public var bottomAnchor: NSLayoutYAxisAnchor { anchor.bottomAnchor } 22 | public var widthAnchor: NSLayoutDimension { anchor.widthAnchor } 23 | public var heightAnchor: NSLayoutDimension { anchor.heightAnchor } 24 | public var centerXAnchor: NSLayoutXAxisAnchor { anchor.centerXAnchor } 25 | public var centerYAnchor: NSLayoutYAxisAnchor { anchor.centerYAnchor } 26 | public var firstBaselineAnchor: NSLayoutYAxisAnchor { anchor.firstBaselineAnchor } 27 | public var lastBaselineAnchor: NSLayoutYAxisAnchor { anchor.lastBaselineAnchor } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Constraints/GroupConstraints.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 5/13/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | /// A protocol to abstract the constraints needed for a given Group 7 | protocol GroupConstraints { 8 | /// Install all constraints 9 | func install() 10 | /// Uninstall all constraints 11 | func uninstall() 12 | /// Spacing between items in the group 13 | var itemSpacing: CGFloat { get set } 14 | /// A set of all NSLayoutConstraints this constraint container contains 15 | var allConstraints: [NSLayoutConstraint] { get } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Extensions/NSLayoutConstraint+Optional.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/22/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | extension NSLayoutConstraint { 7 | public static func activate(_ constraints: [NSLayoutConstraint?]) { 8 | NSLayoutConstraint.activate(constraints.compactMap { $0 }) 9 | } 10 | 11 | public static func deactivate(_ constraints: [NSLayoutConstraint?]) { 12 | NSLayoutConstraint.deactivate(constraints.compactMap { $0 }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Models/GroupItemModeling.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 3/18/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import Foundation 6 | 7 | // MARK: - GroupItemModeling 8 | 9 | /// A type-erased representation of an item in a group 10 | public protocol GroupItemModeling: Diffable { 11 | /// Returns this item model with its type erased to the `AnyGroupItem` type. 12 | func eraseToAnyGroupItem() -> AnyGroupItem 13 | } 14 | 15 | extension Array where Element == GroupItemModeling { 16 | public func eraseToAnyGroupItems() -> [AnyGroupItem] { 17 | map { $0.eraseToAnyGroupItem() } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Models/GroupModelBuilder.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/22/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | /// A result builder for items within a group 7 | public typealias GroupModelBuilder = EpoxyModelArrayBuilder 8 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Models/InternalGroupItemModeling.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 3/25/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import Foundation 6 | 7 | /// An internal type to represent items within a group 8 | protocol InternalGroupItemModeling: GroupItemModeling, EpoxyModeled { 9 | /// The unique identifier for this group item 10 | var dataID: AnyHashable { get } 11 | /// create a constrainable that this group item represents 12 | func makeConstrainable() -> Constrainable 13 | /// update the constrainable with the current content 14 | func update(_ constrainable: Constrainable, animated: Bool) 15 | /// set any behaviors on the constrainable (this is called more frequently than update) 16 | func setBehaviors(on constrainable: Constrainable) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Models/SpacerItem.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 3/19/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import UIKit 6 | 7 | // MARK: - SpacerItem 8 | 9 | /// A `GroupItemModeling` implementation of `LayoutSpacer` to be used within groups 10 | public struct SpacerItem { 11 | 12 | public init( 13 | dataID: AnyHashable, 14 | style: LayoutSpacer.Style = .init()) 15 | { 16 | self.dataID = dataID 17 | self.style = style 18 | } 19 | 20 | // MARK: Private 21 | 22 | public var dataID: AnyHashable 23 | public var style: LayoutSpacer.Style 24 | } 25 | 26 | // MARK: GroupItemModeling 27 | 28 | extension SpacerItem: GroupItemModeling { 29 | public var diffIdentifier: AnyHashable { 30 | DiffIdentifier(dataID: dataID, style: style) 31 | } 32 | 33 | public func eraseToAnyGroupItem() -> AnyGroupItem { 34 | GroupItem(dataID: dataID) { 35 | LayoutSpacer(style: style) 36 | } 37 | .eraseToAnyGroupItem() 38 | } 39 | 40 | public func isDiffableItemEqual(to otherDiffableItem: Diffable) -> Bool { 41 | guard let other = otherDiffableItem as? SpacerItem else { 42 | return false 43 | } 44 | return dataID == other.dataID && style == other.style 45 | } 46 | } 47 | 48 | // MARK: - DiffIdentifier 49 | 50 | private struct DiffIdentifier: Hashable { 51 | let dataID: AnyHashable 52 | let style: LayoutSpacer.Style 53 | } 54 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Providers/AccessibilityAlignmentProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 3/22/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | // MARK: - AccessibilityAlignmentProviding 7 | 8 | /// Describes something that can provide an `accessibilityAlignment` 9 | public protocol AccessibilityAlignmentProviding { 10 | /// The accessibilityAlignment for an item in an `HGroup`. This is applied 11 | /// when an `HGroup` is using its accessibility layout which by default happens 12 | /// when the `preferredContentSizeCategory.isAccessibilityCategory` is `true`. 13 | /// That accessibility layout essentially converts the `HGroup` into a `VGroup` and 14 | /// uses the provided `accessibilityAlignment` value for each item to determine the layout. 15 | /// The default value of this property is `nil`. 16 | var accessibilityAlignment: VGroup.ItemAlignment? { get } 17 | } 18 | 19 | // MARK: - EpoxyModeled + AccessibilityAlignmentProviding 20 | 21 | extension EpoxyModeled where Self: AccessibilityAlignmentProviding { 22 | 23 | // MARK: Public 24 | 25 | /// The accessibilityAlignment value for this model 26 | public var accessibilityAlignment: VGroup.ItemAlignment? { 27 | get { self[accessibilityAlignmentProperty] } 28 | set { self[accessibilityAlignmentProperty] = newValue } 29 | } 30 | 31 | /// Returns a copy of this model replacing the `accessibilityAlignment` value 32 | /// with the one provided. 33 | public func accessibilityAlignment(_ value: VGroup.ItemAlignment?) -> Self { 34 | copy(updating: accessibilityAlignmentProperty, to: value) 35 | } 36 | 37 | // MARK: Private 38 | 39 | private var accessibilityAlignmentProperty: EpoxyModelProperty { 40 | .init(keyPath: \AccessibilityAlignmentProviding.accessibilityAlignment, defaultValue: nil, updateStrategy: .replace) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Providers/GroupItemsProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 3/25/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | // MARK: - GroupItemsProviding 7 | 8 | /// Describes something that can provide an array of group items. 9 | public protocol GroupItemsProviding { 10 | /// The set of group items handled by a group. 11 | var groupItems: [GroupItemModeling] { get } 12 | } 13 | 14 | // MARK: - EpoxyModeled + GroupItemsProviding 15 | 16 | extension EpoxyModeled where Self: GroupItemsProviding { 17 | 18 | // MARK: Public 19 | 20 | /// The set of group items handled by a group. 21 | public var groupItems: [GroupItemModeling] { 22 | get { self[groupItemsProperty] } 23 | set { self[groupItemsProperty] = newValue } 24 | } 25 | 26 | /// Returns a copy of this model replacing the `groupItems` value 27 | /// with the one provided. 28 | public func groupItems(_ value: [GroupItemModeling]) -> Self { 29 | copy(updating: groupItemsProperty, to: value) 30 | } 31 | 32 | // MARK: Private 33 | 34 | private var groupItemsProperty: EpoxyModelProperty<[GroupItemModeling]> { 35 | .init(keyPath: \GroupItemsProviding.groupItems, defaultValue: [], updateStrategy: .replace) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Providers/HorizontalAlignmentProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 3/22/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | // MARK: - HorizontalAlignmentProviding 7 | 8 | /// Describes something that provides a `horizontalAlignment` 9 | public protocol HorizontalAlignmentProviding { 10 | /// Horizontal alignments are used in `VGroup` to specify the default 11 | /// horizontal alignment of the items contained in that group. You can 12 | /// also specify `horizontalAlignments` on a per item basis which will 13 | /// take precedence over the value for the group. 14 | var horizontalAlignment: VGroup.ItemAlignment? { get } 15 | } 16 | 17 | // MARK: - CallbackContextEpoxyModeled 18 | 19 | extension EpoxyModeled where Self: HorizontalAlignmentProviding { 20 | 21 | // MARK: Public 22 | 23 | /// The `horizontalAlignment` value for this model 24 | public var horizontalAlignment: VGroup.ItemAlignment? { 25 | get { self[horizontalAlignmentProperty] } 26 | set { self[horizontalAlignmentProperty] = newValue } 27 | } 28 | 29 | /// Returns a copy of this model replacing the `horizontalAlignment` value 30 | /// with the one provided. 31 | public func horizontalAlignment(_ value: VGroup.ItemAlignment?) -> Self { 32 | copy(updating: horizontalAlignmentProperty, to: value) 33 | } 34 | 35 | // MARK: Private 36 | 37 | private var horizontalAlignmentProperty: EpoxyModelProperty { 38 | .init(keyPath: \HorizontalAlignmentProviding.horizontalAlignment, defaultValue: nil, updateStrategy: .replace) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Providers/MakeConstrainableProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 3/25/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import UIKit 6 | 7 | // MARK: - MakeConstrainableProviding 8 | 9 | /// The capability of constructing a `Constrainable`. 10 | public protocol MakeConstrainableProviding { 11 | 12 | /// A closure that's called to construct a `Constrainable`. 13 | typealias MakeConstrainable = () -> Constrainable 14 | 15 | /// A closure that's called to construct a `Constrainable`. 16 | var makeConstrainable: MakeConstrainable { get } 17 | } 18 | 19 | // MARK: - ViewEpoxyModeled 20 | 21 | extension EpoxyModeled where Self: MakeConstrainableProviding { 22 | 23 | // MARK: Public 24 | 25 | /// A closure that's called to construct a `Constrainable` represented by this model. 26 | public var makeConstrainable: MakeConstrainable { 27 | get { self[makeConstrainableProperty] } 28 | set { self[makeConstrainableProperty] = newValue } 29 | } 30 | 31 | /// Replaces the default closure to construct the constrainable with the given closure. 32 | public func makeConstrainable(_ value: @escaping MakeConstrainable) -> Self { 33 | copy(updating: makeConstrainableProperty, to: value) 34 | } 35 | 36 | // MARK: Private 37 | 38 | private var makeConstrainableProperty: EpoxyModelProperty { 39 | .init( 40 | keyPath: \MakeConstrainableProviding.makeConstrainable, 41 | defaultValue: UIView.init, 42 | updateStrategy: .replace) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Providers/PaddingProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 3/22/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | import UIKit 6 | 7 | // MARK: - PaddingProviding 8 | 9 | /// Describes something that provides `padding` 10 | public protocol PaddingProviding { 11 | /// The padding value 12 | var padding: NSDirectionalEdgeInsets { get } 13 | } 14 | 15 | // MARK: - CallbackContextEpoxyModeled 16 | 17 | extension EpoxyModeled where Self: PaddingProviding { 18 | 19 | // MARK: Public 20 | 21 | /// The padding value represented by this model 22 | public var padding: NSDirectionalEdgeInsets { 23 | get { self[paddingProperty] } 24 | set { self[paddingProperty] = newValue } 25 | } 26 | 27 | /// Returns a copy of this model replacing the `padding` value 28 | /// with the one provided. 29 | public func padding(_ value: NSDirectionalEdgeInsets) -> Self { 30 | copy(updating: paddingProperty, to: value) 31 | } 32 | 33 | /// Returns a copy of this model replacing the `padding` value 34 | /// with one that has all edges set to the provided value. 35 | public func padding(_ value: CGFloat) -> Self { 36 | copy(updating: paddingProperty, to: .init(top: value, leading: value, bottom: value, trailing: value)) 37 | } 38 | 39 | // MARK: Private 40 | 41 | private var paddingProperty: EpoxyModelProperty { 42 | .init(keyPath: \PaddingProviding.padding, defaultValue: .zero, updateStrategy: .replace) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Providers/ReflowsForAccessibilityTypeSizeProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 4/7/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | // MARK: - ReflowsForAccessibilityTypeSizeProviding 7 | 8 | /// Describes something that provides a `reflowsForAccessibilityTypeSizes` flag 9 | public protocol ReflowsForAccessibilityTypeSizeProviding { 10 | /// Only used in HGroup. Whether or not the HGroup should reflow for accessibility 11 | /// type sizes. When this value is `true` the `HGroup` will reflow when 12 | /// `preferredContentSizeCategory.isAccessibilityCategory` is `true`. The `HGroup` reflows 13 | /// to model a `VGroup` and uses the `accessibilityAlignment` value to determine item alignments. 14 | var reflowsForAccessibilityTypeSizes: Bool { get } 15 | } 16 | 17 | // MARK: - CallbackContextEpoxyModeled 18 | 19 | extension EpoxyModeled where Self: ReflowsForAccessibilityTypeSizeProviding { 20 | 21 | // MARK: Public 22 | 23 | /// The `reflowsForAccessibilityTypeSizes` value for this model 24 | public var reflowsForAccessibilityTypeSizes: Bool { 25 | get { self[reflowsForAccessibilityTypeSizeProperty] } 26 | set { self[reflowsForAccessibilityTypeSizeProperty] = newValue } 27 | } 28 | 29 | /// Returns a copy of this model replacing the `reflowsForAccessibilityTypeSizes` value 30 | /// with the one provided. 31 | public func reflowsForAccessibilityTypeSizes(_ value: Bool) -> Self { 32 | copy(updating: reflowsForAccessibilityTypeSizeProperty, to: value) 33 | } 34 | 35 | // MARK: Private 36 | 37 | private var reflowsForAccessibilityTypeSizeProperty: EpoxyModelProperty { 38 | .init( 39 | keyPath: \ReflowsForAccessibilityTypeSizeProviding.reflowsForAccessibilityTypeSizes, 40 | defaultValue: true, 41 | updateStrategy: .replace) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/EpoxyLayoutGroups/Providers/VerticalAlignmentProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 3/22/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | // MARK: - VerticalAlignmentProviding 7 | 8 | /// Describes something that provides vertical alignment 9 | public protocol VerticalAlignmentProviding { 10 | /// Vertical alignments are used in `HGroup` to specify the default 11 | /// vertical alignment of the items contained in that group. You can 12 | /// also specify `verticalAlignment` on a per item basis which will 13 | /// take precedence over the value for the group. 14 | var verticalAlignment: HGroup.ItemAlignment? { get } 15 | } 16 | 17 | // MARK: - CallbackContextEpoxyModeled 18 | 19 | extension EpoxyModeled where Self: VerticalAlignmentProviding { 20 | 21 | // MARK: Public 22 | 23 | /// The `verticalAlignment` value for this model 24 | public var verticalAlignment: HGroup.ItemAlignment? { 25 | get { self[verticalAlignmentProperty] } 26 | set { self[verticalAlignmentProperty] = newValue } 27 | } 28 | 29 | /// Returns a copy of this model replacing the `verticalAlignment` value 30 | /// with the one provided. 31 | public func verticalAlignment(_ value: HGroup.ItemAlignment?) -> Self { 32 | copy(updating: verticalAlignmentProperty, to: value) 33 | } 34 | 35 | // MARK: Private 36 | 37 | private var verticalAlignmentProperty: EpoxyModelProperty { 38 | .init(keyPath: \VerticalAlignmentProviding.verticalAlignment, defaultValue: nil, updateStrategy: .replace) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/EpoxyNavigationController/NavigationModelBuilder.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/15/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import EpoxyCore 5 | 6 | /// A result builder that enables a DSL for building arrays of navigation models. 7 | /// 8 | /// For example: 9 | /// ``` 10 | /// @NavigationModelBuilder var stack: [NavigationModel] { 11 | /// NavigationModel.root(…) 12 | /// 13 | /// if showStep1 { 14 | /// NavigationModel(…) 15 | /// } 16 | /// 17 | /// if showStep2 { 18 | /// NavigationModel(…) 19 | /// } 20 | /// } 21 | /// ``` 22 | /// 23 | /// Will return an array of containing three navigation models when both `showStep1` and `showStep2` 24 | /// are `true` 25 | public typealias NavigationModelBuilder = EpoxyModelArrayBuilder 26 | -------------------------------------------------------------------------------- /Sources/EpoxyNavigationController/StackProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/24/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - StackProviding 5 | 6 | /// The ability to provide a stack of navigation models that drives a stack of view controllers 7 | /// within a declarative navigation controller. 8 | /// 9 | /// Generally conformed to by the content of a declarative navigation controller. 10 | public protocol StackProviding { 11 | /// The stack of navigation models that represent to the view controllers that are present in the 12 | /// navigation controller's stack. 13 | var stack: [NavigationModel?] { get } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/EpoxyPresentations/PresentationModelBuilder.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/15/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | /// A result builder that enables a DSL for building an optional presentation model. 5 | /// 6 | /// For example: 7 | /// ``` 8 | /// @PresentationModelBuilder var presentation: PresentationModel? { 9 | /// if showA { 10 | /// PresentationModel(…) 11 | /// } 12 | /// if showB { 13 | /// PresentationModel(…) 14 | /// } 15 | /// } 16 | /// ``` 17 | /// 18 | /// Will return a `nil` presentation model if `showA` and `showB` are false, else will return the 19 | /// first non-`nil` presentation model. 20 | @resultBuilder 21 | public enum PresentationModelBuilder { 22 | public typealias Expression = PresentationModel 23 | public typealias Component = PresentationModel? 24 | 25 | public static func buildExpression(_ expression: Expression) -> Component { 26 | expression 27 | } 28 | 29 | public static func buildExpression(_ expression: Component) -> Component { 30 | expression 31 | } 32 | 33 | public static func buildBlock(_ children: Component...) -> Component { 34 | for child in children { 35 | if let child = child { 36 | return child 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | public static func buildBlock(_ component: Component) -> Component { 43 | component 44 | } 45 | 46 | public static func buildOptional(_ children: Component?) -> Component { 47 | if let child = children { 48 | return child 49 | } 50 | return nil 51 | } 52 | 53 | public static func buildEither(first child: Component) -> Component { 54 | child 55 | } 56 | 57 | public static func buildEither(second child: Component) -> Component { 58 | child 59 | } 60 | 61 | public static func buildArray(_ components: [Component]) -> Component { 62 | for child in components { 63 | if let child = child { 64 | return child 65 | } 66 | } 67 | return nil 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/EpoxyPresentations/PresentationModelProviding.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 3/23/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | // MARK: - PresentationProviding 5 | 6 | /// The ability to provide a presentation model that drives the modal presentations and dismissals 7 | /// atop a presenting view controller. 8 | /// 9 | /// Generally conformed to by the content of the presenting view controller. 10 | public protocol PresentationProviding { 11 | /// The presentation model for the view controller that should be presented, else `nil` if nothing 12 | /// should be presented or if the previous presentation should be dismissed. 13 | var presentation: PresentationModel? { get } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/EpoxyPresentations/UIViewController+PresentationModel.swift: -------------------------------------------------------------------------------- 1 | // Created by eric_horacek on 10/12/19. 2 | // Copyright © 2019 Airbnb Inc. All rights reserved. 3 | 4 | import ObjectiveC 5 | import UIKit 6 | 7 | // MARK: - UIViewController 8 | 9 | extension UIViewController { 10 | /// Presents the given model as a view controller, optionally animating the presentation. 11 | /// 12 | /// A `nil` `model` is treated as a dismissal. 13 | /// 14 | /// If a transition is in progress when this method is called, the provided model is queued for 15 | /// subsequent presentation. 16 | /// 17 | /// Conceptually similar `setSections(_:animated:)` for Epoxy models. 18 | @nonobjc 19 | public func setPresentation(_ model: PresentationModel?, animated: Bool) { 20 | queue.enqueue(model, animated: animated, from: self) 21 | } 22 | } 23 | 24 | extension UIViewController { 25 | /// The queue of in progress presentations for this view controller. 26 | @nonobjc 27 | fileprivate var queue: PresentationQueue { 28 | if let queue = objc_getAssociatedObject(self, &Keys.queue) as? PresentationQueue { 29 | return queue 30 | } 31 | let queue = PresentationQueue() 32 | objc_setAssociatedObject(self, &Keys.queue, queue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 33 | return queue 34 | } 35 | } 36 | 37 | // MARK: - Keys 38 | 39 | /// Associated object keys. 40 | private enum Keys { 41 | static var queue = 0 42 | } 43 | -------------------------------------------------------------------------------- /Tests/EpoxyTests/BarsTests/BottomBarInstallerSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Cal Stephens on 11/30/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import Nimble 5 | import Quick 6 | import UIKit 7 | 8 | @testable import EpoxyBars 9 | 10 | final class BottomBarInstallerSpec: QuickSpec, BaseBarInstallerSpec { 11 | 12 | func installBarContainer( 13 | in viewController: UIViewController, 14 | configuration: BarInstallerConfiguration) 15 | -> (container: InternalBarContainer, setBars: ([BarModeling], Bool) -> Void) 16 | { 17 | let barInstaller = BottomBarInstaller(viewController: viewController, configuration: configuration) 18 | viewController.view.layoutIfNeeded() 19 | barInstaller.install() 20 | 21 | return ( 22 | container: barInstaller.container!, 23 | setBars: { barInstaller.setBars($0, animated: $1) }) 24 | } 25 | 26 | override func spec() { 27 | baseSpec() 28 | 29 | describe("BottomBarContainer") { 30 | it("has a reference to the BottomBarInstaller when installed") { 31 | let viewController = UIViewController() 32 | viewController.loadView() 33 | let barInstaller = BottomBarInstaller(viewController: viewController) 34 | 35 | barInstaller.install() 36 | expect(barInstaller.container?.barInstaller).toEventually(equal(barInstaller)) 37 | 38 | barInstaller.uninstall() 39 | expect(barInstaller.container?.barInstaller).toEventually(beNil()) 40 | } 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Tests/EpoxyTests/BarsTests/SafeAreaWindow.swift: -------------------------------------------------------------------------------- 1 | // Created by Cal Stephens on 8/23/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import UIKit 5 | 6 | // A `UIWindow` subclass that can provide safe area insets 7 | // to simulate default device safe area insets (e.g. from the status bar) 8 | final class SafeAreaWindow: UIWindow { 9 | 10 | // MARK: Lifecycle 11 | 12 | init( 13 | frame: CGRect, 14 | safeAreaInsets: UIEdgeInsets) 15 | { 16 | customSafeAreaInsets = safeAreaInsets 17 | super.init(frame: frame) 18 | } 19 | 20 | required init?(coder _: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | // MARK: Internal 25 | 26 | override var safeAreaInsets: UIEdgeInsets { 27 | customSafeAreaInsets 28 | } 29 | 30 | // MARK: Private 31 | 32 | private let customSafeAreaInsets: UIEdgeInsets 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Tests/EpoxyTests/BarsTests/StaticHeightBar.swift: -------------------------------------------------------------------------------- 1 | // Created by Cal Stephens on 11/30/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | // MARK: - StaticHeightBar 8 | 9 | final class StaticHeightBar: UIView, EpoxyableView { 10 | 11 | // MARK: Lifecycle 12 | 13 | init(style: Style) { 14 | super.init(frame: .zero) 15 | translatesAutoresizingMaskIntoConstraints = false 16 | 17 | NSLayoutConstraint.activate([ 18 | heightAnchor.constraint(equalToConstant: style.height), 19 | ]) 20 | } 21 | 22 | required init?(coder _: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | // MARK: Internal 27 | 28 | struct Style: Hashable { 29 | var height: CGFloat 30 | } 31 | 32 | } 33 | 34 | // MARK: - EmptyContent 35 | 36 | struct EmptyContent: Equatable { } 37 | -------------------------------------------------------------------------------- /Tests/EpoxyTests/BarsTests/TopBarInstallerSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Cal Stephens on 11/30/20. 2 | // Copyright © 2020 Airbnb Inc. All rights reserved. 3 | 4 | import Nimble 5 | import Quick 6 | import UIKit 7 | 8 | @testable import EpoxyBars 9 | 10 | final class TopBarInstallerSpec: QuickSpec, BaseBarInstallerSpec { 11 | 12 | func installBarContainer( 13 | in viewController: UIViewController, 14 | configuration: BarInstallerConfiguration) 15 | -> (container: InternalBarContainer, setBars: ([BarModeling], Bool) -> Void) 16 | { 17 | let barInstaller = TopBarInstaller(viewController: viewController, configuration: configuration) 18 | viewController.view.layoutIfNeeded() 19 | barInstaller.install() 20 | 21 | return ( 22 | container: barInstaller.container!, 23 | setBars: { barInstaller.setBars($0, animated: $1) }) 24 | } 25 | 26 | override func spec() { 27 | baseSpec() 28 | 29 | describe("TopBarContainer") { 30 | it("has a reference to the TopBarInstaller when installed") { 31 | let viewController = UIViewController() 32 | viewController.loadView() 33 | let barInstaller = TopBarInstaller(viewController: viewController) 34 | 35 | barInstaller.install() 36 | expect(barInstaller.container?.barInstaller).toEventually(equal(barInstaller)) 37 | 38 | barInstaller.uninstall() 39 | expect(barInstaller.container?.barInstaller).toEventually(beNil()) 40 | } 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Tests/EpoxyTests/CollectionViewTests/ProxyDelegate.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 1/20/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Epoxy 5 | import UIKit 6 | 7 | class ProxyDelegate: EpoxyCollectionViewDelegateFlowLayout { 8 | let size = CGSize(width: 12, height: 12) 9 | let sectionInset = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) 10 | let minimumLineSpacing: CGFloat = 12 11 | let minimumInteritemSpacing: CGFloat = 12 12 | let headerSize = CGSize(width: 12, height: 12) 13 | let footerSize = CGSize(width: 12, height: 12) 14 | 15 | func collectionView( 16 | _: UICollectionView, 17 | layout _: UICollectionViewLayout, 18 | sizeForItemWith _: AnyHashable, 19 | inSectionWith _: AnyHashable) 20 | -> CGSize 21 | { 22 | size 23 | } 24 | 25 | func collectionView( 26 | _: UICollectionView, 27 | layout _: UICollectionViewLayout, 28 | insetForSectionWith _: AnyHashable) 29 | -> UIEdgeInsets 30 | { 31 | sectionInset 32 | } 33 | 34 | func collectionView( 35 | _: UICollectionView, 36 | layout _: UICollectionViewLayout, 37 | minimumLineSpacingForSectionWith _: AnyHashable) 38 | -> CGFloat 39 | { 40 | minimumLineSpacing 41 | } 42 | 43 | func collectionView( 44 | _: UICollectionView, 45 | layout _: UICollectionViewLayout, 46 | minimumInteritemSpacingForSectionWith _: AnyHashable) 47 | -> CGFloat 48 | { 49 | minimumInteritemSpacing 50 | } 51 | 52 | func collectionView( 53 | _: UICollectionView, 54 | layout _: UICollectionViewLayout, 55 | referenceSizeForHeaderInSectionWith _: AnyHashable) 56 | -> CGSize 57 | { 58 | headerSize 59 | } 60 | 61 | func collectionView( 62 | _: UICollectionView, 63 | layout _: UICollectionViewLayout, 64 | referenceSizeForFooterInSectionWith _: AnyHashable) 65 | -> CGSize 66 | { 67 | footerSize 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/EpoxyTests/LayoutGroupsTests/ConstrainableContainerSpec.swift: -------------------------------------------------------------------------------- 1 | // Created by Tyler Hedrick on 5/26/21. 2 | // Copyright © 2021 Airbnb Inc. All rights reserved. 3 | 4 | import Nimble 5 | import Quick 6 | import UIKit 7 | 8 | @testable import EpoxyLayoutGroups 9 | 10 | // swiftlint:disable implicitly_unwrapped_optional 11 | 12 | final class ConstraniableContainerSpec: QuickSpec { 13 | 14 | override func spec() { 15 | var constrainable: Constrainable! 16 | 17 | beforeEach { 18 | constrainable = TestView() 19 | .accessibilityAlignment(.trailing) 20 | .horizontalAlignment(.center) 21 | .verticalAlignment(.top) 22 | .padding(5) 23 | } 24 | 25 | describe("when initializing a ConstrainableContainer with another Constrainable") { 26 | it("inherits the values of the provided Constrainable") { 27 | let wrapper = ConstrainableContainer(constrainable) 28 | expect(wrapper.accessibilityAlignment).to(equal(.trailing)) 29 | expect(wrapper.horizontalAlignment).to(equal(.center)) 30 | expect(wrapper.verticalAlignment).to(equal(.top)) 31 | expect(wrapper.padding).to(equal(NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))) 32 | } 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /docs/images/ActionRow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/ActionRow.png -------------------------------------------------------------------------------- /docs/images/CheckboxRow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/CheckboxRow.png -------------------------------------------------------------------------------- /docs/images/IconRow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/IconRow.png -------------------------------------------------------------------------------- /docs/images/MessageRow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/MessageRow.png -------------------------------------------------------------------------------- /docs/images/MessageRowHierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/MessageRowHierarchy.png -------------------------------------------------------------------------------- /docs/images/MessageRowReflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/MessageRowReflow.png -------------------------------------------------------------------------------- /docs/images/bottom_button_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/bottom_button_example.png -------------------------------------------------------------------------------- /docs/images/checkbox_row_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/checkbox_row_bottom.png -------------------------------------------------------------------------------- /docs/images/checkbox_row_center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/checkbox_row_center.png -------------------------------------------------------------------------------- /docs/images/checkbox_row_centered_to_subtitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/checkbox_row_centered_to_subtitle.png -------------------------------------------------------------------------------- /docs/images/checkbox_row_custom_subtitle_first_baseline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/checkbox_row_custom_subtitle_first_baseline.png -------------------------------------------------------------------------------- /docs/images/counter_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/counter_example.gif -------------------------------------------------------------------------------- /docs/images/form_navigation_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/form_navigation_example.gif -------------------------------------------------------------------------------- /docs/images/home_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/home_details.png -------------------------------------------------------------------------------- /docs/images/home_photos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/home_photos.png -------------------------------------------------------------------------------- /docs/images/layout_groups_examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/layout_groups_examples.png -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/messaging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/messaging.png -------------------------------------------------------------------------------- /docs/images/modal_example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/modal_example.gif -------------------------------------------------------------------------------- /docs/images/registration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/registration.png -------------------------------------------------------------------------------- /docs/images/tap_me_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbnb/epoxy-ios/1f9b206bffd75081db404de4ac4de9edf49e7518/docs/images/tap_me_example.png -------------------------------------------------------------------------------- /docs/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Change summary 2 | 3 | ## How was it tested? 4 | *How did you verify that this change accomplished what you expected? Add more detail as needed.* 5 | - [ ] Wrote automated tests 6 | - [ ] Built and ran on the iOS simulator 7 | - [ ] Built and ran on a device 8 | 9 | ## Pull request checklist 10 | *All items in this checklist must be completed before a pull request will be reviewed.* 11 | 12 | - [ ] Risky changes have been put behind a feature flag, e.g. `CollectionViewConfiguration` 13 | - [ ] Added a [`CHANGELOG.md` entry](https://keepachangelog.com/en/1.0.0/) in the "Unreleased" section for any library changes 14 | --------------------------------------------------------------------------------