From a6cf7473b52a7437a8e77beebd31d869c6d9a443 Mon Sep 17 00:00:00 2001 From: Tyler Hedrick Date: Wed, 8 Dec 2021 14:06:18 -0800 Subject: [PATCH] [ios] Add example of animated group items inside CollectionView (#63) * [ios] Add example of animated group items inside CollectionView * add xcpretty to builds * fix function builder errors * address comments --- .../EpoxyExample.xcodeproj/project.pbxproj | 12 ++- .../Data/LayoutGroupsExample.swift | 5 + .../DynamicLayoutGroupsViewController.swift | 68 ++++++++++++ ...UIViewController+LayoutGroupsExample.swift | 2 + .../Views/LayoutGroups/DynamicRow.swift | 101 ++++++++++++++++++ Gemfile | 2 +- Gemfile.lock | 2 +- Rakefile | 6 +- 8 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 Example/EpoxyExample/ViewControllers/LayoutGroups/DynamicLayoutGroupsViewController.swift create mode 100644 Example/EpoxyExample/Views/LayoutGroups/DynamicRow.swift diff --git a/Example/EpoxyExample.xcodeproj/project.pbxproj b/Example/EpoxyExample.xcodeproj/project.pbxproj index 283f2a04..296e1fa1 100644 --- a/Example/EpoxyExample.xcodeproj/project.pbxproj +++ b/Example/EpoxyExample.xcodeproj/project.pbxproj @@ -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 */; }; @@ -99,6 +101,8 @@ 25D39B5B262789E000B3DBF9 /* AlignableTextRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlignableTextRow.swift; sourceTree = ""; }; 25D39B5E26278B1700B3DBF9 /* LayoutGroupsExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutGroupsExample.swift; sourceTree = ""; }; 25D39B6326278DD900B3DBF9 /* LayoutGroupsExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutGroupsExampleViewController.swift; sourceTree = ""; }; + 25F71A9B273D9855004D30CE /* DynamicLayoutGroupsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicLayoutGroupsViewController.swift; sourceTree = ""; }; + 25F71A9D273D990E004D30CE /* DynamicRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRow.swift; sourceTree = ""; }; 25FEB79125AE431100F8EFBD /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 2E8B007523F47E7E00D82A31 /* CustomSizingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSizingView.swift; sourceTree = ""; }; A5AD02A62637CBF9007261BC /* TextFieldRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldRow.swift; sourceTree = ""; }; @@ -191,6 +195,7 @@ 25D39B2F26277F0D00B3DBF9 /* EntirelyInlineViewController.swift */, 25D39B3026277F0D00B3DBF9 /* TextRowExampleViewController.swift */, A67255712718C8E40085346B /* UIViewController+LayoutGroupsExample.swift */, + 25F71A9B273D9855004D30CE /* DynamicLayoutGroupsViewController.swift */, ); path = LayoutGroups; sourceTree = ""; @@ -206,6 +211,7 @@ 25D39B432627809D00B3DBF9 /* Elements */, 25D39B492627809D00B3DBF9 /* MessageRowStackView.swift */, 25D39B4A2627809D00B3DBF9 /* ActionButtonRow.swift */, + 25F71A9D273D990E004D30CE /* DynamicRow.swift */, ); path = LayoutGroups; sourceTree = ""; @@ -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 */, @@ -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; @@ -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; diff --git a/Example/EpoxyExample/Data/LayoutGroupsExample.swift b/Example/EpoxyExample/Data/LayoutGroupsExample.swift index 3f242a45..7e7c08f6 100644 --- a/Example/EpoxyExample/Data/LayoutGroupsExample.swift +++ b/Example/EpoxyExample/Data/LayoutGroupsExample.swift @@ -11,6 +11,7 @@ enum LayoutGroupsExample: CaseIterable { case todoList case entirelyInline case complex + case dynamic // MARK: Internal @@ -32,6 +33,8 @@ enum LayoutGroupsExample: CaseIterable { return "Inline components" case .complex: return "Shuffle" + case .dynamic: + return "Dynamic" } } @@ -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" } } diff --git a/Example/EpoxyExample/ViewControllers/LayoutGroups/DynamicLayoutGroupsViewController.swift b/Example/EpoxyExample/ViewControllers/LayoutGroups/DynamicLayoutGroupsViewController.swift new file mode 100644 index 00000000..0dba1087 --- /dev/null +++ b/Example/EpoxyExample/ViewControllers/LayoutGroups/DynamicLayoutGroupsViewController.swift @@ -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 + } + +} diff --git a/Example/EpoxyExample/ViewControllers/LayoutGroups/UIViewController+LayoutGroupsExample.swift b/Example/EpoxyExample/ViewControllers/LayoutGroups/UIViewController+LayoutGroupsExample.swift index 351cc179..5147f048 100644 --- a/Example/EpoxyExample/ViewControllers/LayoutGroups/UIViewController+LayoutGroupsExample.swift +++ b/Example/EpoxyExample/ViewControllers/LayoutGroups/UIViewController+LayoutGroupsExample.swift @@ -23,6 +23,8 @@ extension UIViewController { viewController = EntirelyInlineViewController() case .complex: viewController = ComplexDeclarativeViewController() + case .dynamic: + viewController = DynamicLayoutGroupsViewController() } viewController.title = example.title return viewController diff --git a/Example/EpoxyExample/Views/LayoutGroups/DynamicRow.swift b/Example/EpoxyExample/Views/LayoutGroups/DynamicRow.swift new file mode 100644 index 00000000..73215d7f --- /dev/null +++ b/Example/EpoxyExample/Views/LayoutGroups/DynamicRow.swift @@ -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 + +} diff --git a/Gemfile b/Gemfile index 2ccf2f4a..9500763a 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index ac482c2f..c0b0410e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -94,4 +94,4 @@ DEPENDENCIES rake (~> 13.0.0)! BUNDLED WITH - 2.1.4 + 2.2.6 diff --git a/Rakefile b/Rakefile index f9b1d2e0..35a14288 100644 --- a/Rakefile +++ b/Rakefile @@ -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 @@ -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