Commit acc48b63 by Dmitriy Stepanets

Finished WidgetPromotion controller UI and swipe down to close gesture

parent 6c9b7835
No preview for this file type
......@@ -57,6 +57,7 @@
CD3884842657BBCC0070FD6F /* DelayedSaveStorage.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CD3884822657BBCC0070FD6F /* DelayedSaveStorage.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CD39F2F225DE94C4009FE398 /* SunPhaseCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD39F2F125DE94C4009FE398 /* SunPhaseCell.swift */; };
CD39F2F525DE9571009FE398 /* ArrowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD39F2F425DE9571009FE398 /* ArrowButton.swift */; };
CD3D567F268C705900DB99B6 /* PromotionPresentationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3D567E268C705900DB99B6 /* PromotionPresentationAnimator.swift */; };
CD3F6E6925FA59D4002DB99B /* ForecastDetailPeriodButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3F6E6825FA59D4002DB99B /* ForecastDetailPeriodButton.swift */; };
CD3F6E6C25FA5A90002DB99B /* PeriodButtonProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3F6E6B25FA5A90002DB99B /* PeriodButtonProtocol.swift */; };
CD4742D0261200500061AC95 /* TodayAlertCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4742CF261200500061AC95 /* TodayAlertCell.swift */; };
......@@ -305,6 +306,7 @@
CD3884822657BBCC0070FD6F /* DelayedSaveStorage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DelayedSaveStorage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CD39F2F125DE94C4009FE398 /* SunPhaseCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunPhaseCell.swift; sourceTree = "<group>"; };
CD39F2F425DE9571009FE398 /* ArrowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrowButton.swift; sourceTree = "<group>"; };
CD3D567E268C705900DB99B6 /* PromotionPresentationAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionPresentationAnimator.swift; sourceTree = "<group>"; };
CD3F6E6825FA59D4002DB99B /* ForecastDetailPeriodButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDetailPeriodButton.swift; sourceTree = "<group>"; };
CD3F6E6B25FA5A90002DB99B /* PeriodButtonProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeriodButtonProtocol.swift; sourceTree = "<group>"; };
CD4742CF261200500061AC95 /* TodayAlertCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayAlertCell.swift; sourceTree = "<group>"; };
......@@ -666,6 +668,14 @@
path = Cells;
sourceTree = "<group>";
};
CD3D567D268C703700DB99B6 /* Animators */ = {
isa = PBXGroup;
children = (
CD3D567E268C705900DB99B6 /* PromotionPresentationAnimator.swift */,
);
path = Animators;
sourceTree = "<group>";
};
CD5692B22653D46100A3CDBE /* SplashAnimation */ = {
isa = PBXGroup;
children = (
......@@ -815,6 +825,7 @@
CD857EA0268B290500B5E069 /* WidgetPromotion */ = {
isa = PBXGroup;
children = (
CD3D567D268C703700DB99B6 /* Animators */,
CD857EA3268B36D000B5E069 /* Views */,
CD857EA1268B290500B5E069 /* WidgetPromotionController.swift */,
);
......@@ -1537,6 +1548,7 @@
CD32CE0E260C770E00235081 /* MenuHeaderView.swift in Sources */,
CD15DB3D25DA6C5100024727 /* ForecastTimePeriodControl.swift in Sources */,
CD67617726259DD70079D273 /* MapLayersPresentationAnimator.swift in Sources */,
CD3D567F268C705900DB99B6 /* PromotionPresentationAnimator.swift in Sources */,
CD7BF1582620410800A30DF5 /* RadarCoordinator.swift in Sources */,
CDD0F1EE25725BCF00CF5017 /* ThemeManager.swift in Sources */,
CD1237F4255D889F00C98139 /* GradientView.swift in Sources */,
......
......@@ -12,7 +12,7 @@
<key>OneWeatherNotificationServiceExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>57</integer>
<integer>58</integer>
</dict>
<key>PG (Playground) 1.xcscheme</key>
<dict>
......
......@@ -420,6 +420,8 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>GADNativeAdValidatorEnabled</key>
<false/>
<key>NSUserTrackingUsageDescription</key>
<string>This will be used to deliver personalized ads to you.</string>
</dict>
......
{
"images" : [
{
"filename" : "round_close.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2B",
"green" : "0x26",
"red" : "0x26"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xFF",
"green" : "0xFF",
"red" : "0xFF"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x2B",
"green" : "0x26",
"red" : "0x26"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
......@@ -282,3 +282,4 @@
"widget.promotion.small" = "Small";
"widget.promotion.medium" = "Medium";
"widget.promotion.large" = "Large";
"widget.promotion.learn" = "Learn to add widget";
......@@ -120,4 +120,13 @@ struct DefaultTheme: ThemeProtocol {
var nativeAdCallToActionColor: UIColor {
return UIColor(named: "native_ad_call_to_action_background") ?? .red
}
//Widget promotion
var widgetPromotionBackground: UIColor {
return UIColor(named: "widget_promotion_background") ?? .red
}
var widgetPromotionText: UIColor {
return UIColor(named: "widget_promotion_text") ?? .red
}
}
......@@ -9,47 +9,51 @@ import UIKit
public protocol ThemeProtocol {
//Base
var name:String { get }
var baseBackgroundColor:UIColor { get }
var containerBackgroundColor:UIColor { get }
var name: String { get }
var baseBackgroundColor: UIColor { get }
var containerBackgroundColor: UIColor { get }
//Navigation bar
var navigationBarBackgroundColor:UIColor { get }
var navigationTintColor:UIColor { get }
var navigationBarBackgroundColor: UIColor { get }
var navigationTintColor: UIColor { get }
//Tab bar
var tabBarBackgroundColor:UIColor { get }
var tabBarTintColor:UIColor { get }
var tabBarNormalColor:UIColor { get }
var tabBarBackgroundColor: UIColor { get }
var tabBarTintColor: UIColor { get }
var tabBarNormalColor: UIColor { get }
//Text
var primaryTextColor:UIColor { get }
var secondaryTextColor:UIColor { get }
var primaryTextColor: UIColor { get }
var secondaryTextColor: UIColor { get }
//Progress indicator
var progressBackgroundColor:UIColor { get }
var progressIndicatorColor:UIColor { get }
var progressBackgroundColor: UIColor { get }
var progressIndicatorColor: UIColor { get }
//Period button
var periodButtonBackgroundColor:UIColor { get }
var periodButtonSelectedBackgroundColor:UIColor { get }
var periodButtonBorderColor:UIColor { get }
var periodButtonShadowColor:UIColor { get }
var periodButtonBackgroundColor: UIColor { get }
var periodButtonSelectedBackgroundColor: UIColor { get }
var periodButtonBorderColor: UIColor { get }
var periodButtonShadowColor: UIColor { get }
//Segment control
var segmentTextColor:UIColor { get }
var segmentSelectedTextColor:UIColor { get }
var segmentBackgroundColor:UIColor { get }
var segmentBorderColor:UIColor { get }
var segmentSelectedGradient:[UIColor] { get }
var segmentTextColor: UIColor { get }
var segmentSelectedTextColor: UIColor { get }
var segmentBackgroundColor: UIColor { get }
var segmentBorderColor: UIColor { get }
var segmentSelectedGradient: [UIColor] { get }
//Graph
var graphColor:UIColor { get }
var graphTintColor:UIColor { get }
var graphColor: UIColor { get }
var graphTintColor: UIColor { get }
//Map
var mapControlsColor:UIColor { get }
var mapControlsColor: UIColor { get }
//Ads
var nativeAdCallToActionColor: UIColor { get }
//Widget promotion
var widgetPromotionBackground: UIColor { get }
var widgetPromotionText: UIColor { get }
}
......@@ -65,7 +65,11 @@ class TodayViewController: UIViewController {
}
@objc private func handleNotificationButton() {
self.coordinator.openNotificationsScreen()
if #available(iOS 14, *) {
self.present(WidgetPromotionController(), animated: true)
}
// self.coordinator.openNotificationsScreen()
}
}
......
//
// PromotionDismissAnimator.swift
// 1Weather
//
// Created by Dmitry Stepanets on 30.06.2021.
//
import UIKit
class PromotionDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private let kDuration = 0.25
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return kDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to)
else {
return
}
// toVC is the Main View Controller
transitionContext.containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
// fromVC is the Modal VC.
// Hide it for now, since we're going to use snapshots instead.
fromVC.view.isHidden = true
// Create the snapshot.
if let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false) {
// Don't forget to add it
transitionContext.containerView.insertSubview(snapshot, aboveSubview: toVC.view)
UIView.animate(withDuration: kDuration) {
snapshot.center.y += UIScreen.main.bounds.height
} completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
}
//
// PromotionPresentationAnimator.swift
// 1Weather
//
// Created by Dmitry Stepanets on 30.06.2021.
//
import UIKit
@available(iOS 14, *)
class PromotionPresentationAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.25
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to) as? WidgetPromotionController else {
return
}
let container = transitionContext.containerView
let contentHeight = toViewController.controllerContentHeight
toViewController.view.frame = .init(x: 0,
y: container.bounds.height,
width: container.frame.width,
height: contentHeight)
container.addSubview(toViewController.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext)) {
toViewController.view.frame.origin.y = container.frame.height - contentHeight
} completion: { finished in
transitionContext.completeTransition(finished)
}
}
}
//
// WidgetPromotionInteractor.swift
// 1Weather
//
// Created by Dmitry Stepanets on 30.06.2021.
//
import UIKit
class WidgetPromotionInteractor: UIPercentDrivenInteractiveTransition {
var hasStarted = false
var shouldFinish = false
}
......@@ -37,16 +37,9 @@ class PromotionHeaderView: UIView {
}
private func updateUI() {
switch interfaceStyle {
case .light:
plusImageView.tintColor = UIColor(hex: 0x26262b)
widgetLabel.textColor = UIColor(hex: 0x26262b)
descriptionLabel.textColor = UIColor(hex: 0x26262b)
case .dark:
plusImageView.tintColor = .white
widgetLabel.textColor = .white
descriptionLabel.textColor = .white
}
plusImageView.tintColor = ThemeManager.currentTheme.widgetPromotionText
widgetLabel.textColor = ThemeManager.currentTheme.widgetPromotionText
descriptionLabel.textColor = ThemeManager.currentTheme.widgetPromotionText
}
}
......
......@@ -21,9 +21,9 @@ class PromotionSmallWidgetView: UIView {
preparePlaceholder()
prepareTopLabel()
prepareHorizontalLine()
prepareSizeNumberLabel()
prepareVerticalLine()
prepareSizesLabel()
prepareVerticalLine()
prepareSizeNumberLabel()
updateUI()
}
......@@ -37,19 +37,17 @@ class PromotionSmallWidgetView: UIView {
}
private func updateUI() {
topLabel.textColor = ThemeManager.currentTheme.widgetPromotionText
sizeNumberLabel.textColor = ThemeManager.currentTheme.widgetPromotionText
sizesLabel.textColor = ThemeManager.currentTheme.widgetPromotionText
switch interfaceStyle {
case .light:
topLabel.textColor = UIColor(hex: 0x26262b)
sizeNumberLabel.textColor = UIColor(hex: 0x26262b)
sizesLabel.textColor = UIColor(hex: 0x26262b)
horizontalLineView.backgroundColor = UIColor.black.withAlphaComponent(0.1)
verticalLineView.backgroundColor = UIColor.black.withAlphaComponent(0.1)
case .dark:
topLabel.textColor = .white
sizeNumberLabel.textColor = .white
sizesLabel.textColor = .white
horizontalLineView.backgroundColor = UIColor(hex: 0xffffff).withAlphaComponent(0.1)
verticalLineView.backgroundColor = UIColor(hex: 0xffffff).withAlphaComponent(0.1)
case .dark:
horizontalLineView.backgroundColor = UIColor.black.withAlphaComponent(0.1)
verticalLineView.backgroundColor = UIColor.black.withAlphaComponent(0.1)
}
}
}
......@@ -111,7 +109,7 @@ private extension PromotionSmallWidgetView {
addSubview(sizeNumberLabel)
sizeNumberLabel.snp.makeConstraints { make in
make.left.equalTo(widgetPlaceholder.snp.right).offset(33)
make.right.equalTo(verticalLineView.snp.left).offset(-20)
make.top.equalTo(horizontalLineView.snp.bottom).offset(17)
}
}
......@@ -123,8 +121,8 @@ private extension PromotionSmallWidgetView {
verticalLineView.snp.makeConstraints { make in
make.width.equalTo(1)
make.top.equalTo(horizontalLineView.snp.bottom).offset(10)
make.height.equalTo(70)
make.centerX.equalTo(horizontalLineView)
make.height.equalTo(sizesLabel)
make.right.equalTo(sizesLabel.snp.left).offset(-13)
}
}
......@@ -133,13 +131,15 @@ private extension PromotionSmallWidgetView {
let medium = "widget.promotion.medium".localized()
let large = "widget.promotion.large".localized()
let paragraph = NSMutableParagraphStyle()
paragraph.lineSpacing = 4
let attrString = NSMutableAttributedString(string: small + "\n" + medium + "\n" + large)
attrString.addAttribute(.font,
value: UIFont.systemFont(ofSize: 14, weight: .medium),
range: .init(location: 0, length: small.count))
attrString.addAttribute(.font,
value: UIFont.systemFont(ofSize: 20, weight: .semibold),
range: .init(location: small.count, length: medium.count + 1))
attrString.addAttributes([.font : UIFont.systemFont(ofSize: 14, weight: .medium),
.paragraphStyle : paragraph],
range: .init(location: 0, length: small.count))
attrString.addAttributes([.font : UIFont.systemFont(ofSize: 20, weight: .semibold)],
range: .init(location: small.count, length: medium.count + 1))
attrString.addAttribute(.font,
value: UIFont.systemFont(ofSize: 28, weight: .bold),
range: .init(location: small.count + medium.count + 1, length: large.count + 1))
......@@ -151,9 +151,8 @@ private extension PromotionSmallWidgetView {
addSubview(sizesLabel)
sizesLabel.snp.makeConstraints { make in
make.left.equalTo(verticalLineView.snp.right).offset(13)
make.top.equalTo(verticalLineView)
make.right.equalToSuperview()
make.top.equalTo(horizontalLineView.snp.bottom).offset(8)
make.right.equalTo(horizontalLineView)
}
}
}
......@@ -10,17 +10,45 @@ import UIKit
@available(iOS 14, *)
class WidgetPromotionController: UIViewController {
//Private
private let kPanTopInset: CGFloat = 10
private let kScrollViewTopInset: CGFloat = 36
private let controllerPanView = UIView()
private let closeButton = UIButton()
private let scrollView = UIScrollView()
private let stackView = UIStackView()
private let footerView = UIView()
private let learnButton = UIButton()
private var initialTouchPoint = CGPoint(x: 0,y: 0)
private lazy var panGesture: UIPanGestureRecognizer = {
let gesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(sender:)))
return gesture
}()
//Public
var controllerContentHeight: CGFloat {
let scrollViewBottomInset: CGFloat = 4
let pan = controllerPanView.frame.height + kPanTopInset
let stackHeight = stackView.frame.height
let footerHeight = footerView.frame.height
let safeAreaInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
return pan + kScrollViewTopInset + stackHeight + scrollViewBottomInset + footerHeight + safeAreaInset
}
init() {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
transitioningDelegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
prepareView()
preparePanView()
prepareCloseButton()
prepareFooterView()
prepareScrollView()
prepareStackView()
updateUI()
......@@ -34,8 +62,45 @@ class WidgetPromotionController: UIViewController {
self.dismiss(animated: true)
}
@objc private func handleLearnButton() {
}
@objc private func handlePanGesture(sender: UIPanGestureRecognizer) {
let touchPoint = sender.location(in: self.view?.window)
let originalOffsetY = UIScreen.main.bounds.height - self.view.bounds.height
switch sender.state {
case .began:
initialTouchPoint = touchPoint
case .changed:
if touchPoint.y - initialTouchPoint.y > 0 {
view.frame.origin.y = originalOffsetY + (touchPoint.y - initialTouchPoint.y)
}
case .cancelled, .ended:
if touchPoint.y - initialTouchPoint.y > 80 {
self.dismiss(animated: true, completion: nil)
} else {
UIView.animate(withDuration: 0.25, animations: {
self.view.frame.origin.y = originalOffsetY
})
}
break
default:
break
}
}
private func updateUI() {
view.backgroundColor = ThemeManager.currentTheme.baseBackgroundColor
view.backgroundColor = ThemeManager.currentTheme.widgetPromotionBackground
footerView.backgroundColor = ThemeManager.currentTheme.widgetPromotionBackground
switch view.interfaceStyle {
case .light:
footerView.layer.shadowColor = UIColor(hex: 0x4c4c4c).cgColor
case .dark:
footerView.layer.shadowColor = UIColor(hex: 0xAAAAAA).cgColor
}
}
}
......@@ -43,9 +108,16 @@ class WidgetPromotionController: UIViewController {
@available(iOS 14, *)
private extension WidgetPromotionController {
func prepareView() {
view.addGestureRecognizer(panGesture)
view.clipsToBounds = true
view.layer.cornerRadius = 24
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.5
view.layer.shadowOffset = .init(width: 0, height: 0)
view.layer.shadowRadius = 18
}
func preparePanView() {
......@@ -56,14 +128,14 @@ private extension WidgetPromotionController {
controllerPanView.snp.makeConstraints { make in
make.width.equalTo(30)
make.height.equalTo(4)
make.top.equalToSuperview().inset(10)
make.top.equalToSuperview().inset(kPanTopInset)
make.centerX.equalToSuperview()
}
}
func prepareCloseButton() {
closeButton.addTarget(self, action: #selector(handleCloseButton), for: .touchUpInside)
closeButton.setImage(UIImage(systemName: "xmark.circle.fill"), for: .normal)
closeButton.setImage(UIImage(named: "round_close"), for: .normal)
closeButton.imageView?.contentMode = .scaleAspectFit
closeButton.tintColor = UIColor(hex: 0xe4e4e4).withAlphaComponent(0.4)
view.addSubview(closeButton)
......@@ -78,8 +150,9 @@ private extension WidgetPromotionController {
func prepareScrollView() {
view.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.left.bottom.right.equalToSuperview()
make.top.equalToSuperview().inset(36)
make.left.right.equalToSuperview()
make.top.equalToSuperview().inset(kScrollViewTopInset)
make.bottom.equalTo(footerView.snp.top).offset(4)
}
}
......@@ -94,5 +167,48 @@ private extension WidgetPromotionController {
stackView.snp.makeConstraints { make in
make.edges.width.equalToSuperview()
}
stackView.layoutIfNeeded()
}
func prepareFooterView() {
footerView.layer.shadowOpacity = 0.5
footerView.layer.shadowRadius = 7
footerView.layer.shadowOffset = .init(width: 0, height: 0)
view.addSubview(footerView)
learnButton.addTarget(self, action: #selector(handleLearnButton), for: .touchUpInside)
learnButton.setTitle("widget.promotion.learn".localized(), for: .normal)
learnButton.setTitleColor(ThemeManager.currentTheme.graphTintColor, for: .normal)
learnButton.setTitleColor(ThemeManager.currentTheme.graphTintColor.darken(by: 10), for: .highlighted)
learnButton.layer.borderWidth = 2
learnButton.layer.borderColor = ThemeManager.currentTheme.graphTintColor.cgColor
learnButton.layer.cornerRadius = 6
footerView.addSubview(learnButton)
//Constraints
footerView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
}
learnButton.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(20)
make.top.equalToSuperview().inset(10)
let safeAreaInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0
make.bottom.equalToSuperview().inset(safeAreaInset + 10)
}
footerView.layoutIfNeeded()
}
}
//MARK:- Transitioning Delegate
@available(iOS 14, *)
extension WidgetPromotionController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PromotionPresentationAnimator()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return nil
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment