diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..3eb199e Binary files /dev/null and b/.swiftpm/xcode/package.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Configs/ScrollStackController.plist b/Configs/ScrollStackController.plist index 9dcce24..5eb0141 100644 --- a/Configs/ScrollStackController.plist +++ b/Configs/ScrollStackController.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0 + $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion diff --git a/Package.swift b/Package.swift index 8b1477f..148940d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:4.0 +// swift-tools-version:5.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/README.md b/README.md index 1dcabf6..62fc46c 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ You can think of it as `UITableView` but with several differences: |--- |--------------------------------------------------------------------------------- | | 🕺 | Create complex layout without the boilerplate required by view recyling of `UICollectionView` or `UITableView`. | | 🧩 | Simplify your architecture by thinking each screen as a separate-indipendent `UIVIewController`. | -| 🌈 | Animate show/hide and resize of rows easily! | +| 🌈 | Animate show/hide and resize of rows easily even with custom animations! | | ⏱ | Compact code base, less than 1k LOC with no external dependencies. | | 🎯 | Easy to use and extensible APIs set. | | 🧬 | It uses standard UIKit components at its core. No magic, just a combination of `UIScrollView`+`UIStackView`. | @@ -42,6 +42,7 @@ You can think of it as `UITableView` but with several differences: - [Removing / Replacing Rows](#removingreplacingrows) - [Move Rows](#moverows) - [Hide / Show Rows](#hideshowrows) + - [Hide / Show Rows with custom animations](#customanimations) - [Reload Rows](#reloadrows) - [Sizing Rows](#sizingrows) - [Fixed Row Size](#fixedrowsize) @@ -232,6 +233,63 @@ Keep in mind: when you hide a rows the row still part of the stack and it's not [↑ Back To Top](#index) + + +### Hide / Show Rows with custom animations + +You can easily show or hide rows with any custom transition; your view controller just need to be conform to the `ScrollStackRowAnimatable` protocol. +This protocol defines a set of animation infos (duration, delay, spring etc.) and two events you can override to perform actions: + +```swift +public protocol ScrollStackRowAnimatable { + /// Animation main info. + var animationInfo: ScrollStackAnimationInfo { get } + + /// Animation will start to hide or show the row. + func willBeginAnimationTransition(toHide: Bool) + + /// Animation to hide/show the row did end. + func didEndAnimationTransition(toHide: Bool) + + /// Animation transition. + func animateTransition(toHide: Bool) +} +``` + +So for example you can replicate the following animation: + +![](./Resources/custom_transition.gif) + +by using the following code: + +```swift +extension WelcomeVC: ScrollStackRowAnimatable { + public var animationInfo: ScrollStackAnimationInfo { + return ScrollStackAnimationInfo(duration: 1, delay: 0, springDamping: 0.8) + } + + public func animateTransition(toHide: Bool) { + switch toHide { + case true: + self.view.transform = CGAffineTransform(translationX: -100, y: 0) + self.view.alpha = 0 + + case false: + self.view.transform = .identity + self.view.alpha = 1 + } + } + + public func willBeginAnimationTransition(toHide: Bool) { + if toHide == false { + self.view.transform = CGAffineTransform(translationX: -100, y: 0) + self.view.alpha = 0 + } + } + +} +``` + ### Reload Rows diff --git a/Resources/custom_transition.gif b/Resources/custom_transition.gif new file mode 100644 index 0000000..5b9b6ae Binary files /dev/null and b/Resources/custom_transition.gif differ diff --git a/ScrollStackController.podspec b/ScrollStackController.podspec index e0ba712..48e7161 100644 --- a/ScrollStackController.podspec +++ b/ScrollStackController.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "ScrollStackController" - s.version = "1.0.3" + s.version = "1.1.0" s.summary = "Create complex scrollable layout using UIViewController and simplify your code" s.homepage = "https://github.com/malcommac/ScrollStackController" s.license = { :type => "MIT", :file => "LICENSE" } diff --git a/ScrollStackController.xcodeproj/project.pbxproj b/ScrollStackController.xcodeproj/project.pbxproj index 3a1e2e7..5d7dbe6 100644 --- a/ScrollStackController.xcodeproj/project.pbxproj +++ b/ScrollStackController.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 6402E1F22347A8540087963C /* Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6402E1F12347A8540087963C /* Extension.swift */; }; 647C77B32348EA1600CAEB9F /* PricingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647C77B22348EA1600CAEB9F /* PricingVC.swift */; }; 6489C0612349C571003E5344 /* NotesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6489C0602349C571003E5344 /* NotesVC.swift */; }; + 649B1E9223B1251400BD6BFD /* ScrollStackRowAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649B1E9123B1251400BD6BFD /* ScrollStackRowAnimator.swift */; }; + 649B1E9323B1251900BD6BFD /* ScrollStackRowAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649B1E9123B1251400BD6BFD /* ScrollStackRowAnimator.swift */; }; 64A8E8B32348CCCE00E893FB /* WelcomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A8E8B02348CCCE00E893FB /* WelcomeVC.swift */; }; 64C02255234735A800A6D844 /* ScrollStackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C0224F234735A800A6D844 /* ScrollStackViewController.swift */; }; 64C02257234735A800A6D844 /* ScrollStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C02250234735A800A6D844 /* ScrollStack.swift */; }; @@ -50,6 +52,7 @@ 6402E1F12347A8540087963C /* Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extension.swift; sourceTree = ""; }; 647C77B22348EA1600CAEB9F /* PricingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PricingVC.swift; sourceTree = ""; }; 6489C0602349C571003E5344 /* NotesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesVC.swift; sourceTree = ""; }; + 649B1E9123B1251400BD6BFD /* ScrollStackRowAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollStackRowAnimator.swift; sourceTree = ""; }; 64A8E8B02348CCCE00E893FB /* WelcomeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeVC.swift; sourceTree = ""; }; 64C0224F234735A800A6D844 /* ScrollStackViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollStackViewController.swift; sourceTree = ""; }; 64C02250234735A800A6D844 /* ScrollStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollStack.swift; sourceTree = ""; }; @@ -131,6 +134,7 @@ children = ( 64C0228523475A0E00A6D844 /* ScrollStack+Protocols.swift */, 64C02253234735A800A6D844 /* UIView+AutoLayout_Extensions.swift */, + 649B1E9123B1251400BD6BFD /* ScrollStackRowAnimator.swift */, ); path = Support; sourceTree = ""; @@ -346,6 +350,7 @@ 64C02259234735A800A6D844 /* ScrollStackRow.swift in Sources */, 64C02255234735A800A6D844 /* ScrollStackViewController.swift in Sources */, 64C02257234735A800A6D844 /* ScrollStack.swift in Sources */, + 649B1E9223B1251400BD6BFD /* ScrollStackRowAnimator.swift in Sources */, 64C0225D234735A800A6D844 /* UIView+AutoLayout_Extensions.swift in Sources */, 64C0225B234735A800A6D844 /* ScrollStackSeparator.swift in Sources */, ); @@ -371,6 +376,7 @@ 64C0226C2347360800A6D844 /* ViewController.swift in Sources */, 64C022812347582D00A6D844 /* ScrollStackSeparator.swift in Sources */, 64C0227E2347582D00A6D844 /* ScrollStack.swift in Sources */, + 649B1E9323B1251900BD6BFD /* ScrollStackRowAnimator.swift in Sources */, 64C022682347360800A6D844 /* AppDelegate.swift in Sources */, 647C77B32348EA1600CAEB9F /* PricingVC.swift in Sources */, 6402E1F22347A8540087963C /* Extension.swift in Sources */, @@ -529,6 +535,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -537,6 +544,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MARKETING_VERSION = 1.1.0; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.ScrollStackController.ScrollStackController-iOS"; PRODUCT_NAME = ScrollStackController; @@ -552,6 +560,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CURRENT_PROJECT_VERSION = 0; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -560,6 +569,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.ScrollStackController.ScrollStackController-iOS"; PRODUCT_NAME = ScrollStackController; SKIP_INSTALL = YES; diff --git a/ScrollStackController.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate b/ScrollStackController.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate index 0503094..6c7f08b 100644 Binary files a/ScrollStackController.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate and b/ScrollStackController.xcodeproj/project.xcworkspace/xcuserdata/daniele.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ScrollStackController.xcodeproj/xcshareddata/xcschemes/ScrollStackController-tvOS.xcscheme b/ScrollStackController.xcodeproj/xcshareddata/xcschemes/ScrollStackController-tvOS.xcscheme deleted file mode 100644 index ee49815..0000000 --- a/ScrollStackController.xcodeproj/xcshareddata/xcschemes/ScrollStackController-tvOS.xcscheme +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ScrollStackController.xcodeproj/xcuserdata/daniele.xcuserdatad/xcschemes/xcschememanagement.plist b/ScrollStackController.xcodeproj/xcuserdata/daniele.xcuserdatad/xcschemes/xcschememanagement.plist index daa61db..0bfd023 100644 --- a/ScrollStackController.xcodeproj/xcuserdata/daniele.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/ScrollStackController.xcodeproj/xcuserdata/daniele.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,15 +9,10 @@ orderHint 0 - ScrollStackController-tvOS.xcscheme_^#shared#^_ - - orderHint - 1 - ScrollStackControllerDemo.xcscheme_^#shared#^_ orderHint - 2 + 1 diff --git a/ScrollStackControllerDemo/Base.lproj/Main.storyboard b/ScrollStackControllerDemo/Base.lproj/Main.storyboard index 45831c3..fc3ee39 100644 --- a/ScrollStackControllerDemo/Base.lproj/Main.storyboard +++ b/ScrollStackControllerDemo/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -165,8 +165,8 @@ - - + + @@ -233,20 +233,20 @@ - - + + - - - - + + + + - + @@ -261,8 +261,8 @@ - - + + @@ -275,7 +275,7 @@ - + @@ -357,21 +357,21 @@ - - + + - + - + - - + + - + @@ -386,8 +386,8 @@ - - + + @@ -420,19 +420,19 @@ - - + + - + - + - + - + @@ -446,8 +446,8 @@ - - + + @@ -536,23 +536,23 @@ - - - + + + - - + + - + - + - + - + @@ -566,8 +566,8 @@ - - + + @@ -599,18 +599,18 @@ Set in the heart of the Culture Village, less than 15 minutes away from Dubai In - + - - + + - + - + diff --git a/ScrollStackControllerDemo/Child View Controllers/TagsVC.swift b/ScrollStackControllerDemo/Child View Controllers/TagsVC.swift index 9385900..e7b53e9 100644 --- a/ScrollStackControllerDemo/Child View Controllers/TagsVC.swift +++ b/ScrollStackControllerDemo/Child View Controllers/TagsVC.swift @@ -49,7 +49,8 @@ public class TagsVC: UIViewController, ScrollStackContainableController { } public func scrollStackRowSizeForAxis(_ axis: NSLayoutConstraint.Axis, row: ScrollStackRow, in stackView: ScrollStack) -> ScrollStack.ControllerSize? { - return (isExpanded == false ? .fixed(170) : .fixed(170 + collectionView.contentSize.height + 20)) + collectionView.layoutIfNeeded() + return (isExpanded == false ? .fixed(130) : .fixed(130 + collectionView.contentSize.height + 20)) } public func reloadContentFromStackView(stackView: ScrollStack, row: ScrollStackRow, animated: Bool) { diff --git a/ScrollStackControllerDemo/Child View Controllers/WelcomeVC.swift b/ScrollStackControllerDemo/Child View Controllers/WelcomeVC.swift index 466927e..b442127 100644 --- a/ScrollStackControllerDemo/Child View Controllers/WelcomeVC.swift +++ b/ScrollStackControllerDemo/Child View Controllers/WelcomeVC.swift @@ -24,7 +24,34 @@ public class WelcomeVC: UIViewController, ScrollStackContainableController { } public func reloadContentFromStackView(stackView: ScrollStack, row: ScrollStackRow, animated: Bool) { + + } + +} + +extension WelcomeVC: ScrollStackRowAnimatable { + + public var animationInfo: ScrollStackAnimationInfo { + return ScrollStackAnimationInfo(duration: 1, delay: 0, springDamping: 0.8) + } + public func animateTransition(toHide: Bool) { + switch toHide { + case true: + self.view.transform = CGAffineTransform(translationX: -100, y: 0) + self.view.alpha = 0 + + case false: + self.view.transform = .identity + self.view.alpha = 1 + } + } + + public func willBeginAnimationTransition(toHide: Bool) { + if toHide == false { + self.view.transform = CGAffineTransform(translationX: -100, y: 0) + self.view.alpha = 0 + } } } diff --git a/Sources/ScrollStackController/ScrollStack.swift b/Sources/ScrollStackController/ScrollStack.swift index e9a2ab7..0ae5081 100644 --- a/Sources/ScrollStackController/ScrollStack.swift +++ b/Sources/ScrollStackController/ScrollStack.swift @@ -209,7 +209,7 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate { internal var onChangeRow: ((_ row: ScrollStackRow, _ isRemoved: Bool) -> Void)? /// Innert stack view. - private let stackView = UIStackView() + public let stackView = UIStackView() /// Constraints to manage the main axis set. private var axisConstraint: NSLayoutConstraint? @@ -446,10 +446,11 @@ open class ScrollStack: UIScrollView, UIScrollViewDelegate { return } - row.layoutIfNeeded() - UIView.execute({ + let coordinator = ScrollStackRowAnimator(row: row, toHidden: isHidden, internalHandler: { row.isHidden = isHidden - }, completion: completion) + row.layoutIfNeeded() + }) + coordinator.execute() } /// Hide/Show selected rows. diff --git a/Sources/ScrollStackController/Support/ScrollStackRowAnimator.swift b/Sources/ScrollStackController/Support/ScrollStackRowAnimator.swift new file mode 100644 index 0000000..a543b8a --- /dev/null +++ b/Sources/ScrollStackController/Support/ScrollStackRowAnimator.swift @@ -0,0 +1,151 @@ +/* + * ScrollStackController + * Create complex scrollable layout using UIViewController and simplify your code + * + * Created by: Daniele Margutti + * Email: hello@danielemargutti.com + * Web: http://www.danielemargutti.com + * Twitter: @danielemargutti + * + * Copyright © 2019 Daniele Margutti + * + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +import UIKit + +// MARK: - ScrollStackRowAnimatable + +public protocol ScrollStackRowAnimatable { + + /// Animation main info. + var animationInfo: ScrollStackAnimationInfo { get } + + /// Animation will start to hide or show the row. + /// - Parameter toHide: hide or show transition. + func willBeginAnimationTransition(toHide: Bool) + + /// Animation to hide/show the row did end. + /// - Parameter toHide: hide or show transition. + func didEndAnimationTransition(toHide: Bool) + + /// Animation transition. + /// - Parameter toHide: hide or show transition. + func animateTransition(toHide: Bool) + +} + +// MARK: - ScrollStackRowAnimatable Extension + +public extension ScrollStackRowAnimatable where Self: UIViewController { + + var animationInfo: ScrollStackAnimationInfo { + return ScrollStackAnimationInfo() + } + + func animateTransition(toHide: Bool) { + + } + + func willBeginAnimationTransition(toHide: Bool) { + + } + + func didEndAnimationTransition(toHide: Bool) { + + } + +} + + +// MARK: - ScrollStackAnimationInfo + +public struct ScrollStackAnimationInfo { + + /// Duration of the animation. By default is set to `0.25`. + var duration: TimeInterval + + /// Delay before start animation. + var delay: TimeInterval + + /// The springDamping value used to determine the amount of `bounce`. + /// Default Value is `0.8`. + var springDamping: CGFloat + + public init(duration: TimeInterval = 0.25, delay: TimeInterval = 0, springDamping: CGFloat = 0.8) { + self.duration = duration + self.delay = delay + self.springDamping = springDamping + } + +} + +// MARK: - ScrollStackRowAnimator + +internal class ScrollStackRowAnimator { + + /// Row to animate. + private let targetRow: ScrollStackRow + + /// Final state after animation, hidden or not. + private let toHidden: Bool + + /// Animation handler, used to perform actions for animation in `ScrollStack`. + private let internalHandler: () -> Void + + /// Completion handler. + private let completion: ((Bool) -> Void)? + + /// Target row if animatable. + private var animatableRow: ScrollStackRowAnimatable? { + return targetRow.controller as? ScrollStackRowAnimatable + } + + // MARK: - Initialization + + init(row: ScrollStackRow, toHidden: Bool, + internalHandler: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) { + self.targetRow = row + self.toHidden = toHidden + self.internalHandler = internalHandler + self.completion = completion + } + + /// Execute animation. + func execute() { + animatableRow?.willBeginAnimationTransition(toHide: toHidden) + + let duration = animatableRow?.animationInfo.duration ?? 0.25 + UIView.animate(withDuration: duration, + delay: 0, + usingSpringWithDamping: animatableRow?.animationInfo.springDamping ?? 1, + initialSpringVelocity: 0, + options: [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState], + animations: { + self.animatableRow?.animateTransition(toHide: self.toHidden) + self.internalHandler() + }) { finished in + self.animatableRow?.didEndAnimationTransition(toHide: self.toHidden) + self.completion?(finished) + } + } + +}