import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import AccountContext import TelegramPresentationData import PresentationDataUtils import ComponentFlow import ViewControllerComponent import SheetComponent import MultilineTextComponent import BundleIconComponent import SolidRoundedButtonComponent import Markdown private class PremiumLimitAnimationComponent: Component { private let iconName: String private let inactiveColor: UIColor private let activeColors: [UIColor] private let textColor: UIColor private let badgeText: String? init( iconName: String, inactiveColor: UIColor, activeColors: [UIColor], textColor: UIColor, badgeText: String? ) { self.iconName = iconName self.inactiveColor = inactiveColor self.activeColors = activeColors self.textColor = textColor self.badgeText = badgeText } static func ==(lhs: PremiumLimitAnimationComponent, rhs: PremiumLimitAnimationComponent) -> Bool { if lhs.iconName != rhs.iconName { return false } if lhs.inactiveColor != rhs.inactiveColor { return false } if lhs.activeColors != rhs.activeColors { return false } if lhs.textColor != rhs.textColor { return false } if lhs.badgeText != rhs.badgeText { return false } return true } final class View: UIView { private let container: SimpleLayer private let inactiveBackground: SimpleLayer private let activeContainer: SimpleLayer private let activeBackground: SimpleLayer private let badgeView: UIView private let badgeMaskView: UIView private let badgeMaskBackgroundView: UIView private let badgeMaskArrowView: UIImageView private let badgeForeground: SimpleLayer private let badgeIcon: UIImageView private let badgeCountLabel: RollingLabel override init(frame: CGRect) { self.container = SimpleLayer() self.container.masksToBounds = true self.container.cornerRadius = 6.0 self.inactiveBackground = SimpleLayer() self.activeContainer = SimpleLayer() self.activeContainer.masksToBounds = true self.activeBackground = SimpleLayer() self.badgeView = UIView() self.badgeView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0) self.badgeMaskBackgroundView = UIView() self.badgeMaskBackgroundView.backgroundColor = .white self.badgeMaskBackgroundView.layer.cornerRadius = 24.0 self.badgeMaskArrowView = UIImageView() self.badgeMaskArrowView.image = generateImage(CGSize(width: 44.0, height: 12.0), rotatedContext: { size, context in context.clear(CGRect(origin: .zero, size: size)) context.setFillColor(UIColor.white.cgColor) context.scaleBy(x: 3.76, y: 3.76) context.translateBy(x: -9.3, y: -12.7) try? drawSvgPath(context, path: "M6.4,0.0 C2.9,0.0 0.0,2.84 0.0,6.35 C0.0,9.86 2.9,12.7 6.4,12.7 H9.302 H11.3 C11.7,12.7 12.1,12.87 12.4,13.17 L14.4,15.13 C14.8,15.54 15.5,15.54 15.9,15.13 L17.8,13.17 C18.1,12.87 18.5,12.7 18.9,12.7 H20.9 H23.6 C27.1,12.7 29.9,9.86 29.9,6.35 C29.9,2.84 27.1,0.0 23.6,0.0 Z ") }) self.badgeMaskView = UIView() self.badgeMaskView.addSubview(self.badgeMaskBackgroundView) self.badgeMaskView.addSubview(self.badgeMaskArrowView) self.badgeView.mask = self.badgeMaskView self.badgeForeground = SimpleLayer() self.badgeIcon = UIImageView() self.badgeIcon.contentMode = .center self.badgeCountLabel = RollingLabel() self.badgeCountLabel.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: []) self.badgeCountLabel.textColor = .white self.badgeCountLabel.text(num: 0) super.init(frame: frame) self.layer.addSublayer(self.container) self.container.addSublayer(self.inactiveBackground) self.container.addSublayer(self.activeContainer) self.activeContainer.addSublayer(self.activeBackground) self.addSubview(self.badgeView) self.badgeView.layer.addSublayer(self.badgeForeground) self.badgeView.addSubview(self.badgeIcon) self.badgeView.addSubview(self.badgeCountLabel) self.isUserInteractionEnabled = false } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private var didPlayAppearanceAnimation = false func playAppearanceAnimation(component: PremiumLimitAnimationComponent, availableSize: CGSize) { self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) let now = self.badgeView.layer.convertTime(CACurrentMediaTime(), from: nil) let positionAnimation = CABasicAnimation(keyPath: "position.x") positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: -availableSize.width / 2.0, y: 0.0)) positionAnimation.toValue = NSValue(cgPoint: CGPoint()) positionAnimation.isAdditive = true positionAnimation.duration = 0.5 positionAnimation.fillMode = .forwards positionAnimation.beginTime = now let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") rotateAnimation.fromValue = 0.0 as NSNumber rotateAnimation.toValue = 0.2 as NSNumber rotateAnimation.isAdditive = true rotateAnimation.duration = 0.2 rotateAnimation.beginTime = now + 0.5 rotateAnimation.fillMode = .forwards rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) let returnAnimation = CABasicAnimation(keyPath: "transform.rotation.z") returnAnimation.fromValue = 0.2 as NSNumber returnAnimation.toValue = 0.0 as NSNumber returnAnimation.isAdditive = true returnAnimation.duration = 0.18 returnAnimation.beginTime = now + 0.5 + 0.2 returnAnimation.fillMode = .forwards returnAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn) self.badgeView.layer.add(positionAnimation, forKey: "appearance1") self.badgeView.layer.add(rotateAnimation, forKey: "appearance2") self.badgeView.layer.add(returnAnimation, forKey: "appearance3") if let badgeText = component.badgeText, let num = Int(badgeText) { self.badgeCountLabel.text(num: num) } } var previousAvailableSize: CGSize? func update(component: PremiumLimitAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize { self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor self.activeBackground.backgroundColor = component.activeColors.last?.cgColor self.badgeIcon.image = UIImage(bundleImageName: component.iconName)?.withRenderingMode(.alwaysTemplate) self.badgeIcon.tintColor = component.textColor let lineHeight: CGFloat = 30.0 let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - lineHeight), size: CGSize(width: availableSize.width, height: lineHeight)) self.container.frame = containerFrame self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: containerFrame.width / 2.0, height: lineHeight)) self.activeContainer.frame = CGRect(origin: CGPoint(x: containerFrame.width / 2.0, y: 0.0), size: CGSize(width: containerFrame.width / 2.0, height: lineHeight)) self.activeBackground.bounds = CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 3.0 / 2.0, height: lineHeight)) if self.activeBackground.animation(forKey: "movement") == nil { self.activeBackground.position = CGPoint(x: containerFrame.width * 3.0 / 4.0 - self.activeBackground.frame.width * 0.35, y: lineHeight / 2.0) } let countWidth: CGFloat if let badgeText = component.badgeText { switch badgeText.count { case 1: countWidth = 20.0 case 2: countWidth = 35.0 case 3: countWidth = 51.0 default: countWidth = 51.0 } } else { countWidth = 51.0 } let badgeWidth: CGFloat = countWidth + 62.0 let badgeSize = CGSize(width: badgeWidth, height: 48.0 + 12.0) self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeSize) self.badgeMaskBackgroundView.frame = CGRect(origin: .zero, size: CGSize(width: badgeSize.width, height: 48.0)) self.badgeMaskArrowView.frame = CGRect(origin: CGPoint(x: (badgeSize.width - 44.0) / 2.0, y: badgeSize.height - 12.0), size: CGSize(width: 44.0, height: 12.0)) self.badgeView.bounds = CGRect(origin: .zero, size: badgeSize) self.badgeView.center = CGPoint(x: availableSize.width / 2.0, y: 82.0) self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeSize.width * 3.0, height: badgeSize.height)) if self.badgeForeground.animation(forKey: "movement") == nil { self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeSize.height / 2.0) } self.badgeIcon.frame = CGRect(x: 15.0, y: 9.0, width: 30.0, height: 30.0) self.badgeCountLabel.frame = CGRect(x: badgeSize.width - countWidth - 11.0, y: 10.0, width: countWidth, height: 48.0) if !self.didPlayAppearanceAnimation { self.didPlayAppearanceAnimation = true self.playAppearanceAnimation(component: component, availableSize: availableSize) } if self.previousAvailableSize != availableSize { self.previousAvailableSize = availableSize var locations: [CGFloat] = [] let delta = 1.0 / CGFloat(component.activeColors.count - 1) for i in 0 ..< component.activeColors.count { locations.append(delta * CGFloat(i)) } let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: component.activeColors, locations: locations, direction: .horizontal) self.badgeForeground.contentsGravity = .resizeAspectFill self.badgeForeground.contents = gradient?.cgImage self.activeBackground.contentsGravity = .resizeAspectFill self.activeBackground.contents = gradient?.cgImage self.setupGradientAnimations() } return availableSize } private func setupGradientAnimations() { if let _ = self.badgeForeground.animation(forKey: "movement") { } else { CATransaction.begin() let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0 let badgePreviousValue = self.badgeForeground.position.x var badgeNewValue: CGFloat = badgeOffset if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 { badgeNewValue -= self.badgeForeground.frame.width * 0.35 } self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0) let lineOffset = (self.activeBackground.frame.width - self.activeContainer.bounds.width) / 2.0 let linePreviousValue = self.activeBackground.position.x var lineNewValue: CGFloat = lineOffset if lineOffset - linePreviousValue < self.activeBackground.frame.width * 0.25 { lineNewValue -= self.activeBackground.frame.width * 0.35 } self.activeBackground.position = CGPoint(x: lineNewValue, y: self.activeBackground.bounds.size.height / 2.0) let badgeAnimation = CABasicAnimation(keyPath: "position.x") badgeAnimation.duration = 4.5 badgeAnimation.fromValue = badgePreviousValue badgeAnimation.toValue = badgeNewValue badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) CATransaction.setCompletionBlock { [weak self] in self?.setupGradientAnimations() } self.badgeForeground.add(badgeAnimation, forKey: "movement") let lineAnimation = CABasicAnimation(keyPath: "position.x") lineAnimation.duration = 4.5 lineAnimation.fromValue = linePreviousValue lineAnimation.toValue = lineNewValue lineAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) self.activeBackground.add(lineAnimation, forKey: "movement") CATransaction.commit() } } } func makeView() -> View { return View(frame: CGRect()) } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } } public final class PremiumLimitDisplayComponent: CombinedComponent { public let inactiveColor: UIColor public let activeColors: [UIColor] public let inactiveTitle: String public let inactiveTitleColor: UIColor public let activeTitle: String public let activeValue: String public let activeTitleColor: UIColor public let badgeIconName: String public let badgeText: String? public init( inactiveColor: UIColor, activeColors: [UIColor], inactiveTitle: String, inactiveTitleColor: UIColor, activeTitle: String, activeValue: String, activeTitleColor: UIColor, badgeIconName: String, badgeText: String? ) { self.inactiveColor = inactiveColor self.activeColors = activeColors self.inactiveTitle = inactiveTitle self.inactiveTitleColor = inactiveTitleColor self.activeTitle = activeTitle self.activeValue = activeValue self.activeTitleColor = activeTitleColor self.badgeIconName = badgeIconName self.badgeText = badgeText } public static func ==(lhs: PremiumLimitDisplayComponent, rhs: PremiumLimitDisplayComponent) -> Bool { if lhs.inactiveColor != rhs.inactiveColor { return false } if lhs.activeColors != rhs.activeColors { return false } if lhs.inactiveTitle != rhs.inactiveTitle { return false } if lhs.inactiveTitleColor != rhs.inactiveTitleColor { return false } if lhs.activeTitle != rhs.activeTitle { return false } if lhs.activeValue != rhs.activeValue { return false } if lhs.activeTitleColor != rhs.activeTitleColor { return false } if lhs.badgeIconName != rhs.badgeIconName { return false } if lhs.badgeText != rhs.badgeText { return false } return true } public static var body: Body { let inactiveTitle = Child(MultilineTextComponent.self) let activeTitle = Child(MultilineTextComponent.self) let activeValue = Child(MultilineTextComponent.self) let animation = Child(PremiumLimitAnimationComponent.self) return { context in let component = context.component let height: CGFloat = 120.0 let lineHeight: CGFloat = 30.0 let inactiveTitle = inactiveTitle.update( component: MultilineTextComponent( text: .plain( NSAttributedString( string: component.inactiveTitle, font: Font.semibold(15.0), textColor: component.inactiveTitleColor ) ) ), availableSize: context.availableSize, transition: context.transition ) let activeTitle = activeTitle.update( component: MultilineTextComponent( text: .plain( NSAttributedString( string: component.activeTitle, font: Font.semibold(15.0), textColor: component.activeTitleColor ) ) ), availableSize: context.availableSize, transition: context.transition ) let activeValue = activeValue.update( component: MultilineTextComponent( text: .plain( NSAttributedString( string: component.activeValue, font: Font.semibold(15.0), textColor: component.activeTitleColor ) ) ), availableSize: context.availableSize, transition: context.transition ) let animation = animation.update( component: PremiumLimitAnimationComponent( iconName: component.badgeIconName, inactiveColor: component.inactiveColor, activeColors: component.activeColors, textColor: component.activeTitleColor, badgeText: component.badgeText ), availableSize: CGSize(width: context.availableSize.width, height: height), transition: context.transition ) context.add(animation .position(CGPoint(x: context.availableSize.width / 2.0, y: height / 2.0)) ) context.add(inactiveTitle .position(CGPoint(x: inactiveTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0)) ) context.add(activeTitle .position(CGPoint(x: context.availableSize.width / 2.0 + 1.0 + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0)) ) context.add(activeValue .position(CGPoint(x: context.availableSize.width - activeValue.size.width / 2.0 - 12.0, y: height - lineHeight / 2.0)) ) return CGSize(width: context.availableSize.width, height: height) } } } private final class LimitSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let subject: PremiumLimitScreen.Subject let action: () -> Void let dismiss: () -> Void init(context: AccountContext, subject: PremiumLimitScreen.Subject, action: @escaping () -> Void, dismiss: @escaping () -> Void) { self.context = context self.subject = subject self.action = action self.dismiss = dismiss } static func ==(lhs: LimitSheetContent, rhs: LimitSheetContent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.subject != rhs.subject { return false } return true } final class State: ComponentState { private let context: AccountContext private var disposable: Disposable? var initialized = false var limits: EngineConfiguration.UserLimits var premiumLimits: EngineConfiguration.UserLimits init(context: AccountContext, subject: PremiumLimitScreen.Subject) { self.context = context self.limits = EngineConfiguration.UserLimits.defaultValue self.premiumLimits = EngineConfiguration.UserLimits.defaultValue super.init() self.disposable = (context.engine.data.get( TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) ) |> deliverOnMainQueue).start(next: { [weak self] result in if let strongSelf = self { let (limits, premiumLimits) = result strongSelf.initialized = true strongSelf.limits = limits strongSelf.premiumLimits = premiumLimits strongSelf.updated(transition: .immediate) } }) } deinit { self.disposable?.dispose() } } func makeState() -> State { return State(context: self.context, subject: self.subject) } static var body: Body { let title = Child(MultilineTextComponent.self) let text = Child(MultilineTextComponent.self) let limit = Child(PremiumLimitDisplayComponent.self) let button = Child(SolidRoundedButtonComponent.self) return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let component = context.component let theme = environment.theme let strings = environment.strings let state = context.state let subject = component.subject let sideInset: CGFloat = 16.0 + environment.safeInsets.left let textSideInset: CGFloat = 24.0 + environment.safeInsets.left let iconName: String let badgeText: String let string: String let premiumValue: String switch subject { case .folders: let limit = state.limits.maxFoldersCount let premiumLimit = state.premiumLimits.maxFoldersCount iconName = "Premium/Folder" badgeText = "\(limit)" string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string premiumValue = "\(premiumLimit)" case .chatsInFolder: let limit = state.limits.maxFolderChatsCount let premiumLimit = state.premiumLimits.maxFolderChatsCount iconName = "Premium/Chat" badgeText = "\(limit)" string = strings.Premium_MaxChatsInFolderCountText("\(limit)", "\(premiumLimit)").string premiumValue = "\(premiumLimit)" case .pins: let limit = state.limits.maxPinnedChatCount let premiumLimit = state.premiumLimits.maxPinnedChatCount iconName = "Premium/Pin" badgeText = "\(limit)" string = strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string premiumValue = "\(premiumLimit)" case .files: let limit: Int64 = 2048 * 1024 * 1024 * Int64(state.limits.maxUploadFileParts) let premiumLimit: Int64 = 4096 * 1024 * 1024 * Int64(state.limits.maxUploadFileParts) iconName = "Premium/File" badgeText = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) string = strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string premiumValue = dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator)) } let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: strings.Premium_LimitReached, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center )), horizontalAlignment: .center, maximumNumberOfLines: 1 ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), transition: .immediate ) let textFont = Font.regular(17.0) let boldTextFont = Font.semibold(17.0) let textColor = theme.actionSheet.primaryTextColor let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: textColor), linkAttribute: { _ in return nil }) let text = text.update( component: MultilineTextComponent( text: .markdown(text: string, attributes: markdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.0 ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), transition: .immediate ) if state.initialized { let limit = limit.update( component: PremiumLimitDisplayComponent( inactiveColor: UIColor(rgb: 0xE9E9EA), activeColors: [ UIColor(rgb: 0x0077ff), UIColor(rgb: 0x6b93ff), UIColor(rgb: 0x8878ff), UIColor(rgb: 0xe46ace) ], inactiveTitle: strings.Premium_Free, inactiveTitleColor: .black, activeTitle: strings.Premium_Premium, activeValue: premiumValue, activeTitleColor: .white, badgeIconName: iconName, badgeText: badgeText ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: .immediate ) context.add(limit .position(CGPoint(x: context.availableSize.width / 2.0, y: limit.size.height / 2.0 + 44.0)) ) } let button = button.update( component: SolidRoundedButtonComponent( title: strings.Premium_IncreaseLimit, theme: SolidRoundedButtonComponent.Theme( backgroundColor: .black, backgroundColors: [ UIColor(rgb: 0x0077ff), UIColor(rgb: 0x6b93ff), UIColor(rgb: 0x8878ff), UIColor(rgb: 0xe46ace) ], foregroundColor: .white ), font: .bold, fontSize: 17.0, height: 50.0, cornerRadius: 10.0, gloss: true, iconName: "Premium/X2", iconPosition: .right, action: { [weak component] in guard let component = component else { return } component.dismiss() component.action() } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), transition: context.transition ) context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: 28.0)) ) context.add(text .position(CGPoint(x: context.availableSize.width / 2.0, y: 228.0)) ) let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: 228.0 + ceil(text.size.height / 2.0) + 38.0), size: button.size) context.add(button .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) ) let contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom) return contentSize } } } private final class LimitSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext let subject: PremiumLimitScreen.Subject let action: () -> Void init(context: AccountContext, subject: PremiumLimitScreen.Subject, action: @escaping () -> Void) { self.context = context self.subject = subject self.action = action } static func ==(lhs: LimitSheetComponent, rhs: LimitSheetComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.subject != rhs.subject { return false } return true } static var body: Body { let sheet = Child(SheetComponent.self) let animateOut = StoredActionSlot(Action.self) return { context in let environment = context.environment[EnvironmentType.self] let controller = environment.controller let sheet = sheet.update( component: SheetComponent( content: AnyComponent(LimitSheetContent( context: context.component.context, subject: context.component.subject, action: context.component.action, dismiss: { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } )), backgroundColor: environment.theme.actionSheet.opaqueItemBackgroundColor, animateOut: animateOut ), environment: { environment SheetComponentEnvironment( isDisplaying: environment.value.isVisible, dismiss: { animateOut.invoke(Action { _ in if let controller = controller() { controller.dismiss(completion: nil) } }) } ) }, availableSize: context.availableSize, transition: context.transition ) context.add(sheet .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) return context.availableSize } } } public class PremiumLimitScreen: ViewControllerComponentContainer { public enum Subject { case folders case chatsInFolder case pins case files } public init(context: AccountContext, subject: PremiumLimitScreen.Subject, action: @escaping () -> Void) { super.init(context: context, component: LimitSheetComponent(context: context, subject: subject, action: action), navigationBarAppearance: .none) self.navigationPresentation = .flatModal } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func viewDidLoad() { super.viewDidLoad() self.view.disablesInteractiveModalDismiss = true } }