Skip to content

Commit

Permalink
[ios] Add example of animated group items inside CollectionView (#63)
Browse files Browse the repository at this point in the history
* [ios] Add example of animated group items inside CollectionView

* add xcpretty to builds

* fix function builder errors

* address comments
  • Loading branch information
thedrick authored Dec 8, 2021
1 parent 87aecbe commit a6cf747
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 7 deletions.
12 changes: 10 additions & 2 deletions Example/EpoxyExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
25D39B5C262789E000B3DBF9 /* AlignableTextRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25D39B5B262789E000B3DBF9 /* AlignableTextRow.swift */; };
25D39B5F26278B1700B3DBF9 /* LayoutGroupsExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25D39B5E26278B1700B3DBF9 /* LayoutGroupsExample.swift */; };
25D39B6426278DD900B3DBF9 /* LayoutGroupsExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25D39B6326278DD900B3DBF9 /* LayoutGroupsExampleViewController.swift */; };
25F71A9C273D9855004D30CE /* DynamicLayoutGroupsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F71A9B273D9855004D30CE /* DynamicLayoutGroupsViewController.swift */; };
25F71A9E273D990E004D30CE /* DynamicRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F71A9D273D990E004D30CE /* DynamicRow.swift */; };
25FEB79225AE431100F8EFBD /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25FEB79125AE431100F8EFBD /* MainViewController.swift */; };
2E8B007623F47E7E00D82A31 /* CustomSizingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E8B007523F47E7E00D82A31 /* CustomSizingView.swift */; };
A5AD02A72637CBF9007261BC /* TextFieldRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD02A62637CBF9007261BC /* TextFieldRow.swift */; };
Expand Down Expand Up @@ -99,6 +101,8 @@
25D39B5B262789E000B3DBF9 /* AlignableTextRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlignableTextRow.swift; sourceTree = "<group>"; };
25D39B5E26278B1700B3DBF9 /* LayoutGroupsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutGroupsExample.swift; sourceTree = "<group>"; };
25D39B6326278DD900B3DBF9 /* LayoutGroupsExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutGroupsExampleViewController.swift; sourceTree = "<group>"; };
25F71A9B273D9855004D30CE /* DynamicLayoutGroupsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicLayoutGroupsViewController.swift; sourceTree = "<group>"; };
25F71A9D273D990E004D30CE /* DynamicRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRow.swift; sourceTree = "<group>"; };
25FEB79125AE431100F8EFBD /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
2E8B007523F47E7E00D82A31 /* CustomSizingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSizingView.swift; sourceTree = "<group>"; };
A5AD02A62637CBF9007261BC /* TextFieldRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldRow.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -191,6 +195,7 @@
25D39B2F26277F0D00B3DBF9 /* EntirelyInlineViewController.swift */,
25D39B3026277F0D00B3DBF9 /* TextRowExampleViewController.swift */,
A67255712718C8E40085346B /* UIViewController+LayoutGroupsExample.swift */,
25F71A9B273D9855004D30CE /* DynamicLayoutGroupsViewController.swift */,
);
path = LayoutGroups;
sourceTree = "<group>";
Expand All @@ -206,6 +211,7 @@
25D39B432627809D00B3DBF9 /* Elements */,
25D39B492627809D00B3DBF9 /* MessageRowStackView.swift */,
25D39B4A2627809D00B3DBF9 /* ActionButtonRow.swift */,
25F71A9D273D990E004D30CE /* DynamicRow.swift */,
);
path = LayoutGroups;
sourceTree = "<group>";
Expand Down Expand Up @@ -414,11 +420,13 @@
A6F0929325B9ED0C007D902B /* CounterViewController.swift in Sources */,
25D39B502627809D00B3DBF9 /* BaseRow.swift in Sources */,
25D39B4B2627809D00B3DBF9 /* MessageRow.swift in Sources */,
25F71A9C273D9855004D30CE /* DynamicLayoutGroupsViewController.swift in Sources */,
A61AFF5F2602B99E005356A8 /* TapMeViewController.swift in Sources */,
A6F0928B25B9E983007D902B /* TextRow.swift in Sources */,
25D39B522627809D00B3DBF9 /* ImageView.swift in Sources */,
A67255722718C8E40085346B /* UIViewController+LayoutGroupsExample.swift in Sources */,
25D39B4C2627809D00B3DBF9 /* ColorsRow.swift in Sources */,
25F71A9E273D990E004D30CE /* DynamicRow.swift in Sources */,
25D39B532627809D00B3DBF9 /* Button.swift in Sources */,
25D39B4F2627809D00B3DBF9 /* Label.swift in Sources */,
25B6766525AE8BEA00C00B20 /* UIImageView+RemoteImage.swift in Sources */,
Expand Down Expand Up @@ -527,7 +535,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down Expand Up @@ -583,7 +591,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
Expand Down
5 changes: 5 additions & 0 deletions Example/EpoxyExample/Data/LayoutGroupsExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum LayoutGroupsExample: CaseIterable {
case todoList
case entirelyInline
case complex
case dynamic

// MARK: Internal

Expand All @@ -32,6 +33,8 @@ enum LayoutGroupsExample: CaseIterable {
return "Inline components"
case .complex:
return "Shuffle"
case .dynamic:
return "Dynamic"
}
}

