Animated Expand/Collapse group in horizontal ScrollView (SwiftUI) - ios

I have a simple ForEach showing 5 or 10 Text views according to the expanded flag. everything is wrapped in a horizontal ScrollView because the text can be very long.
The animation when expanding the group looks fine, but when collapsing the group there is a small bouncing, the views goes up and down. This does not happen if I remove the ScrollView. Any idea what could cause this bouncing?
struct ContentView: View {
#State var expanded = false
let colors: [Color] = [.red, .green, .blue, .orange, .blue, .brown, .cyan, .gray, .indigo, .mint]
var body: some View {
VStack {
ScrollView(.horizontal) {
VStack(spacing: 20) {
ForEach(colors.prefix(upTo: expanded ? 10 : 5), id: \.self) { color in
Text(color.description.capitalized)
}
}
}
Button("Expand/collapse") {
expanded.toggle()
}
Spacer()
}.animation(.easeIn(duration: 1))
}
}

You need to use value for animation:
struct ContentView: View {
#State var expanded = false
let colors: [Color] = [.red, .green, .blue, .orange, .blue, .red, .green, .blue, .orange, .blue]
var body: some View {
VStack {
ScrollView(.horizontal) {
VStack(spacing: 20) {
ForEach(colors.prefix(upTo: expanded ? 10 : 5), id: \.self) { color in
Text(color.description.capitalized)
}
}
}
Button("Expand/collapse") {
expanded.toggle()
}
Spacer()
}.animation(.easeIn(duration: 1), value: expanded) // <<: Here!
}
}

Related

SwiftUI Rotation Animation off center

I am trying to make a simple dropdown list item in SwiftUI. This is what the code looks like:
struct SomeObject: Hashable {
var title: String = "title"
var entries: [String] = ["details", "details2", "details3"]
}
struct ContentView: View {
var data: [SomeObject] = [SomeObject()]
var body: some View {
List(data, id: \.self) { item in
HStack {
Text(item.title)
Spacer()
}
ForEach(item.entries, id: \.self) { entry in
ListItemView(entry)
}
}.listStyle(.plain)
}
}
struct ListItemView: View {
#State var expanded: Bool = false
#State var rotation: Double = 0
private let entry: String
init(_ entry: String) {
self.entry = entry
}
var body: some View {
VStack {
Divider().frame(maxWidth: .infinity)
.overlay(.black)
HStack {
Text(entry)
.fixedSize(horizontal: false, vertical: true)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.black)
.padding()
.rotationEffect(.degrees(expanded ? 180 : 360))
.animation(.linear(duration: 0.3), value: expanded)
}.padding(.horizontal)
.padding(.vertical, 6)
if expanded {
Text("Details")
}
Divider().frame(maxWidth: .infinity)
.overlay(.black)
}
.listRowSeparator(.hidden)
.listRowInsets(.init())
.onTapGesture {
expanded.toggle()
}
}
}
For some reason when clicking on the list item the animation looks like this:
How can I make the arrow rotate on its center point without moving up or down at all?
The problem you have there is that the arrow is animated but when the hidden text appears, that vertical expansion is not animated. That contrast between an element animated and another that is not makes the chevron looks like it is not doing it properly. So, try to animate the VStack like this:
struct CombineView: View {
#State var expanded: Bool = false
#State var rotation: Double = 0
let entry: String = "Detalle"
var body: some View {
VStack {
Divider().frame(maxWidth: .infinity)
.overlay(.black)
HStack(alignment: .center) {
Text(entry)
.fixedSize(horizontal: false, vertical: true)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.black)
.padding()
.rotationEffect(.degrees(expanded ? 180 : 360))
.animation(.linear(duration: 0.3), value: expanded)
}.padding(.horizontal)
.padding(.vertical, 6)
.background(.green)
if expanded {
Text("Details")
}
Divider().frame(maxWidth: .infinity)
.overlay(.black)
}.animation(.linear(duration: 0.3), value: expanded)//Animation added
.listRowSeparator(.hidden)
.listRowInsets(.init())
.onTapGesture {
expanded.toggle()
}
}
}
I hope this works for you ;)

