diff --git a/CTNotificationContent.xcodeproj/project.pbxproj b/CTNotificationContent.xcodeproj/project.pbxproj index 8fa421b..22da571 100644 --- a/CTNotificationContent.xcodeproj/project.pbxproj +++ b/CTNotificationContent.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ 525715EE1E67B8C0000E455B /* CTNotificationContent.h in Headers */ = {isa = PBXBuildFile; fileRef = 525715EC1E67B8C0000E455B /* CTNotificationContent.h */; settings = {ATTRIBUTES = (Public, ); }; }; 525715F71E67B990000E455B /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 525715F51E67B990000E455B /* UserNotifications.framework */; }; 525715F81E67B990000E455B /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 525715F61E67B990000E455B /* UserNotificationsUI.framework */; }; + 58614E962F98B4BA000D1C8A /* CTTimerBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58614E942F98B4BA000D1C8A /* CTTimerBoxView.swift */; }; A1B2C3D4E5F60001A1B2C3D4 /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = A1B2C3D4E5F60002A1B2C3D4 /* SDWebImage */; }; /* End PBXBuildFile section */ @@ -91,6 +92,7 @@ 525715EC1E67B8C0000E455B /* CTNotificationContent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTNotificationContent.h; sourceTree = ""; }; 525715F51E67B990000E455B /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; 525715F61E67B990000E455B /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; + 58614E942F98B4BA000D1C8A /* CTTimerBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CTTimerBoxView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -222,6 +224,7 @@ 32DFD82F28BCCCBB00E72588 /* Timer */ = { isa = PBXGroup; children = ( + 58614E952F98B4BA000D1C8A /* View */, 32DFD83028BCCCBB00E72588 /* Controller */, 32DFD83228BCCCBB00E72588 /* Model */, ); @@ -426,6 +429,14 @@ name = Frameworks; sourceTree = ""; }; + 58614E952F98B4BA000D1C8A /* View */ = { + isa = PBXGroup; + children = ( + 58614E942F98B4BA000D1C8A /* CTTimerBoxView.swift */, + ); + path = View; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -548,6 +559,7 @@ 3270BFDA28E4D80E003528ED /* CTProductDisplayVerticalViewController.swift in Sources */, 32DFD84628BCCCBB00E72588 /* CTZeroBezelController.swift in Sources */, 488F31C12817012500AE3AC8 /* CTNotificationViewController.m in Sources */, + 58614E962F98B4BA000D1C8A /* CTTimerBoxView.swift in Sources */, 32DFD84B28BCCCBB00E72588 /* CTCarouselController.swift in Sources */, 3270BFF128E4D89C003528ED /* RatingProperties.swift in Sources */, ); diff --git a/CTNotificationContent/Templates/Timer/Controller/CTTimerTemplateController.swift b/CTNotificationContent/Templates/Timer/Controller/CTTimerTemplateController.swift index ddc23c6..d75560e 100644 --- a/CTNotificationContent/Templates/Timer/Controller/CTTimerTemplateController.swift +++ b/CTNotificationContent/Templates/Timer/Controller/CTTimerTemplateController.swift @@ -3,6 +3,7 @@ import UserNotificationsUI import SDWebImage @objc public class CTTimerTemplateController: BaseCTNotificationContentViewController { + var contentView: UIView = UIView(frame: .zero) @objc public var data: String = "" @objc public var templateCaption: String = "" @@ -54,16 +55,51 @@ import SDWebImage subcaptionLabel.translatesAutoresizingMaskIntoConstraints = false return subcaptionLabel }() - private var timerLabel: UILabel = { - let timerLabel = UILabel() - timerLabel.textAlignment = .center - timerLabel.adjustsFontSizeToFitWidth = false - timerLabel.font = UIFont.boldSystemFont(ofSize: 18.0) - timerLabel.textColor = UIColor.black - timerLabel.translatesAutoresizingMaskIntoConstraints = false - return timerLabel - }() - + private var timerBoxView: CTTimerBoxView? + private var timerLabel: UILabel? + private var captionTrailingConstraint: NSLayoutConstraint? + private var subcaptionTrailingConstraint: NSLayoutConstraint? + + private enum CTTimerStyleCapability { + static var supportsRichTimerBox: Bool { + if #available(iOS 13.0, *) { return true } + return false + } + } + + private func setTimerText(_ text: String) { + timerBoxView?.timerLabel.text = text + timerLabel?.text = text + } + + private func hideTimerDisplay() { + timerBoxView?.isHidden = true + timerLabel?.isHidden = true + } + + private func showTimerForExpandedState() { + timerBoxView?.isHidden = false + timerLabel?.isHidden = false + captionTrailingConstraint?.constant = -Constraints.kTimerLabelWidth + subcaptionTrailingConstraint?.constant = -Constraints.kTimerLabelWidth + } + + private func isDarkMode() -> Bool { + if #available(iOS 12.0, *) { + return traitCollection.userInterfaceStyle == .dark + } + return false + } + + private func hasRichTimerStyling(_ props: TimerTemplateProperties) -> Bool { + return props.pt_chrono_bg_clr != nil + || props.pt_chrono_grad_clr1 != nil || props.pt_chrono_grad_clr2 != nil + || props.pt_chrono_grad_dir != nil + || props.pt_chrono_style != nil + || props.pt_chrono_border_clr != nil + || props.pt_chrono_border_width != nil || props.pt_chrono_border_radius != nil + } + @objc public override func viewDidLoad() { super.viewDidLoad() @@ -95,7 +131,23 @@ import SDWebImage contentView.addSubview(imageView) contentView.addSubview(captionLabel) contentView.addSubview(subcaptionLabel) - contentView.addSubview(timerLabel) + + if CTTimerStyleCapability.supportsRichTimerBox, + let props = jsonContent, hasRichTimerStyling(props) { + let box = CTTimerBoxView() + timerBoxView = box + contentView.addSubview(box) + } else { + let label = UILabel() + label.textAlignment = .center + label.adjustsFontSizeToFitWidth = false + label.font = UIFont.boldSystemFont(ofSize: 18.0) + label.textColor = UIColor.black + label.translatesAutoresizingMaskIntoConstraints = false + timerLabel = label + contentView.addSubview(label) + } + hideTimerDisplay() captionLabel.setHTMLText(templateCaption) subcaptionLabel.setHTMLText(templateSubcaption) @@ -160,7 +212,11 @@ import SDWebImage } updateInterfaceColors() - + + if let box = timerBoxView { + box.applyStyle(properties: jsonContent, isDarkMode: isDarkMode()) + } + // Handle image loading // Load image only if timer is not ended. if thresholdSeconds > 0 { @@ -206,27 +262,36 @@ import SDWebImage imageView.backgroundColor = UIColor(hex: isDarkMode ? bgColorDark : bgColor) captionLabel.textColor = UIColor(hex: isDarkMode ? captionColorDark : captionColor) subcaptionLabel.textColor = UIColor(hex: isDarkMode ? subcaptionColorDark : subcaptionColor) - timerLabel.textColor = UIColor(hex: isDarkMode ? timerColorDark : timerColor) + timerLabel?.textColor = UIColor(hex: isDarkMode ? timerColorDark : timerColor) + + if let box = timerBoxView, let props = jsonContent { + box.applyStyle(properties: props, isDarkMode: isDarkMode) + } } func setupConstraints() { + let activeTimerView: UIView = timerBoxView ?? timerLabel! + let captionTrailing = captionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constraints.kCaptionLeftPadding) + let subcaptionTrailing = subcaptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constraints.kCaptionLeftPadding) + captionTrailingConstraint = captionTrailing + subcaptionTrailingConstraint = subcaptionTrailing + NSLayoutConstraint.activate([ captionLabel.topAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -(CTUtiltiy.getCaptionHeight() - Constraints.kCaptionTopPadding)), captionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constraints.kCaptionLeftPadding), - captionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constraints.kTimerLabelWidth), + captionTrailing, captionLabel.heightAnchor.constraint(equalToConstant: Constraints.kCaptionHeight), - + subcaptionLabel.topAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -(Constraints.kSubCaptionHeight + Constraints.kSubCaptionTopPadding)), subcaptionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Constraints.kCaptionLeftPadding), - subcaptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constraints.kTimerLabelWidth), + subcaptionTrailing, subcaptionLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Constraints.kSubCaptionTopPadding), subcaptionLabel.heightAnchor.constraint(equalToConstant: Constraints.kSubCaptionHeight), - - timerLabel.topAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -CTUtiltiy.getCaptionHeight()), - timerLabel.leadingAnchor.constraint(equalTo: captionLabel.trailingAnchor, constant: Constraints.kCaptionLeftPadding), - timerLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constraints.kCaptionLeftPadding), - timerLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Constraints.kSubCaptionTopPadding), - timerLabel.heightAnchor.constraint(equalToConstant: CTUtiltiy.getCaptionHeight()) + + activeTimerView.topAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -(CTUtiltiy.getCaptionHeight() - Constraints.kCaptionTopPadding)), + activeTimerView.leadingAnchor.constraint(equalTo: captionLabel.trailingAnchor, constant: Constraints.kCaptionLeftPadding), + activeTimerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Constraints.kCaptionLeftPadding), + activeTimerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Constraints.kSubCaptionTopPadding) ]) } @@ -236,15 +301,15 @@ import SDWebImage let sec = thresholdSeconds % 60 if thresholdSeconds > 0 { if hr < 1 { - self.timerLabel.text = String(format: "%02i:%02i", min, sec) + setTimerText(String(format: "%02i:%02i", min, sec)) } else { - self.timerLabel.text = String(format: "%02i:%02i:%02i", hr, min, sec) + setTimerText(String(format: "%02i:%02i:%02i", hr, min, sec)) } thresholdSeconds -= 1 } else { timer.invalidate() - self.timerLabel.isHidden = true + hideTimerDisplay() updateViewForExpiredTime() } } @@ -298,6 +363,10 @@ import SDWebImage view.frame = frame contentView.frame = frame preferredContentSize = CGSize(width: viewWidth, height: viewHeight) + + if thresholdSeconds > 0 { + showTimerForExpandedState() + } } func activateImageViewContraints() { diff --git a/CTNotificationContent/Templates/Timer/Model/TimerTemplateProperties.swift b/CTNotificationContent/Templates/Timer/Model/TimerTemplateProperties.swift index ec480b7..47e2c39 100644 --- a/CTNotificationContent/Templates/Timer/Model/TimerTemplateProperties.swift +++ b/CTNotificationContent/Templates/Timer/Model/TimerTemplateProperties.swift @@ -30,9 +30,31 @@ struct TimerTemplateProperties: Decodable { let pt_big_img_alt_alt_text: String? let pt_gif: String? let pt_gif_alt: String? - + + // Timer box background + let pt_chrono_bg_clr: String? + let pt_chrono_bg_clr_dark: String? + let pt_chrono_grad_clr1: String? + let pt_chrono_grad_clr2: String? + let pt_chrono_grad_dir: String? + + // Timer box style ("solid" | "gradient_linear" | "gradient_radial", default "solid") + let pt_chrono_style: String? + + // Timer box border + let pt_chrono_border_clr: String? + let pt_chrono_border_clr_dark: String? + let pt_chrono_border_width: FlexibleDouble? + let pt_chrono_border_radius: FlexibleDouble? + enum CodingKeys: String, CodingKey { case pt_title, pt_title_alt, pt_msg, pt_msg_alt, pt_msg_summary, pt_dl1, pt_big_img, pt_big_img_alt, pt_bg, pt_bg_dark, pt_chrono_title_clr, pt_chrono_title_clr_dark, pt_timer_threshold, pt_timer_end, pt_title_clr, pt_title_clr_dark, pt_msg_clr, pt_msg_clr_dark, pt_big_img_alt_text, pt_big_img_alt_alt_text, pt_gif, pt_gif_alt + case pt_chrono_bg_clr, pt_chrono_bg_clr_dark + case pt_chrono_grad_clr1, pt_chrono_grad_clr2 + case pt_chrono_grad_dir + case pt_chrono_style + case pt_chrono_border_clr, pt_chrono_border_clr_dark + case pt_chrono_border_width, pt_chrono_border_radius } init(from decoder: Decoder) throws { @@ -58,7 +80,18 @@ struct TimerTemplateProperties: Decodable { pt_big_img_alt_alt_text = try container.decodeIfPresent(String.self, forKey: .pt_big_img_alt_alt_text) pt_gif = try container.decodeIfPresent(String.self, forKey: .pt_gif) pt_gif_alt = try container.decodeIfPresent(String.self, forKey: .pt_gif_alt) - + + pt_chrono_bg_clr = try container.decodeIfPresent(String.self, forKey: .pt_chrono_bg_clr) + pt_chrono_bg_clr_dark = try container.decodeIfPresent(String.self, forKey: .pt_chrono_bg_clr_dark) + pt_chrono_grad_clr1 = try container.decodeIfPresent(String.self, forKey: .pt_chrono_grad_clr1) + pt_chrono_grad_clr2 = try container.decodeIfPresent(String.self, forKey: .pt_chrono_grad_clr2) + pt_chrono_grad_dir = try container.decodeIfPresent(String.self, forKey: .pt_chrono_grad_dir) + pt_chrono_style = try container.decodeIfPresent(String.self, forKey: .pt_chrono_style) + pt_chrono_border_clr = try container.decodeIfPresent(String.self, forKey: .pt_chrono_border_clr) + pt_chrono_border_clr_dark = try container.decodeIfPresent(String.self, forKey: .pt_chrono_border_clr_dark) + pt_chrono_border_width = try container.decodeIfPresent(FlexibleDouble.self, forKey: .pt_chrono_border_width) + pt_chrono_border_radius = try container.decodeIfPresent(FlexibleDouble.self, forKey: .pt_chrono_border_radius) + // Value for pt_timer_threshold and pt_timer_end key can be Int or String if received from JSON data or individual keys respectively, so checked for both case if present or else nil. var thresholdValue: Int? = nil do { diff --git a/CTNotificationContent/Templates/Timer/View/CTTimerBoxView.swift b/CTNotificationContent/Templates/Timer/View/CTTimerBoxView.swift new file mode 100644 index 0000000..2a2ca86 --- /dev/null +++ b/CTNotificationContent/Templates/Timer/View/CTTimerBoxView.swift @@ -0,0 +1,122 @@ +import UIKit + +class CTTimerBoxView: UIView { + + var timerLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.adjustsFontSizeToFitWidth = false + label.font = UIFont.boldSystemFont(ofSize: 18.0) + label.textColor = UIColor.black + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private var gradientLayer: CAGradientLayer? + + override init(frame: CGRect) { + super.init(frame: frame) + translatesAutoresizingMaskIntoConstraints = false + setupLabel() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLabel() { + addSubview(timerLabel) + NSLayoutConstraint.activate([ + timerLabel.topAnchor.constraint(equalTo: topAnchor), + timerLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + timerLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + timerLabel.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + + override func layoutSubviews() { + super.layoutSubviews() + gradientLayer?.frame = bounds + gradientLayer?.cornerRadius = layer.cornerRadius + } + + func applyStyle(properties: TimerTemplateProperties, isDarkMode: Bool) { + // Corner radius + if let radius = properties.pt_chrono_border_radius?.value { + layer.cornerRadius = CGFloat(radius) + clipsToBounds = true + } + + // Border width + if let width = properties.pt_chrono_border_width?.value { + layer.borderWidth = CGFloat(width) + } + + // Border color + let borderClrHex = isDarkMode + ? (properties.pt_chrono_border_clr_dark ?? properties.pt_chrono_border_clr) + : properties.pt_chrono_border_clr + if let hex = borderClrHex { + layer.borderColor = UIColor(hex: hex)?.cgColor + } + + // Timer text color — falls back to the title color so the timer is + // never left at the lazy-init UIColor.black against a dark backdrop. + let textClrHex: String = isDarkMode + ? (properties.pt_chrono_title_clr_dark + ?? properties.pt_chrono_title_clr + ?? properties.pt_title_clr_dark + ?? properties.pt_title_clr + ?? ConstantKeys.kHexWhiteColor) + : (properties.pt_chrono_title_clr + ?? properties.pt_title_clr + ?? ConstantKeys.kHexBlackColor) + timerLabel.textColor = UIColor(hex: textClrHex) + + let style = properties.pt_chrono_style?.lowercased() ?? "solid" + let isGradient = style == "gradient_linear" || style == "gradient_radial" + + if isGradient, + let startHex = properties.pt_chrono_grad_clr1, + let endHex = properties.pt_chrono_grad_clr2, + let c1 = UIColor(hex: startHex), let c2 = UIColor(hex: endHex) { + gradientLayer?.removeFromSuperlayer() + let grad = CAGradientLayer() + grad.frame = bounds + grad.cornerRadius = layer.cornerRadius + grad.colors = [c1.cgColor, c2.cgColor] + + if style == "gradient_radial" { + grad.type = .radial + grad.startPoint = CGPoint(x: 0.5, y: 0.5) + grad.endPoint = CGPoint(x: 1.0, y: 1.0) + } else { + let angleStr = properties.pt_chrono_grad_dir ?? "90" + if let degrees = Double(angleStr.trimmingCharacters(in: .whitespaces)) { + let radians = degrees * .pi / 180.0 + let endX = 0.5 + 0.5 * sin(radians) + let endY = 0.5 - 0.5 * cos(radians) + grad.startPoint = CGPoint(x: 1.0 - endX, y: 1.0 - endY) + grad.endPoint = CGPoint(x: endX, y: endY) + } else { + grad.startPoint = CGPoint(x: 0, y: 0.5) + grad.endPoint = CGPoint(x: 1, y: 0.5) + } + } + + layer.insertSublayer(grad, at: 0) + gradientLayer = grad + backgroundColor = .clear + } else { + gradientLayer?.removeFromSuperlayer() + gradientLayer = nil + + let bgClrHex = isDarkMode + ? (properties.pt_chrono_bg_clr_dark ?? properties.pt_chrono_bg_clr) + : properties.pt_chrono_bg_clr + if let hex = bgClrHex { + backgroundColor = UIColor(hex: hex) + } + } + } +}