Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion Example/Source/Control/NavigationTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Fluidable
class NavigationTableViewController: NavigationBaseViewController, Fluidable {
/** Dummy value to prevent UIViewPropertyAnimator from finishing immediately. */
@objc dynamic var transitionProgress: CGFloat = 0
private var dismissCellContentWidth: CGFloat?

@IBOutlet weak var tableView: TableView!
@IBOutlet weak var footerOverlayView: UIView!
Expand Down Expand Up @@ -124,6 +125,7 @@ extension NavigationTableViewController: FluidTransitionDestinationConfiguration
"navigation: " + String(describing: navigation),
])
guard transitionStyle.isFluid else { return nil }
self.dismissCellContentWidth = initialDimension.frame().width
var animators: [FluidAnimatorCompatible] = [FluidAnimatorCompatible]()
/* NOTE: Hide button in 30% of duration without delay (UIViewPropertyAnimator) */
let buttonAnimator: FluidPropertyAnimator = .init(duration: duration * 0.3, /* NOTE: 30% of transition duration */
Expand Down Expand Up @@ -172,6 +174,14 @@ extension NavigationTableViewController: FluidTransitionDestinationConfiguration
self.view.layoutIfNeeded()
})
animators.append(constraintAnimator)
/* NOTE: Table cell layout (UIViewPropertyAnimator) */
let cellLayoutAnimator: FluidPropertyAnimator = .init(duration: duration, easing: easing, id: "cellLayoutAnimator (Dismiss)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line Length Violation: Line should be 120 characters or less: currently 133 characters (line_length)

cellLayoutAnimator.add({ [weak self] in
guard let `self`: NavigationTableViewController = self else { return }
self.layoutVisibleCellsForDismiss()
})
animators.append(cellLayoutAnimator)
self.layoutVisibleCellsForDismiss()
return animators
}
}
Expand All @@ -186,6 +196,16 @@ extension NavigationTableViewController: FluidTransitionDestinationActionDelegat
"state:".lpad() + String(describing: state),
"progress:".lpad() + String(describing: progress),
])
switch state {
case .begin, .update:
if transitionStyle.isFluid {
self.layoutVisibleCellsForDismiss()
}
case .cancel:
self.resetVisibleCellsAfterDismissCancel()
case .end:
self.dismissCellContentWidth = nil
}
}

func transitionDismissAnimationDidProgress(from destination: FluidDestinationViewController, to source: FluidSourceViewController,
Expand Down Expand Up @@ -222,17 +242,33 @@ extension NavigationTableViewController: FluidTransitionDestinationActionDelegat
case .update:
if transitionStyle.isFluid {
self.closeButton.alpha = 1 - progress
self.layoutVisibleCellsForDismiss()
} else {
self.closeButton.alpha = (1 - progress * 3).clamped(0, 1)
}
case .cancel:
self.resetVisibleCellsAfterDismissCancel()
let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: 0.1, easing: .linear)
animator.addAnimations({ [weak self] in
self?.closeButton.alpha = 1
})
animator.startAnimation()
case .end:
break
self.dismissCellContentWidth = nil
}
}
}

private extension NavigationTableViewController {
func layoutVisibleCellsForDismiss() {
guard self.tableView.window != nil else { return }
self.view.layoutIfNeeded()
self.tableView.layoutVisibleCellsImmediately(constrainingTextTo: self.dismissCellContentWidth)
}

func resetVisibleCellsAfterDismissCancel() {
self.dismissCellContentWidth = nil
guard self.tableView.window != nil else { return }
self.tableView.layoutVisibleCellsImmediately()
}
}
38 changes: 37 additions & 1 deletion Example/Source/Control/TransitionTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class TransitionTableViewController: TransitionBaseViewController, Fluidable {

/** Dummy value to prevent UIViewPropertyAnimator from finishing immediately. */
@objc dynamic var transitionProgress: CGFloat = 0
private var dismissCellContentWidth: CGFloat?

/** Views */
@IBOutlet weak var tableView: TableView!
Expand Down Expand Up @@ -148,6 +149,7 @@ extension TransitionTableViewController: FluidTransitionDestinationConfiguration
"navigation: " + String(describing: navigation),
])
guard transitionStyle.isFluid else { return nil }
self.dismissCellContentWidth = initialDimension.frame().width
var animators: [FluidAnimatorCompatible] = [FluidAnimatorCompatible]()
/* NOTE: Hide button in 30% of duration without delay (Core Animation) */
// if let buttonAnimator: FluidCoreAnimator = FluidCoreAnimator(for: self.closeButton.layer, id: "buttonAnimator (Dismiss)",
Expand Down Expand Up @@ -208,6 +210,14 @@ extension TransitionTableViewController: FluidTransitionDestinationConfiguration
self.view.layoutIfNeeded()
})
animators.append(constraintAnimator)
/* NOTE: Table cell layout (UIViewPropertyAnimator) */
let cellLayoutAnimator: FluidPropertyAnimator = .init(duration: duration, easing: easing, id: "cellLayoutAnimator (Dismiss)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line Length Violation: Line should be 120 characters or less: currently 133 characters (line_length)

cellLayoutAnimator.add({ [weak self] in
guard let `self`: TransitionTableViewController = self else { return }
self.layoutVisibleCellsForDismiss()
})
animators.append(cellLayoutAnimator)
self.layoutVisibleCellsForDismiss()
return animators
}
}
Expand Down Expand Up @@ -238,6 +248,16 @@ extension TransitionTableViewController: FluidTransitionDestinationActionDelegat
"state:".lpad() + String(describing: state),
"progress:".lpad() + String(describing: progress),
])
switch state {
case .begin, .update:
if transitionStyle.isFluid {
self.layoutVisibleCellsForDismiss()
}
case .cancel:
self.resetVisibleCellsAfterDismissCancel()
case .end:
self.dismissCellContentWidth = nil
}
}