Expand All @@ -53,6 +56,8 @@ enum LayoutGroupsExample: CaseIterable {
return "An example showcasing creating components inline in an EpoxyCollectionView ItemModel"
case .complex:
return "An example showing how groups handle updates to the contained items"
case .dynamic:
return "An example showcasing layout groups with dynamic subviews in a CollectionView"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Created by Tyler Hedrick on 11/11/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.

import Epoxy
import UIKit

final class DynamicLayoutGroupsViewController: CollectionViewController {

// MARK: Lifecycle

init() {
super.init(layout: UICollectionViewCompositionalLayout.list)
setItems(items, animated: false)
}

// MARK: Private

private enum DataID: CaseIterable {
case row1
case row2
case row3
}

private var openOptions: [AnyHashable: Bool] = [:]

private var items: [ItemModeling] {
DataID.allCases.map { id in
DynamicRow.itemModel(
dataID: id,
content: .init(
title: "Want to know more?",
subtitle: "Tap below to reveal a set of options you can choose from",
revealOptionsButton: openOptions(id) ? nil : "Reveal options",
options: options(for: id),
footer: "Thank you"),
behaviors: .init(didTapRevealOptions: { [weak self] in
self?.openOptions[id] = true
self?.updateData()
}, didTapOption: { [weak self] option in
print("Selected option \(option)")
self?.openOptions[id] = false
self?.updateData()
}))
}
}

private func updateData() {
setItems(items, animated: true)
}

private func openOptions(_ dataID: AnyHashable) -> Bool {
openOptions[dataID] ?? false
}

private func options(for dataID: AnyHashable) -> [String]? {
if openOptions(dataID) {
return [
"Option 1",
"Option 2",
"Option 3",
"Option 4",
"Option 5",
]
}
return nil
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ extension UIViewController {
viewController = EntirelyInlineViewController()
case .complex:
viewController = ComplexDeclarativeViewController()
case .dynamic:
viewController = DynamicLayoutGroupsViewController()
}
viewController.title = example.title
return viewController
Expand Down
101 changes: 101 additions & 0 deletions Example/EpoxyExample/Views/LayoutGroups/DynamicRow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Created by Tyler Hedrick on 11/11/21.
// Copyright © 2021 Airbnb Inc. All rights reserved.

import Epoxy
import UIKit

final class DynamicRow: BaseRow, EpoxyableView {

// MARK: Lifecycle

override init() {
super.init()
layout.install(in: self)
layout.constrainToMarginsWithHighPriorityBottom()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: Internal

// MARK: ContentConfigurableView

struct Content: Equatable {
let title: String
let subtitle: String
let revealOptionsButton: String?
let options: [String]?
let footer: String
}

// MARK: BehaviorsConfigurableView

struct Behaviors {
let didTapRevealOptions: (() -> Void)?
let didTapOption: ((String) -> Void)?
}

func setContent(_ content: Content, animated: Bool) {
layout.setItems {
Label.groupItem(
dataID: DataID.title,
content: content.title,
style: Label.Style.style(with: .title2))
// force text to hug tightly to avoid height changes during animation
.contentHuggingPriority(.required, for: .vertical)

Label.groupItem(
dataID: DataID.subtitle,
content: content.subtitle,
style: Label.Style.style(with: .body))
// force text to hug tightly to avoid height changes during animation
.contentHuggingPriority(.required, for: .vertical)

if let revealOptionsText = content.revealOptionsButton {
Button.groupItem(
dataID: DataID.revealOptions,
content: .init(title: revealOptionsText),
behaviors: .init { [weak self] _ in
self?.didTapRevealOptions?()
},
style: .init())
} else if let options = content.options {
options.map { option in
Button.groupItem(
dataID: option,
content: .init(title: option),
behaviors: .init { [weak self] _ in
self?.didTapOption?(option)
},
style: .init())
}
}

Label.groupItem(
dataID: DataID.footer,
content: content.footer,
style: .style(with: .footnote))
}
}

func setBehaviors(_ behaviors: Behaviors?) {
didTapRevealOptions = behaviors?.didTapRevealOptions
didTapOption = behaviors?.didTapOption
}

// MARK: Private

private enum DataID {
case title
case subtitle
case revealOptions
case footer
}

private let layout = VGroup()
private var didTapRevealOptions: (() -> Void)? = nil
private var didTapOption: ((String) -> Void)? = nil

}
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
source 'https://rubygems.org' do
gem 'cocoapods', '~> 1.10.0'
gem "rake", "~> 13.0.0"
gem 'rake', "~> 13.0.0"
end
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,4 @@ DEPENDENCIES
rake (~> 13.0.0)!

BUNDLED WITH
2.1.4
2.2.6
6 changes: 3 additions & 3 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace :build do

desc 'Builds the EpoxyExample app'
task :example do
sh 'xcodebuild build -scheme EpoxyExample -destination "platform=iOS Simulator,name=iPhone 8"'
sh 'xcodebuild build -scheme EpoxyExample -destination "platform=iOS Simulator,name=iPhone 12"'
end
end

Expand All @@ -16,12 +16,12 @@ namespace :test do

desc 'Runs unit tests'
task :unit do
sh 'xcodebuild test -scheme EpoxyTests -destination "platform=iOS Simulator,name=iPhone 8"'
sh 'xcodebuild test -scheme EpoxyTests -destination "platform=iOS Simulator,name=iPhone 12"'
end

desc 'Runs performance tests'
task :performance do
sh 'xcodebuild test -scheme PerformanceTests -destination "platform=iOS Simulator,name=iPhone 8"'
sh 'xcodebuild test -scheme PerformanceTests -destination "platform=iOS Simulator,name=iPhone 12"'
end
end

Expand Down

0 comments on commit a6cf747

Please sign in to comment.