diff --git a/Kickstarter-iOS/Features/ProjectPage/Views/ProjectPageNavigationBarView.swift b/Kickstarter-iOS/Features/ProjectPage/Views/ProjectPageNavigationBarView.swift index 6b9245e302..42902118c0 100644 --- a/Kickstarter-iOS/Features/ProjectPage/Views/ProjectPageNavigationBarView.swift +++ b/Kickstarter-iOS/Features/ProjectPage/Views/ProjectPageNavigationBarView.swift @@ -3,6 +3,7 @@ import KsApi import Library import PassKit import Prelude +import SwiftUI import UIKit protocol ProjectPageNavigationBarViewDelegate: AnyObject { @@ -26,17 +27,24 @@ final class ProjectPageNavigationBarView: UIView { private lazy var navigationShareButton: UIButton = { UIButton(type: .custom) }() - private lazy var navigationCloseButton: UIButton = { - let buttonView = UIButton(type: .custom) - |> UIButton.lens.title(for: .normal) .~ nil - |> UIButton.lens.image(for: .normal) .~ image(named: "icon--cross") - |> UIButton.lens.tintColor .~ LegacyColors.ksr_support_700.uiColor() - |> UIButton.lens.accessibilityLabel %~ { _ in Strings.accessibility_projects_buttons_close() } - |> UIButton.lens.accessibilityHint %~ { _ in Strings.Closes_project() } - - return buttonView + private lazy var closeButtonHostingController: UIHostingController = { + let closeButtonView = CloseButtonView { [weak self] in + self?.closeButtonTapped() + } + let hostingController = UIHostingController(rootView: closeButtonView) + hostingController.view.backgroundColor = UIColor.clear + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + return hostingController }() + private var navigationCloseButton: UIView { + guard let view = self.closeButtonHostingController.view else { + assertionFailure("UIHostingController view should always exist") + return UIView() + } + return view + } + private lazy var navigationSaveButton: UIButton = { UIButton(type: .custom) }() private lazy var rootStackView: UIStackView = { @@ -149,7 +157,13 @@ final class ProjectPageNavigationBarView: UIView { private func setupConstraints() { _ = (self.rootStackView, self) |> ksr_addSubviewToParent() - |> ksr_constrainViewToEdgesInParent() + + NSLayoutConstraint.activate([ + self.rootStackView.topAnchor.constraint(equalTo: self.topAnchor), + self.rootStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), + self.rootStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + self.rootStackView.bottomAnchor.constraint(equalTo: self.bottomAnchor) + ]) _ = ( [ @@ -169,7 +183,9 @@ final class ProjectPageNavigationBarView: UIView { self.navigationSaveButton.widthAnchor .constraint(equalTo: self.navigationSaveButton.heightAnchor), self.navigationCloseButton.widthAnchor - .constraint(equalTo: self.navigationCloseButton.heightAnchor) + .constraint(equalToConstant: 44), + self.navigationCloseButton.heightAnchor + .constraint(equalToConstant: 44) ]) } @@ -192,11 +208,6 @@ final class ProjectPageNavigationBarView: UIView { } private func configureSubviews() { - self.addTargetAction( - buttonItem: self.navigationCloseButton, - targetAction: #selector(ProjectPageNavigationBarView.closeButtonTapped), - event: .touchUpInside - ) self.addTargetAction( buttonItem: self.navigationShareButton, targetAction: #selector(ProjectPageNavigationBarView.shareButtonTapped), diff --git a/Kickstarter-iOS/Features/ProjectPage/Views/__Snapshots__/ProjectPageNavigationBarViewTests/testNavigationBar_Unwatched_Success.unwatched.png b/Kickstarter-iOS/Features/ProjectPage/Views/__Snapshots__/ProjectPageNavigationBarViewTests/testNavigationBar_Unwatched_Success.unwatched.png index 2553858554..02ed0e885c 100644 Binary files a/Kickstarter-iOS/Features/ProjectPage/Views/__Snapshots__/ProjectPageNavigationBarViewTests/testNavigationBar_Unwatched_Success.unwatched.png and b/Kickstarter-iOS/Features/ProjectPage/Views/__Snapshots__/ProjectPageNavigationBarViewTests/testNavigationBar_Unwatched_Success.unwatched.png differ diff --git a/Kickstarter-iOS/Features/ProjectPage/Views/__Snapshots__/ProjectPageNavigationBarViewTests/testNavigationBar_Watched_Success.watched.png b/Kickstarter-iOS/Features/ProjectPage/Views/__Snapshots__/ProjectPageNavigationBarViewTests/testNavigationBar_Watched_Success.watched.png index 03950d6a78..0f5400f72d 100644 Binary files a/Kickstarter-iOS/Features/ProjectPage/Views/__Snapshots__/ProjectPageNavigationBarViewTests/testNavigationBar_Watched_Success.watched.png and b/Kickstarter-iOS/Features/ProjectPage/Views/__Snapshots__/ProjectPageNavigationBarViewTests/testNavigationBar_Watched_Success.watched.png differ diff --git a/Kickstarter-iOS/SharedViews/SwiftUIHelpers/CloseButtonView.swift b/Kickstarter-iOS/SharedViews/SwiftUIHelpers/CloseButtonView.swift new file mode 100644 index 0000000000..75858bf9b7 --- /dev/null +++ b/Kickstarter-iOS/SharedViews/SwiftUIHelpers/CloseButtonView.swift @@ -0,0 +1,24 @@ +import KDS +import Library +import SwiftUI + +/// A SwiftUI close button with liquid glass effect. +/// +/// This view is designed to be used within UIKit views via UIHostingController. +struct CloseButtonView: View { + let onClose: () -> Void + + var body: some View { + Button(action: self.onClose) { + Image(uiImage: image(named: "icon--cross") ?? UIImage()) + .renderingMode(.template) + .foregroundColor(Color(LegacyColors.ksr_support_700.uiColor())) + .frame(width: 20, height: 20) + } + .frame(width: 44, height: 44) + .contentShape(Circle()) + .glassedEffect(in: Circle(), interactive: true) + .accessibilityLabel(Strings.accessibility_projects_buttons_close()) + .accessibilityHint(Strings.Closes_project()) + } +} diff --git a/Kickstarter-iOS/SharedViews/SwiftUIHelpers/GlassEffect.swift b/Kickstarter-iOS/SharedViews/SwiftUIHelpers/GlassEffect.swift new file mode 100644 index 0000000000..79382bb1fe --- /dev/null +++ b/Kickstarter-iOS/SharedViews/SwiftUIHelpers/GlassEffect.swift @@ -0,0 +1,42 @@ +import SwiftUI + +extension View { + /// Applies a glass/liquid glass effect to a view within a shape. + /// - Parameters: + /// - shape: The shape to apply the glass effect within + /// - interactive: Whether the effect should be interactive (iOS 26+) + /// - Returns: A view with the glass effect applied + @ViewBuilder + func glassedEffect(in shape: some Shape, interactive: Bool = false) -> some View { + if #available(iOS 26.0, *) { + self.glassEffect(interactive ? .regular.interactive() : .regular, in: shape) + } else { + self.background { + shape.glassed() + } + } + } +} + +extension Shape { + /// Creates a glass-like effect fallback for iOS < 26 + fileprivate func glassed() -> some View { + self + .fill(.ultraThinMaterial) + .fill( + .linearGradient( + colors: [ + .primary.opacity(0.08), + .primary.opacity(0.05), + .primary.opacity(0.01), + .clear, + .clear, + .clear + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .stroke(.primary.opacity(0.2), lineWidth: 0.7) + } +} diff --git a/Kickstarter.xcodeproj/project.pbxproj b/Kickstarter.xcodeproj/project.pbxproj index 40216056c6..3f87ffd583 100644 --- a/Kickstarter.xcodeproj/project.pbxproj +++ b/Kickstarter.xcodeproj/project.pbxproj @@ -1242,6 +1242,8 @@ AAEABEA42DA8BDCD00B09688 /* ProjectWebURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEABEA32DA8BDCA00B09688 /* ProjectWebURL.swift */; }; AAEABEA62DA8BFA400B09688 /* ProjectPamphletMainCellProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEABEA52DA8BF9F00B09688 /* ProjectPamphletMainCellProperties.swift */; }; AAEABEA82DA8C03D00B09688 /* VideoViewProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEABEA72DA8C03B00B09688 /* VideoViewProperties.swift */; }; + B807448E2F0F2A9E00D9C667 /* CloseButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B807448C2F0F2A9E00D9C667 /* CloseButtonView.swift */; }; + B807448F2F0F2A9E00D9C667 /* GlassEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B807448D2F0F2A9E00D9C667 /* GlassEffect.swift */; }; D002CAE1218CF8F1009783F2 /* WatchProjectMutation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D002CAE0218CF8F1009783F2 /* WatchProjectMutation.swift */; }; D002CAE3218CF91D009783F2 /* WatchProjectInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = D002CAE2218CF91D009783F2 /* WatchProjectInput.swift */; }; D002CAE5218CF951009783F2 /* WatchProjectResponseEnvelope.swift in Sources */ = {isa = PBXBuildFile; fileRef = D002CAE4218CF951009783F2 /* WatchProjectResponseEnvelope.swift */; }; @@ -2999,6 +3001,8 @@ AAEABEA32DA8BDCA00B09688 /* ProjectWebURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectWebURL.swift; sourceTree = ""; }; AAEABEA52DA8BF9F00B09688 /* ProjectPamphletMainCellProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectPamphletMainCellProperties.swift; sourceTree = ""; }; AAEABEA72DA8C03B00B09688 /* VideoViewProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoViewProperties.swift; sourceTree = ""; }; + B807448C2F0F2A9E00D9C667 /* CloseButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseButtonView.swift; sourceTree = ""; }; + B807448D2F0F2A9E00D9C667 /* GlassEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassEffect.swift; sourceTree = ""; }; D002CAE0218CF8F1009783F2 /* WatchProjectMutation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchProjectMutation.swift; sourceTree = ""; }; D002CAE2218CF91D009783F2 /* WatchProjectInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchProjectInput.swift; sourceTree = ""; }; D002CAE4218CF951009783F2 /* WatchProjectResponseEnvelope.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchProjectResponseEnvelope.swift; sourceTree = ""; }; @@ -5881,6 +5885,8 @@ 19C8E56729A923F4007C3504 /* SwiftUIHelpers */ = { isa = PBXGroup; children = ( + B807448C2F0F2A9E00D9C667 /* CloseButtonView.swift */, + B807448D2F0F2A9E00D9C667 /* GlassEffect.swift */, 19C8E56829A9249D007C3504 /* TextInputFieldModifiers.swift */, ); path = SwiftUIHelpers; @@ -9307,6 +9313,8 @@ 19C8E56929A9249D007C3504 /* TextInputFieldModifiers.swift in Sources */, D6B6766720FF8D3C0082717D /* SettingsNewslettersViewController.swift in Sources */, 77E6440320F65074005F6B38 /* HelpViewController.swift in Sources */, + B807448E2F0F2A9E00D9C667 /* CloseButtonView.swift in Sources */, + B807448F2F0F2A9E00D9C667 /* GlassEffect.swift in Sources */, 8ACD29F7243E48260044BC17 /* PledgePaymentMethodsDataSource.swift in Sources */, 8A13D15F249559CB007E2C0B /* PledgeExpandableRewardsHeaderViewController.swift in Sources */, 77FA6CA920F3E18F00809E31 /* SettingsTableViewCell.swift in Sources */,