diff --git a/Sources/TGCardViewController/TGCardViewController.swift b/Sources/TGCardViewController/TGCardViewController.swift index 314a032..40d2649 100644 --- a/Sources/TGCardViewController/TGCardViewController.swift +++ b/Sources/TGCardViewController/TGCardViewController.swift @@ -150,6 +150,7 @@ open class TGCardViewController: UIViewController { } } + @IBOutlet weak var headerEffectView: UIVisualEffectView! @IBOutlet weak var headerView: UIView! @IBOutlet weak var mapViewWrapper: UIView! @IBOutlet weak var mapShadow: UIView! @@ -173,8 +174,6 @@ open class TGCardViewController: UIViewController { @IBOutlet weak var cardWrapperHeightConstraint: NSLayoutConstraint! @IBOutlet weak var cardWrapperDynamicLeadingConstraint: NSLayoutConstraint! @IBOutlet weak var cardWrapperStaticLeadingConstraint: NSLayoutConstraint! - @IBOutlet weak var cardWrapperDynamicTrailingConstraint: NSLayoutConstraint! - @IBOutlet weak var cardWrapperDynamicBottomConstraint: NSLayoutConstraint! // Positioning the header view @IBOutlet weak var headerViewHeightConstraint: NSLayoutConstraint! @@ -193,8 +192,6 @@ open class TGCardViewController: UIViewController { var mapViewController = TGMapViewController() public var mapView: UIView! { mapViewController.mapView } - private var initialScrollOffset: CGFloat = 0 - var panner: UIPanGestureRecognizer! var cardTapper: UITapGestureRecognizer! var mapShadowTapper: UITapGestureRecognizer! @@ -298,18 +295,11 @@ open class TGCardViewController: UIViewController { if #available(iOS 26.0, *) { statusBarBlurView.isHidden = true } - let cardIsNextToMap = self.cardIsNextToMap(in: traitCollection) // mode-specific styling TGCornerView.roundedCorners = mode == .floating cardWrapperDynamicLeadingConstraint.isActive = mode == .floating cardWrapperStaticLeadingConstraint.isActive = mode == .sidebar - cardWrapperDynamicTrailingConstraint.isActive = mode == .floating && !cardIsNextToMap - if #available(iOS 26.0, *) { - cardWrapperDynamicBottomConstraint.isActive = mode == .floating - } else { - cardWrapperDynamicBottomConstraint.isActive = false - } toggleCardWrappers(hide: true) sidebarSeparator.backgroundColor = .separator @@ -328,25 +318,17 @@ open class TGCardViewController: UIViewController { #if compiler(>=6.2) // Xcode 26 if #available(iOS 26.0, *) { cardWrapperEffectView.effect = UIGlassEffect(style: .regular) - if cardIsNextToMap { - cardWrapperEffectView.cornerConfiguration = .corners(radius: 12) - } else { - cardWrapperEffectView.cornerConfiguration = .corners( - topLeftRadius: 44, - topRightRadius: 44, - bottomLeftRadius: .containerConcentric(minimum: 44), - bottomRightRadius: .containerConcentric(minimum: 44) - ) - } - cardWrapperEffectView.clipsToBounds = true - - // Map floating bar buttons don't need to be styled here, that's - // handled in `applyToolbarItemStyle` + cardWrapperEffectView.cornerConfiguration = .corners(radius: 12) + + headerEffectView.effect = UIGlassEffect(style: .regular) + headerEffectView.cornerConfiguration = .corners(topLeftRadius: nil, topRightRadius: nil, bottomLeftRadius: 12, bottomRightRadius: 12) } else { cardWrapperEffectView.effect = nil + headerEffectView.effect = nil } #else cardWrapperEffectView.effect = nil + headerEffectView.effect = nil #endif setupGestures() @@ -375,7 +357,7 @@ open class TGCardViewController: UIViewController { mapViewController.additionalSafeAreaInsets = updateCardPosition(y: collapsedMinY) // Add a bit of a shadow behind card. - if mode == .floating { + if mode == .floating, #unavailable(iOS 26.0) { cardWrapperShadow.layer.shadowColor = UIColor.black.cgColor cardWrapperShadow.layer.shadowOffset = .init(width: 0, height: 2) cardWrapperShadow.layer.shadowRadius = 4 @@ -493,36 +475,6 @@ open class TGCardViewController: UIViewController { cardWrapperHeightConstraint.constant = extendedMinY * -1 - let cardIsNextToMap = cardIsNextToMap(in: traitCollection) - if #available(iOS 26.0, *) { - if cardIsNextToMap { - cardWrapperEffectView.cornerConfiguration = .corners(radius: 12) - } else { - cardWrapperEffectView.cornerConfiguration = .corners( - topLeftRadius: 44, - topRightRadius: 44, - bottomLeftRadius: .containerConcentric(minimum: 44), - bottomRightRadius: .containerConcentric(minimum: 44) - ) - } - } - - // 1. Deactivate potentially conflicting constraints first - cardWrapperStaticLeadingConstraint.isActive = false - cardWrapperDynamicLeadingConstraint.isActive = false - cardWrapperDynamicTrailingConstraint.isActive = false - - // 2. Restore iOS 26 dynamic padding after trait collection changes (rotation/backgrounding) - updateMapShadow(for: cardPosition) - - // 3. Reactivate the correct constraints based on mode and layout - cardWrapperStaticLeadingConstraint.isActive = mode == .sidebar - cardWrapperDynamicLeadingConstraint.isActive = mode == .floating - cardWrapperDynamicTrailingConstraint.isActive = mode == .floating && !cardIsNextToMap - - // 4. Force layout with consistent state - view.layoutIfNeeded() - // When trait collection changes, try to keep the same card position if let previous = previousCardPosition { // Note: Ideally, we'd determine the direction by whether the available @@ -694,26 +646,6 @@ open class TGCardViewController: UIViewController { fileprivate func updateMapShadow(for position: TGCardPosition) { mapShadow.alpha = position == .extended ? Constants.mapShadowVisibleAlpha : 0 mapShadow.isUserInteractionEnabled = position == .extended - - if #available(iOS 26.0, *) { - let background: UIColor = position == .extended ? .systemBackground : .clear - topCardView?.grabHandle?.backgroundColor = background - topCardView?.titleView?.backgroundColor = background - cardWrapperContent.backgroundColor = background - - let cardIsNextToMap = cardIsNextToMap(in: traitCollection) - let padding: CGFloat = switch (position, cardIsNextToMap) { - case (_, true): 12 - case (.extended, _): 0 - case (.peaking, _): 6 - case (.collapsed, _): 22 - } - - cardWrapperDynamicLeadingConstraint.constant = padding - cardWrapperDynamicTrailingConstraint.constant = padding - cardWrapperDynamicBottomConstraint.constant = padding - view.setNeedsUpdateConstraints() - } } private func toggleCardWrappers(hide: Bool, prepareOnly: Bool = false) { @@ -868,7 +800,7 @@ extension TGCardViewController { oldTop?.card.willDisappear(animated: animated) } - if let oldTop = oldTop { + if let oldTop { cards.removeLast() cards.append( (oldTop.card, cardPosition, oldTop.view) ) } @@ -877,7 +809,7 @@ extension TGCardViewController { // Copying style from old card to new card MUST be called before // new card builds its card view. - if let oldCard = oldTop?.card, copyStyle { + if copyStyle, let oldCard = oldTop?.card { oldCard.copyStyling(to: top) } @@ -900,16 +832,17 @@ extension TGCardViewController { // This allows us to continuously pull down the card view while its // content is scrolled to the top. Note this only applies when the // card isn't being forced into the extended position. - if !forceExtended && panningAllowed { + if !forceExtended, panningAllowed { cardView.contentScrollView?.panGestureRecognizer.addTarget(self, action: #selector(handleInnerPan(_:))) } // 4. Place the new view coming, preparing to animate in from the bottom cardView.frame = cardWrapperContent.bounds - cardView.alpha = 0 if animated { let offset = cardView.convert(.init(x: 0, y: mapViewWrapper.frame.maxY), to: cardWrapperShadow).y cardView.frame.origin.y = offset + + cardWrapperEffectView.frame.origin.y = offset } cardWrapperContent.addSubview(cardView) @@ -991,29 +924,43 @@ extension TGCardViewController { // old. Only do that if the previous transition completed, i.e., we didn't // already have such a shadow. - if oldTop != nil, animated, cardTransitionShadow == nil { - let shadow = TGCornerView(frame: cardWrapperEffectView.bounds) - shadow.frame.size.height += 50 // for bounciness - shadow.backgroundColor = .black - shadow.alpha = 0 - cardWrapperEffectView.contentView.insertSubview(shadow, belowSubview: cardWrapperContent) - cardTransitionShadow = shadow + if let oldTop, animated, let cardView, cardTransitionShadow == nil { + if #unavailable(iOS 26.0) { + let shadow = TGCornerView(frame: cardWrapperContent.bounds) + shadow.frame.size.height += 50 // for bounciness + shadow.backgroundColor = .black + shadow.alpha = 0 + cardWrapperContent.insertSubview(shadow, belowSubview: cardView) + cardTransitionShadow = shadow + + } else if let oldView = oldTop.view, let container = cardWrapperShadow.superview, let snapshot = oldView.snapshotView(afterScreenUpdates: false) { + + var oldFrame = oldView.bounds + oldFrame.origin = oldView.convert(oldView.bounds.origin, to: container) + oldView.alpha = 0 + + let visualEffectView = UIVisualEffectView(effect: UIGlassEffect(style: .regular)) + visualEffectView.contentView.addSubview(snapshot) + visualEffectView.frame = oldFrame + container.insertSubview(visualEffectView, belowSubview: cardWrapperShadow) + cardTransitionShadow = visualEffectView + } } - + let cardAnimations = { self.toggleCardWrappers(hide: cardView == nil, prepareOnly: true) guard let cardView else { return } self.updateMapShadow(for: animateTo.position) cardView.frame = self.cardWrapperContent.bounds - cardView.alpha = 1 - oldTop?.view?.alpha = 0 + self.cardWrapperEffectView.frame = cardView.frame self.cardTransitionShadow?.alpha = 0.15 } if self.mode != .floating { cardAnimations() + oldTop?.view?.alpha = 0 } - + // In some cases, the cardWrapperShadow frame might already have been // updated before the animation was applied. If that happaned, we reset // it temporarily back and animate it. @@ -1043,12 +990,14 @@ extension TGCardViewController { completion: { _ in self.updateCardScrolling(allow: animateTo.position == .extended, view: cardView) self.previousCardPosition = animateTo.position + oldTop?.view?.alpha = 0 if notify { oldTop?.card.didDisappear(animated: animated) top.didAppear(animated: animated) top.didMove(to: animateTo.position, animated: animated) } self.cardTransitionShadow?.removeFromSuperview() + self.cardTransitionShadow = nil self.updateForNewPosition(position: animateTo.position) self.updateResponderChainForNewTopCard() self.toggleCardWrappers(hide: cardView == nil) @@ -1131,7 +1080,7 @@ extension TGCardViewController { } // 4. Determine and set new position of the card wrapper (relative to header!) - newTop?.view?.alpha = 0 + newTop?.view?.alpha = 1 // We only animate to the previous position if the card obscures the map updateCardStructure(card: newTop?.view, position: newTop?.lastPosition) @@ -1161,29 +1110,45 @@ extension TGCardViewController { // 5. Do the transition, optionally animated. // We animate the view moving back down to the bottom // we also temporarily insert a shadow view again, if there's a card below - if animated, newTop != nil, cardTransitionShadow == nil { - let shadow = TGCornerView(frame: cardWrapperEffectView.bounds) - shadow.backgroundColor = .black - shadow.alpha = 0.15 - cardWrapperEffectView.contentView.insertSubview(shadow, belowSubview: cardWrapperContent) - cardTransitionShadow = shadow + if animated, cardTransitionShadow == nil, let topView { + if #unavailable(iOS 26.0) { + let shadow = TGCornerView(frame: cardWrapperContent.bounds) + shadow.backgroundColor = .black + shadow.alpha = 0.15 + cardWrapperContent.insertSubview(shadow, belowSubview: topView) + cardTransitionShadow = shadow + + } else if let container = cardWrapperShadow.superview, let snapshot = topView.snapshotView(afterScreenUpdates: false) { + + topView.alpha = 0 + var newFrame = topView.bounds + newFrame.origin = topView.convert(topView.bounds.origin, to: container) + + let visualEffectView = UIVisualEffectView(effect: UIGlassEffect(style: .regular)) + visualEffectView.contentView.addSubview(snapshot) + visualEffectView.frame = newFrame + container.insertSubview(visualEffectView, aboveSubview: cardWrapperShadow) + cardTransitionShadow = visualEffectView + } } let cardAnimations = { self.toggleCardWrappers(hide: newTop?.view == nil, prepareOnly: true) self.updateMapShadow(for: animateTo) - topView?.frame.origin.y = self.cardWrapperContent.frame.maxY - self.cardTransitionShadow?.alpha = 0 + if #unavailable(iOS 26.0) { + self.cardTransitionShadow?.alpha = 0 + topView?.frame.origin.y = self.cardWrapperContent.frame.maxY + } else { + self.cardTransitionShadow?.frame.origin.y = self.cardWrapperContent.frame.maxY + 100 + } newTop?.view?.adjustContentAlpha(to: animateTo == .collapsed ? 0 : 1) newTop?.view?.setSeparatorVisibility(forceHidden: animateTo == .collapsed) - - newTop?.view?.alpha = 1 - topView?.alpha = 0 } if mode != .floating { cardAnimations() + topView?.alpha = 0 } UIView.animate( @@ -1209,7 +1174,9 @@ extension TGCardViewController { } // This line did crash in Adrian's simulator but only happens rarely; when?!? topView?.removeFromSuperview() + topView?.alpha = 1 self.cardTransitionShadow?.removeFromSuperview() + self.cardTransitionShadow = nil self.updateForNewPosition(position: animateTo) self.updateResponderChainForNewTopCard() self.isPopping = false @@ -1560,7 +1527,7 @@ extension TGCardViewController { switchTo(.peaking, direction: .down, animated: true) } - + @objc fileprivate func handleInnerPan(_ recogniser: UIPanGestureRecognizer) { guard @@ -1569,23 +1536,11 @@ extension TGCardViewController { scrollView == topCardView?.contentScrollView, panner.isEnabled, scrollView.refreshControl == nil - else { - return - } + else { return } - let negativity: CGFloat - if #available(iOS 26.0, *), scrollView.contentOffset.y <= 0 { - negativity = (recogniser.translation(in: cardWrapperContent).y - initialScrollOffset) * -1 - } else { - negativity = scrollView.contentOffset.y + scrollView.contentInset.top - } + let negativity = scrollView.contentOffset.y + scrollView.contentInset.top switch (negativity, recogniser.state) { - - - case (_, .began) : - self.initialScrollOffset = scrollView.contentOffset.y - case (0 ..< CGFloat.infinity, _): // Reset the transformation whenever we get back to positive offset scrollView.transform = .identity @@ -1629,11 +1584,9 @@ extension TGCardViewController { // the scroll view appear to stay in place (it's important to not // set the content offset to zero here!) self.mapViewController.additionalSafeAreaInsets = updateCardPosition(y: extendedMinY - negativity) - if #unavailable(iOS 26.0) { - scrollView.transform = CGAffineTransform(translationX: 0, y: negativity) - scrollView.verticalScrollIndicatorInsets.top = scrollView.contentInset.top + negativity * -1 - } - + scrollView.transform = CGAffineTransform(translationX: 0, y: negativity) + scrollView.verticalScrollIndicatorInsets.top = scrollView.contentInset.top + negativity * -1 + default: // Ignore other states such as began, failed, etc. break @@ -1876,22 +1829,6 @@ extension TGCardViewController { apply(on: topFloatingViewWrapper) apply(on: bottomFloatingViewWrapper) - - if showDefaultButtons, let defaultButtons, let customTint = buttonStyle.trackingColor { - for view in defaultButtons { - for subview in view.subviews { - if let tracker = subview as? MKUserTrackingButton { - tracker.tintColor = customTint - - // For some reason the MKUserTrackingButton's internal doesn't want - // to inherit the tint of the button. So we go in deep. - for internalView in tracker.subviews { - internalView.tintColor = customTint - } - } - } - } - } } public func updateMapToolbarItems() { @@ -2009,12 +1946,7 @@ extension TGCardViewController { private func updateHeaderStyle() { @MainActor func applyCornerStyle(to view: UIView) { - let radius: CGFloat - if #available(iOS 26.0, *) { - radius = 22 - } else { - radius = 16 - } + let radius: CGFloat = 12 let roundAllCorners = cardIsNextToMap(in: traitCollection) view.layer.maskedCorners = roundAllCorners @@ -2033,7 +1965,7 @@ extension TGCardViewController { updateStatusBar(headerIsVisible: isShowingHeader) if #available(iOS 26.0, *) { - // No header. Rely on this being a UIVisualEffectView + // No shadow. Rely on headerEffectView } else { // same shadow as for card wrapper headerView.layer.shadowColor = UIColor.black.cgColor @@ -2198,16 +2130,7 @@ extension TGCardViewController: UIGestureRecognizerDelegate { } else if let pager = (topCardView as? TGPageCardView)?.pager, other == pager.panGestureRecognizer { // When our panner fires, block panning of the page card - if #available(iOS 26.0, *) { - // iOS 26: Allow vertical card dragging when swiping vertically, - // but block it during horizontal swipes to enable clean paging - let velocity = panner.velocity(in: cardWrapperContent) - let swipeHorizontally = abs(velocity.x) > abs(velocity.y) - return !swipeHorizontally - - } else { - return false - } + return false } else { // We don't want to interfere with any existing horizontal swipes, e.g., swipe to delete diff --git a/Sources/TGCardViewController/TGCardViewController.xib b/Sources/TGCardViewController/TGCardViewController.xib index 3cb4642..4fd8c02 100644 --- a/Sources/TGCardViewController/TGCardViewController.xib +++ b/Sources/TGCardViewController/TGCardViewController.xib @@ -1,9 +1,9 @@ - - + + - + @@ -16,14 +16,13 @@ - - - + + @@ -45,29 +44,29 @@ - + - + - + - + - + - + @@ -85,19 +84,19 @@ - + - + - + - + - + @@ -110,7 +109,7 @@ - + @@ -128,8 +127,16 @@ + + + + + + + + - + @@ -137,13 +144,13 @@ - + - + @@ -167,37 +174,33 @@ - + - - - - + + + + - - - - - - - - - - - - + + + + - + - - - - + + + + + + + + @@ -216,6 +219,7 @@ + @@ -228,31 +232,33 @@ + - + + - + - + + - @@ -277,10 +283,10 @@ - + @@ -294,15 +300,15 @@ - + - + - + @@ -324,7 +330,7 @@ - + diff --git a/Sources/TGCardViewController/cards/TGPageCardView.xib b/Sources/TGCardViewController/cards/TGPageCardView.xib index db0330b..4064fb3 100644 --- a/Sources/TGCardViewController/cards/TGPageCardView.xib +++ b/Sources/TGCardViewController/cards/TGPageCardView.xib @@ -1,9 +1,9 @@ - + - + @@ -13,7 +13,7 @@ - + diff --git a/Sources/TGCardViewController/cards/TGPageHeaderView.swift b/Sources/TGCardViewController/cards/TGPageHeaderView.swift index 06755d5..87136c9 100644 --- a/Sources/TGCardViewController/cards/TGPageHeaderView.swift +++ b/Sources/TGCardViewController/cards/TGPageHeaderView.swift @@ -13,7 +13,6 @@ public class TGPageHeaderView: TGHeaderView { @IBOutlet weak var accessoryWrapperView: UIView! @IBOutlet weak var accessoryWrapperHeightConstraint: NSLayoutConstraint! - @IBOutlet weak var accessoryLeadingConstraint: NSLayoutConstraint! @IBOutlet weak var accessoryTrailingConstraint: NSLayoutConstraint! @IBOutlet weak var buttonTrailingConstraint: NSLayoutConstraint! @@ -27,14 +26,6 @@ public class TGPageHeaderView: TGHeaderView { override public func awakeFromNib() { super.awakeFromNib() - if #available(iOS 26.0, *) { - accessoryLeadingConstraint.constant = 0 - accessoryTrailingConstraint.constant = 0 - } else { - accessoryLeadingConstraint.constant = 8 - accessoryTrailingConstraint.constant = 8 - } - rightButton?.isHidden = true accessoryWrapperView.isHidden = true preferredStatusBarStyle = .default diff --git a/Sources/TGCardViewController/cards/TGPageHeaderView.xib b/Sources/TGCardViewController/cards/TGPageHeaderView.xib index bd64d45..8021910 100644 --- a/Sources/TGCardViewController/cards/TGPageHeaderView.xib +++ b/Sources/TGCardViewController/cards/TGPageHeaderView.xib @@ -1,9 +1,9 @@ - + - + @@ -14,14 +14,14 @@ - +