Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iOS-8994 feed back evolution #303

Merged
merged 13 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import SwiftUI
struct FeedbackCatalogView: View {
@State var title: String = "Title of the feedback"
@State var message: String = "Insert here the message that will be included in the feedback."
@State var slot: Bool = false

@State var selectedStyleIndex = 0
@State var styles: [FeedbackStyle] = [
Expand All @@ -27,6 +28,7 @@ struct FeedbackCatalogView: View {
List {
section("Title") { TextField("Title", text: $title) }
section("Message") { TextField("Message", text: $message) }
section("Slot") { Toggle("Show slot", isOn: $slot) }

section("Style") { stylePicker }

Expand All @@ -38,18 +40,46 @@ struct FeedbackCatalogView: View {
}
}
.sheet(isPresented: $presentingFeedback) {
Feedback(
style: styles[selectedStyleIndex],
title: title,
message: message,
primaryButton: { Button("Primary", action: {}) },
secondaryButton: { Button("Secondary", action: {}) }
)
.titleAccessibilityLabel("Title")
.imageAccessibilityIdentifier("Image")
if slot {
FeedBackSlot
} else {
FeedBack
}
}
}

@ViewBuilder
var FeedBack: some View {
Feedback(
style: styles[selectedStyleIndex],
title: title,
message: message,
primaryButton: { Button("Primary", action: {}) },
secondaryButton: { Button("Secondary", action: {}) }
)
.titleAccessibilityLabel("Title")
.imageAccessibilityIdentifier("Image")
}

@ViewBuilder
var FeedBackSlot: some View {
Feedback(
style: styles[selectedStyleIndex],
title: title,
message: message,
contentView: { text },
primaryButton: { Button("Primary", action: {}) },
secondaryButton: { Button("Secondary", action: {}) }
)
.titleAccessibilityLabel("Title")
.imageAccessibilityIdentifier("Image")
}

@ViewBuilder
var text: some View {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vitae suscipit purus. Nullam quis venenatis lorem. Curabitur laoreet sem sed eros rutrum dictum. Vivamus fermentum vestibulum lacus non euismod. Vestibulum imperdiet sem et neque convallis tempus. Curabitur at lectus enim. Donec vehicula, tortor in pulvinar ornare, nisl justo accumsan ipsum, et sodales magna arcu vel odio. Sed tincidunt ante ligula, sed venenatis eros rutrum ac. Aenean fringilla elit mollis venenatis tempor. Aliquam facilisis, erat quis congue faucibus, enim erat pulvinar justo, ac mollis erat nulla ut dolor. Etiam rhoncus nulla mi, non pretium eros lobortis nec. Ut vulputate ex eu nibh laoreet, in luctus tortor elementum. Ut tristique lectus vel arcu suscipit, sit amet consequat enim porta. Suspendisse vulputate placerat lorem a luctus. Sed suscipit lacus vehicula sapien malesuada semper. Mauris urna orci, maximus non eleifend blandit, accumsan id mi.\nInteger a hendrerit sapien, nec gravida diam. Donec semper vehicula eros ut pharetra. In non convallis tellus, sed ultrices nulla. Cras a arcu neque. Quisque sit amet arcu congue, molestie risus non, facilisis felis. Vivamus et accumsan ipsum, et condimentum quam. Suspendisse eleifend velit turpis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla rutrum mauris vel felis porttitor, ut luctus turpis mollis. Suspendisse a nulla ultricies, malesuada augue quis, varius mauris. Morbi eget lacinia orci. Phasellus vel varius nisi.")
}

@ViewBuilder
var stylePicker: some View {
picker($selectedStyleIndex, options: styles)
Expand Down
142 changes: 114 additions & 28 deletions Sources/Mistica/Components/Feedback/FeedbackView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,40 @@ import UIKit

public class FeedbackView: UIView {
private enum Constants {
static let animationDelay: TimeInterval = 0.2
static let animationDuration: TimeInterval = 0.8
static let animationCurveControlPoint1 = CGPoint(x: 0.215, y: 0.61)
static let animationCurveControlPoint2 = CGPoint(x: 0.355, y: 1)
static let iconSize: CGFloat = 48
enum Animation {
enum Animator {
static let duration: TimeInterval = 0.8
static let curveControlPoint1 = CGPoint(x: 0.215, y: 0.61)
static let curveControlPoint2 = CGPoint(x: 0.355, y: 1)
}

enum Delay {
static let initial: TimeInterval = 0.2
static let large: TimeInterval = 0.6
static let small: TimeInterval = 0.3
}

enum Opacity {
static let initial = CGFloat.zero
static let final = CGFloat(1)
}

enum Offset {
static let initial = CGFloat(20)
static let final = CGFloat.zero
}
}
}

private lazy var animator = UIViewPropertyAnimator(
duration: Constants.animationDuration,
controlPoint1: Constants.animationCurveControlPoint1,
controlPoint2: Constants.animationCurveControlPoint2
)
private var animators = [UIViewPropertyAnimator]()
private var animator: UIViewPropertyAnimator {
UIViewPropertyAnimator(
duration: Constants.Animation.Animator.duration,
controlPoint1: Constants.Animation.Animator.curveControlPoint1,
controlPoint2: Constants.Animation.Animator.curveControlPoint2
)
}

// Setup properties
private let style: FeedbackStyle
Expand All @@ -36,6 +59,10 @@ public class FeedbackView: UIView {
let icon = UIImageView()
icon.contentMode = .scaleAspectFit
icon.tintColor = .brand
NSLayoutConstraint.activate([
icon.widthAnchor.constraint(equalToConstant: Constants.iconSize),
icon.heightAnchor.constraint(equalToConstant: Constants.iconSize)
])
icon.isAccessibilityElement = true
icon.accessibilityIdentifier = DefaultIdentifiers.Feedback.asset
return icon
Expand All @@ -47,6 +74,10 @@ public class FeedbackView: UIView {
animation.contentMode = .scaleAspectFit
animation.loopMode = .playOnce
animation.isUserInteractionEnabled = false
NSLayoutConstraint.activate([
animation.widthAnchor.constraint(equalToConstant: Constants.iconSize),
animation.heightAnchor.constraint(equalToConstant: Constants.iconSize)
])
animation.isAccessibilityElement = true
animation.accessibilityIdentifier = DefaultIdentifiers.Feedback.asset
return animation
Expand Down Expand Up @@ -194,7 +225,8 @@ public class FeedbackView: UIView {
scrollStackView.translatesAutoresizingMaskIntoConstraints = false
scrollStackView.stackView.spacing = 24
scrollStackView.stackView.alignment = .leading
scrollStackView.stackView.layoutMargins = UIEdgeInsets(top: 64, left: 24, bottom: 16, right: 24)
scrollStackView.stackView.layoutMargins = UIEdgeInsets(top: 64, left: 16, bottom: 16, right: 16)
scrollStackView.stackView.preservesSuperviewLayoutMargins = false
return scrollStackView
}()

Expand Down Expand Up @@ -239,11 +271,10 @@ public extension FeedbackView {
func startAnimation() {
guard style.shouldAnimate, !animationFired else { return }
animationFired = true
animator.startAnimation(afterDelay: Constants.animationDelay)
triggerHapticFeedback()

if UIView.areAnimationsEnabled {
animatedIcon.play()
startAnimations()
} else {
animatedIcon.stop()
animatedIcon.currentProgress = 1
Expand All @@ -257,6 +288,7 @@ private extension FeedbackView {
setupContent()
setupBackground()
prepareAnimation()
prepareHapticFeedback()
}

func setupContent() {
Expand Down Expand Up @@ -290,32 +322,86 @@ private extension FeedbackView {
scrollStackView.stackView.insertArrangedSubview(icon, at: 0)
case .animation(let animation):
animatedIcon.animation = animation
let width = animation.bounds.width
let height = animation.bounds.height
NSLayoutConstraint.activate([
animatedIcon.widthAnchor.constraint(equalToConstant: width),
animatedIcon.heightAnchor.constraint(equalToConstant: height)
])
// To center animations that are not 1:1 squares
let leftMargin = (width - height) / 2
animatedIcon.transform = CGAffineTransform(translationX: -leftMargin, y: 0)
scrollStackView.stackView.insertArrangedSubview(animatedIcon, at: 0)
}
}

func prepareAnimation() {
guard style.shouldAnimate else { return }
// Initial state
func prepare(view: UIView) {
view.alpha = Constants.Animation.Opacity.initial
view.transform = CGAffineTransform(
translationX: 0,
y: Constants.Animation.Offset.initial
)
}
// Final state
func animation(for view: UIView) -> () -> Void {
{
view.alpha = Constants.Animation.Opacity.final
view.transform = CGAffineTransform(
translationX: 0,
y: Constants.Animation.Offset.final
)
}
}

guard UIView.areAnimationsEnabled, style.shouldAnimate else { return }
animationFired = false
contentContainerStackView.alpha = 0
contentContainerStackView.transform = CGAffineTransform(translationX: 0, y: 20)
animator.addAnimations {
self.contentContainerStackView.alpha = 1
self.contentContainerStackView.transform = CGAffineTransform(translationX: 0, y: 0)

// Views that should animate
var views = [UIView]()
if title.isEmpty == false {
views.append(titleLabel)
}
if let subtitle, subtitle.isEmpty == false {
views.append(subtitleLabel)
}
if let errorReference, errorReference.isEmpty == false {
views.append(errorReferenceLabel)
}
Comment on lines +354 to +362
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified by using the ! operator

Suggested change
if title.isEmpty == false {
views.append(titleLabel)
}
if let subtitle, subtitle.isEmpty == false {
views.append(subtitleLabel)
}
if let errorReference, errorReference.isEmpty == false {
views.append(errorReferenceLabel)
}
if !title.isEmpty {
views.append(titleLabel)
}
if let subtitle, !subtitle.isEmpty {
views.append(subtitleLabel)
}
if let errorReference, !errorReference.isEmpty {
views.append(errorReferenceLabel)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a big fan of inverting booleans as I consider it is less readable.

if let extraContent {
views.append(extraContent)
}

// Prepare
views.forEach(prepare(view:))
// Generate animators
animators = views.map(animation).map { animation in
let animator = animator
animator.addAnimations(animation)
return animator
}
}

func startAnimations() {
// Start the initial
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.Animation.Delay.initial) { [weak self] in
self?.animatedIcon.play()

// Animate views
guard let animators = self?.animators, animators.isEmpty == false else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.Animation.Delay.large) {
self?.animate(remaining: animators)
}
}
}

func animate(remaining animators: [UIViewPropertyAnimator]) {
var animators = animators

let animator = animators.removeFirst()
animator.startAnimation()

// Animate other views
guard animators.isEmpty == false else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.Animation.Delay.small) { [weak self] in
self?.animate(remaining: animators)
}
prepareHapticFeedback()
}

func prepareHapticFeedback() {
guard UIView.areAnimationsEnabled else { return }
feedbackGenerator?.prepare()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,12 @@ public extension FeedbackViewController {
view.addSubview(withDefaultConstraints: feedbackView)
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
feedbackView.startAnimation()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if configuration.backButton == .none {
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
feedbackView.startAnimation()
}

override func viewWillDisappear(_ animated: Bool) {
Expand Down
Loading