From 2d55c9ab58e0d06575d71b2ea321d22cd118de7c Mon Sep 17 00:00:00 2001 From: rlarjsdn3 Date: Fri, 21 Mar 2025 11:45:46 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=84=9C=EB=A1=9C=20=EB=8B=A4?= =?UTF-8?q?=EB=A5=B8=20=EB=AA=A8=EC=84=9C=EB=A6=AC=EC=97=90=20=EA=B0=81?= =?UTF-8?q?=EA=B8=B0=20=EB=8B=A4=EB=A5=B8=20CornerRadius=20=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EC=A4=84=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Example/Pods/Pods.xcodeproj/project.pbxproj | 4 ++ .../TSAlertController+ViewConfiguration.swift | 44 +++++++++++++- Sources/TSAlert/TSAlertController.swift | 20 ++++++- .../DefaultAlertView/DefaultAlertView.swift | 7 +-- .../Extensions/UIBezierPath+Extension.swift | 59 +++++++++++++++++++ .../Utils/Extensions/UIView+Extension.swift | 48 ++++++++++++++- 6 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 Sources/Utils/Extensions/UIBezierPath+Extension.swift diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index e04e56f..38ecb5e 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 620CE73B15C504B32DB819EAD5979CCB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384DDA2CB25005BD6479B5987C619DD4 /* Foundation.framework */; }; 655262048678A7D25BFF931ACB3B32C2 /* Pods-TSAlertController_Example-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 0276C3F2BA9F25C18E5983826ED58A78 /* Pods-TSAlertController_Example-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; B28E065F8186E15FB50B6583D8F5D387 /* TSAlertController-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 72485BDD7C643F0AAC9F1C24B82D07CB /* TSAlertController-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C700A0A02D8C716F00D5EFD8 /* UIBezierPath+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C700A09F2D8C716F00D5EFD8 /* UIBezierPath+Extension.swift */; }; C79BCEAB2D5D6F0000213363 /* SlideUpAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79BCE8B2D5D6F0000213363 /* SlideUpAnimator.swift */; }; C79BCEAC2D5D6F0000213363 /* TSAlertController+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79BCE972D5D6F0000213363 /* TSAlertController+Configuration.swift */; }; C79BCEAD2D5D6F0000213363 /* UIView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C79BCEA72D5D6F0000213363 /* UIView+Extension.swift */; }; @@ -88,6 +89,7 @@ 9E4F285A27DF239162DD0D9F1179C563 /* TSAlertController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TSAlertController.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A43131703CCAA6A7B9991C036977C19D /* Pods-TSAlertController_Tests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-TSAlertController_Tests.modulemap"; sourceTree = ""; }; BF03836614A23B9646B68177E4CAEC11 /* Pods-TSAlertController_Example.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-TSAlertController_Example.modulemap"; sourceTree = ""; }; + C700A09F2D8C716F00D5EFD8 /* UIBezierPath+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Extension.swift"; sourceTree = ""; }; C79BCE8A2D5D6F0000213363 /* FadeInAndScaleDownAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FadeInAndScaleDownAnimator.swift; sourceTree = ""; }; C79BCE8B2D5D6F0000213363 /* SlideUpAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideUpAnimator.swift; sourceTree = ""; }; C79BCE8D2D5D6F0000213363 /* TSAlertPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSAlertPresentationController.swift; sourceTree = ""; }; @@ -304,6 +306,7 @@ C79BCEA62D5D6F0000213363 /* UITextfield+Extension.swift */, C79BCEA72D5D6F0000213363 /* UIView+Extension.swift */, C79BCEC42D5D846E00213363 /* UIColor+Extension.swift */, + C700A09F2D8C716F00D5EFD8 /* UIBezierPath+Extension.swift */, ); path = Extensions; sourceTree = ""; @@ -544,6 +547,7 @@ buildActionMask = 2147483647; files = ( FCDD72576E464B783CB0432E06954BBA /* TSAlertController-dummy.m in Sources */, + C700A0A02D8C716F00D5EFD8 /* UIBezierPath+Extension.swift in Sources */, C79BCEAB2D5D6F0000213363 /* SlideUpAnimator.swift in Sources */, C79BCEAC2D5D6F0000213363 /* TSAlertController+Configuration.swift in Sources */, C79BCEAD2D5D6F0000213363 /* UIView+Extension.swift in Sources */, diff --git a/Sources/TSAlert/TSAlertController+ViewConfiguration.swift b/Sources/TSAlert/TSAlertController+ViewConfiguration.swift index 983f83e..1ca28b4 100644 --- a/Sources/TSAlert/TSAlertController+ViewConfiguration.swift +++ b/Sources/TSAlert/TSAlertController+ViewConfiguration.swift @@ -78,7 +78,7 @@ public extension TSAlertController { public var shadow: Shadow? /// The corner radius of the alert view. - public var cornerRadius: CGFloat + public var cornerRadius: CornerRadius /// The background color of the dimmed overlay behind the alert. public var dimmedBackgroundViewColor: Background? @@ -95,6 +95,46 @@ public extension TSAlertController { /// The axis layout of the button group (horizontal or vertical). public var buttonGroupAxis: ButtonGroupAxis + /// + public struct CornerRadius: ExpressibleByFloatLiteral { + + /// + public var topLeft: CGFloat + + /// + public var topRight: CGFloat + + /// + public var bottomLeft: CGFloat + + /// + public var bottomRight: CGFloat + + /// + public init(allCorners: CGFloat = 20) { + self.init(topLeft: allCorners, + topRight: allCorners, + bottomLeft: allCorners, + bottomRight: allCorners) + } + + /// + public init(topLeft: CGFloat = 20, + topRight: CGFloat = 20, + bottomLeft: CGFloat = 20, + bottomRight: CGFloat = 20) { + self.topLeft = topLeft + self.topRight = topRight + self.bottomLeft = bottomLeft + self.bottomRight = bottomRight + } + + /// + public init(floatLiteral value: FloatLiteralType) { + self.init(allCorners: value) + } + } + /// Defines the shadow properties of the alert. public struct Shadow { @@ -172,7 +212,7 @@ public extension TSAlertController { backgroundBorderColor: CGColor? = nil, backgroundBorderWidth: CGFloat = 0, shadow: Shadow? = nil, - cornerRadius: CGFloat = 20, + cornerRadius: CornerRadius = .init(allCorners: 20), dimmedBackgroundViewColor: Background? = .color(.black.withAlphaComponent(0.75)), margin: LayoutMargin = .init(), spacing: LayoutSpacing = .init(), diff --git a/Sources/TSAlert/TSAlertController.swift b/Sources/TSAlert/TSAlertController.swift index a74c405..bff41d3 100644 --- a/Sources/TSAlert/TSAlertController.swift +++ b/Sources/TSAlert/TSAlertController.swift @@ -233,6 +233,12 @@ public final class TSAlertController: UIViewController { activateFirstResponderIfNeeded() } + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + view.applyRoundCorners(viewConfiguration.cornerRadius) + } + public override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) @@ -303,6 +309,7 @@ public final class TSAlertController: UIViewController { /// based on `TSAlertController.Style`. /// This ensures that the alert is presented correctly and prevents unintended behavior. private func adjustViewConfigurationForcibly() { + adjustCornerRadiusForcibly() adjustButtonGroupAxisForcibly() adjustActionOrderForcibly() } @@ -333,6 +340,17 @@ public final class TSAlertController: UIViewController { } } + /// + private func adjustCornerRadiusForcibly() { + switch preferredStyle { + case .actionSheet: + viewConfiguration.cornerRadius.bottomLeft = 0 + viewConfiguration.cornerRadius.bottomRight = 0 + default: + break + } + } + private func initializeAlertView() { setupAlertView() setupConstraints() @@ -384,7 +402,7 @@ public final class TSAlertController: UIViewController { view.layer.borderColor = viewConfiguration.backgroundBorderColor view.layer.borderWidth = viewConfiguration.backgroundBorderWidth - view.layer.cornerRadius = viewConfiguration.cornerRadius + view.applyRoundCorners(viewConfiguration.cornerRadius) guard let shadow = viewConfiguration.shadow else { return } view.layer.shadowColor = shadow.color diff --git a/Sources/TSAlert/TSAlertView/DefaultAlertView/DefaultAlertView.swift b/Sources/TSAlert/TSAlertView/DefaultAlertView/DefaultAlertView.swift index ec6f941..fa10d1b 100644 --- a/Sources/TSAlert/TSAlertView/DefaultAlertView/DefaultAlertView.swift +++ b/Sources/TSAlert/TSAlertView/DefaultAlertView/DefaultAlertView.swift @@ -87,12 +87,11 @@ class DefaultAlertView: UIView, TSAlertView { let actionHeight: CGFloat = viewConfiguration.buttonHeight let spacing: CGFloat = viewConfiguration.spacing.buttonSpacing - let height: CGFloat = if !isEmpty { - isHorizontal + var height: CGFloat = 0 + if !isEmpty { + height = isHorizontal ? actionHeight : (actionHeight * actionsCount) + ((actionsCount - 1) * spacing) - } else { - 0 } buttonGroupView.setHeight(equalTo: height) } diff --git a/Sources/Utils/Extensions/UIBezierPath+Extension.swift b/Sources/Utils/Extensions/UIBezierPath+Extension.swift new file mode 100644 index 0000000..0a1e98f --- /dev/null +++ b/Sources/Utils/Extensions/UIBezierPath+Extension.swift @@ -0,0 +1,59 @@ +// +// UIBezierPath+Extension.swift +// TSAlertController +// +// Created by 김건우 on 3/21/25. +// + +import UIKit + +extension UIBezierPath { + + convenience init(roundedRect rect: CGRect, + topLeftRadius: CGFloat = .zero, + topRightRadius: CGFloat = .zero, + bottomLeftRadius: CGFloat = .zero, + bottomRightRadius: CGFloat = .zero) { + self.init() + + let path = CGMutablePath() + + let topLeftPoint = CGPoint(x: rect.minX, y: rect.minY) + let topRightPoint = CGPoint(x: rect.maxX, y: rect.minY) + let bottomLeftPoint = CGPoint(x: rect.minX, y: rect.maxY) + let bottomRightPoint = CGPoint(x: rect.maxX, y: rect.maxY) + + path.move(to: bottomLeftPoint) + + path.addArc(tangent1End: topLeftPoint, + tangent2End: topRightPoint, + radius: topLeftRadius) + + path.addArc(tangent1End: topRightPoint, + tangent2End: bottomRightPoint, + radius: topRightRadius) + + path.addArc(tangent1End: bottomRightPoint, + tangent2End: bottomLeftPoint, + radius: bottomRightRadius) + + path.addArc(tangent1End: bottomLeftPoint, + tangent2End: topLeftPoint, + radius: bottomLeftRadius) + + path.closeSubpath() + cgPath = path + } +} + + +extension CGMutablePath { + + func addLine(toX x: CGFloat, y: CGFloat) { + addLine(to: CGPoint(x: x, y: y)) + } + + func move(toX x: CGFloat, y: CGFloat) { + move(to: CGPoint(x: x, y: y)) + } +} diff --git a/Sources/Utils/Extensions/UIView+Extension.swift b/Sources/Utils/Extensions/UIView+Extension.swift index 6394b2a..4b96b73 100644 --- a/Sources/Utils/Extensions/UIView+Extension.swift +++ b/Sources/Utils/Extensions/UIView+Extension.swift @@ -219,7 +219,7 @@ extension UIView { if let configuration = configuration { blurEffectView.layer.borderColor = configuration.backgroundBorderColor blurEffectView.layer.borderWidth = configuration.backgroundBorderWidth - blurEffectView.layer.cornerRadius = configuration.cornerRadius +// blurEffectView.layer.cornerRadius = configuration.cornerRadius blurEffectView.layer.masksToBounds = true } insertSubview(blurEffectView, at: 0) @@ -242,14 +242,14 @@ extension UIView { locations) if let viewConfiguration = viewConfiguration { - gradientView.gradientLayer?.cornerRadius = viewConfiguration.cornerRadius +// gradientView.gradientLayer?.cornerRadius = viewConfiguration.cornerRadius gradientView.gradientLayer?.masksToBounds = true } insertSubview(gradientView, at: 0) gradientView.fill(to: self) } - class GradientView: UIView { + final class GradientView: UIView { let gradientLayer: CAGradientLayer? @@ -310,3 +310,45 @@ extension UIView { self.layer.anchorPoint = anchorPoint } } + +extension UIView { + + // MARK: - Apply Round Corners + + /// + func applyRoundCorners(_ cornerRadius: CGFloat) { + + applyRoundCorners(topLeftRadius: cornerRadius, + topRightRadius: cornerRadius, + bottomLeftRadius: cornerRadius, + bottomRightRadius: cornerRadius) + } + + /// + func applyRoundCorners(_ cornerRadius: TSAlertController.ViewConfiguration.CornerRadius) { + + applyRoundCorners(topLeftRadius: cornerRadius.topLeft, + topRightRadius: cornerRadius.topRight, + bottomLeftRadius: cornerRadius.bottomLeft, + bottomRightRadius: cornerRadius.bottomRight) + } + + /// + func applyRoundCorners(topLeftRadius: CGFloat, + topRightRadius: CGFloat, + bottomLeftRadius: CGFloat, + bottomRightRadius: CGFloat) { + + let roundedRect = self.bounds + + let path = UIBezierPath(roundedRect: roundedRect, + topLeftRadius: topLeftRadius, + topRightRadius: topRightRadius, + bottomLeftRadius: bottomLeftRadius, + bottomRightRadius: bottomRightRadius) + + let shapeLayer = CAShapeLayer() + shapeLayer.path = path.cgPath + layer.mask = shapeLayer + } +} From 71dfa54dfb36fecc5ad4bbe4a1cd92c136319eea Mon Sep 17 00:00:00 2001 From: rlarjsdn3 Date: Fri, 8 Aug 2025 21:18:19 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20iPad=20=ED=99=98=EA=B2=BD=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=82=A4=EB=B3=B4=EB=93=9C=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=EA=B0=80=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project.pbxproj | 6 ++ Example/Tests/TSAlertControllerTests.swift | 35 ++++++- Sources/TSAlert/TSAlertController.swift | 92 ++++++++----------- 3 files changed, 77 insertions(+), 56 deletions(-) diff --git a/Example/TSAlertController.xcodeproj/project.pbxproj b/Example/TSAlertController.xcodeproj/project.pbxproj index 0c0a4cf..3238df0 100644 --- a/Example/TSAlertController.xcodeproj/project.pbxproj +++ b/Example/TSAlertController.xcodeproj/project.pbxproj @@ -545,9 +545,12 @@ MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -569,9 +572,12 @@ MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; diff --git a/Example/Tests/TSAlertControllerTests.swift b/Example/Tests/TSAlertControllerTests.swift index 8adefbe..b265849 100644 --- a/Example/Tests/TSAlertControllerTests.swift +++ b/Example/Tests/TSAlertControllerTests.swift @@ -3,6 +3,8 @@ import XCTest final class TSAlertControllerTests: XCTestCase { + // MARK: - Verifies that TSAlertController initializes with correct title, message, and preferredStyle + func testTSAlertController_WhenInitialized_ShouldSetTitleMessageAndPreferredStyle() { // given let alertController = TSAlertController( @@ -22,6 +24,9 @@ final class TSAlertControllerTests: XCTestCase { XCTAssertEqual(mockViewController.presentViewControllerTarget, alertController) } + + // MARK: - Verifies that TSAlertController initializes with correct textfields + func testTSAlertController_WhenIntializedWithTextFields_ShouldSetTextFields() { // given let alertController = TSAlertController( @@ -41,6 +46,10 @@ final class TSAlertControllerTests: XCTestCase { XCTAssertEqual(mockViewController.presentViewControllerTarget, alertController) } + + + // MARK: - Verifies that TSAlertController initializes with the correct actions order and count + func testTSAlertController_WhenInitializedWithTwoButtonsAndAutomaticAxis_ShouldSetCorrectActionsOrderAndCount() { // given let alertController = TSAlertController( @@ -95,6 +104,9 @@ final class TSAlertControllerTests: XCTestCase { } + + + func testTSAlertController_WhenIntlaizedWithAlertStyle_ShouldSetDefaultConfiguration() { // given let alertController = TSAlertController( @@ -205,12 +217,11 @@ final class TSAlertControllerTests: XCTestCase { // then let config = alertController.configuration XCTAssertFalse(alertController.options.contains(.stretchyDragging)) - XCTAssertEqual(config.prefersGrabberVisible, true) XCTAssertEqual(mockViewController.presentViewControllerTarget, alertController) XCTAssertEqual(mockViewController.presentViewControllerTarget, alertController) } - func testTSAlertController_WhenIntializedWithActionSheetStyle_ShouldSetDefaultCconfiguration() { + func testTSAlertController_WhenIntializedWithActionSheetStyle_ShouldSetDefaultConfiguration() { // given let alertController = TSAlertController( title: "The title of the alert", @@ -263,8 +274,26 @@ final class TSAlertControllerTests: XCTestCase { // then let config = alertController.configuration XCTAssertFalse(alertController.options.contains(.interactiveScaleAndDrag)) - XCTAssertEqual(config.prefersGrabberVisible, true) XCTAssertEqual(mockViewController.presentViewControllerTarget, alertController) + } + + func testTSAlertController_WhenIntializedWithActionSheetStyle_ShouldSetCorrectViewConfigurationForcibly() { + // given + let alertController = TSAlertController( + title: "The title of the alert", + preferredStyle: .actionSheet + ) + let mockViewController = MockViewController() + + // when + alertController.viewConfiguration.cornerRadius = 25.0 + + mockViewController.present(alertController, animated: true) + + // then + let viewConfig = alertController.viewConfiguration + XCTAssertEqual(viewConfig.cornerRadius.bottomLeft, 0.0) + XCTAssertEqual(viewConfig.cornerRadius.bottomRight, 0.0) XCTAssertEqual(mockViewController.presentViewControllerTarget, alertController) } diff --git a/Sources/TSAlert/TSAlertController.swift b/Sources/TSAlert/TSAlertController.swift index bff41d3..0d1b2fe 100644 --- a/Sources/TSAlert/TSAlertController.swift +++ b/Sources/TSAlert/TSAlertController.swift @@ -236,6 +236,7 @@ public final class TSAlertController: UIViewController { public override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() + initialViewTopY = view.frame.origin.y view.applyRoundCorners(viewConfiguration.cornerRadius) } @@ -413,14 +414,12 @@ public final class TSAlertController: UIViewController { // Registers notifications to handle keyboard appearance and disappearance. private func registerKeyboardNotifications() { - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillShow(_:)), - name: UIResponder.keyboardWillShowNotification, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillHide(_:)), - name: UIResponder.keyboardWillHideNotification, - object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillChangeFrame(_:)), + name: UIResponder.keyboardWillChangeFrameNotification, + object: nil + ) } // Registers notifications to handle keyboard appearance and disappearance. @@ -691,7 +690,6 @@ private extension TSAlertController { } } - /// @objc private func handleStretchyDragging(_ gesture: UIPanGestureRecognizer) { let interpolationFactor: CGFloat = 0.025 @@ -729,57 +727,45 @@ private extension TSAlertController { private extension TSAlertController { - // Adjusts the alert’s position when the keyboard appears. - @objc func keyboardWillShow(_ notification: Notification) { - // If this method is not blocked, the alert view's position may animate incorrectly - // when the keyboard suggestion bar appears or disappears. - guard !isKeyboardShown else { return } + @objc func keyboardWillChangeFrame(_ notification: Notification) { guard let userInfo = notification.userInfo, let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect, - let keyboardAnimationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else { - return - } - - let viewHeight = view.frame.height - let keyboardTopY = keyboardFrame.origin.y - - let duration = keyboardAnimationDuration.doubleValue - let adjustedViewTopY = keyboardTopY - viewConfiguration.spacing.keyboardSpacing - viewHeight - - // Move the alert up only if the spacing is smaller than the configured value. - // If the space between the alert and the keyboard is greater than the configured value, the alert will not move. - if adjustedViewTopY < initialViewTopY { - UIView.animate(withDuration: duration, - delay: 0, - options: .curveEaseIn) { - self.view.frame.origin.y = adjustedViewTopY - } - } + let keyboardAnimationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber + else { return } - isKeyboardShown = true - } - - // Resets the alert’s position when the keyboard disappears. - @objc func keyboardWillHide(_ notification: Notification) { - // If this method is not blocked, the alert view's position may animate incorrectly - // when the keyboard suggestion bar appears or disappears. - guard isKeyboardShown else { return } + let alertViewHeight = view.frame.height // Alert 뷰의 전체 높이 + let keyboardOriginY = keyboardFrame.origin.y // 상위 뷰 기준, 키보드의 상단 y 좌표 - guard let userInfo = notification.userInfo, - let keyboardAnimationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else { - return - } let duration = keyboardAnimationDuration.doubleValue - - // Ensure the software keyboard is enabled for proper testing (Command + K). - UIView.animate(withDuration: duration, - delay: 0, - options: .curveEaseIn) { - self.view.frame.origin.y = self.initialViewTopY + let adjustedViewTopY = keyboardOriginY - viewConfiguration.spacing.keyboardSpacing - alertViewHeight + // 키보드 상단 y 좌표에서 지정된 간격(keyboardSpacing)을 뺀 뒤, + // Alert 뷰의 전체 높이를 빼면 Alert 뷰의 새로운 y 좌표를 구할 수 있음 + + // 키보드가 나타난 상태일 때 실행 + if isKeyboardShown { + UIView.animate( + withDuration: duration, + delay: 0, + options: .curveEaseIn + ) { + self.view.frame.origin.y = self.initialViewTopY + } completion: { _ in + self.isKeyboardShown = false + } + } else if adjustedViewTopY < initialViewTopY && !isKeyboardShown { + // Move the alert up only if the spacing is smaller than the configured value. + // If the space between the alert and the keyboard is greater than the configured value, the alert will not move. + UIView.animate( + withDuration: duration, + delay: 0, + options: .curveEaseIn + ) { + self.view.frame.origin.y = adjustedViewTopY + } completion: { _ in + self.isKeyboardShown = true + } } - - isKeyboardShown = false } }