Skip to content

Commit 78c3bfe

Browse files
committed
Expose LNPopupShadowedImageView as public API
Support smooth transitions from and to LNPopupShadowedImageView instances
1 parent 83c8653 commit 78c3bfe

30 files changed

+1002
-371
lines changed

LNPopupController/LNPopupController.xcodeproj/project.pbxproj

Lines changed: 76 additions & 8 deletions
Large diffs are not rendered by default.

LNPopupController/LNPopupController/LNPopupBar.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#import <LNPopupController/LNPopupItem.h>
1212
#import <LNPopupController/LNPopupCustomBarViewController.h>
1313
#import <LNPopupController/LNPopupBarAppearance.h>
14+
#import <LNPopupController/LNPopupShadowedImageView.h>
1415

1516
#define LN_UNAVAILABLE_PREVIEWING_MSG "Add context menu interaction or register for previewing directly on the popup bar view."
1617

@@ -73,7 +74,7 @@ NS_SWIFT_UI_ACTOR
7374
@property (nullable, nonatomic, copy, readonly) NSArray<UIBarButtonItem*>* trailingBarButtonItems;
7475

7576
/// An image view displayed when the bar style is prominent. (read-only)
76-
@property (nonatomic, strong, readonly) UIImageView* imageView;
77+
@property (nonatomic, strong, readonly) LNPopupShadowedImageView* imageView;
7778

7879
/// The popup bar style.
7980
@property (nonatomic, assign) LNPopupBarStyle barStyle UI_APPEARANCE_SELECTOR;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// LNPopupShadowedImageView.h
3+
// LNPopupController
4+
//
5+
// Created by Léo Natan on 2023-09-25.
6+
// Copyright © 2015-2024 Léo Natan. All rights reserved.
7+
//
8+
9+
#import <UIKit/UIKit.h>
10+
11+
NS_ASSUME_NONNULL_BEGIN
12+
13+
/// A specialized `UIImageView` subclass, allowing setting a shadow and corner radius.
14+
///
15+
/// When used inside a popup content view, instances of this class are especially suited as image transition targets.
16+
///
17+
/// See `UIViewController.viewForPopupTransition(from:to:)`.
18+
@interface LNPopupShadowedImageView : UIImageView
19+
20+
/// The corner radius of the image view.
21+
@property (nonatomic, assign) CGFloat cornerRadius;
22+
/// The shadow displayed underneath the image view.
23+
@property (nonatomic, copy) NSShadow* shadow;
24+
25+
@end
26+
27+
NS_ASSUME_NONNULL_END

LNPopupController/LNPopupController/Private/LNPopupBar.mm

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
#import "_LNPopupSwizzlingUtils.h"
1313
#import "_LNPopupBase64Utils.hh"
1414
#import "NSAttributedString+LNPopupSupport.h"
15-
#import "_LNPopupBarShadowedImageView.h"
15+
#import "LNPopupShadowedImageView+Private.h"
1616
#import "UIView+LNPopupSupportPrivate.h"
1717

