SwiftUI Rotation Animation off center - ios

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 ;)

Related

How to hide TabView in NavigationLink?

First of all I did a full research for my problem and NOTHING on Google helped me,
the problem is simple, it seems that the solution should also be simple, but I can't hide the tabbar in NavigationLink, and if something works out, then wierd behavior of the buttons and the transition back, etc...
TabView itself
import SwiftUI
struct Main: View {
#State var currentTab: Int = 0
var body: some View {
TabView(selection: $currentTab) {
HomeView().tag(0)
AccountInfoView().tag(1)
SettingsView().tag(2)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.edgesIgnoringSafeArea(.bottom)
.overlay(
TabBarView(currentTab: $currentTab),
alignment: .top
)
}
}
struct TabBarView: View {
#Binding var currentTab: Int
#Namespace var namespace
var tabBarOptions: [String] = ["Search", "Items", "Account"]
var body: some View {
HStack(spacing: 0) {
ForEach(Array(zip(self.tabBarOptions.indices,
self.tabBarOptions)),
id: \.0,
content: {
index, name in
TabBarItem(currentTab: self.$currentTab,
namespace: namespace.self,
tabBarItemName: name,
tab: index)
})
}
.padding(.top)
.background(Color.clear)
.frame(height: 100)
.edgesIgnoringSafeArea(.all)
}
}
struct TabBarItem: View {
#Binding var currentTab: Int
let namespace: Namespace.ID
var tabBarItemName: String
var tab: Int
var body: some View {
Button {
self.currentTab = tab
} label: {
VStack {
Spacer()
Text(tabBarItemName)
if currentTab == tab {
CustomColor.myColor
.frame(height: 2)
.matchedGeometryEffect(id: "underline",
in: namespace,
properties: .frame)
} else {
Color.clear.frame(height: 2)
}
}
.animation(.spring(), value: self.currentTab)
}
.fontWeight(.heavy)
.buttonStyle(.plain)
}
}
NavigationLink -> this is just the part of the code that contains the NavigationLink, this VStack of course is inside the NavigationView.
struct HomeView: View {
NavigationView {
...
VStack(spacing: 15) {
ForEach(self.data.datas.filter {(self.search.isEmpty ? true : $0.title.localizedCaseInsensitiveContains(self.search))}, id: \.id) { rs in
NavigationLink(
destination: ItemDetails(data: rs)
){
RecentItemsView(data: rs)
}
.buttonStyle(PlainButtonStyle())
}
}
}
}
ItemDetails
struct ItemDetails: View {
let data: DataType
var body : some View {
NavigationView {
VStack {
AsyncImage(url: URL(string: data.pic), content: { image in
image.resizable()
}, placeholder: {
ProgressView()
})
.aspectRatio(contentMode: .fill)
.frame(width: 250, height: 250)
.clipShape(RoundedRectangle(cornerRadius: 12.5))
.padding(10)
VStack(alignment: .leading, spacing: 8, content: {
Text(data.title)
.fontWeight(.bold)
.frame(maxWidth: .infinity, alignment: .center)
Text(data.description)
.font(.caption)
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .center)
})
.padding(20)
}
.padding(.horizontal)
}
.navigationBarBackButtonHidden(true)
}
}
I apologize for the garbage in the code, it seemed to me that there is not much of it and it does not interfere with understanding the code, also during the analysis of this problem on the Google\SO, I did not need to work with other parts of the code anywhere, except for those that I provided above, but if I missed something, then please let me know, thanks.

SwiftUI List only taps content

