refine iOS swipe surface reveal
This commit is contained in:
@@ -47,39 +47,10 @@ struct RoundView: View {
|
|||||||
)
|
)
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
HermesTheme.surface
|
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
if round != nil {
|
|
||||||
HermesVideoPlayerView(coordinator: playerCoordinator, cornerRadius: 0, fixedHeight: nil)
|
|
||||||
.ignoresSafeArea()
|
|
||||||
}
|
|
||||||
|
|
||||||
LinearGradient(
|
|
||||||
colors: [.black.opacity(0.72), .black.opacity(0.12), .black.opacity(0.78)],
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
.ignoresSafeArea()
|
|
||||||
|
|
||||||
sideSelectionOverlay(round: round)
|
sideSelectionOverlay(round: round)
|
||||||
|
|
||||||
if let feedback = swipeFeedback {
|
swipeSurface(round: round, remaining: remaining, timerLocked: timerLocked, bannerMessage: bannerMessage)
|
||||||
swipeFeedbackOverlay(feedback)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
header(round: round, remaining: remaining)
|
|
||||||
Spacer()
|
|
||||||
if let bannerMessage {
|
|
||||||
banner(message: bannerMessage)
|
|
||||||
.padding(.horizontal, HermesTheme.screenPadding)
|
|
||||||
.padding(.bottom, 16)
|
|
||||||
}
|
|
||||||
bottomOverlay(round: round, timerLocked: timerLocked)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.offset(x: dragOffset.width * 0.12)
|
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.gesture(swipeGesture(round: round, timerLocked: timerLocked))
|
.gesture(swipeGesture(round: round, timerLocked: timerLocked))
|
||||||
.onAppear {
|
.onAppear {
|
||||||
@@ -113,6 +84,43 @@ struct RoundView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func swipeSurface(round: HermesRound?, remaining: TimeInterval, timerLocked: Bool, bannerMessage: String?) -> some View {
|
||||||
|
ZStack {
|
||||||
|
HermesTheme.surface
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
if round != nil {
|
||||||
|
HermesVideoPlayerView(coordinator: playerCoordinator, cornerRadius: 0, fixedHeight: nil)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearGradient(
|
||||||
|
colors: [.black.opacity(0.72), .black.opacity(0.12), .black.opacity(0.78)],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
if let feedback = swipeFeedback {
|
||||||
|
swipeFeedbackOverlay(feedback)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
header(round: round, remaining: remaining)
|
||||||
|
Spacer()
|
||||||
|
if let bannerMessage {
|
||||||
|
banner(message: bannerMessage)
|
||||||
|
.padding(.horizontal, HermesTheme.screenPadding)
|
||||||
|
.padding(.bottom, 16)
|
||||||
|
}
|
||||||
|
bottomOverlay(round: round, timerLocked: timerLocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.offset(x: dragOffset.width * 0.72)
|
||||||
|
.rotationEffect(.degrees(max(min(Double(dragOffset.width / 40.0), 6), -6)))
|
||||||
|
.shadow(color: .black.opacity(0.28), radius: 22, x: 0, y: 10)
|
||||||
|
}
|
||||||
|
|
||||||
private func header(round: HermesRound?, remaining: TimeInterval) -> some View {
|
private func header(round: HermesRound?, remaining: TimeInterval) -> some View {
|
||||||
HStack(alignment: .top, spacing: 16) {
|
HStack(alignment: .top, spacing: 16) {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
@@ -243,7 +251,7 @@ struct RoundView: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func sideSelectionOverlay(round: HermesRound?) -> some View {
|
private func sideSelectionOverlay(round: HermesRound?) -> some View {
|
||||||
if let round, phase == .preview {
|
if let round, phase == .preview {
|
||||||
let dragThreshold: CGFloat = 18
|
let dragThreshold: CGFloat = 10
|
||||||
let isDraggingRight = dragOffset.width > dragThreshold
|
let isDraggingRight = dragOffset.width > dragThreshold
|
||||||
let isDraggingLeft = dragOffset.width < -dragThreshold
|
let isDraggingLeft = dragOffset.width < -dragThreshold
|
||||||
|
|
||||||
@@ -252,13 +260,13 @@ struct RoundView: View {
|
|||||||
title: sortedOutcomes(for: round).dropFirst().first.map(outcomeTitle) ?? localization.string(for: "round.no"),
|
title: sortedOutcomes(for: round).dropFirst().first.map(outcomeTitle) ?? localization.string(for: "round.no"),
|
||||||
subtitle: localization.string(for: "round.swipe_left"),
|
subtitle: localization.string(for: "round.swipe_left"),
|
||||||
color: .red,
|
color: .red,
|
||||||
opacity: isDraggingLeft ? min(abs(dragOffset.width) / 180.0, 0.78) : 0
|
opacity: isDraggingLeft ? min(abs(dragOffset.width) / 240.0, 0.95) : 0
|
||||||
)
|
)
|
||||||
sideTint(
|
sideTint(
|
||||||
title: sortedOutcomes(for: round).first.map(outcomeTitle) ?? localization.string(for: "round.yes"),
|
title: sortedOutcomes(for: round).first.map(outcomeTitle) ?? localization.string(for: "round.yes"),
|
||||||
subtitle: localization.string(for: "round.swipe_right"),
|
subtitle: localization.string(for: "round.swipe_right"),
|
||||||
color: HermesTheme.positive,
|
color: HermesTheme.positive,
|
||||||
opacity: isDraggingRight ? min(abs(dragOffset.width) / 180.0, 0.78) : 0
|
opacity: isDraggingRight ? min(abs(dragOffset.width) / 240.0, 0.95) : 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
@@ -287,7 +295,7 @@ struct RoundView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func swipeGesture(round: HermesRound?, timerLocked: Bool) -> some Gesture {
|
private func swipeGesture(round: HermesRound?, timerLocked: Bool) -> some Gesture {
|
||||||
DragGesture(minimumDistance: 24)
|
DragGesture(minimumDistance: 12)
|
||||||
.onChanged { value in
|
.onChanged { value in
|
||||||
dragOffset = value.translation
|
dragOffset = value.translation
|
||||||
}
|
}
|
||||||
@@ -298,7 +306,7 @@ struct RoundView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard abs(value.translation.width) > 72, abs(value.translation.width) > abs(value.translation.height) else {
|
guard abs(value.translation.width) > 145, abs(value.translation.width) > abs(value.translation.height) else {
|
||||||
swipeFeedback = nil
|
swipeFeedback = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,7 +322,7 @@
|
|||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HermesPrediction.app/HermesApp";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HermesPrediction.app/HermesPrediction";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -344,7 +344,7 @@
|
|||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HermesPrediction.app/HermesApp";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HermesPrediction.app/HermesPrediction";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import XCTest
|
import XCTest
|
||||||
@testable import HermesApp
|
@testable import HermesPrediction
|
||||||
|
|
||||||
private struct TestFailure: Error, CustomStringConvertible {
|
private struct TestFailure: Error, CustomStringConvertible {
|
||||||
let description: String
|
let description: String
|
||||||
|
|||||||
Reference in New Issue
Block a user