1818
#ifdef DEBUG
@@ -296,7 +296,7 @@ @implementation LNPopupBar
296296
{
297297
BOOL _delaysBarButtonItemLayout;
298298

299-
UIImageView* _imageView;
299+
LNPopupShadowedImageView* _imageView;
300300

301301
_LNPopupBarTitlesView* _titlesView;
302302
NSLayoutConstraint* _titlesViewLeadingConstraint;
@@ -518,14 +518,13 @@ - (nonnull instancetype)initWithFrame:(CGRect)frame
518518

519519
_needsLabelsLayout = YES;
520520

521-
_imageView = [[_LNPopupBarShadowedImageView alloc] initWithContainingPopupBar:self];;
521+
_imageView = [[LNPopupShadowedImageView alloc] initWithContainingPopupBar:self];;
522522
_imageView.autoresizingMask = UIViewAutoresizingNone;
523523
_imageView.contentMode = UIViewContentModeScaleAspectFit;
524524
_imageView.accessibilityTraits = UIAccessibilityTraitImage;
525525
_imageView.isAccessibilityElement = YES;
526526
_imageView.layer.cornerCurve = kCACornerCurveContinuous;
527-
_imageView.layer.masksToBounds = YES;
528-
static_cast<_LNPopupBarShadowedImageView*>(_imageView).cornerRadius = 6;
527+
_imageView.cornerRadius = 6;
529528

530529
// support smart invert and therefore do not invert image view colors
531530
_imageView.accessibilityIgnoresInvertColors = YES;
@@ -1054,7 +1053,7 @@ - (void)_appearanceDidChange
10541053

10551054
_floatingBackgroundShadowView.shadow = self.activeAppearance.floatingBarBackgroundShadow;
10561055

1057-
static_cast<_LNPopupBarShadowedImageView*>(_imageView).shadow = self.activeAppearance.imageShadow;
1056+
_imageView.shadow = self.activeAppearance.imageShadow;
10581057

10591058
[self.customBarViewController _activeAppearanceDidChange:self.activeAppearance];
10601059

LNPopupController/LNPopupController/Private/LNPopupController.mm

Lines changed: 27 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
#import "UIView+LNPopupSupportPrivate.h"
1818
#import "LNPopupCustomBarViewController+Private.h"
1919
#import "_LNPopupTransitionView.h"
20-
#import "_LNPopupBarShadowedImageView.h"
20+
#import "_LNPopupTransitionPreferredOpenAnimator.h"
21+
#import "_LNPopupTransitionGenericOpenAnimator.h"
22+
#import "_LNPopupTransitionGenericCloseAnimator.h"
23+
#import "_LNPopupTransitionPreferredCloseAnimator.h"
24+
2125
#import <objc/runtime.h>
2226
#import <os/log.h>
2327

@@ -371,60 +375,24 @@ - (UIView*)_supportedUserTransitionViewFromState:(LNPopupPresentationState)fromS
371375
return userView;
372376
}
373377