I have a List in SwiftUI that I populate with a custom SwiftUI cell, the issue is that on tap I need to do some stuff and the tap only works when you click the text in the cell, if you click any empty space it will not work. How can I fix this?
struct SelectDraftView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var viewModel = SelectDraftViewModel()
var body: some View {
VStack {
List {
ForEach(viewModel.drafts.indices, id: \.self) { index in
DraftPostCell(draft: viewModel.drafts[index])
.contentShape(Rectangle())
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
.onDelete { indexSet in
guard let delete = indexSet.map({ viewModel.drafts[$0] }).first else { return }
viewModel.delete(draft: delete)
}
}
.background(Color.white)
Spacer()
}
}
}
struct DraftPostCell: View {
var draft: CDDraftPost
var body: some View {
VStack(alignment: .leading) {
Text(draft.title ?? "")
.frame(alignment: .leading)
.font(Font(UIFont.uStadium.helvetica(ofSize: 14)))
.padding(.bottom, 10)
if let body = draft.body {
Text(body)
.frame(alignment: .leading)
.multilineTextAlignment(.leading)
.frame(maxHeight: 40)
.font(Font(UIFont.uStadium.helvetica(ofSize: 14)))
}
Text(draft.date?.toString(format: "EEEE, MMM d, yyyy") ?? "")
.frame(alignment: .leading)
.font(Font(UIFont.uStadium.helvetica(ofSize: 12)))
}
.padding(.horizontal, 16)
}
}
try adding .frame(idealWidth: .infinity, maxWidth: .infinity) just after DraftPostCell(...). You can also use a minWidth: if required.
EDIT-1: the code I use for testing (on real devices ios 15.6, macCatalyst, not Previews):
import Foundation
import SwiftUI
struct ContentView: View {
var body: some View {
SelectDraftView()
}
}
class SelectDraftViewModel: ObservableObject {
#Published var drafts: [
CDDraftPost] = [
CDDraftPost(title: "item 1", date: Date(), body: "body 1"),
CDDraftPost(title: "item 2", date: Date(), body: "body 4"),
CDDraftPost(title: "item 3", date: Date(), body: "body 3")]
func delete(draft: CDDraftPost) { }
}
struct CDDraftPost: Codable {
var title: String?
var date: Date?
var body: String?
}
struct SelectDraftView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var viewModel = SelectDraftViewModel()
var body: some View {
VStack {
List {
ForEach(viewModel.drafts.indices, id: \.self) { index in
DraftPostCell(draft: viewModel.drafts[index])
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.border(.red) // <-- for testing
.onTapGesture {
print("----> onTapGesture")
// presentationMode.wrappedValue.dismiss()
}
}
.onDelete { indexSet in
guard let delete = indexSet.map({ viewModel.drafts[$0] }).first else { return }
viewModel.delete(draft: delete)
}
}
.background(Color.white)
Spacer()
}
}
}
struct DraftPostCell: View {
var draft: CDDraftPost
var body: some View {
VStack(alignment: .leading) {
Text(draft.title ?? "")
.frame(alignment: .leading)
// .font(Font(UIFont.uStadium.helvetica(ofSize: 14)))
.padding(.bottom, 10)
if let body = draft.body {
Text(body)
.frame(alignment: .leading)
.multilineTextAlignment(.leading)
.frame(maxHeight: 40)
// .font(Font(UIFont.uStadium.helvetica(ofSize: 14)))
}
Text(draft.date?.formatted() ?? "")
.frame(alignment: .leading)
// .font(Font(UIFont.uStadium.helvetica(ofSize: 12)))
}
.padding(.horizontal, 16)
}
}
I'm probably late but this will be useful for anyone checking this in the future.
You need to add .background() modifier to your view before you do .onTapGesture{...}
so in your ForEach code would be modified like this:
ForEach(viewModel.drafts.indices, id: \.self) { index in
DraftPostCell(draft: viewModel.drafts[index])
.contentShape(Rectangle())
.frame(maxWidth: .infinity) // you should use the frame parameter according to your needs, but if you want your cell to occupy the whole width of your scroll view, use this one
.background() // this makes the empty portions of view 'non-transparent', so those portions also receive the tap gesture
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
P.S if you need the whole portion of your scroll view cell to receive the tap gesture you'll also need to add .frame(...) modifier, so it has the exact background you want

how to appear a list (using animation) once the button is pressed?

I want once I press the button search
VStack{
Text("Enter friends first name")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(Color("Color"))
TextField("firstname", text: $firstname)
.padding()
.keyboardType(.default)
.background(Color.white)
.autocapitalization(.none)
.textFieldStyle(.roundedBorder)
.shadow(color: Color.gray.opacity(0.1), radius: 5, x: 0, y: 2)
Text("Enter friends last Name")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(Color("Color"))
TextField("lastname", text: $lastname)
.padding()
.keyboardType(.default)
.background(Color.white)
.autocapitalization(.none)
.textFieldStyle(.roundedBorder)
.shadow(color: Color.gray.opacity(0.1), radius: 5, x: 0, y: 2)
Button (action:{
searchUser()
},label:{
Text("Search")
})
}
the list that is in searchUser()that shows the names of friends with this first name and last name and their details appears on the this view under the search button and once the button is pressed but with animation ? thanks
I tried to do the animation but it didn't work. does anyone know how can I do it ?
You can show/hide views conditionally by putting them inside if block.
struct ContentView: View {
#State var shouldShowList = false
var body: some View {
VStack {
if shouldShowList {
VStack {
ForEach(0 ..< 5) { item in
Text("Hello, world!")
.padding()
}
}
}
Button( shouldShowList ? "Hide" : "Show") {
shouldShowList.toggle()
}
}
.animation(.easeInOut, value: shouldShowList) // animation
}
}
Instead,
You can use a view modifier to show/hide.
1. create your own ViewModifire
struct Show: ViewModifier {
let isVisible: Bool
#ViewBuilder
func body(content: Content) -> some View {
if isVisible {
EmptyView()
} else {
content
}
}
}
extension View {
func show(isVisible: Bool) -> some View {
ModifiedContent(content: self, modifier: Show(isVisible: isVisible))
}
}
Usage
struct ContentView: View {
#State var shouldShowList = false
var body: some View {
VStack {
VStack {
ForEach(0 ..< 5) { item in
Text("Hello, world!")
.padding()
}
}
.show(isVisible: shouldShowList) //<= here
Button( shouldShowList ? "Hide" : "Show") {
shouldShowList.toggle()
}
}
.animation(.easeInOut, value: shouldShowList) // animation
}
}

Jumpy performance issue when offset and opacity used

I have a header that is fixed in place using an offset relative to the scroll position. Strangely enough though, when the contents of the scroll view has a dynamic opacity to the buttons, the offset is very jumpy:
This is the scroll view code, the HeaderView is "fixed" in place by pinning the offset to the scroll view's offset. The opacity seems to be causing the performance issue is on the MyButtonStyle style on the last line of code:
struct ContentView: View {
#State private var isPresented = false
#State private var offsetY: CGFloat = 0
#State private var headerHeight: CGFloat = 200
var body: some View {
GeometryReader { screenGeometry in
ZStack {
Color(.label)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 0.0) {
Color.clear
.frame(height: headerHeight)
.overlay(
HeaderView(isPresented: $isPresented)
.offset(y: offsetY != 0 ? headerHeight + screenGeometry.safeAreaInsets.top - offsetY : 0)
)
VStack(spacing: 16) {
VStack(alignment: .leading) {
ForEach(0...10, id: \.self) { index in
Button("Button \(index)") {}
.buttonStyle(MyButtonStyle(icon: Image(systemName: "alarm")))
}
}
Spacer()
}
.frame(maxWidth: .infinity, minHeight: screenGeometry.size.height)
.padding()
.background(
GeometryReader { geometry in
Color.white
.cornerRadius(32)
.onChange(of: geometry.frame(in: .global).minY) { newValue in
offsetY = newValue
}
}
)
}
}
}
.alert(isPresented: $isPresented) { Alert(title: Text("Button tapped")) }
}
}
}
struct HeaderView: View {
#Binding var isPresented: Bool
var body: some View {
VStack {
Image(systemName: "bell")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(Color(.systemBackground))
Button(action: { isPresented = false }) {
Text("Press")
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(16)
}
}
.padding()
}
}
struct MyButtonStyle: ButtonStyle {
let icon: Image
func makeBody(configuration: Configuration) -> some View {
Content(
configuration: configuration,
icon: icon
)
}
struct Content: View {
let configuration: Configuration
let icon: Image
var body: some View {
HStack(spacing: 18) {
Label(
title: { configuration.label },
icon: { icon.padding(.trailing, 8) }
)
Spacer()
Image(systemName: "chevron.right")
.accessibilityHidden(true)
}
.padding(18)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(8)
.opacity(configuration.isPressed ? 0.5 : 1) // <-- Comment this out and jumpiness goes away!!
}
}
}
Is there a performance improvement that can be done to use the opacity on the button press and make the jumpiness go away? Or a different way to approach this sticky offset because not sure if this is actually the source of the issue and I use opacity in a lot of places in my app (not just button presses)? The purpose of doing it this way is so the button can be tapped instead of putting it in the background of the scroll view. Thanks for any help or insight!