How to add a bottom curve in TabView in SwiftUI?

I just want a Bottom curve in center of my tabView but i am not able to access tabView shape property.
This is what i want.
Note:
The curve should always remain in center. And the items should swap, which is already achieved in the given code.
import SwiftUI
struct DashboardTabBarView: View {
#State private var selection: String = "home"
struct Item {
let title: String
let color: Color
let icon: String
}
#State var items = [
Item(title: "cart", color: .red, icon: "cart"),
Item(title: "home", color: .blue, icon: "house"),
Item(title: "car", color: .green, icon: "car"),
]
var body: some View {
TabView(selection: $selection) {
ForEach(items, id: \.title) { item in // << dynamically !!
item.color
.tabItem {
Image(systemName: item.icon)
Text(item.title)
}
}
}
.onChange(of: selection) { title in // << reorder with centered item
let target = 1
if var i = items.firstIndex(where: { $0.title == title }) {
if i > target {
i += 1
}
items.move(fromOffsets: IndexSet(integer: target), toOffset: i)
}
}
}
}
Ok, actually we need to solve two problems here, first - find a height of tab bar, and second - correctly align view custom view with represented selected item over standard tab bar. Everything else is mechanics.
Here is simplified demo. Tested with Xcode 14 / iOS 16.
Main part:
a possible solution for problem #1
struct TabContent<V: View>: View {
#Binding var height: CGFloat
#ViewBuilder var content: () -> V
var body: some View {
GeometryReader { gp in // << read bottom edge !!
content()
.onAppear {
height = gp.safeAreaInsets.bottom
}
.onChange(of: gp.size) { _ in
height = gp.safeAreaInsets.bottom
}
}
}
}
a possible solution for problem #2
// Just put customisation in z-ordered over TabView
ZStack(alignment: .bottom) {
TabView(selection: $selection) {
// .. content here
}
TabSelection(height: tbHeight, item: selected)
}
struct TabSelection: View {
let height: CGFloat
let item: Item
var body: some View {
VStack {
Spacer()
Curve() // put curve over tab bar !!
.frame(maxWidth: .infinity, maxHeight: height)
.foregroundColor(item.color)
}
.ignoresSafeArea() // << push to bottom !!
.overlay(
// Draw overlay
Circle().foregroundColor(.black)
.frame(height: height).aspectRatio(contentMode: .fit)
.shadow(radius: 4)
.overlay(Image(systemName: item.icon)
.font(.title)
.foregroundColor(.white))
, alignment: .bottom)
}
}
Test module is here

SwiftUI make scrollview item by item

I need to create a list view with item like a square on a scrollview.
But I need to make the scroll only work to one item to another.
I could see the article allowing to set up this system on a HStack but I can't adapt it on a VStack
This article : https://levelup.gitconnected.com/snap-to-item-scrolling-debccdcbb22f
If someone has an idea to help me in this research?
I join an example of scrollview with the square :
struct ScrollingSnapped: View {
var colors: [Color] = [.blue, .green, .red, .orange, .gray, .brown, .yellow, .purple]
var body: some View {
ScrollView {
VStack(alignment: .trailing, spacing: 30) {
ForEach(0..<colors.count) { index in
colors[index]
.frame(width: 250, height: 250, alignment: .center)
.cornerRadius(10)
}
}
}
}
}
struct ScrollingSnapped_Previews: PreviewProvider {
static var previews: some View {
ScrollingSnapped()
}
}
I tried to work with a ScrollViewReader that allows to scrollTo. It works when clicking on a button but I can't get it to work when scrolling :
import SwiftUI
struct ScrollingSnapped: View {
var colors: [Color] = [.blue, .green, .red, .orange, .gray, .brown, .yellow, .purple]
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Button {
proxy.scrollTo(4, anchor: .top)
} label: {
Text("Scroll to square 5")
}
VStack(alignment: .trailing, spacing: 30) {
ForEach(0..<colors.count) { index in
colors[index]
.frame(width: 250, height: 250, alignment: .center)
.cornerRadius(10)
.id(index)
}
}
}
}
}
}
struct ScrollingSnapped_Previews: PreviewProvider {
static var previews: some View {
ScrollingSnapped()
}
}