374-
static const void* _LNPopupOpenCloseTransitionViewKey = &_LNPopupOpenCloseTransitionViewKey;
375-
376378
- (void)animateOpenTransitionIfNeededWithAnimator:(UIViewPropertyAnimator*)animator userTransitionView:(UIView*)userView otherAnimations:(void(^)(void))otherAnimations
377379
{
378380
if(userView == nil)
379381
{
380382
return;
381383
}
382384

383-
__block CGRect sourceFrame;
384-
__block _LNPopupTransitionView* transitionView;
385-
__block NSShadow* targetShadow;
386-
[UIView performWithoutAnimation:^{
387-
[self.popupContentView layoutIfNeeded];
388-
self.popupBar.imageView.alpha = 0.0;
389-
390-
sourceFrame = [self.popupBar.imageView.window convertRect:self.popupBar.imageView.bounds fromView:self.popupBar.imageView];
391-
392-
transitionView = [[_LNPopupTransitionView alloc] initWithFrame:sourceFrame sourceView:userView];
393-
transitionView.shadow = [((_LNPopupBarShadowedImageView*)self.popupBar.imageView).shadow copy];
394-
395-
targetShadow = [transitionView.shadow copy];
396-
targetShadow.shadowColor = [targetShadow.shadowColor colorWithAlphaComponent:0.0];
397-
398-
objc_setAssociatedObject(userView, _LNPopupOpenCloseTransitionViewKey, transitionView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
399-
}];
400-
401-
[animator addAnimations:otherAnimations];
385+
_LNPopupTransitionOpenAnimator* handler;
386+
if([userView isKindOfClass:LNPopupShadowedImageView.class])
387+
{
388+
handler = [[_LNPopupTransitionPreferredOpenAnimator alloc] initWithUserView:(LNPopupShadowedImageView*)userView popupBar:self.popupBar popupContentView:self.popupContentView];
389+
}
390+
else
391+
{
392+
handler = [[_LNPopupTransitionGenericOpenAnimator alloc] initWithUserView:userView popupBar:self.popupBar popupContentView:self.popupContentView];
393+
}
402394

403-
[animator addAnimations:^{
404-
__block CGRect targetFrame;
405-
[UIView performWithoutAnimation:^{
406-
targetFrame = [self.popupContentView.window convertRect:userView.bounds fromView:userView];
407-
CGFloat ratioX = sourceFrame.size.width / targetFrame.size.width;
408-
CGFloat ratioY = sourceFrame.size.height / targetFrame.size.height;
409-
transitionView.sourceViewTransform = CGAffineTransformMakeScale(ratioX, ratioY);
410-
411-
[self.popupContentView.window addSubview:transitionView];
412-
}];
413-
414-
transitionView.frame = targetFrame;
415-
transitionView.sourceViewTransform = CGAffineTransformIdentity;
416-
transitionView.shadow = targetShadow;
417-
}];
418-
}
419-
420-
- (void)completeOpenTransitionIfNeededWithUserTransitionView:(UIView*)userView
421-
{
422-
[UIView performWithoutAnimation:^{
423-
UIView* transitionView = objc_getAssociatedObject(userView, _LNPopupOpenCloseTransitionViewKey);
424-
[transitionView removeFromSuperview];
425-
objc_setAssociatedObject(userView, _LNPopupOpenCloseTransitionViewKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
426-
self.popupBar.imageView.alpha = 1.0;
427-
}];
395+
[handler animateWithAnimator:animator otherAnimations:otherAnimations];
428396
}
429397

430398
- (void)animateCloseTransitionIfNeededWithAnimator:(UIViewPropertyAnimator*)animator userTransitionView:(UIView*)userView otherAnimations:(void(^)(void))otherAnimations
@@ -434,66 +402,18 @@ - (void)animateCloseTransitionIfNeededWithAnimator:(UIViewPropertyAnimator*)anim
434402
return;
435403
}
436404

437-
__block CGRect sourceFrame;
438-
__block _LNPopupTransitionView* transitionView;
439-
__block NSShadow* targetShadow;
440-
[UIView performWithoutAnimation:^{
441-
[self.popupContentView layoutIfNeeded];
442-
self.popupBar.imageView.alpha = 0.0;
443-
444-
sourceFrame = [self.popupContentView.window convertRect:userView.bounds fromView:userView];
445-
446-
transitionView = [[_LNPopupTransitionView alloc] initWithFrame:sourceFrame sourceView:userView];
447-
448-
_LNPopupBarShadowedImageView* imageView = (id)self.popupBar.imageView;
449-
450-
targetShadow = [imageView.shadow copy];
451-
452-
NSShadow* hiddenShadow = [targetShadow copy];
453-
hiddenShadow.shadowColor = [targetShadow.shadowColor colorWithAlphaComponent:0.0];
454-
transitionView.shadow = hiddenShadow;
455-
456-
objc_setAssociatedObject(userView, _LNPopupOpenCloseTransitionViewKey, transitionView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
457-
458-
[self.popupContentView.window addSubview:transitionView];
459-
}];
405+
_LNPopupTransitionCloseAnimator* handler;
460406

461-
[animator addAnimations:^{
462-
otherAnimations();
463-
464-
CGRect targetFrame = [self.popupBar.imageView.window convertRect:self.popupBar.imageView.bounds fromView:self.popupBar.imageView];
465-
[transitionView setTargetFrameUpdatingTransform:targetFrame];
466-
467-
transitionView.shadow = targetShadow;
468-
469-
if(self.containerController._ln_shouldPopupContentViewFadeForTransition)
470-
{
471-
self.popupContentView.alpha = 0.0;
472-
}
473-
else
474-
{
475-
self.currentContentController.view.alpha = 0.0;
476-
}
477-
}];
407+
if([userView isKindOfClass:LNPopupShadowedImageView.class])
408+
{
409+
handler = [[_LNPopupTransitionPreferredCloseAnimator alloc] initWithUserView:userView popupBar:self.popupBar popupContentView:self.popupContentView currentContentController:self.currentContentController containerController:self.containerController];
410+
}
411+
else
412+
{
413+
handler = [[_LNPopupTransitionGenericCloseAnimator alloc] initWithUserView:userView popupBar:self.popupBar popupContentView:self.popupContentView currentContentController:self.currentContentController containerController:self.containerController];
414+
}
478415

479-
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((animator.duration * 0.38) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
480-
[animator addAnimations:^{
481-
_LNPopupBarShadowedImageView* imageView = (id)self.popupBar.imageView;
482-
transitionView.cornerRadius = imageView.cornerRadius;
483-
}];
484-
});
485-
}
486-
487-
- (void)completeCloseTransitionIfNeededWithUserTransitionView:(UIView*)userView
488-
{
489-
[UIView performWithoutAnimation:^{
490-
UIView* transitionView = objc_getAssociatedObject(userView, _LNPopupOpenCloseTransitionViewKey);
491-
[transitionView removeFromSuperview];
492-
objc_setAssociatedObject(userView, _LNPopupOpenCloseTransitionViewKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
493-
self.popupBar.imageView.alpha = 1.0;
494-
self.popupContentView.alpha = 1.0;
495-
self.currentContentController.view.alpha = 1.0;
496-
}];
416+
[handler animateWithAnimator:animator otherAnimations:otherAnimations];
497417
}
498418

499419
- (void)_transitionToState:(LNPopupPresentationState)state notifyDelegate:(BOOL)notifyDelegate animated:(BOOL)animated useSpringAnimation:(BOOL)spring allowPopupBarAlphaModification:(BOOL)allowBarAlpha allowFeedbackGeneration:(BOOL)allowFeedbackGeneration forceFeedbackGenerationAtStart:(BOOL)forceFeedbackAtStart completion:(void(^)(void))completion
@@ -726,26 +646,20 @@ - (void)_transitionToState:(LNPopupPresentationState)state notifyDelegate:(BOOL)
726646
if(userView != nil)
727647
{
728648
animationDuration *= 1.25;
649+
// animationDuration = 4;
729650
}
730651

731652
_runningPopupAnimation = [[UIViewPropertyAnimator alloc] initWithDuration:animationDuration dampingRatio:spring && userView == nil ? 0.85 : 1.0 animations:nil];
653+
// _runningPopupAnimation = [[UIViewPropertyAnimator alloc] initWithDuration:animationDuration curve:UIViewAnimationCurveLinear animations:nil];
732654
_runningPopupAnimation.userInteractionEnabled = NO;
733655

734656
if(stateAtStart == LNPopupPresentationStateBarPresented && userView != nil)
735657
{
736658
[self animateOpenTransitionIfNeededWithAnimator:_runningPopupAnimation userTransitionView:userView otherAnimations:animationBlock];
737-
738-
[_runningPopupAnimation addCompletion:^(UIViewAnimatingPosition finalPosition) {
739-
[self completeOpenTransitionIfNeededWithUserTransitionView:userView];
740-
}];
741659
}
742660
else if(state == LNPopupPresentationStateBarPresented && userView != nil)
743661
{
744662
[self animateCloseTransitionIfNeededWithAnimator:_runningPopupAnimation userTransitionView:userView otherAnimations:animationBlock];
745-
746-
[_runningPopupAnimation addCompletion:^(UIViewAnimatingPosition finalPosition) {
747-
[self completeCloseTransitionIfNeededWithUserTransitionView:userView];
748-
}];
749663
}
750664
else
751665
{
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// LNPopupShadowedImageView+Private.h
3+
// LNPopupController
4+
//
5+
// Created by Léo Natan on 24/3/25.
6+
// Copyright © 2025 Léo Natan. All rights reserved.
7+
//
8+
9+
#import "LNPopupShadowedImageView.h"
10+
#import "LNPopupBar+Private.h"
11+
12+
NS_ASSUME_NONNULL_BEGIN
13+
14+
@interface LNPopupShadowedImageView ()
15+
16+
- (instancetype)initWithContainingPopupBar:(LNPopupBar*)popupBar;
17+
18+
@property (nonatomic, strong, nullable) NSNumber* transitionCornerRadius;
19+
@property (nonatomic, copy, nullable) NSShadow* transitionShadow;
20+
21+
@end
22+
23+
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)