|
| 1 | +// Copyright © 2025 Booket. All rights reserved |
| 2 | + |
| 3 | +import BKDesign |
| 4 | +import UIKit |
| 5 | +import SnapKit |
| 6 | + |
| 7 | +final class NotificationSettingsView: BaseView { |
| 8 | + // MARK: - Closures |
| 9 | + var onNotificationToggleChanged: ((Bool) -> Void)? |
| 10 | + var onPermissionRequestViewTapped: (() -> Void)? |
| 11 | + |
| 12 | + // MARK: - UI Components |
| 13 | + private let scrollView: UIScrollView = { |
| 14 | + let scrollView = UIScrollView() |
| 15 | + scrollView.alwaysBounceVertical = true |
| 16 | + scrollView.showsVerticalScrollIndicator = false |
| 17 | + return scrollView |
| 18 | + }() |
| 19 | + |
| 20 | + private let contentView = UIView() |
| 21 | + |
| 22 | + private let permissionRequestContainer = UIView() |
| 23 | + private let permissionRequestView = UIView() |
| 24 | + private let permissionRequestLabelStack: UIStackView = { |
| 25 | + let stackView = UIStackView() |
| 26 | + stackView.axis = .vertical |
| 27 | + stackView.spacing = LayoutConstants.permissionRequestStackViewSpacing |
| 28 | + stackView.alignment = .leading |
| 29 | + return stackView |
| 30 | + }() |
| 31 | + |
| 32 | + private let permissionRequestTitle = BKLabel( |
| 33 | + text: "알림을 켜주세요.", |
| 34 | + fontStyle: .body1(weight: .semiBold), |
| 35 | + color: .bkContentColor(.brand) |
| 36 | + ) |
| 37 | + |
| 38 | + private let permissionRequestContent = BKLabel( |
| 39 | + text: """ |
| 40 | + 기기 설정에서 Reed 알림을 설정하세요. |
| 41 | + 독서 기록에 도움되는 알림을 받을 수 있어요. |
| 42 | + """, |
| 43 | + fontStyle: .label2(weight: .regular), |
| 44 | + color: .bkContentColor(.tertiary) |
| 45 | + ) |
| 46 | + |
| 47 | + private let permissionRequestButton: UIImageView = { |
| 48 | + let imageView = UIImageView(image: BKImage.Icon.chevronRight) |
| 49 | + imageView.tintColor = .bkContentColor(.brand) |
| 50 | + return imageView |
| 51 | + }() |
| 52 | + |
| 53 | + private let permissionToggleContainer = UIView() |
| 54 | + private let permissionToggleLabelStack: UIStackView = { |
| 55 | + let stackView = UIStackView() |
| 56 | + stackView.axis = .vertical |
| 57 | + stackView.spacing = .zero |
| 58 | + stackView.alignment = .leading |
| 59 | + return stackView |
| 60 | + }() |
| 61 | + |
| 62 | + private let permissionToggleTitle = BKLabel( |
| 63 | + text: "알림 받기", |
| 64 | + fontStyle: .body1(weight: .medium), |
| 65 | + color: .bkContentColor(.primary) |
| 66 | + ) |
| 67 | + |
| 68 | + private let permissionToggleContent = BKLabel( |
| 69 | + text: "리드에서 알림을 보내드려요.", |
| 70 | + fontStyle: .label1(weight: .regular), |
| 71 | + color: .bkContentColor(.tertiary) |
| 72 | + ) |
| 73 | + |
| 74 | + private let permissionToggle: UISwitch = { |
| 75 | + let toggle = UISwitch() |
| 76 | + toggle.onTintColor = .bkBackgroundColor(.primary) |
| 77 | + return toggle |
| 78 | + }() |
| 79 | + |
| 80 | + private var permissionToggleContainerTopToRequestConstraint: Constraint? |
| 81 | + private var permissionToggleContainerTopToSafeAreaConstraint: Constraint? |
| 82 | + private var isAnimating = false |
| 83 | + |
| 84 | + override func setupView() { |
| 85 | + addSubview(scrollView) |
| 86 | + scrollView.addSubview(contentView) |
| 87 | + contentView.addSubviews(permissionRequestContainer, permissionToggleContainer) |
| 88 | + |
| 89 | + permissionRequestContainer.addSubview(permissionRequestView) |
| 90 | + permissionRequestView.addSubviews(permissionRequestLabelStack, permissionRequestButton) |
| 91 | + [permissionRequestTitle, permissionRequestContent] |
| 92 | + .forEach(permissionRequestLabelStack.addArrangedSubview(_:)) |
| 93 | + |
| 94 | + permissionToggleContainer.addSubviews(permissionToggleLabelStack, permissionToggle) |
| 95 | + [permissionToggleTitle, permissionToggleContent] |
| 96 | + .forEach(permissionToggleLabelStack.addArrangedSubview(_:)) |
| 97 | + } |
| 98 | + |
| 99 | + override func setupLayout() { |
| 100 | + scrollView.snp.makeConstraints { |
| 101 | + $0.edges.equalToSuperview() |
| 102 | + } |
| 103 | + |
| 104 | + contentView.snp.makeConstraints { |
| 105 | + $0.edges.equalToSuperview() |
| 106 | + $0.width.equalToSuperview() |
| 107 | + } |
| 108 | + |
| 109 | + permissionRequestContainer.snp.makeConstraints { |
| 110 | + $0.top.equalTo(contentView.safeAreaLayoutGuide.snp.top) |
| 111 | + $0.leading.trailing.equalToSuperview() |
| 112 | + } |
| 113 | + |
| 114 | + permissionRequestView.snp.makeConstraints { |
| 115 | + $0.verticalEdges.equalToSuperview() |
| 116 | + .inset(LayoutConstants.permissionRequestViewVerticalInset) |
| 117 | + $0.horizontalEdges.equalToSuperview() |
| 118 | + .inset(LayoutConstants.permissionRequestViewHorizontalInset) |
| 119 | + } |
| 120 | + |
| 121 | + permissionRequestLabelStack.snp.makeConstraints { |
| 122 | + $0.verticalEdges.equalToSuperview() |
| 123 | + .inset(LayoutConstants.permissionRequestLabelPadding) |
| 124 | + $0.leading.equalToSuperview() |
| 125 | + .inset(LayoutConstants.commonHorizontalInset) |
| 126 | + } |
| 127 | + |
| 128 | + permissionRequestButton.snp.makeConstraints { |
| 129 | + $0.centerY.equalToSuperview() |
| 130 | + $0.trailing.equalToSuperview() |
| 131 | + .inset(LayoutConstants.commonHorizontalInset) |
| 132 | + } |
| 133 | + |
| 134 | + permissionToggleContainer.snp.makeConstraints { |
| 135 | + self.permissionToggleContainerTopToRequestConstraint = |
| 136 | + $0.top |
| 137 | + .equalTo(permissionRequestContainer.snp.bottom) |
| 138 | + .offset(LayoutConstants.toggleSpacingWithRequest) |
| 139 | + .constraint |
| 140 | + self.permissionToggleContainerTopToSafeAreaConstraint = |
| 141 | + $0.top |
| 142 | + .equalTo(contentView.safeAreaLayoutGuide.snp.top) |
| 143 | + .inset(LayoutConstants.toggleEmptySpacing) |
| 144 | + .constraint |
| 145 | + $0.leading.trailing.equalToSuperview() |
| 146 | + .inset(LayoutConstants.commonHorizontalInset) |
| 147 | + $0.bottom.lessThanOrEqualToSuperview() |
| 148 | + } |
| 149 | + |
| 150 | + permissionToggleContainerTopToRequestConstraint?.activate() |
| 151 | + permissionToggleContainerTopToSafeAreaConstraint?.deactivate() |
| 152 | + |
| 153 | + permissionToggleLabelStack.snp.makeConstraints { |
| 154 | + $0.verticalEdges.equalToSuperview() |
| 155 | + .inset(LayoutConstants.permissionToggleLabelPadding) |
| 156 | + $0.leading.equalToSuperview() |
| 157 | + } |
| 158 | + |
| 159 | + permissionToggle.snp.makeConstraints { |
| 160 | + $0.centerY.equalToSuperview() |
| 161 | + $0.trailing.equalToSuperview() |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + override func configure() { |
| 166 | + permissionRequestView.backgroundColor = .bkBaseColor(.secondary) |
| 167 | + permissionRequestView.layer.cornerRadius = BKRadius.medium |
| 168 | + permissionToggle.addTarget(self, action: #selector(didTapPermissionToggle), for: .valueChanged) |
| 169 | + |
| 170 | + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPermissionRequestView)) |
| 171 | + permissionRequestView.addGestureRecognizer(tapGesture) |
| 172 | + permissionRequestView.isUserInteractionEnabled = true |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +// MARK: - Public Methods |
| 177 | +extension NotificationSettingsView { |
| 178 | + func updateNotificationToggle(isEnabled: Bool, animated: Bool = true) { |
| 179 | + guard permissionToggle.isOn != isEnabled else { return } |
| 180 | + |
| 181 | + permissionToggle.setOn(isEnabled, animated: false) |
| 182 | + updatePermissionRequestVisibility(isToggleOn: isEnabled, animated: animated) |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +private extension NotificationSettingsView { |
| 187 | + @objc |
| 188 | + func didTapPermissionToggle() { |
| 189 | + onNotificationToggleChanged?(permissionToggle.isOn) |
| 190 | + updatePermissionRequestVisibility(isToggleOn: permissionToggle.isOn, animated: true) |
| 191 | + } |
| 192 | + |
| 193 | + @objc |
| 194 | + func didTapPermissionRequestView() { |
| 195 | + onPermissionRequestViewTapped?() |
| 196 | + } |
| 197 | + |
| 198 | + func updatePermissionRequestVisibility(isToggleOn: Bool, animated: Bool) { |
| 199 | + /// 초기엔 애니메이션 없이 즉시 화면 로드 |
| 200 | + if !animated { |
| 201 | + if isToggleOn { |
| 202 | + self.permissionRequestContainer.isHidden = true |
| 203 | + self.permissionToggleContainerTopToRequestConstraint?.deactivate() |
| 204 | + self.permissionToggleContainerTopToSafeAreaConstraint?.activate() |
| 205 | + } else { |
| 206 | + self.permissionToggleContainerTopToSafeAreaConstraint?.deactivate() |
| 207 | + self.permissionToggleContainerTopToRequestConstraint?.activate() |
| 208 | + self.permissionRequestContainer.isHidden = false |
| 209 | + } |
| 210 | + self.layoutIfNeeded() |
| 211 | + return |
| 212 | + } |
| 213 | + |
| 214 | + guard !isAnimating else { return } |
| 215 | + /// 탭 하여 권한 안내 화면이 나오는 경우 애니메이션이 없으면 부자연스러우므로 애니메이션 적용 |
| 216 | + isAnimating = true |
| 217 | + |
| 218 | + if isToggleOn { |
| 219 | + UIView.animate(withDuration: 0.3, animations: { |
| 220 | + self.permissionRequestContainer.isHidden = true |
| 221 | + self.permissionToggleContainerTopToRequestConstraint?.deactivate() |
| 222 | + self.permissionToggleContainerTopToSafeAreaConstraint?.activate() |
| 223 | + self.layoutIfNeeded() |
| 224 | + }, completion: { _ in |
| 225 | + self.isAnimating = false |
| 226 | + }) |
| 227 | + } else { |
| 228 | + UIView.animate(withDuration: 0.3, animations: { |
| 229 | + self.permissionToggleContainerTopToSafeAreaConstraint?.deactivate() |
| 230 | + self.permissionToggleContainerTopToRequestConstraint?.activate() |
| 231 | + self.layoutIfNeeded() |
| 232 | + }, completion: { _ in |
| 233 | + self.permissionRequestContainer.isHidden = false |
| 234 | + self.isAnimating = false |
| 235 | + }) |
| 236 | + } |
| 237 | + } |
| 238 | +} |
| 239 | + |
| 240 | +private extension NotificationSettingsView { |
| 241 | + enum LayoutConstants { |
| 242 | + static let permissionRequestStackViewSpacing = BKInset.inset05 |
| 243 | + static let permissionRequestViewVerticalInset = BKInset.inset2 |
| 244 | + static let permissionRequestViewHorizontalInset = BKInset.inset5 |
| 245 | + static let commonHorizontalInset = BKInset.inset5 |
| 246 | + static let toggleEmptySpacing = BKSpacing.spacing4 |
| 247 | + static let toggleSpacingWithRequest = BKSpacing.spacing2 |
| 248 | + static let permissionToggleLabelPadding = BKSpacing.spacing4 |
| 249 | + static let permissionRequestLabelPadding = BKSpacing.spacing6 |
| 250 | + } |
| 251 | +} |
0 commit comments