I have two issues that would love some help on. I have a landing page, split up between two views. One view shows an image, and another displays two buttons (login and sign up). When navigating to the buttons, I would like to stay on the view and not navigate fully to a new view.
I would also like the background to remain the same as we navigate. I attached a gif of the desired effect (minus the background).
In the code below, I added a ZStack which adds a background modifier, but navigating goes fully to a new view, not remaining on the same page.
extension View {
func animatableGradient(fromGradient: Gradient, toGradient: Gradient, progress: CGFloat) -> some View {
self.modifier(AnimatableGradientModifier(fromGradient: fromGradient, toGradient: toGradient, progress: progress))
}
}
struct AnimatableGradientModifier: AnimatableModifier {
let fromGradient: Gradient
let toGradient: Gradient
var progress: CGFloat = 0.0 //keeps track of gradient change
var animatableData: CGFloat {
get { progress }
set { progress = newValue }
}
func body(content: Content) -> some View {
var gradientColors = [Color]()
for i in 0..<fromGradient.stops.count {
let fromColor = UIColor(fromGradient.stops[i].color)
let toColor = UIColor(toGradient.stops[i].color)
gradientColors.append(colorMixer(fromColor: fromColor, toColor: toColor, progress: progress))
}
return LinearGradient(gradient: Gradient(colors: gradientColors), startPoint: .topLeading, endPoint: .bottomTrailing)
}
func colorMixer(fromColor: UIColor, toColor: UIColor, progress: CGFloat) -> Color {
guard let fromColor = fromColor.cgColor.components else { return Color(fromColor) }
guard let toColor = toColor.cgColor.components else { return Color(toColor) }
let red = fromColor[0] + (toColor[0] - fromColor[0]) * progress
let green = fromColor[1] + (toColor[1] - fromColor[1]) * progress
let blue = fromColor[2] + (toColor[2] - fromColor[2]) * progress
return Color(red: Double(red), green: Double(green), blue: Double(blue))
}
}
struct LandingPage: View {
#AppStorage("signedIn") var signedIn = false
#State private var isPressed = false
#Environment (\.dismiss) var dismiss
#State private var progress: CGFloat = 0
//colors for background on landing page
let gradient1 = Gradient(colors: [.yellow, .orange])
let gradient2 = Gradient(colors: [.yellow, .pink])
#State private var animateGradient = false
#State var scale: CGFloat = 1.0
#State var offsetValue: CGFloat = -60 // << image
#State var isMealJournalTitleShowing = false
#ViewBuilder
var body: some View {
NavigationView{
ZStack{
Rectangle()
.animatableGradient(fromGradient: gradient1, toGradient: gradient2, progress: progress)
.ignoresSafeArea()
VStack{
test()
VStack{
NavigationLink(destination: testb() .navigationBarHidden(true),
label:{
Text("Get Started").fontWeight(.bold)
.frame(minWidth: 0, maxWidth: 200)
.padding(10)
.foregroundColor(.white)
//draw rectange around buttons
.background(
RoundedRectangle(cornerRadius: 20)
.fill(
LinearGradient(
colors: [.orange, .yellow],
startPoint: .topLeading,
endPoint: .bottomTrailing
)))
})
.simultaneousGesture(TapGesture().onEnded{
offsetValue = -125
scale -= 0.50
isMealJournalTitleShowing = true
})
NavigationLink(destination: testb().navigationBarHidden(true), label: {
Text("Login").fontWeight(.semibold)
.frame(minWidth:0, maxWidth: 200)
.padding(10)
.foregroundColor(.black)
.overlay( RoundedRectangle(cornerRadius: 25)
.stroke(Color.gray, lineWidth: 3)
)
})
}
.frame(height: 500)
}
.frame(height: 500)
}
.onAppear {
DispatchQueue.main.async {
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: true)) {
self.progress = 1.0
}
}
}
.animation(.easeInOut(duration: 0.50), value: offsetValue)
}
}
}
Navigating creates and shows a complete new view, that's why the background changes, of course. Instead of navigating, maybe you could use something like this?
You could play around with transitions like .transition(.slide.combined(with: .opacity)) for example to get the effect you want.
To get the image animating and scaling, you could just lookup .matchedGeometryEffect.
import SwiftUI
enum ViewState {
case landingPage
case login
case register
case isLoggedIn
}
struct ContentView: View {
#State private var viewState: ViewState = .landingPage
var body: some View {
ZStack {
Color.teal.opacity(0.4)
switch viewState {
case .landingPage:
VStack {
Text("LandingPage")
.transition(.slide)
.padding(50)
Button("Login") {
viewState = .login
}
Button("Register") {
viewState = .register
}
}
case .login:
Text("Login")
.transition(.slide)
case .register:
Text("Register")
.transition(.slide)
case .isLoggedIn:
Text("LoggedIn")
.transition(.slide)
}
}
.animation(.default, value: viewState)
}
}
Related
I am making a CustomGridView without using Apple Grid, my codes works fine but it has an extra rendering issue when I add new row! As long as I see in my codes, if I add new row, Xcode should render just for new Items! But in the fact it renders all the items! which I can not find the issue! I am okay if Xcode renders all Items if I add new column, because the size put effect on every single Item, but with adding row, there is no general effect on items!
import SwiftUI
struct GridItemView: View {
let rowItem: GridItemRowType
let columnItem: GridItemColumnType
let string: String
let size: CGFloat
#State private var color: Color = Color.randomColor
init(rowItem: GridItemRowType, columnItem: GridItemColumnType, size: CGFloat) {
self.rowItem = rowItem
self.columnItem = columnItem
self.size = size
self.string = "(" + String(describing: rowItem.index) + "," + String(describing: columnItem.index) + ")"
}
var body: some View {
print("rendering for:", string)
return RoundedRectangle(cornerRadius: 10.0)
.fill(color)
.padding(5.0)
.frame(width: size, height: size)
.overlay(
Text(string)
.bold()
.foregroundColor(Color.white)
.shadow(color: .black, radius: 2, x: 0.0, y: 0.0)
)
.onTapGesture {
color = Color.white
}
.shadow(radius: 20.0)
}
}
extension Color {
static var randomColor: Color {
get { return Color(red: Double.random(in: 0...1), green: Double.random(in: 0...1), blue: Double.random(in: 0...1)) }
}
}
struct CustomGridView: View {
let gridItemColumn: [GridItemColumnType]
let gridItemRow: [GridItemRowType]
var body: some View {
GeometryReader { proxy in
let size: CGFloat = (proxy.size.width - 5.0)/CGFloat(gridItemColumn.count)
ScrollView {
VStack(spacing: .zero) {
ForEach(gridItemRow) { rowItem in
HStack(spacing: .zero) {
ForEach(gridItemColumn) { columnItem in
GridItemView(rowItem: rowItem, columnItem: columnItem, size: size)
}
}
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: .top), removal: AnyTransition.move(edge: .top)))
}
Spacer()
}
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: .trailing), removal: AnyTransition.move(edge: .trailing)))
}
.position(x: proxy.size.width/2.0, y: proxy.size.height/2.0)
}
.animation(Animation.default, value: [gridItemRow.count, gridItemColumn.count])
}
}
struct GridItemColumnType: Identifiable {
let id: UUID = UUID()
var index: Int
static let initializingGridItemColumn: [GridItemColumnType] = [GridItemColumnType(index: 0),
GridItemColumnType(index: 1),
GridItemColumnType(index: 2),
GridItemColumnType(index: 3),
GridItemColumnType(index: 4)]
}
struct GridItemRowType: Identifiable {
let id: UUID = UUID()
var index: Int
static let initializingGridItemRowType: [GridItemRowType] = [GridItemRowType(index: 0),
GridItemRowType(index: 1),
GridItemRowType(index: 2)]
}
struct ContentView: View {
#State private var gridItemColumn: [GridItemColumnType] = GridItemColumnType.initializingGridItemColumn
#State private var gridItemRow: [GridItemRowType] = GridItemRowType.initializingGridItemRowType
var body: some View {
CustomGridView(gridItemColumn: gridItemColumn, gridItemRow: gridItemRow)
Spacer()
Button("add Column") { gridItemColumn.append(GridItemColumnType(index: gridItemColumn.count)) }
Button("add Row") { gridItemRow.append(GridItemRowType(index: gridItemRow.count)) }
}
}
You need to make cell view equatable, so SwiftUI would know if it should be re-rendered.
Tested with Xcode 13.2 / iOS 15.2
struct GridItemView: View, Equatable {
// The `string` property is used just for demo, in real you
// need some unique identifier which represents that view
// has same content
static func == (lhs: GridItemView, rhs: GridItemView) -> Bool {
lhs.string == rhs.string
}
// ... other code
}
and then inside grid use it as
ForEach(gridItemColumn) { columnItem in
EquatableView(content: GridItemView(rowItem: rowItem,
columnItem: columnItem, size: size))
}
I have created a viewModifier which blurs a view when added.
Problem is when I add it on parent view of all the contents, all views are blurred differently.
I assume it's because it goes to all the contents and blur each of them individually instead of adding an overlay blur to the parent view.
I used Swift UI's native blur because UIBlurEffect has very limited configuration i.e. I can't adjust the blur intensity to a specific value.
Here it is when the modifier is added in the parent ZStack:
Here when I add it on background image only:
Adding it to background image looks good but I need it to be on top of all views. Here is my code:
Main View
import SwiftUI
struct BlurViewDemo: View {
#State private var isPressed = true
#State private var blurColor: BlurColor = .none
var body: some View {
ZStack {
GeometryReader { proxy in
let frame = proxy.frame(in: .global)
Image("blur-view-demo-image")
.resizable()
.frame(width: frame.size.width, height: frame.size.height)
// When added here only the background image is blurred.
.modifier(
BlurModifier(
showBlur: $isPressed, blurColor: $blurColor
)
)
}
VStack(spacing: 30) {
// TITLE STYLE
Text(getSelectedBlurTitle())
.font(.system(size: 60))
.offset(y: -40)
.foregroundColor(
blurColor == .dark && isPressed ? .white : .black
)
// TOGGLE BLUR BUTTON
Button(action: {
isPressed.toggle()
}, label: {
Text(isPressed ? "Blur: On" : "Blur: Off")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color(#colorLiteral(red: 0.2, green: 0.4, blue: 0.4, alpha: 1)))
// DEFAULT BUTTON
Button(action: {
isPressed = true
blurColor = .none
}, label: {
Text("Default")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color.gray)
// LIGHT BUTTON
Button(action: {
isPressed = true
blurColor = .light
}, label: {
Text("Light")
.foregroundColor(.black)
})
.frame(width: 80, height: 40)
.background(Color.white)
// DARK BUTTON
Button(action: {
isPressed = true
blurColor = .dark
}, label: {
Text("Dark")
.foregroundColor(.white)
})
.frame(width: 80, height: 40)
.background(Color.black)
} //: VSTACK
} //: ZSTACK
.edgesIgnoringSafeArea(.all)
// When added here, the buttons are not blurred properly.
.modifier(
BlurModifier(
showBlur: $isPressed, blurColor: $blurColor
)
)
}
private func getSelectedBlurTitle() -> String {
guard isPressed else { return "Clear"}
switch blurColor {
case .none:
return "Default"
case .light:
return "Light"
case .dark:
return "Dark"
}
}
}
struct BlurViewDemo_Previews: PreviewProvider {
static var previews: some View {
BlurViewDemo()
}
}
View Modifier
public enum BlurColor {
case none
case light
case dark
}
import SwiftUI
struct BlurModifier: ViewModifier {
#Binding private var showBlur: Bool
#Binding private var blurColor: BlurColor
#State private var blurRadius: CGFloat = 14
public init(showBlur: Binding<Bool>, blurColor: Binding<BlurColor>) {
self._showBlur = showBlur
self._blurColor = blurColor
}
func body(content: Content) -> some View {
ZStack {
content
.blur(radius: showBlur ? blurRadius : 0, opaque: true)
.animation(Animation.easeInOut(duration: 0.3))
.edgesIgnoringSafeArea(.all)
.navigationBarHidden(showBlur ? true : false)
Rectangle()
.fill(showBlur ? getBlurColor() : Color.white.opacity(0.0001))
.edgesIgnoringSafeArea(.all)
}
}
private func getBlurColor() -> Color {
switch blurColor {
case .none:
return Color.white.opacity(0.5)
case .light:
return Color.white.opacity(0.6)
case .dark:
return Color.black.opacity(0.5)
}
}
}
I got a problem while trying to display a custom loading view in SwiftUI.
I created a custom struct view OrangeActivityIndicator:
struct OrangeActivityIndicator: View {
var style = StrokeStyle(lineWidth: 6, lineCap: .round)
#State var animate = false
let orangeColor = Color.orOrangeColor
let orangeColorOpaque = Color.orOrangeColor.opacity(0.5)
init(lineWidth: CGFloat = 6) {
style.lineWidth = lineWidth
}
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false))
}.onAppear() {
self.animate.toggle()
}
}
}
I use it inside different screens or views, my problem is that it appears weirdly, for example in CampaignsView of the app I display it when the server call is in progress.
struct CampaignsView: View {
#ObservedObject var viewModel: CampaignsViewModel
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 0) {
CustomNavigationBar(campaignsNumber: viewModel.cardCampaigns.count)
.padding([.leading, .trailing], 24)
.frame(height: 25)
CarouselView(x: $viewModel.x, screen: viewModel.screen, op: $viewModel.op, count: $viewModel.index, cardCampaigns: $viewModel.cardCampaigns).frame(height: 240)
CampaignDescriptionView(idx: viewModel.index, cardCampaigns: viewModel.cardCampaigns)
.padding([.leading, .trailing], 24)
Spacer()
}
.onAppear {
self.viewModel.getCombineCampaigns()
}
if viewModel.isLoading {
OrangeActivityIndicator()
.frame(width: 40, height: 40)
}
}
.padding(.top, 34)
.background(Color.orBackgroundGrayColor.edgesIgnoringSafeArea(.all))
.navigationBarHidden(true)
}
}
}
The Indicator itself is correctly spinning, the problem is when it appears, it appears as a translation animation coming from the bottom to the middle of the screen. This is my viewModel with the server call and isLoading property:
class CampaignsViewModel: ObservableObject {
#Published var index: Int = 0
#Published var cardCampaigns: [CardCampaign] = [CardCampaign]()
#Published var isLoading: Bool = false
var cancellable: AnyCancellable?
func getCombineCampaigns() {
self.isLoading = true
let campaignLoader = CampaignLoader()
cancellable = campaignLoader.getCampaigns()
.receive(on: DispatchQueue.main)
//Handle Events operator is used for debugging.
.handleEvents(receiveSubscription: { print("Receive subscription: \($0)") },
receiveOutput: { print("Receive output: \($0)") },
receiveCompletion: { print("Receive completion: \($0)") },
receiveCancel: { print("Receive cancel") },
receiveRequest: { print("Receive request: \($0)") })
.sink { completion in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { campaignResult in
self.isLoading = false
guard let campaignsList = campaignResult.content else {
return
}
self.cardCampaigns = campaignsList.map { campaign in
return CardCampaign(campaign: campaign)
}
self.moveToFirstCard()
}
}
}
Use animation with linked related state
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [orangeColor, orangeColorOpaque]), center: .center), style: style
)
.rotationEffect(Angle(degrees: animate ? 360 : 0))
.animation(Animation.linear(duration: 0.7)
.repeatForever(autoreverses: false),
value: animate) // << here !!
}.onAppear() {
self.animate.toggle()
}
}
SwiftUI 3
(iOS 15)
use the new API
.animation(.default, value: isAppeared)
SwiftUI 1, 2
Like below, when I first load a view, the button was animated.
So, I used the GeometryReader.
The animation is set to nil while the coordinates of the dummy are changing.
RecordView.swift
struct RecordView: View {
var body: some View {
Button(action: { }) {
Color(.red)
.frame(width: 38, height: 38)
.cornerRadius(6)
.padding(34)
.overlay(Circle().stroke(Color(.red), lineWidth: 8))
}
.buttonStyle(ButtonStyle1()) // 👈 customize the button.
}
}
ButtonStyle.swift
import SwiftUI
struct ButtonStyle1: ButtonStyle {
// In my case, the #State isn't worked, so I used class.
#ObservedObject private var data = ButtonStyle1Data()
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
// Dummy for tracking the view status
GeometryReader {
Color.clear
.preference(key: FramePreferenceKey.self, value: $0.frame(in: .global))
}
.frame(width: 0, height: 0)
.onPreferenceChange(FramePreferenceKey.self) { frame in
guard !data.isAppeared else { return }
// ⬇️ This is the key
data.isLoaded = 0 <= frame.origin.x
}
// Content View
configuration.label
.opacity(configuration.isPressed ? 0.5 : 1)
.scaleEffect(configuration.isPressed ? 0.92 : 1)
.animation(data.isAppeared ? .easeInOut(duration: 0.18) : nil)
}
}
}
ButtonStyleData.swift
final class ButtonStyle1Data: ObservableObject {
#Published var isAppeared = false
}
FramePreferenceKey.swift
struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
}
Result
There are a lot of examples of bottom sheet out there for SwiftUI, however they all specify some type of maximum height the sheet can grow to using a GeometryReader. What I would like is to create a bottom sheet that becomes only as tall as the content within it. I've come up with the solution below using preference keys, but there must be a better solution. Perhaps using some type of dynamic scrollView is the solution?
struct ContentView: View{
#State private var offset: CGFloat = 0
#State private var size: CGSize = .zero
var body: some View{
ZStack(alignment:.bottom){
VStack{
Button(offset == 0 ? "Hide" : "Show"){
withAnimation(.linear(duration: 0.2)){
if offset == 0{
offset = size.height
} else {
offset = 0
}
}
}
.animation(nil)
.padding()
.font(.largeTitle)
Spacer()
}
BottomView(offset: $offset, size: $size)
}.edgesIgnoringSafeArea(.all)
}
}
struct BottomView: View{
#Binding var offset: CGFloat
#Binding var size: CGSize
var body: some View{
VStack(spacing: 0){
ForEach(0..<5){ value in
Rectangle()
.fill(value.isMultiple(of: 2) ? Color.blue : Color.red)
.frame(height: 100)
}
}
.offset(x: 0, y: offset)
.getSize{
size = $0
offset = $0.height
}
}
}
struct SizePreferenceKey: PreferenceKey {
struct SizePreferenceData {
let bounds: Anchor<CGRect>
}
static var defaultValue: [SizePreferenceData] = []
static func reduce(value: inout [SizePreferenceData], nextValue: () -> [SizePreferenceData]) {
value.append(contentsOf: nextValue())
}
}
struct SizePreferenceModifier: ViewModifier {
let onAppear: (CGSize)->Void
func body(content: Content) -> some View {
content
.anchorPreference(key: SizePreferenceKey.self, value: .bounds, transform: { [SizePreferenceKey.SizePreferenceData( bounds: $0)] })
.backgroundPreferenceValue(SizePreferenceKey.self) { preferences in
GeometryReader { geo in
Color.clear
.onAppear{
let size = CGSize(width: geo.size.width, height: geo.size.height)
onAppear(size)
}
}
}
}
}
extension View{
func getSize(_ onAppear: #escaping (CGSize)->Void) -> some View {
return self.modifier(SizePreferenceModifier(onAppear: onAppear))
}
}
Talk about over engineering the problem. All you have to do is specify a height of 0 if you want the sheet to be hidden, and not specify a height when it's shown. Additionally set the frame alignment to be top.
struct ContentView: View{
#State private var hide = false
var body: some View{
ZStack(alignment: .bottom){
Color.blue
.overlay(
Text("Is hidden : \(hide.description)").foregroundColor(.white)
.padding(.bottom, 200)
)
.onTapGesture{
hide.toggle()
}
VStack(spacing: 0){
ForEach(0..<5){ index in
Rectangle()
.foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
.frame(height: 50)
.layoutPriority(2)
}
}
.layoutPriority(1)
.frame(height: hide ? 0 : nil, alignment: .top)
.animation(.linear(duration: 0.2))
}.edgesIgnoringSafeArea(.all)
}
}
My approach is SwiftUI Sheet based solution feel free to check the gist
you just need to add the modifier to the view and let iOS do the rest for you, no need to re-do the math ;)
Plus you will have the sheet native behavior (swipe to dismiss) and i added "tap elsewhere" to dismiss.
struct ContentView: View {
#State var activeSheet: Bool = false
#State var activeBottomSheet: Bool = false
var body: some View {
VStack(spacing: 16){
Button {
activeSheet.toggle()
} label: {
HStack {
Text("Activate Normal sheet")
.padding()
}.background(
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 2)
.foregroundColor(.yellow)
)
}
Button {
activeBottomSheet.toggle()
} label: {
HStack {
Text("Activate Bottom sheet")
.padding()
}.background(
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 2)
.foregroundColor(.yellow)
)
}
}
.sheet(isPresented: $activeSheet) {
// Regular sheet
sheetView
}
.sheet(isPresented: $activeBottomSheet) {
// Responsive sheet
sheetView
.asResponsiveSheet()
}
}
var sheetView: some View {
VStack(spacing: 0){
ForEach(0..<5){ index in
Rectangle()
.foregroundColor(index.isMultiple(of: 2) ? Color.gray : .orange)
.frame(height: 50)
}
}
}
iPhone:
iPad :
I am following Apple's Swift UI Animating Views And Transitions and I noticed a bug in the Hike Graph View. When I click on the graph it does not allow me to switch from Elevation to Heart Rate or Pace. It does not let me and just exits the view. I think this has something to do with the List here:
VStack(alignment: .leading) {
Text("Recent Hikes")
.font(.headline)
HikeView(hike: hikeData[0])
}
Hike View Contains:
import SwiftUI
struct HikeView: View {
var hike: Hike
#State private var showDetail = false
var transition: AnyTransition {
let insertion = AnyTransition.move(edge: .trailing)
.combined(with: .opacity)
let removal = AnyTransition.scale
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
var body: some View {
VStack {
HStack {
HikeGraph(hike: hike, path: \.elevation)
.frame(width: 50, height: 30)
.animation(nil)
VStack(alignment: .leading) {
Text(hike.name)
.font(.headline)
Text(hike.distanceText)
}
Spacer()
Button(action: {
withAnimation {
self.showDetail.toggle()
}
}) {
Image(systemName: "chevron.right.circle")
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.scaleEffect(showDetail ? 1.5 : 1)
.padding()
}
}
if showDetail {
HikeDetail(hike: hike)
.transition(transition)
}
}
}
}
Hike Detail Contains:
struct HikeDetail: View {
let hike: Hike
#State var dataToShow = \Hike.Observation.elevation
var buttons = [
("Elevation", \Hike.Observation.elevation),
("Heart Rate", \Hike.Observation.heartRate),
("Pace", \Hike.Observation.pace),
]
var body: some View {
return VStack {
HikeGraph(hike: hike, path: dataToShow)
.frame(height: 200)
HStack(spacing: 25) {
ForEach(buttons, id: \.0) { value in
Button(action: {
self.dataToShow = value.1
}) {
Text(value.0)
.font(.system(size: 15))
.foregroundColor(value.1 == self.dataToShow
? Color.gray
: Color.accentColor)
.animation(nil)
}
}
}
}
}
}
Hike Graoh Contains:
import SwiftUI
func rangeOfRanges<C: Collection>(_ ranges: C) -> Range<Double>
where C.Element == Range<Double> {
guard !ranges.isEmpty else { return 0..<0 }
let low = ranges.lazy.map { $0.lowerBound }.min()!
let high = ranges.lazy.map { $0.upperBound }.max()!
return low..<high
}
func magnitude(of range: Range<Double>) -> Double {
return range.upperBound - range.lowerBound
}
extension Animation {
static func ripple(index: Int) -> Animation {
Animation.spring(dampingFraction: 0.5)
.speed(2)
.delay(0.03 * Double(index))
}
}
struct HikeGraph: View {
var hike: Hike
var path: KeyPath<Hike.Observation, Range<Double>>
var color: Color {
switch path {
case \.elevation:
return .gray
case \.heartRate:
return Color(hue: 0, saturation: 0.5, brightness: 0.7)
case \.pace:
return Color(hue: 0.7, saturation: 0.4, brightness: 0.7)
default:
return .black
}
}
var body: some View {
let data = hike.observations
let overallRange = rangeOfRanges(data.lazy.map { $0[keyPath: self.path] })
let maxMagnitude = data.map { magnitude(of: $0[keyPath: path]) }.max()!
let heightRatio = (1 - CGFloat(maxMagnitude / magnitude(of: overallRange))) / 2
return GeometryReader { proxy in
HStack(alignment: .bottom, spacing: proxy.size.width / 120) {
ForEach(data.indices) { index in
GraphCapsule(
index: index,
height: proxy.size.height,
range: data[index][keyPath: self.path],
overallRange: overallRange)
.colorMultiply(self.color)
.transition(.slide)
.animation(.ripple(index: index))
}
.offset(x: 0, y: proxy.size.height * heightRatio)
}
}
}
}
Graph Capsule Contains:
import SwiftUI
struct GraphCapsule: View {
var index: Int
var height: CGFloat
var range: Range<Double>
var overallRange: Range<Double>
var heightRatio: CGFloat {
max(CGFloat(magnitude(of: range) / magnitude(of: overallRange)), 0.15)
}
var offsetRatio: CGFloat {
CGFloat((range.lowerBound - overallRange.lowerBound) / magnitude(of: overallRange))
}
var body: some View {
Capsule()
.fill(Color.white)
.frame(height: height * heightRatio)
.offset(x: 0, y: height * -offsetRatio)
}
}
Is there any way to fix this? Thanks
The problem might be deeper in SwiftUI - if you comment out transition(.slide) in HikeGraph (and restart the XCODE), it will start working