SwiftUI - Segment control using buttons not deselecting other buttons properly

I am trying to make a custom segment control using a button array.
When a button is tapped, the correct value is passed to the main view and can be pressed multiple times without issue. The problem is that the previous button which was selected stays selected. The code below should work to recreate the issue. Thanks for the help.
Segment view
struct WaveTypeGridView: View {
var waveTypes = ["Beach Break" ,"Reef Break", "Point Break", "Rivermouth"]
#Binding var waveTypeSelected: String
#State var typeSelected: String
let columnSpacing: CGFloat = 5
let rowSpacing: CGFloat = 10
var gridLayout: [GridItem] {
return Array(repeating: GridItem(.flexible(), spacing: rowSpacing), count: 1)
}
var body: some View {
VStack {
Text("Wave Type \(typeSelected)")
ScrollView(.horizontal, showsIndicators: false, content: {
LazyHGrid(rows: gridLayout, alignment: .center, spacing: columnSpacing, pinnedViews: [], content: {
ForEach(waveTypes, id: \.self) { type in
if type == typeSelected {
TypeItemView(selected: true, typeSelected: self.$typeSelected, name: type)
} else {
TypeItemView(selected: false, typeSelected: self.$typeSelected, name: type)
}
}
})//: GRID
.frame(height: 18)
.padding(.vertical, 8)
.padding(.horizontal, 10)
})//: SCROLL
.background(Color(.systemGray5).cornerRadius(8))
}
}
}
struct WaveTypeGridView_Previews: PreviewProvider {
static var previews: some View {
WaveTypeGridView(waveTypeSelected: .constant(surfDataTests[0].waveType), typeSelected: "Beach Break")
.previewLayout(.sizeThatFits)
}
}
Button View
struct TypeItemView: View {
#State var selected: Bool
#Binding var typeSelected: String
let name: String
func test() {
}
var body: some View {
Button(action: {
selected.toggle()
typeSelected = name
}, label: {
HStack(alignment: .center, spacing: 6, content: {
Text(name)
.font(.footnote)
.fontWeight(.medium)
.foregroundColor(selected ? Color.white : Color.black)
})//: HSTACK
.frame(height: 18)
.padding(6)
.background(selected ? Color.blue
.cornerRadius(8)
: Color.white
.cornerRadius(8))
.background (
RoundedRectangle(cornerRadius: 8)
)
})//: BUTTON
}
}
struct TypeItemView_Previews: PreviewProvider {
static var previews: some View {
TypeItemView(selected: false, typeSelected: .constant("Rivermouth"), name:"Rivermouth")
.previewLayout(.sizeThatFits)
.padding()
.background(Color.gray)
}
}
You seem to have made your state tracking more complex than it needs to be. All you need to know is the currently selected wave type. You don't need to track a separate selected state, since the button can determine this from it's own value and the currently selected value.
For the grid view you can just have a single #State property for the selected wave type (You could inject this as an #Binding if required).
Pass the selected break type and the view's target value to the TypeItemView
struct WaveTypeGridView: View {
var waveTypes = ["Beach Break" ,"Reef Break", "Point Break", "Rivermouth"]
#State var typeSelected: String = "Beach Break"
let columnSpacing: CGFloat = 5
let rowSpacing: CGFloat = 10
var gridLayout: [GridItem] {
return Array(repeating: GridItem(.flexible(), spacing: rowSpacing), count: 1)
}
var body: some View {
VStack {
Text("Wave Type \(typeSelected)")
ScrollView(.horizontal, showsIndicators: false, content: {
LazyHGrid(rows: gridLayout, alignment: .center, spacing: columnSpacing, pinnedViews: [], content: {
ForEach(waveTypes, id: \.self) { type in
TypeItemView(typeSelected: self.$typeSelected, name: type)
}
})//: GRID
.frame(height: 18)
.padding(.vertical, 8)
.padding(.horizontal, 10)
})//: SCROLL
.background(Color(.systemGray5).cornerRadius(8))
}
}
}
Then in your TypeItemView you can create a computed property for selected based on the current value and this view's target value.
struct TypeItemView: View {
#Binding var typeSelected: String
let name: String
private var selected: Bool {
return typeSelected == name
}
func test() {
}
var body: some View {
Button(action: {
typeSelected = name
}, label: {
HStack(alignment: .center, spacing: 6, content: {
Text(name)
.font(.footnote)
.fontWeight(.medium)
.foregroundColor(self.selected ? Color.white : Color.black)
})//: HSTACK
.frame(height: 18)
.padding(6)
.background(self.selected ? Color.blue
.cornerRadius(8)
: Color.white
.cornerRadius(8))
.background (
RoundedRectangle(cornerRadius: 8)
)
})//: BUTTON
}
}
Now the whole control only depends on one #State item.

Resources