From 6fad9a5c590ffa3f54588a375688ed63e9c2b056 Mon Sep 17 00:00:00 2001 From: Adrian Schoenig Date: Thu, 4 Dec 2025 14:31:32 +1100 Subject: [PATCH 1/6] So far so good --- .../TGButtonPosition.swift | 8 +- .../TGCardViewController.swift | 1 + .../TGCardViewController/cards/TGCard.swift | 13 +++ .../cards/TGCardStyle.swift | 20 +++- .../cards/TGCardView.swift | 13 ++- .../cards/TGHostingCard.swift | 102 ++++++++++++++---- .../cards/TGPageCardView.swift | 2 +- .../cards/TGPlainCard.swift | 2 +- .../cards/TGPlainCardView.swift | 21 ++-- .../style/TGCardStyleKit.swift | 24 ++++- .../style/TGCornerView.swift | 8 +- .../views/TGCardDefaultTitleView.swift | 23 ++-- .../views/TGCardDefaultTitleView.xib | 19 ++-- 13 files changed, 194 insertions(+), 62 deletions(-) diff --git a/Sources/TGCardViewController/TGButtonPosition.swift b/Sources/TGCardViewController/TGButtonPosition.swift index 3c12925..9599c0a 100644 --- a/Sources/TGCardViewController/TGButtonPosition.swift +++ b/Sources/TGCardViewController/TGButtonPosition.swift @@ -20,9 +20,10 @@ public enum TGButtonPosition { public struct TGButtonStyle { /// Default style is rounded rect, no special tint colour and translucent - public init(shape: TGButtonStyle.Shape = .roundedRect, tintColor: UIColor? = nil, isTranslucent: Bool = true) { + public init(shape: TGButtonStyle.Shape = .roundedRect, tintColor: UIColor? = nil, trackingColor: UIColor? = nil, isTranslucent: Bool = true) { self.shape = shape self.tintColor = tintColor + self.trackingColor = trackingColor self.isTranslucent = isTranslucent } @@ -42,6 +43,9 @@ public struct TGButtonStyle { /// Custom tint colour. Uses default tint colour if set to `nil` public let tintColor: UIColor? - + + /// Prominent colour to pass to tracking button. Uses default tint colour if set to `nil` + public let trackingColor: UIColor? + public let isTranslucent: Bool } diff --git a/Sources/TGCardViewController/TGCardViewController.swift b/Sources/TGCardViewController/TGCardViewController.swift index 27797eb..bfdb82c 100644 --- a/Sources/TGCardViewController/TGCardViewController.swift +++ b/Sources/TGCardViewController/TGCardViewController.swift @@ -1083,6 +1083,7 @@ extension TGCardViewController { topView?.frame.origin.y = self.cardWrapperContent.frame.maxY self.cardTransitionShadow?.alpha = 0 newTop?.view?.adjustContentAlpha(to: animateTo == .collapsed ? 0 : 1) + newTop?.view?.setSeparatorVisibility(forceHidden: animateTo == .collapsed) } if mode != .floating { diff --git a/Sources/TGCardViewController/cards/TGCard.swift b/Sources/TGCardViewController/cards/TGCard.swift index b4a176b..b922d54 100644 --- a/Sources/TGCardViewController/cards/TGCard.swift +++ b/Sources/TGCardViewController/cards/TGCard.swift @@ -58,10 +58,12 @@ open class TGCard: UIResponder, TGPreferrableView { } /// The default image for the close button on a card, with default color + @available(*, deprecated, message: "Use `configureCloseButton` instead.") public static let closeButtonImage = TGCardStyleKit.imageOfCardCloseIcon() /// The default image for the close button on a card, with custom background /// color + @available(*, deprecated, message: "Use `configureCloseButton` instead.") public static func closeButtonImage(background: UIColor) -> UIImage { TGCardStyleKit.imageOfCardCloseIcon(closeButtonBackground: background) } @@ -72,6 +74,7 @@ open class TGCard: UIResponder, TGPreferrableView { /// /// - Parameter style: The style to use /// - Returns: A styled icon for use in a close button on a card + @available(*, deprecated, message: "Use `configureCloseButton` instead.") public static func closeButtonImage(style: TGCardStyle) -> UIImage { TGCardStyleKit.imageOfCardCloseIcon( closeButtonBackground: style.closeButtonBackgroundColor, @@ -79,6 +82,16 @@ open class TGCard: UIResponder, TGPreferrableView { ) } + public static func configureCloseButton(_ button: UIButton, style: TGCardStyle = .default) { + let image = TGCardStyleKit.imageOfCardCloseIcon( + closeButtonBackground: style.closeButtonBackgroundColor, + closeButtonCross: style.closeButtonCrossColor + ) + + button.setImage(image, for: .normal) + button.setTitle(nil, for: .normal) + } + /// A default image for an arrow pointing up or down, similar to the close button image public static func arrowButtonImage(direction: TGArrowDirection, background: UIColor, arrow: UIColor) -> UIImage { switch direction { diff --git a/Sources/TGCardViewController/cards/TGCardStyle.swift b/Sources/TGCardViewController/cards/TGCardStyle.swift index 645cca6..6ff5324 100644 --- a/Sources/TGCardViewController/cards/TGCardStyle.swift +++ b/Sources/TGCardViewController/cards/TGCardStyle.swift @@ -13,7 +13,17 @@ public struct TGCardStyle { public static let `default` = TGCardStyle() /// Font to use for title, defaults to bold system font with size 17pt. - public var titleFont: UIFont = .boldSystemFont(ofSize: 17) + public var titleFont: UIFont = { + if #available(iOS 26.0, *) { + if let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .largeTitle).withSymbolicTraits(.traitBold) { + return UIFont.init(descriptor: descriptor, size: descriptor.pointSize) + } else { + return .preferredFont(forTextStyle: .largeTitle) + } + } else { + return .boldSystemFont(ofSize: 17) + } + }() /// Title colour, defaults to system label color public var titleTextColor: UIColor = .label @@ -25,7 +35,13 @@ public struct TGCardStyle { public var subtitleTextColor: UIColor = .secondaryLabel /// Colour to use for the background, defaults to system background color - public var backgroundColor: UIColor = .systemBackground + public var backgroundColor: UIColor = { + if #available(iOS 26.0, *) { + return .clear + } else { + return .systemBackground + } + }() /// Colour to use for the grab handle on the card, defaults to system secondary label color public var grabHandleColor: UIColor = .secondaryLabel diff --git a/Sources/TGCardViewController/cards/TGCardView.swift b/Sources/TGCardViewController/cards/TGCardView.swift index adb1a3d..21e97ed 100644 --- a/Sources/TGCardViewController/cards/TGCardView.swift +++ b/Sources/TGCardViewController/cards/TGCardView.swift @@ -242,9 +242,20 @@ public class TGCardView: TGCornerView, TGPreferrableView { // MARK: - Content view configuration + private var forceHideSeparator = false + + func setSeparatorVisibility(forceHidden: Bool) { + forceHideSeparator = forceHidden + // Trigger separator update with current scroll state + if let contentScrollView = contentScrollView { + let actualOffset = contentScrollView.transform.ty < 0 ? 0 : contentScrollView.contentOffset.y + showSeparator(actualOffset > 0, offset: actualOffset) + } + } + func showSeparator(_ show: Bool, offset: CGFloat) { if let owningCard, owningCard.shouldToggleSeparator(show: show, offset: offset) { - contentSeparator?.isHidden = !show + contentSeparator?.isHidden = forceHideSeparator ? true : !show } else if let owningCard, owningCard.title.isExtended, owningCard.autoIgnoreContentInset, let contentScrollView, contentScrollView.isDecelerating, offset < 0 { // This handles the case where you fling the content down further than the diff --git a/Sources/TGCardViewController/cards/TGHostingCard.swift b/Sources/TGCardViewController/cards/TGHostingCard.swift index a4b970f..541a460 100644 --- a/Sources/TGCardViewController/cards/TGHostingCard.swift +++ b/Sources/TGCardViewController/cards/TGHostingCard.swift @@ -1,10 +1,11 @@ // // TGHostingCard.swift -// +// // // Created by Adrian Schönig on 21/4/21. // + import SwiftUI /// A hosting card can be used to use a SwiftUI `View` as the card's content. @@ -15,16 +16,37 @@ import SwiftUI @available(iOS 13.0, *) open class TGHostingCard: TGCard where Content: View { - private let host: UIHostingController + private let host: UIHostingController + private let relay: _TGSizeRelay public init(title: CardTitle, rootView: Content, mapManager: TGCompatibleMapManager? = nil, initialPosition: TGCardPosition? = nil) { - self.host = TGHostingController(rootView: rootView) - + let relay = _TGSizeRelay() + let observedRoot = rootView._tgOnSizeChange { [weak relay] in + relay?.onSize?($0) + } + self.host = UIHostingController(rootView: AnyView(observedRoot)) + self.relay = relay + super.init(title: title, mapManager: mapManager, initialPosition: mapManager != nil ? initialPosition : .extended) + + // After init, connect size changes to intrinsic invalidation so Auto Layout + // updates content height. UIHostingController doesn't manage to reliably + // do that itself, but nudging it this way does the trick. + relay.onSize = { [weak host = self.host] size in + guard let view = host?.view else { return } + view.invalidateIntrinsicContentSize() + view.setNeedsLayout() + } + } + + open func didBuild(scrollView: UIScrollView) { + } + + open func didBuild(scrollView: UIScrollView, cardView: TGCardView) { } // MARK: - Constructing views @@ -32,37 +54,71 @@ open class TGHostingCard: TGCard where Content: View { open override func buildCardView() -> TGCardView? { let view = TGScrollCardView.instantiate(extended: title.isExtended) - host.beginAppearanceTransition(true, animated: false) - let scroller = UIScrollView(frame: .zero) + view.configure(scroller, with: self) + host.beginAppearanceTransition(true, animated: false) host.view.translatesAutoresizingMaskIntoConstraints = false + host.view.backgroundColor = .clear scroller.addSubview(host.view) + NSLayoutConstraint.activate([ - host.view.leadingAnchor.constraint(equalTo: scroller.leadingAnchor), - host.view.topAnchor.constraint(equalTo: scroller.topAnchor), - host.view.trailingAnchor.constraint(equalTo: scroller.trailingAnchor), + host.view.leadingAnchor.constraint(equalTo: scroller.contentLayoutGuide.leadingAnchor), + host.view.topAnchor.constraint(equalTo: scroller.contentLayoutGuide.topAnchor), + host.view.trailingAnchor.constraint(equalTo: scroller.contentLayoutGuide.trailingAnchor), + host.view.bottomAnchor.constraint(equalTo: scroller.contentLayoutGuide.bottomAnchor), + host.view.widthAnchor.constraint(equalTo: scroller.frameLayoutGuide.widthAnchor), ]) + host.endAppearanceTransition() - view.configure(scroller, with: self) + return view + } + + override public final func didBuild(cardView: TGCardView?, headerView: TGHeaderView?) { - host.view.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true + defer { super.didBuild(cardView: cardView, headerView: headerView) } - host.endAppearanceTransition() - return view + guard let cardView, let scrollView = (cardView as? TGScrollCardView)?.embeddedScrollView else { + preconditionFailure() + } + + didBuild(scrollView: scrollView) + didBuild(scrollView: scrollView, cardView: cardView) } } -@available(iOS 13.0, *) -fileprivate class TGHostingController: UIHostingController where Content: View { - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - if let scroller = view.superview as? UIScrollView { - let size = sizeThatFits(in: scroller.bounds.size) - scroller.contentSize = size - view.heightAnchor.constraint(equalToConstant: size.height).isActive = true - } + +// MARK: - SwiftUI size reporting helper + +private struct _TGSizeKey: PreferenceKey { + static var defaultValue: CGSize = .zero + static func reduce(value: inout CGSize, nextValue: () -> CGSize) { + value = nextValue() } } + +private struct _TGSizeReader: ViewModifier { + let onChange: (CGSize) -> Void + func body(content: Content) -> some View { + content + .background( + GeometryReader { proxy in + Color.clear + .preference(key: _TGSizeKey.self, value: proxy.size) + .onPreferenceChange(_TGSizeKey.self, perform: onChange) + } + ) + } +} + +private extension View { + func _tgOnSizeChange(_ perform: @escaping (CGSize) -> Void) -> some View { + modifier(_TGSizeReader(onChange: perform)) + } +} + +private final class _TGSizeRelay { + var onSize: ((CGSize) -> Void)? +} + diff --git a/Sources/TGCardViewController/cards/TGPageCardView.swift b/Sources/TGCardViewController/cards/TGPageCardView.swift index 4104de6..0367bce 100644 --- a/Sources/TGCardViewController/cards/TGPageCardView.swift +++ b/Sources/TGCardViewController/cards/TGPageCardView.swift @@ -357,7 +357,7 @@ extension TGPageCardView: UIScrollViewDelegate { visiblePageLogical = logical delegate?.didChangeCurrentPage(to: logical, animated: true) - lastHorizontalOffset = scrollView.contentOffset.y + lastHorizontalOffset = scrollView.contentOffset.x let topMost = cardView(index: logical) self.accessibilityElements = [topMost].compactMap { $0 } diff --git a/Sources/TGCardViewController/cards/TGPlainCard.swift b/Sources/TGCardViewController/cards/TGPlainCard.swift index 91f4634..7307728 100644 --- a/Sources/TGCardViewController/cards/TGPlainCard.swift +++ b/Sources/TGCardViewController/cards/TGPlainCard.swift @@ -58,7 +58,7 @@ open class TGPlainCard: TGCard { open override func buildCardView() -> TGCardView? { let view = TGPlainCardView.instantiate(extended: title.isExtended) - view.configure(with: self) + view.configure(with: self, contentView: contentView) return view } diff --git a/Sources/TGCardViewController/cards/TGPlainCardView.swift b/Sources/TGCardViewController/cards/TGPlainCardView.swift index bb021ea..de4bc6a 100644 --- a/Sources/TGCardViewController/cards/TGPlainCardView.swift +++ b/Sources/TGCardViewController/cards/TGPlainCardView.swift @@ -25,12 +25,8 @@ class TGPlainCardView: TGCardView { // MARK: - Configuration - override func configure(with card: TGCard) { - guard let plainCard = card as? TGPlainCard else { - preconditionFailure() - } - - super.configure(with: plainCard) + func configure(with card: TGCard, contentView: UIView?) { + super.configure(with: card) // build the header var adjustment: CGFloat = 1.0 // accounted for the separator @@ -48,15 +44,14 @@ class TGPlainCardView: TGCardView { contentViewHeightEqualToSuperviewHeightConstraint.constant = -1*adjustment } - // build the main content - if let content = plainCard.contentView { + if let content = contentView { content.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(content) - content.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true - content.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true - contentView.trailingAnchor.constraint(equalTo: content.trailingAnchor).isActive = true - contentView.bottomAnchor.constraint(equalTo: content.bottomAnchor).isActive = true + self.contentView.addSubview(content) + content.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor).isActive = true + content.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true + self.contentView.trailingAnchor.constraint(equalTo: content.trailingAnchor).isActive = true + self.contentView.bottomAnchor.constraint(equalTo: content.bottomAnchor).isActive = true } } diff --git a/Sources/TGCardViewController/style/TGCardStyleKit.swift b/Sources/TGCardViewController/style/TGCardStyleKit.swift index 00ef421..6947a29 100644 --- a/Sources/TGCardViewController/style/TGCardStyleKit.swift +++ b/Sources/TGCardViewController/style/TGCardStyleKit.swift @@ -190,8 +190,16 @@ class TGCardStyleKit : NSObject { } @objc dynamic class func imageOfCardCloseIcon(closeButtonBackground: UIColor = UIColor(red: 0.130, green: 0.160, blue: 0.200, alpha: 0.080), closeButtonCross: UIColor = UIColor(red: 0.440, green: 0.460, blue: 0.480, alpha: 1.000)) -> UIImage { - UIGraphicsBeginImageContextWithOptions(CGSize(width: 24, height: 24), false, 0) - TGCardStyleKit.drawCardCloseIcon(closeButtonBackground: closeButtonBackground, closeButtonCross: closeButtonCross) + let width: CGFloat + if #available(iOS 26.0, *) { + width = 44 + } else { + width = 24 + } + + let frame = CGRect(origin: .init(x: 0, y: 0), size: CGSize(width: width, height: width)) + UIGraphicsBeginImageContextWithOptions(frame.size, false, 0) + TGCardStyleKit.drawCardCloseIcon(frame: frame, closeButtonBackground: closeButtonBackground, closeButtonCross: closeButtonCross) let imageOfCardCloseIcon = UIGraphicsGetImageFromCurrentImageContext()!.withRenderingMode(.alwaysOriginal) UIGraphicsEndImageContext() @@ -200,8 +208,16 @@ class TGCardStyleKit : NSObject { } @objc dynamic class func imageOfCardArrowIcon(closeButtonBackground: UIColor = UIColor(red: 0.130, green: 0.160, blue: 0.200, alpha: 0.080), closeButtonCross: UIColor = UIColor(red: 0.440, green: 0.460, blue: 0.480, alpha: 1.000), arrowRotation: CGFloat = 0) -> UIImage { - UIGraphicsBeginImageContextWithOptions(CGSize(width: 24, height: 24), false, 0) - TGCardStyleKit.drawCardArrowIcon(closeButtonBackground: closeButtonBackground, closeButtonCross: closeButtonCross, arrowRotation: arrowRotation) + let width: CGFloat + if #available(iOS 26.0, *) { + width = 44 + } else { + width = 24 + } + + let frame = CGRect(origin: .init(x: 0, y: 0), size: CGSize(width: width, height: width)) + UIGraphicsBeginImageContextWithOptions(frame.size, false, 0) + TGCardStyleKit.drawCardArrowIcon(frame: frame, closeButtonBackground: closeButtonBackground, closeButtonCross: closeButtonCross, arrowRotation: arrowRotation) let imageOfCardArrowIcon = UIGraphicsGetImageFromCurrentImageContext()!.withRenderingMode(.alwaysOriginal) UIGraphicsEndImageContext() diff --git a/Sources/TGCardViewController/style/TGCornerView.swift b/Sources/TGCardViewController/style/TGCornerView.swift index f55a957..bedeb99 100644 --- a/Sources/TGCardViewController/style/TGCornerView.swift +++ b/Sources/TGCardViewController/style/TGCornerView.swift @@ -17,9 +17,15 @@ public class TGCornerView: UIView { super.layoutSubviews() if Self.roundedCorners { + let radius: CGFloat + if #available(iOS 26.0, *) { + radius = 44 + } else { + radius = 16 + } let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: [.topLeft, .topRight], - cornerRadii: CGSize(width: 16, height: 16)) + cornerRadii: CGSize(width: radius, height: radius)) let mask = CAShapeLayer() mask.path = path.cgPath layer.mask = mask diff --git a/Sources/TGCardViewController/views/TGCardDefaultTitleView.swift b/Sources/TGCardViewController/views/TGCardDefaultTitleView.swift index 3f150af..90d2262 100644 --- a/Sources/TGCardViewController/views/TGCardDefaultTitleView.swift +++ b/Sources/TGCardViewController/views/TGCardDefaultTitleView.swift @@ -17,6 +17,10 @@ class TGCardDefaultTitleView: UIView, TGPreferrableView { @IBOutlet weak var dismissButton: UIButton! @IBOutlet weak var accessoryViewContainer: UIView! + @IBOutlet weak var topLevelTopConstraint: NSLayoutConstraint! + @IBOutlet weak var labelStackLeadingConstraint: NSLayoutConstraint! + @IBOutlet weak var innerTrailingConstraint: NSLayoutConstraint! + // By default, the top level stack snaps to all edges // of the default title view. The space to the bottom // edge is exposed, so that we can allow the accessory @@ -26,6 +30,18 @@ class TGCardDefaultTitleView: UIView, TGPreferrableView { override func awakeFromNib() { super.awakeFromNib() + if #available(iOS 26.0, *) { + topLevelTopConstraint.constant = 0 + labelStackLeadingConstraint.constant = 22 + innerTrailingConstraint.constant = 37 // 9 pixels extra space to the side + + titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 32).isActive = true + } else { + topLevelTopConstraint.constant = 8 + labelStackLeadingConstraint.constant = 16 + innerTrailingConstraint.constant = 28 + } + // Here we set the minimum width and height to provide sufficient hit // target. The priority is lowered because we may need to hide the // button and in such case, stack view will reduce its size to zero, @@ -116,12 +132,7 @@ class TGCardDefaultTitleView: UIView, TGPreferrableView { if isInitial { dismissButton.isHidden = false - let closeButtonImage = TGCardStyleKit.imageOfCardCloseIcon( - closeButtonBackground: style.closeButtonBackgroundColor, - closeButtonCross: style.closeButtonCrossColor - ) - dismissButton.setImage(closeButtonImage, for: .normal) - dismissButton.setTitle(nil, for: .normal) + TGCard.configureCloseButton(dismissButton, style: style) } } diff --git a/Sources/TGCardViewController/views/TGCardDefaultTitleView.xib b/Sources/TGCardViewController/views/TGCardDefaultTitleView.xib index 178086c..2dd4b4e 100644 --- a/Sources/TGCardViewController/views/TGCardDefaultTitleView.xib +++ b/Sources/TGCardViewController/views/TGCardDefaultTitleView.xib @@ -1,9 +1,9 @@ - + - + @@ -17,19 +17,19 @@ - + - + - + - + @@ -76,11 +76,14 @@ + + + From 6356b01c0561a49cb3d954ae5e9451d9cf5be66d Mon Sep 17 00:00:00 2001 From: Adrian Schoenig Date: Thu, 4 Dec 2025 13:58:36 +1100 Subject: [PATCH 2/6] Fine intermediate --- .../TGCardViewController.swift | 34 +++++++++++++-- .../TGCardViewController.xib | 43 ++++++++++++------- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/Sources/TGCardViewController/TGCardViewController.swift b/Sources/TGCardViewController/TGCardViewController.swift index bfdb82c..103f1dd 100644 --- a/Sources/TGCardViewController/TGCardViewController.swift +++ b/Sources/TGCardViewController/TGCardViewController.swift @@ -155,6 +155,7 @@ open class TGCardViewController: UIViewController { @IBOutlet weak var mapShadow: UIView! @IBOutlet weak var cardWrapperShadow: UIView! @IBOutlet public weak var cardWrapperContent: UIView! + @IBOutlet weak var cardWrapperEffectView: UIVisualEffectView! fileprivate weak var cardTransitionShadow: UIView? @IBOutlet weak var statusBarBlurView: UIVisualEffectView! @IBOutlet weak var topFloatingView: UIStackView! @@ -290,6 +291,10 @@ open class TGCardViewController: UIViewController { override open func viewDidLoad() { super.viewDidLoad() + if #available(iOS 26.0, *) { + statusBarBlurView.isHidden = true + } + // mode-specific styling TGCornerView.roundedCorners = mode == .floating cardWrapperDynamicLeadingConstraint.isActive = mode == .floating @@ -309,6 +314,17 @@ open class TGCardViewController: UIViewController { mapView.translatesAutoresizingMaskIntoConstraints = false mapViewController.didMove(toParent: self) +#if compiler(>=6.2) // Xcode 26 + if #available(iOS 26.0, *) { + cardWrapperEffectView.effect = UIGlassEffect(style: .regular) + cardWrapperEffectView.cornerConfiguration = .corners(radius: 12) + } else { + cardWrapperEffectView.effect = nil + } +#else + cardWrapperEffectView.effect = nil +#endif + setupGestures() // Create the default buttons @@ -501,6 +517,7 @@ open class TGCardViewController: UIViewController { statusBarBlurHeightConstraint.constant = topOverlap topCardView?.adjustContentAlpha(to: cardPosition == .collapsed ? 0 : 1) + topCardView?.setSeparatorVisibility(forceHidden: cardPosition == .collapsed) updateFloatingViewsConstraints() updateTopInfoViewConstraints() view.setNeedsUpdateConstraints() @@ -793,7 +810,7 @@ extension TGCardViewController { let cardView = top.buildCardView() cards.append( (top, animateTo.position, cardView) ) - if let cardView = cardView { + if let cardView { cardView.dismissButton?.addTarget(self, action: #selector(closeTapped(sender:)), for: .touchUpInside) let showClose = (delegate != nil || cards.count > 1) && top.showCloseButton cardView.updateDismissButton(show: showClose, isSpringLoaded: navigationButtonsAreSpringLoaded) @@ -804,6 +821,7 @@ extension TGCardViewController { // which is an additional 34px on iPhone X, we will see part of the card content // coming through. cardView.adjustContentAlpha(to: animateTo.position == .collapsed ? 0 : 1) + cardView.setSeparatorVisibility(forceHidden: animateTo.position == .collapsed) // This allows us to continuously pull down the card view while its // content is scrolled to the top. Note this only applies when the @@ -910,7 +928,7 @@ extension TGCardViewController { let cardAnimations = { self.toggleCardWrappers(hide: cardView == nil, prepareOnly: true) - guard let cardView = cardView else { return } + guard let cardView else { return } self.updateMapShadow(for: animateTo.position) cardView.frame = self.cardWrapperContent.bounds self.cardTransitionShadow?.alpha = 0.15 @@ -1360,6 +1378,7 @@ extension TGCardViewController { animations: { self.updateMapShadow(for: snapTo.position) self.topCardView?.adjustContentAlpha(to: snapTo.position == .collapsed ? 0 : 1) + self.topCardView?.setSeparatorVisibility(forceHidden: snapTo.position == .collapsed) self.updateFloatingViewsVisibility(for: snapTo.position) self.view.layoutIfNeeded() self.mapViewController.additionalSafeAreaInsets = mapInset @@ -1569,6 +1588,7 @@ extension TGCardViewController { animations: { self.updateMapShadow(for: animateTo.position) self.topCardView?.adjustContentAlpha(to: animateTo.position == .collapsed ? 0 : 1) + self.topCardView?.setSeparatorVisibility(forceHidden: animateTo.position == .collapsed) self.updateFloatingViewsVisibility(for: animateTo.position) self.view.layoutIfNeeded() self.mapViewController.additionalSafeAreaInsets = mapInsets @@ -1736,12 +1756,20 @@ extension TGCardViewController { if let customTint = buttonStyle.tintColor { view.tintColor = customTint + } else { + view.tintColor = nil } guard let visualView = view as? UIVisualEffectView else { return assertionFailure() } - if buttonStyle.isTranslucent { + if #available(iOS 26.0, *) { +#if compiler(>=6.2) // Xcode 26 proxy + visualView.effect = UIGlassEffect(style: .regular) +#endif + visualView.layer.borderWidth = 0 + visualView.layer.shadowOpacity = 0 + } else if buttonStyle.isTranslucent { visualView.effect = UIBlurEffect(style: .regular) visualView.layer.borderWidth = 0 visualView.layer.shadowOpacity = 0 diff --git a/Sources/TGCardViewController/TGCardViewController.xib b/Sources/TGCardViewController/TGCardViewController.xib index 1f8b506..d46c90c 100644 --- a/Sources/TGCardViewController/TGCardViewController.xib +++ b/Sources/TGCardViewController/TGCardViewController.xib @@ -1,9 +1,9 @@ - + - + @@ -17,6 +17,7 @@ + @@ -82,19 +83,19 @@ - + - + - + - + - + @@ -107,7 +108,7 @@ - + @@ -140,7 +141,7 @@ - + @@ -166,6 +167,14 @@ + + + + + + + + @@ -174,22 +183,26 @@ + + + + - + - + @@ -255,13 +268,13 @@ - + - + @@ -271,11 +284,11 @@ - + - + From 793cf5fddf8b4d7449d660da3af306c111aac070 Mon Sep 17 00:00:00 2001 From: Adrian Schoenig Date: Thu, 4 Dec 2025 14:31:07 +1100 Subject: [PATCH 3/6] Fix the scroll don't tap issue --- Sources/TGCardViewController/TGCardViewController.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/TGCardViewController/TGCardViewController.swift b/Sources/TGCardViewController/TGCardViewController.swift index 103f1dd..171fa0f 100644 --- a/Sources/TGCardViewController/TGCardViewController.swift +++ b/Sources/TGCardViewController/TGCardViewController.swift @@ -2132,7 +2132,14 @@ extension TGCardViewController: UIGestureRecognizerDelegate { scrollView.isScrollEnabled = true } - return false + // iOS 26 and up automatically handles the dragging the outer card while + // we do the inner pan. So we can let it pass. This works in combination + // with the early exist in handleInnerPan. + if #available(iOS 26.0, *) { + return scrollView.contentOffset.y <= 0 + } else { + return false + } } } From 49cdc4dd3224a8b147300c3aa62a3d79710ccd89 Mon Sep 17 00:00:00 2001 From: Mariano Tucat Date: Sun, 16 Nov 2025 05:23:43 -0300 Subject: [PATCH 4/6] Add SECURITY.md (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add SECURITY.md placeholder * Revise supported versions and update last modified date Updated supported versions and last updated date in SECURITY.md. --------- Co-authored-by: Adrian Schönig --- .github/workflows/swift.yml | 42 ++++++------ SECURITY.md | 132 ++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 SECURITY.md diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 1cf5463..f299c07 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -11,32 +11,32 @@ jobs: runs-on: macos-14 steps: - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: 15.3 # latest-stable - - uses: actions/checkout@v4 - - name: Build TGCardVC - run: xcodebuild -workspace . -scheme TGCardViewController -destination 'platform=iOS Simulator,name=iPhone 14' + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: 26.0.1 # 26.1 is missing simulators on GitHub, as of November 2025 + - uses: actions/checkout@v4 + - name: Build TGCardVC + run: set -o pipefail && xcodebuild -workspace . -scheme TGCardViewController -destination 'platform=iOS Simulator,name=iPhone 17' | xcbeautify build_xcode_ventura: runs-on: macos-13 steps: - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - uses: actions/checkout@v4 - - name: Build TGCardVC - run: xcodebuild -workspace . -scheme TGCardViewController -destination 'platform=iOS Simulator,name=iPhone 14' - + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: 26.0.1 # 26.1 is missing simulators on GitHub, as of November 2025 + - uses: actions/checkout@v4 + - name: Build TGCardVC + run: set -o pipefail && xcodebuild -workspace . -scheme TGCardViewController -destination 'platform=iOS Simulator,name=iPhone 16' | xcbeautify + examples: runs-on: macos-14 steps: - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: 15.3 # latest-stable - - uses: actions/checkout@v4 - - name: Build Example - run: | - cd Example - xcodebuild build -scheme 'Example' -destination 'platform=iOS Simulator,name=iPhone 14' + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: 26.0.1 # 26.1 is missing simulators on GitHub, as of November 2025 + - uses: actions/checkout@v4 + - name: Build Example + run: | + cd Example + set -o pipefail && xcodebuild build -scheme 'Example' -destination 'platform=iOS Simulator,name=iPhone 16' | xcbeautify diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ec7cc93 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,132 @@ +# Security Policy + +## Supported Versions + +We release patches for security vulnerabilities in the following versions: + +| Version | Supported | +| ------- | ------------------ | +| 2.3.0 | :white_check_mark: | +| < 2.3 | :x: | + +--- + +## Reporting a Vulnerability + +We take the security of our software seriously. If you believe you have found a security vulnerability, please report it to us as described below. + +### How to Report + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please email us at **[security@skedgo.com](mailto:security@skedgo.com)**. + +You should receive a response within 48 hours. If for some reason you do not, please follow up via email to ensure we received your original message. + +### What to Include + +Please include the following information in your report: + +- Type of issue (e.g., buffer overflow, SQL injection, cross-site scripting, etc.) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit it + +This information will help us triage your report more quickly. + +### What to Expect + +After you submit a report, we will: + +1. **Acknowledge** your email within 48 hours +2. **Investigate** the issue and confirm the vulnerability +3. **Keep you informed** of our progress toward a fix +4. **Release** a security patch as appropriate +5. **Credit** you in our release notes (if you wish to be named) + +--- + +## Security Best Practices for Contributors + +If you're contributing to this project, please follow these security guidelines: + +### Code Review +- All code changes must go through Pull Requests +- PRs require approval from at least one maintainer before merging +- No direct commits to `main` or protected branches + +### Dependencies +- Keep dependencies up to date +- Review dependency changes for known vulnerabilities +- Use automated tools like Dependabot to monitor security issues + +### Secrets Management +- **Never** commit credentials, API keys, tokens, or other secrets +- Use environment variables or secure secret management systems +- Review commits for accidentally included secrets before pushing + +### Secure Coding +- Follow [OWASP Top 10](https://owasp.org/www-project-top-ten/) best practices +- Validate and sanitize all user inputs +- Use parameterized queries to prevent SQL injection +- Implement proper authentication and authorization +- Use HTTPS/TLS for all network communications + +--- + +## Security Features + +This project includes the following security measures: + +- **Dependabot alerts** enabled for vulnerable dependencies +- **Secret scanning** enabled to prevent credential leaks +- **Code review** required for all changes +- **Branch protection** rules enforced on main branches + +--- + +## Disclosure Policy + +We follow a **coordinated disclosure** approach: + +1. Security issues are privately investigated and patched +2. A security advisory is prepared but not published +3. We notify relevant parties (e.g., major users, downstream projects) +4. A patch release is made available +5. The security advisory is published after users have had time to update + +We aim to complete this process within 90 days of the initial report, though complex issues may take longer. + +--- + +## Security Update Policy + +Security updates are released as: +- **Patch versions** (x.x.X) for currently supported versions +- **Security advisories** published on our GitHub Security Advisories page +- **Release notes** clearly marking security-related changes + +--- + +## Additional Resources + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [CWE Top 25 Most Dangerous Software Weaknesses](https://cwe.mitre.org/top25/) +- [GitHub Security Best Practices](https://docs.github.com/en/code-security) + +--- + +## Contact + +For general security questions or concerns, please contact: +- **Email:** [security@skedgo.com](mailto:security@skedgo.com) + +--- + +> **Last Updated:** 14 November 2025 +> **Version:** 1.0 +> +> This security policy is maintained by the repository maintainers and reviewed regularly. From 9e03d26f505fce979c555c5cd7efc98ad439644a Mon Sep 17 00:00:00 2001 From: Adrian Schoenig Date: Thu, 4 Dec 2025 16:40:39 +1100 Subject: [PATCH 5/6] Add visual effect view behind header view, manged by TGCardVC --- .../TGCardViewController.swift | 22 +++++-- .../TGCardViewController.xib | 61 +++++++++++-------- .../cards/TGPageHeaderView.xib | 12 ++-- 3 files changed, 59 insertions(+), 36 deletions(-) diff --git a/Sources/TGCardViewController/TGCardViewController.swift b/Sources/TGCardViewController/TGCardViewController.swift index 171fa0f..4b6c7d7 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! @@ -318,11 +319,16 @@ open class TGCardViewController: UIViewController { if #available(iOS 26.0, *) { cardWrapperEffectView.effect = UIGlassEffect(style: .regular) 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() @@ -1903,7 +1909,7 @@ extension TGCardViewController { private func updateHeaderStyle() { @MainActor func applyCornerStyle(to view: UIView) { - let radius: CGFloat = 16 + let radius: CGFloat = 12 let roundAllCorners = cardIsNextToMap(in: traitCollection) view.layer.maskedCorners = roundAllCorners @@ -1921,11 +1927,15 @@ extension TGCardViewController { updateStatusBar(headerIsVisible: isShowingHeader) - // same shadow as for card wrapper - headerView.layer.shadowColor = UIColor.black.cgColor - headerView.layer.shadowOffset = .zero - headerView.layer.shadowRadius = 12 - headerView.layer.shadowOpacity = 0.5 + if #available(iOS 26.0, *) { + // No shadow. Rely on headerEffectView + } else { + // same shadow as for card wrapper + headerView.layer.shadowColor = UIColor.black.cgColor + headerView.layer.shadowOffset = .zero + headerView.layer.shadowRadius = 12 + headerView.layer.shadowOpacity = 0.5 + } } private func updateHeaderConstraints() { diff --git a/Sources/TGCardViewController/TGCardViewController.xib b/Sources/TGCardViewController/TGCardViewController.xib index d46c90c..a226e25 100644 --- a/Sources/TGCardViewController/TGCardViewController.xib +++ b/Sources/TGCardViewController/TGCardViewController.xib @@ -1,6 +1,6 @@ - + @@ -22,6 +22,7 @@ + @@ -43,29 +44,29 @@ - + - + - + - + - + - + @@ -83,19 +84,19 @@ - + - + - + - + - + @@ -108,7 +109,7 @@ - + @@ -126,8 +127,16 @@ + + + + + + + + - + @@ -135,13 +144,13 @@ - + - + @@ -165,18 +174,18 @@ - + - + - + - + @@ -210,6 +219,7 @@ + @@ -232,6 +242,7 @@ + @@ -239,12 +250,14 @@ + + @@ -293,9 +306,9 @@ - + - + @@ -317,7 +330,7 @@ - + diff --git a/Sources/TGCardViewController/cards/TGPageHeaderView.xib b/Sources/TGCardViewController/cards/TGPageHeaderView.xib index 0652e1d..8021910 100644 --- a/Sources/TGCardViewController/cards/TGPageHeaderView.xib +++ b/Sources/TGCardViewController/cards/TGPageHeaderView.xib @@ -1,9 +1,9 @@ - + - + @@ -14,14 +14,14 @@ - +