├── .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 |
--------------------------------------------------------------------------------