I created a UISlider via SwiftUI, but there are just too many "step marks" along the track, which doesn't have the "look and feel" I wish to achieve. Anyone knows the trick to remove them other than turning the tint color to black?
It seems that the step/tick marks are always there as long as I pass any step values during UISlider initialization.
struct setLoggingView: View {
#State var restfullness: Int
#State var elapsedRestTime: Double
var totalRestTime: Double
var stepValue: Int
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
Text("Rested \(Int(elapsedRestTime)) seconds")
Slider(value: $elapsedRestTime,
in: 0...totalRestTime,
step: Double.Stride(stepValue),
label: {
Text("Slider")
}, minimumValueLabel: {
Text("-\(stepValue)")
}, maximumValueLabel: {
Text("+\(stepValue)")
})
.tint(Color.white)
.padding(.bottom)
Divider()
Spacer()
Text("Restfullness")
.frame(minWidth: 0, maxWidth: .infinity)
restfullnessStepper(restfullnessIndex: restfullness)
Button(action: {
print("Update Button Pressed")
}) {
HStack {
Text("Update")
.fontWeight(.medium)
}
}
.cornerRadius(40)
}
.border(Color.yellow)
}
}
}
}
Tried research and asked a few mentors, but there seems to be no way to remove the steppers if you are using the default UISlider in SwiftUI. The only way is to create completely custom Slider, but I think I will just live with the default version.
Until Apple improves SwiftUI, the way to do this is by providing a custom binding INSTEAD OF using the step parameter:
Slider(value: Binding(get: { dailyGoalMinutes },
set: { newValue in
let base: Int = Int(newValue.rounded())
let modulo: Int = base % 10 // Your step amount. Here, 10.
dailyGoalMinutes = Double(base - modulo)
}),
in: 0...1440)
In this example, dailyGoalMinutes is declared on the view like this:
#State private var dailyGoalMinutes: Double = 180.0
The slider allows the user to select between 0 and 1,440 minutes (1 day) in 10-minute increments. While the slider itself won't snap to those increments while dragging, the value of dailyGoalMinutes will be properly constrained to multiples of 10 within the defined range.
(Note: AppKit behaves the same way; when NSSlider does not have any tick marks visible, the slider does not snap to values.)
Related
I’m after a transition that reverse animates between two values. Below demonstrates exactly what I’m looking for w/ code:
struct RootView: View {
#State var pressed: Bool = false
var body: some View {
VStack {
if !pressed {
Text("1")
.animation(.easeIn)
.transition(.move(edge: .top).combined(with: .opacity))
}
else {
Text("0")
.animation(.easeIn)
.transition(.move(edge: .bottom).combined(with: .opacity))
}
Button(action: {
withAnimation {
pressed.toggle()
}
}, label: {
Text("toggle")
}).buttonStyle(.bordered)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.red)
}
}
That said, I don’t want to be animating between two separate views to achieve the intended effect. This because the content type/presentation style of the view is consistent between value changes (to be clear, it’s for a calendar with the transition occurring when you enter a different month, i.e. transitions from bottom when you enter a new month, and from top when you return to a previous month).
So, is there a way to execute this transition with a single view? Using some creativity I’ve sort of managed to pull it off like this:
struct RootView: View {
#State var pressed: Bool = false
#State var value: Int = 0
var body: some View {
VStack {
if !pressed {
Text("\(value)")
.transition(.move(edge: value == 0 ? .bottom : .top).combined(with: .opacity))
}
else {
Text("\(value)")
.hidden()
}
Button(action: {
withAnimation(.easeOut.speed(2)) {
pressed.toggle()
if value == 0 {
value = 1
}
else {
value = 0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation(.easeOut) {
pressed.toggle()
}
}
}
}, label: {
Text("toggle")
}).buttonStyle(.bordered)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.red)
}
The gif captured below doesn’t accurately how it actually plays in real-time. It’s almost acceptable, but not as fluid as the first (more conventional) method. Even if I removed the delay, it doesn’t really help. It appears the view comes in from the correct position (top or bottom) but the sequence has stutter. I’m not surprised because this doesn’t seem the correct way to handle this request. But I’m wondering whether it’s only a matter of a missing container, misplaced/missing modifiers etc. From experience, it usually is!
I created this View modifier which animates a change in the offset of a view based on a boolean Binding and resets itself when the animation finishes (also resetting the boolean Binding).
struct OffsetViewModifier: AnimatableModifier {
let amount: Double // amount of offset
#Binding var animate: Bool // determines if the animation should begin
private(set) var pct = 0.0 // percentage of the animation complete
var animatableData: Double {
get { animate ? 1.0 : 0.0 }
set { pct = newValue }
}
func body(content: Content) -> some View {
content
.offset(x: amount * pct)
.onChange(of: pct) { newValue in
if newValue == 1.0 { // If newValue is 1.0 that means animation is complete
withAnimation(.none) { animate = false } // When animation completes, reset binding
// Since I don't want this to cause an animation, I wrap it with withAnimation(.none)
}
}
}
}
I used this new modifier like this:
VStack {
Text(tapped.description)
Circle()
.frame(width: 100, height: 100)
.onTapGesture {
tapped = true
}
.modifier(OffsetViewModifier(amount: 50, animate: $tapped))
.animation(Animation.linear(duration: 3), value: tapped)
}
However, the withAnimation(.none) didn't work, and this view still takes 3 seconds to reset.
How do I force the explicit animation to occur and not the .linear(duration: 3) one?
Thanks in advance.
If I understood this correctly, what you should do is wrap tapped within withAnimation because the .animation is modifying the whole view like this.
tapped is binded to animate, when you change animate's value in the modifier, you're changing the value of tapped, therefore, executing the linear animation
VStack {
Text(tapped.description)
Circle()
.frame(width: 100, height: 100)
.onTapGesture {
withAnimation(.linear(duration: 3)) {
tapped = true
}
}
.modifier(OffsetViewModifier(amount: 50, animate: $tapped))
}
My personal advice is to avoid using .animation() because like I said before it will animate the entire view and will cause problems like this one
I have a custom swiftUI picker that takes a duration in the form of the picture above. The issue is, I have to use hard coded frame in my code to make it show and appear. I will explain further below.
var body: some View {
let hours = [Int](0...maxHours)
let minutes = [Int](0...maxMinutes)
HStack(spacing: 0) {
Picker(selection: self.selection.hours, label: Text("")) {
ForEach(0..<maxHours, id: \.self) { index in
Text("\(hours[index]) hr")
.foregroundColor(Color(Asset.Color.V2.white.color))
}
}
.labelsHidden()
.pickerStyle(.wheel)
Picker(selection: self.selection.minutes, label: Text("")) {
ForEach(0..<maxMinutes, id: \.self) { index in
Text("\(minutes[index]) min")
.foregroundColor(Color(Asset.Color.V2.white.color))
}
.frame(maxWidth: .infinity, alignment: .center)
}
.labelsHidden()
.pickerStyle(.wheel)
}
}
The issue is I am combining two pickers and it is not a native approach. So it'll end up looking like this:
The frame becomes very small and off when it is part of a larger component.
If I remove the HStack and have one picker, the frame and sizing will fix themselves.
note: Ignore the colors, my only concern is the frame sizes that get messed up when I have two pickers.
problem 1: This picker is part of another large component. Here is how the structure is set up, and apologies in advance as I cannot share all of the code as it will be be more than 5000 lines of code.
We have this:
VStack {
element1
element2
...
element4
.onTap{
showCustomPickerView = true
}
.frame(width: 200, height: 200)
if showCustomPickerView {
CustomPickerView()
}
element5
}
So when we click on element4 which is essentially an HStack, we want our custom picker view to appear underneath it. The issue is I do not want hard coded frame values, but when I remove the frame, the CustomPickerView collapses and becomes like the picture I posted. If my CustomPickerView has only one picker in it, it shows just fine without the frame. But since I have two and I they are in an HStack, it does not show their default size, and I am guessing it shows the HStack size instead.
update 1: I added
extension UIPickerView {
override open var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: super.intrinsicContentSize.height)
}
}
at my file, as without it, the right picker would get mixed with the first one, but the framing issue still persists.
I've slightly modified your solution, and it works fine for me:
#main
struct Test: App {
#State var hourSelection = 0
#State var minuteSelection = 0
var body: some Scene {
WindowGroup {
CustomDatePicker(hourSelection: $hourSelection, minuteSelection: $minuteSelection)
}
}
}
struct CustomDatePicker: View {
#Binding var hourSelection: Int
#Binding var minuteSelection: Int
static private let maxHours = 24
static private let maxMinutes = 60
private let hours = [Int](0...Self.maxHours)
private let minutes = [Int](0...Self.maxMinutes)
var body: some View {
GeometryReader { geometry in
HStack(spacing: .zero) {
Picker(selection: $hourSelection, label: Text("")) {
ForEach(hours, id: \.self) { value in
Text("\(value) hr")
.tag(value)
}
}
.pickerStyle(.wheel)
.frame(width: geometry.size.width / 2, alignment: .center)
Picker(selection: $minuteSelection, label: Text("")) {
ForEach(minutes, id: \.self) { value in
Text("\(value) min")
.tag(value)
}
.frame(maxWidth: .infinity, alignment: .center)
}
.pickerStyle(.wheel)
.frame(width: geometry.size.width / 2, alignment: .center)
}
}
}
}
The main idea here is that:
You do not specify the height of the picker, so the GeometryReader adjusts its size to correspond to the default Picker height.
Change the width from geometry.size.width / 3 to geometry.size.width / 2 (as there are only two pickers).
Remove the unnecessary modifiers (.compositeGroup(), .clipped(), .etc).
Move the picker into a separate struct for ease of use.
Alternatively, you can manually specify a fixed size for the custom component using the .frame(height:) modifier.
Let me know if it still collapses
I have an app in which I'm trying to animate different properties differently upon change. In the following demonstration app, a spring animation applies to both size and position when the "Flip" button is pressed:
Here is the code:
class Thing: Identifiable {
var id: Int
init(id: Int) {
self.id = id
}
}
struct ContentView: View {
#State var isFlipped: Bool = false
let thing1 = Thing(id: 1)
let thing2 = Thing(id: 2)
var body: some View {
VStack(spacing: 12) {
HStack(spacing: 20) {
ForEach(isFlipped ? [thing2,thing1] : [thing1, thing2]) { thing in
Text("\(thing.id)").font(.system(size: 150, weight: .heavy))
.scaleEffect(isFlipped ? CGFloat(thing.id)*0.4 : 1.0)
.animation(.spring(response: 0.5, dampingFraction: 0.3))
}
}
Button("Flip") { isFlipped.toggle() }
}
}
}
My question is: how can I animate the positions without animating the scale?
If I remove the .scaleEffect() modifier, just the positions are animated.
But if I then insert it after the .animation() modifier, then no animation at all occurs, not even the positions. Which seems very strange to me!
I'm familiar with the "animation stack" concept - that which animations apply to which view properties depends on the order in which modifiers and animations are applied to the view. But I can't make sense of where the positions lie in that stack… or else how to think about what's going on.
Any thoughts?
EDITED: I changed the .scaleEffect() modifier to operate differently on the different Thing objects, to include that aspect of the problem I face; thank you to #Bill for the solution for the case when it doesn't.
How about scaling the HStack instead of Text?
var body: some View {
VStack(spacing: 12) {
HStack(spacing: 20) {
ForEach(isFlipped ? [thing2,thing1] : [thing1, thing2]) { thing in
Text("\(thing.id)").font(.system(size: 150, weight: .heavy))
}
}
.animation(.spring(response: 0.5, dampingFraction: 0.3))
.scaleEffect(isFlipped ? 0.5 : 1.0)
Button("Flip") { isFlipped.toggle() }
}
}
This is only one year and five months late, but hey!
To stop the animation of scaleEffect it might work to follow the .scaleEffect modifier with an animation(nil, value: isFlipped) modifier.
Paul Hudson (Hacking With Swift) discusses multiple animations and the various modifiers here. You asked about the concepts involved and Paul provides a quick overview.
Alternatively, take a look at the code below. It is my iteration of the solution that Paul suggests.
/*
Project Purpose:
Shows how to control animations for a single view,
when multiple animations are possible
This view has a single button, with both the button's
background color and the clipShape under control
of a boolean. Tapping the button toggles the boolean.
The object is to make the change in clipShape
animated, while the change in background color
is instantaneous.
Take Home: if you follow an "animatable" modifier
like `.background(...)` with an `.animation` modifier
with an `animation(nil)' modifier then that will cancel
animation of the background change. In contrast,
`.animation(.default)` allows the previous animatable
modifier to undergo animation.
*/
import SwiftUI
struct ContentView: View {
#State private var isEnabled = false
var body: some View {
Button( "Background/ClipShape" )
{
isEnabled.toggle()
}
.foregroundColor( .white )
.frame( width: 200, height: 200 )
// background (here dependent on isEnabled) is animatable.
.background( isEnabled ? Color.green : Color.red )
// the following suppresses the animation of "background"
.animation( nil, value: isEnabled )
// clipshape (here dependent on isEnabled) is animatable
.clipShape(
RoundedRectangle(
cornerRadius: isEnabled ? 100 : 0 ))
// the following modifier permits animation of clipShape
.animation( .default, value: isEnabled )
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm getting some odd animation behaviour with DatePickers in a SwiftUI form. A picture is worth a thousand words, so I'm sure a video is worth a million words: https://imgur.com/a/UHXqXOh
I'm trying to get the date picker to expand and then collapse within the form, exactly like the behaviour when creating a new event in Calendar.app
What is happening for me is:
Any expanding item in a Section (other than the last one) will open normally, but when it closes the expanded part slides down and fades, instead of sliding up and fading.
The last item in the section slides correctly but doesn't fade at all. It simply appears and then disappears at the start/end of the transition
These behaviours only happen if there is a non-DatePicker element (e.g. Text, Slider) somewhere in the form (doesn't have to be in that particular section)
Here's my ContentView:
struct ContentView: View {
#State var date = Date()
#State var isDateShown = false
var body: some View {
Form {
Section(header: Text("Title")) {
DatePicker("Test", selection:$date)
DatePicker("Test", selection:$date)
Text("Pick a date").onTapGesture {
withAnimation {
self.isDateShown.toggle()
}
}
if(isDateShown) {
DatePicker("", selection: $date).datePickerStyle(WheelDatePickerStyle()).labelsHidden()
}
}
Section(header: Text("hello")) {
Text("test")
}
}
}
}
Happy to provide anything else required
Here are two possible workarounds for iOS <14: 1) simple one is to disable animation at all, and 2) complex one is to mitigate incorrect animation by injecting custom animatable modifier
Tested both with Xcode 11.4 / iOS 13.4
1) simple solution - wrap DatePicker into container and set animation to nil
VStack {
DatePicker("Test", selection:$date).id(2)
}.animation(nil)
2) complex solution - grab DatePicker changing frame using a) view preference reader ViewHeightKey and b) animate this frame explicitly using AnimatingCellHeight from my other solutions.
struct TestDatePickersInForm: View {
#State var date = Date()
#State var isDateShown = false
#State private var height = CGFloat.zero
var body: some View {
Form {
Section(header: Text("Title")) {
// demo of complex solution
VStack {
DatePicker("Test", selection:$date).id(1)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height) })
}
.onPreferenceChange(ViewHeightKey.self) { self.height = $0 }
.modifier(AnimatingCellHeight(height: height))
.animation(.default)
// demo of simple solution
VStack {
DatePicker("Test", selection:$date).id(2)
}.animation(nil)
Text("Pick a date").onTapGesture {
withAnimation {
self.isDateShown.toggle()
}
}
if(isDateShown) {
DatePicker("", selection: $date).datePickerStyle(WheelDatePickerStyle()).labelsHidden().id(3)
}
}
Section(header: Text("hello")) {
Text("test")
}
}
}
}
Funny enough.. with the new beta, they apparently changed the DatePicker.
So if you have no problem with iOS 14+ only...
closest solution is to move the datepickers to its own sections
Form {
Section(header: Text("Title")) {
DatePicker(selection:$date1, label: {Text("Test")} )
}
DatePicker("Test", selection:$date2)
Section{
Text("Pick a date").onTapGesture {
withAnimation {
self.isDateShown.toggle()
}
}
if(isDateShown) {
DatePicker("", selection: $date3).datePickerStyle(WheelDatePickerStyle()).labelsHidden()
}
}
Section(header: Text("Hello")){
Text("Hello")
}
}