How to get button itself in action closure with SwiftUI?

Is possible to get button itself in action closure with SwiftUI ?
struct ContentView: View {
var body: some View {
Button("test") {
// change the color of button when user is tapping on it
}
}
}
If you want to access the properties of your button you can create a custom ButtonStyle.
Use its configuration to set the desired behaviour.
struct CustomButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [.red, .orange]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.scaleEffect(configuration.isPressed ? 0.9 : 1)
}
}
The above example was taken from here: SwiftUI Tip: ButtonStyle and Animated Buttons
In your case you can adapt it to set a custom background:
.background(configuration.isPressed ? Color.red : Color.blue)
Here's how:
struct ContentView: View {
#State var color = Color.red
var body: some View {
Button("test") {
self.color = Color.green
}.background(color)
}
}
Ya... I just figure out by myself too
struct ContentView: View {
#State var buttonColor: Color = Color.clear
var body: some View {
Button(action: {
self.buttonColor = Color(red: Double.random(in: 0...1),
green: Double.random(in: 0...1),
blue: Double.random(in: 0...1))
}, label: {
Text("Button").background(buttonColor)
})
}
}

SwiftUI Create a Custom Segmented Control also in a ScrollView

Below is my code to create a standard segmented control.
struct ContentView: View {
#State private var favoriteColor = 0
var colors = ["Red", "Green", "Blue"]
var body: some View {
VStack {
Picker(selection: $favoriteColor, label: Text("What is your favorite color?")) {
ForEach(0..<colors.count) { index in
Text(self.colors[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Value: \(colors[favoriteColor])")
}
}
}
My question is how could I modify it to have a customized segmented control where I can have the boarder rounded along with my own colors, as it was somewhat easy to do with UIKit? Has any one done this yet.
I prefect example is the Uber eats app, when you select a restaurant you can scroll to the particular portion of the menu by selecting an option in the customized segmented control.
Included are the elements I'm looking to have customized:
* UPDATE *
Image of the final design
Is this what you are looking for?
import SwiftUI
struct CustomSegmentedPickerView: View {
#State private var selectedIndex = 0
private var titles = ["Round Trip", "One Way", "Multi-City"]
private var colors = [Color.red, Color.green, Color.blue]
#State private var frames = Array<CGRect>(repeating: .zero, count: 3)
var body: some View {
VStack {
ZStack {
HStack(spacing: 10) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.titles[index])
}.padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)).background(
GeometryReader { geo in
Color.clear.onAppear { self.setFrame(index: index, frame: geo.frame(in: .global)) }
}
)
}
}
.background(
Capsule().fill(
self.colors[self.selectedIndex].opacity(0.4))
.frame(width: self.frames[self.selectedIndex].width,
height: self.frames[self.selectedIndex].height, alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
, alignment: .leading
)
}
.animation(.default)
.background(Capsule().stroke(Color.gray, lineWidth: 3))
Picker(selection: self.$selectedIndex, label: Text("What is your favorite color?")) {
ForEach(0..<self.titles.count) { index in
Text(self.titles[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
Text("Value: \(self.titles[self.selectedIndex])")
Spacer()
}
}
func setFrame(index: Int, frame: CGRect) {
self.frames[index] = frame
}
}
struct CustomSegmentedPickerView_Previews: PreviewProvider {
static var previews: some View {
CustomSegmentedPickerView()
}
}
If I'm following the question aright the starting point might be something like the code below. The styling, clearly, needs a bit of attention. This has a hard-wired width for segments. To be more flexible you'd need to use a Geometry Reader to measure what was available and divide up the space.
struct ContentView: View {
#State var selection = 0
var body: some View {
let item1 = SegmentItem(title: "Some Way", color: Color.blue, selectionIndex: 0)
let item2 = SegmentItem(title: "Round Zip", color: Color.red, selectionIndex: 1)
let item3 = SegmentItem(title: "Multi-City", color: Color.green, selectionIndex: 2)
return VStack() {
Spacer()
Text("Selected Item: \(selection)")
SegmentControl(selection: $selection, items: [item1, item2, item3])
Spacer()
}
}
}
struct SegmentControl : View {
#Binding var selection : Int
var items : [SegmentItem]
var body : some View {
let width : CGFloat = 110.0
return HStack(spacing: 5) {
ForEach (items, id: \.self) { item in
SegmentButton(text: item.title, width: width, color: item.color, selectionIndex: item.selectionIndex, selection: self.$selection)
}
}.font(.body)
.padding(5)
.background(Color.gray)
.cornerRadius(10.0)
}
}
struct SegmentButton : View {
var text : String
var width : CGFloat
var color : Color
var selectionIndex = 0
#Binding var selection : Int
var body : some View {
let label = Text(text)
.padding(5)
.frame(width: width)
.background(color).opacity(selection == selectionIndex ? 1.0 : 0.5)
.cornerRadius(10.0)
.foregroundColor(Color.white)
.font(Font.body.weight(selection == selectionIndex ? .bold : .regular))
return Button(action: { self.selection = self.selectionIndex }) { label }
}
}
struct SegmentItem : Hashable {
var title : String = ""
var color : Color = Color.white
var selectionIndex = 0
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
None of the above solutions worked for me as the GeometryReader returns different values once placed in a Navigation View that throws off the positioning of the active indicator in the background. I found alternate solutions, but they only worked with fixed length menu strings. Perhaps there is a simple modification to make the above code contributions work, and if so, I would be eager to read it. If you're having the same issues I was, then this may work for you instead.
Thanks to inspiration from a Reddit user "End3r117" and this SwiftWithMajid article, https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/, I was able to craft a solution. This works either inside or outside of a NavigationView and accepts menu items of various lengths.
struct SegmentMenuPicker: View {
var titles: [String]
var color: Color
#State private var selectedIndex = 0
#State private var frames = Array<CGRect>(repeating: .zero, count: 5)
var body: some View {
VStack {
ZStack {
HStack(spacing: 10) {
ForEach(self.titles.indices, id: \.self) { index in
Button(action: {
print("button\(index) pressed")
self.selectedIndex = index
}) {
Text(self.titles[index])
.foregroundColor(color)
.font(.footnote)
.fontWeight(.semibold)
}
.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5))
.modifier(FrameModifier())
.onPreferenceChange(FramePreferenceKey.self) { self.frames[index] = $0 }
}
}
.background(
Rectangle()
.fill(self.color.opacity(0.4))
.frame(
width: self.frames[self.selectedIndex].width,
height: 2,
alignment: .topLeading)
.offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX, y: self.frames[self.selectedIndex].height)
, alignment: .leading
)
}
.padding(.bottom, 15)
.animation(.easeIn(duration: 0.2))
Text("Value: \(self.titles[self.selectedIndex])")
Spacer()
}
}
}
struct FramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
struct FrameModifier: ViewModifier {
private var sizeView: some View {
GeometryReader { geometry in
Color.clear.preference(key: FramePreferenceKey.self, value: geometry.frame(in: .global))
}
}
func body(content: Content) -> some View {
content.background(sizeView)
}
}
struct NewPicker_Previews: PreviewProvider {
static var previews: some View {
VStack {
SegmentMenuPicker(titles: ["SuperLongValue", "1", "2", "Medium", "AnotherSuper"], color: Color.blue)
NavigationView {
SegmentMenuPicker(titles: ["SuperLongValue", "1", "2", "Medium", "AnotherSuper"], color: Color.red)
}
}
}
}

Resources