func transitionPresentInteractionDidProgress(from source: FluidSourceViewController, to destination: FluidDestinationViewController,
Expand All @@ -264,17 +284,33 @@ extension TransitionTableViewController: FluidTransitionDestinationActionDelegat
case .update:
if transitionStyle.isFluid {
self.closeButton.alpha = 1 - progress
self.layoutVisibleCellsForDismiss()
} else {
self.closeButton.alpha = (1 - progress * 3).clamped(0, 1)
}
case .cancel:
self.resetVisibleCellsAfterDismissCancel()
let animator: UIViewPropertyAnimator = UIViewPropertyAnimator(duration: 0.1, easing: .linear)
animator.addAnimations({ [weak self] in
self?.closeButton.alpha = 1
})
animator.startAnimation()
case .end:
break
self.dismissCellContentWidth = nil
}
}
}

private extension TransitionTableViewController {
func layoutVisibleCellsForDismiss() {
guard self.tableView.window != nil else { return }
self.view.layoutIfNeeded()
self.tableView.layoutVisibleCellsImmediately(constrainingTextTo: self.dismissCellContentWidth)
}

func resetVisibleCellsAfterDismissCancel() {
self.dismissCellContentWidth = nil
guard self.tableView.window != nil else { return }
self.tableView.layoutVisibleCellsImmediately()
}
}
2 changes: 1 addition & 1 deletion Example/Source/Helper/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ extension UINib {
extension UITableView {
func register<T: UITableViewCell>(cellType: T.Type) {
let className: String = cellType.className
let nib = UINib(nibName: className, bundle: nil)
let nib = UINib(nibName: className, bundle: Bundle(for: cellType))
register(nib, forCellReuseIdentifier: className)
}

Expand Down
2 changes: 1 addition & 1 deletion Example/Source/View/HeaderCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class HeaderCell: UITableViewCell {
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }

static func instantiate(model: RootModel) -> HeaderCell {
let view: HeaderCell = UINib.instantiate(nibName: className)
let view: HeaderCell = UINib.instantiate(nibName: className, bundle: Bundle(for: HeaderCell.self))
view.configure(model: model)
return view
}
Expand Down
15 changes: 15 additions & 0 deletions Example/Source/View/TableCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import Foundation
import UIKit

class TableCell: UITableViewCell {
private struct Const {
static let titleTrailingMargin: CGFloat = 20
}

@IBOutlet weak var thumbView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var captionLabel: UILabel!
Expand All @@ -30,4 +34,15 @@ class TableCell: UITableViewCell {

self.clipsToBounds = true
}

func layoutTextLabels(forContentWidth contentWidth: CGFloat) {
self.contentView.layoutIfNeeded()
self.titleLabel.lineBreakMode = .byTruncatingTail
self.captionLabel.lineBreakMode = .byTruncatingTail

let titleWidth: CGFloat = contentWidth - self.titleLabel.frame.minX - Const.titleTrailingMargin
let captionWidth: CGFloat = contentWidth - self.captionLabel.frame.minX - self.contentView.layoutMargins.right
self.titleLabel.frame.size.width = max(0, titleWidth)
self.captionLabel.frame.size.width = max(0, captionWidth)
}
}
14 changes: 14 additions & 0 deletions Example/Source/View/TableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ extension TableView {
/* NOTE: Reload */
self.reloadData()
}

func layoutVisibleCellsImmediately(constrainingTextTo contentWidth: CGFloat? = nil) {
self.setNeedsLayout()
self.layoutIfNeeded()
self.visibleCells.forEach {
$0.setNeedsLayout()
$0.contentView.setNeedsLayout()
$0.contentView.layoutIfNeeded()
$0.layoutIfNeeded()
guard let contentWidth: CGFloat = contentWidth,
let cell: TableCell = $0 as? TableCell else { return }
cell.layoutTextLabels(forContentWidth: contentWidth)
}
}
}

extension TableView: UITableViewDataSource {
Expand Down
83 changes: 83 additions & 0 deletions UITests/MainSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,86 @@ final class MainSpec: QuickSpec {
}

}

final class FluidModalTableDismissalLayoutTests: XCTestCase {
class TestFluidSourceViewController: UIViewController, Fluidable {}

func testConstrainsTableCellLabelsToDismissTargetWidth() {
let bundle = Bundle(for: FluidModalTableDismissalLayoutTests.self)
let cell = UINib(nibName: "TableCell", bundle: bundle)
.instantiate(withOwner: nil, options: nil).first as! TableCell

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force Cast Violation: Force casts should be avoided. (force_cast)

cell.frame = CGRect(x: 0, y: 0, width: 430, height: 64)
cell.contentView.frame = cell.bounds
cell.configure(row: 0)
cell.layoutIfNeeded()

cell.layoutTextLabels(forContentWidth: 390)

XCTAssertEqual(cell.titleLabel.lineBreakMode, .byTruncatingTail)
XCTAssertEqual(cell.captionLabel.lineBreakMode, .byTruncatingTail)
XCTAssertEqual(cell.titleLabel.frame.width,
390 - cell.titleLabel.frame.minX - 20,
accuracy: 0.5)
XCTAssertLessThan(cell.titleLabel.frame.maxX, cell.contentView.frame.maxX)
}

func testUpdatesVisibleTableCellsWhileTheModalShrinks() {
let model: RootModel = .navigationFluidFullScreen
let bundle = Bundle(for: FluidModalTableDismissalLayoutTests.self)
let navigation = UINib(nibName: "NavigationRootNavigationController", bundle: bundle)
.instantiate(withOwner: nil, options: nil).first as! NavigationRootNavigationController

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force Cast Violation: Force casts should be avoided. (force_cast)

let destination = UINib(nibName: "NavigationTableViewController", bundle: bundle)
.instantiate(withOwner: nil, options: nil).first as! NavigationTableViewController

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force Cast Violation: Force casts should be avoided. (force_cast)

let source: TestFluidSourceViewController = TestFluidSourceViewController()

navigation.configure(modelIndex: model.rawValue)
navigation.pushViewController(destination, animated: false)
destination.configure(modelIndex: model.rawValue)
destination.loadViewIfNeeded()
source.loadViewIfNeeded()

let animators: [FluidAnimatorCompatible] = destination.transitionAdditionalDismissAnimations(
from: destination,
to: source,
with: navigation,
on: UIView(frame: CGRect(x: 0, y: 0, width: 430, height: 930)),
initialDimension: FluidInitialFrameDimension(for: model.transitionStyle,
contentOrigin: CGPoint(x: 20, y: 100),
contentSize: CGSize(width: 390, height: 700),
contentTransform: CATransform3DIdentity),
finalDimension: FluidFinalFrameDimension(for: model.transitionStyle,
portraitContentSize: CGSize(width: 430, height: 930),
landscapeContentSize: CGSize(width: 930, height: 430),
portraitContentTransform: CATransform3DIdentity,
landscapeContentTransform: CATransform3DIdentity),
initialStyle: model.initialFrameStyle!,
finalStyle: model.finalFrameStyle!,
transitionStyle: model.transitionStyle,
duration: 0.5,
easing: FluidAnimatorEasing.linear) ?? []

XCTAssertTrue(animators.contains { $0.identifier == "cellLayoutAnimator (Dismiss)" })
}
}

final class NavigationFluidFullScreenDismissUITests: XCTestCase {
func testFinishAnimatedDismissWithTableContent() {
let app: XCUIApplication = XCUIApplication()
let orientation: UIDeviceOrientation = .portrait
let model: RootModel = .navigationFluidFullScreen

addTeardownBlock {
app.terminate()
XCUIDevice.shared.orientation = .portrait
}

XCUIDevice.shared.orientation = orientation
app.setEnv(MainSpec.env)
app.launch()

MainSpec.finishAnimatedPresent(app: app, orientation: orientation, model: model)
MainSpec.pushViewController(app: app, orientation: orientation, model: model)
MainSpec.popViewControllerByTappingBackButton(app: app, orientation: orientation, model: model)
MainSpec.finishAnimatedDismissByTappingContainer(app: app, orientation: orientation, model: model)
}
}