I am trying to make an "expandable" view that when tapped, expands the frame downward. I created a state variable isExpanded to toggle when the view is tapped. Then, I added a .frame modifier that sets the height to 180 when expanded and 80 when not. The issue is that when the frame expands, it does so from the middle in a very buggy-looking and ugly manner. I'd like it to expand from the top, as if it were dropping down. Here's the code:
#State private var isExpanded: Bool = false
ZStack {
VStack {
Rectangle()
.foregroundColor(.white)
}
HStack {
Text("test")
Spacer()
// this image acts like a "dropdown" toggle
Image(systemName: "chevron.left")
.font(.custom("Helvetica Neue Medium", size: 16))
.rotationEffect(Angle(degrees: self.isExpanded ? -90 : 0))
}
}
.onTapGesture {
withAnimation(.spring()) {
self.isExpanded.toggle()
}
}
.frame(maxHeight: isExpanded ? 180 : 80)
Related
I have a Button with an overlay, but it is not clickable. If I take the overlay off, it becomes clickable. How can I make it work with the overlay?
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
UIPasteboard.general.string = viewModel.promoCode?.code ?? ""
} label: {
HStack {
Text(viewModel.promoCode?.code ?? "")
.foregroundColor(Color(UIColor.uStadium.primary))
.font(Font(UIFont.uStadium.helvetica(ofSize: 14)))
.padding(.leading, 20)
Spacer()
Image("copyIcon")
.foregroundColor(Color(UIColor.uStadium.primary))
.padding(.trailing, 20)
}
}
.overlay (
RoundedRectangle(cornerRadius: 8)
.stroke(Color(UIColor.uStadium.primary), style: StrokeStyle(lineWidth: 2, dash: [10]))
.frame(height: 42)
.background(Color(UIColor.uStadium.primary.withAlphaComponent(0.2)))
)
.frame(height: 42)
.cornerRadius(8)
Another solution is adding .allowsHitTesting(false) modifier at the view inside of the overlay.
The reason why touch is not working is overlay modifier layers a secondary view in front of current view. So when user tap the button they were actually tapping the overlayed view.
The allowsHitTesting(false) stop a view from receiving any kind of taps, and any taps automatically continue through the view on to whatever is behind it.
Simple example:
struct ContentView: View {
var body: some View {
Button {
print("dd")
} label: {
Image(systemName: "house").resizable().frame(width: 50, height: 50)
}
.overlay (
RoundedRectangle(cornerRadius: 8).allowsHitTesting(false)
)
}
}
and touch works!
How would I set a blur on all of a scrollview's children (inside a vstack) except for the topmost visible child in the scrollview?
I've tried something like the following but it does not work.
ScrollViewReader { action in
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(lyrics, id: \.self) { lyric in
Text(lyric)
.onAppear {
blurOn = 0.0
}
.onDisappear {
blurOn = 5.0
}
.padding()
.blur(radius: blurOn)
}
}
.font(.system(size: 20, weight: .medium, design: .rounded))
.padding(.bottom)
}
.cornerRadius(10)
.frame(height: 130)
}
After thinking about it, I guess I could put the VStack in a ZStack and place a 0 opacity view on the bottom with a blur...just wondering if there's a better way to do this?
This is the code for my content view. As you can see, I've tried both an HStack to contain the TextField, as well as just the TextField on its own. The corner radius doesn't have any bearing on the grey search rectangle - the edges are still perfectly rectangular.
#State var searchText = ""
var body: some View {
ZStack {
//function that's the content argument to ZStack
Color((.systemGreen))
VStack {
Text("App")
.font(.largeTitle)
.foregroundColor(Color.white)
//HStack {
TextField("Searchstring", text: $searchText)
.padding()
.background(Color(.systemGray6))
.padding()
.cornerRadius(12)
//}
// .padding()
// .background(Color(.systemGray6))
// .padding(.horizontal)
// .cornerRadius(12)
}
}
}
}
And this is what the preview looks like, in all cases:
corner radius fails to show in preview
The problem is here:
.padding() /// 1.
.background(Color(.systemGray6)) /// 2.
.padding() /// 3.
.cornerRadius(12) /// 4.
Your text field has a padding
background gets applied, after the padding
You add another padding, after the background
You apply another cornerRadius on top of padding
As a result, it's the padding that gets rounded, not the background.
Instead, you want to apply the cornerRadius immediately after the background.
struct ContentView: View {
#State var searchText = ""
var body: some View {
ZStack {
//function that's the content argument to ZStack
Color((.systemGreen))
VStack {
Text("App")
.font(.largeTitle)
.foregroundColor(Color.white)
TextField("Searchstring", text: $searchText)
.padding()
.background(Color(.systemGray6))
.cornerRadius(12) /// corner radius immediately after the background
.padding() /// extra padding outside the background
}
}
}
}
Result:
i am trying to to make the button of an alert view fit the parent VStack. But I can only see two options:
button width as is, no frame modifier. that is not ideal as the button is not wide enough
set the frame modifier to .frame(maxWidth: .infinity). that is not ideal, because it not also fills its parent, but also makes it extend to the edges of the screen.
What I actually want is, that the VStack stays at its width and the button just fills up to the edges. No extending of the VStack. The size of the VStack is defined by the title and message, not by the button. Is this possible to achieve with SwiftUI?
Code:
Color.white
.overlay(
ZStack {
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all)
VStack(spacing: 15) {
Text("Alert View")
.font(.headline)
Text("This is just a message in an alert")
Button("Okay", action: {})
.padding()
.frame(maxWidth: .infinity)
.background(Color.yellow)
}
.padding()
.background(Color.white)
}
)
As alluded to in the comments, if you want the width to be tied to the message size, you'll have to use a PreferenceKey to pass the value up the view hierarchy:
struct ContentView: View {
#State private var messageWidth: CGFloat = 0
var body: some View {
Color.white
.overlay(
ZStack {
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all)
VStack(spacing: 15) {
Text("Alert View")
.font(.headline)
Text("This is just a message in an alert")
.background(GeometryReader {
Color.clear.preference(key: MessageWidthPreferenceKey.self,
value: $0.frame(in: .local).size.width)
})
Button("Okay", action: {})
.padding()
.frame(width: messageWidth)
.background(Color.yellow)
}
.padding()
.background(Color.white)
}
.onPreferenceChange(MessageWidthPreferenceKey.self) { pref in
self.messageWidth = pref
}
)
}
struct MessageWidthPreferenceKey : PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
}
I'd bet that there are scenarios where you would also want to set a minimum width (like if the alert message were one word long), so a real-world application of this would probably use max(minValue, messageWidth) or something like that to account for short messages.
I'm adding controls to a SwiftUI Form to assist the user enter data (and constrain the entries!). Although there is a lot to like about Forms, I've discovered that things that work nicely outside this container do very unexpected things inside it and it's not always obvious how to compensate for this.
The plan is to have the data field displayed as a single row. When the row is tapped, the control slides out from behind the data field - the row will need to expand (height) to accommodate the control.
I'm using Swift Playgrounds to develop the proof of concept (or failure in my case). The idea is to use a ZStack which will allow a nice sliding animation by overlaying the views and giving them a different zIndex and applying the offset when the data field view is tapped. Sounds simple but of course the Form row does not expand when the ZStack is expanded.
Adjusting the frame of the ZStack while expanding causes all sorts of weird changes in padding (or at least it looks like it) which can be compensated for by counter-offsetting the "top" view but this causes other unpredictable behaviour. Pointers and ideas gratefully accepted.
import SwiftUI
struct MyView: View {
#State var isDisclosed = false
var body: some View {
Form {
Spacer()
VStack {
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
.zIndex(1)
.onTapGesture { self.isDisclosed.toggle() }
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.offset(y: isDisclosed ? 50 : 0)
.animation(.easeOut)
}
}
Spacer()
}
}
}
Collapsed stack
Expanded stack - view overlaps adjacent row
Result when adjusting ZStack vertical frame when expanded - top padding increases
Here is possible solution with fluent row height change (using AnimatingCellHeight modifier taken from my solution in SwiftUI - Animations triggered inside a View that's in a list doesn't animate the list as well ).
Tested with Xcode 11.4 / iOS 13.4
struct MyView: View {
#State var isDisclosed = false
var body: some View {
Form {
Spacer()
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
.zIndex(1)
.onTapGesture { withAnimation { self.isDisclosed.toggle() } }
HStack {
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
}.frame(maxHeight: .infinity, alignment: .bottom)
}
.modifier(AnimatingCellHeight(height: isDisclosed ? 150 : 100))
Spacer()
}
}
}
Use alignmentGuide instead of offset.
...
//.offset(y: isDisclosed ? 50 : 0)
.alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isDisclosed ? 50 : 0) })
...
offset doesn't affect its view's frame. that's why Form doesn't react as expected. On the contrary, alignmentGuide does.
I now have a working implementation using alignment guides as suggested by Kyokook. I have softened the somewhat jarring row height change by adding an opacity animation to the Stepper as it slides out. This also helps to prevent a slightly glitchy overlap of the row title when the control is closed.
struct ContentView: View {
// MARK: Logic state
#State private var years = 0
#State private var months = 0
#State private var weeks = 0
// MARK: UI state
#State var isStepperVisible = false
var body: some View {
Form {
Text("Row 1")
VStack {
// alignment guide must be explicit for the ZStack & all child ZStacks
// must use the same alignment guide - weird stuff happens otherwise
ZStack(alignment: .top) {
HStack {
Text("AGE")
.bold()
.font(.footnote)
Spacer()
Text("\(years) years \(months) months \(weeks) weeks")
.foregroundColor(self.isStepperVisible ? Color.blue : Color.gray)
}
.frame(height: 35) // TODO: Without this, text in HStack vertically offset. Investigate. (HStack align doesn't help)
.background(Color.white) // Prevents overlap of text during transition
.zIndex(3)
.contentShape(Rectangle())
.onTapGesture {
self.isStepperVisible.toggle()
}
HStack(alignment: .center) {
StepperComponent(value: $years, label: "Years", bounds: 0...30, isVisible: $isStepperVisible)
StepperComponent(value: $months, label: "Months", bounds: 0...12, isVisible: $isStepperVisible)
StepperComponent(value: $weeks, label: "Weeks", bounds: 0...4, isVisible: $isStepperVisible)
}
.alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isStepperVisible ? 40 : 0) })
}
}
Text("Row 3")
}
}
}
struct StepperComponent<V: Strideable>: View {
// MARK: Logic state
#Binding var value: V
var label: String
var bounds: ClosedRange<V>
//MARK: UI state
#Binding var isVisible: Bool
var body: some View {
ZStack(alignment: .top) {
Text(label.uppercased()).font(.caption).bold()
.frame(alignment: .center)
.zIndex(1)
.opacity(self.isVisible ? 1 : 0)
.animation(.easeOut)
Stepper(label, value: self.$value, in: bounds)
.labelsHidden()
.alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isVisible ? 25 : 0) })
.frame(alignment: .center)
.zIndex(2)
.opacity(self.isVisible ? 1 : 0)
.animation(.easeOut)
}
}
}
There is still some room for improvement here but on the whole I'm pleased with the result :-)
Thanks to both Kyokook (for putting me straight on offset()) and Asperi.
I think the Kyokook's solution (using AlignmentGuides) is simpler and would be my preference in that it's leveraging Apple's existing API and seems to cause less unpredictable movement of the views in their container. However, the row height changes abruptly and isn't synchronised. The animation in the Asperi's example is smoother but there is some bouncing of the views within the row (it's almost as if the padding or insets are changing and then being reset at the end of the animation). My approach to animation is a bit hit-and-miss so any further comments would be welcome.
Solution 1 (frame consistent, animation choppy):
struct ContentView: View {
#State var isDisclosed = false
var body: some View {
Form {
Text("Row 1")
VStack {
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
.zIndex(1)
.onTapGesture {
self.isDisclosed.toggle()
}
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (self.isDisclosed ? 100 : 0) })
.animation(.easeOut)
Text("Row 3")
}
}
Text("Row 3")
}
}
}
Solution 2 (smoother animation but frame variance):
struct ContentView: View {
#State var isDisclosed = false
var body: some View {
Form {
Text("Row 1")
VStack {
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.red)
.frame(width: 100, height: 100)
.zIndex(1)
.onTapGesture {
withAnimation { self.isDisclosed.toggle() }
}
HStack {
Rectangle()
.fill(Color.blue)
.frame(width: 100, height: 100)
}.frame(maxHeight: .infinity, alignment: .bottom)
}
.modifier(AnimatingCellHeight(height: isDisclosed ? 200 : 100))
}
Text("Row 3")
}
}
}
struct AnimatingCellHeight: AnimatableModifier {
var height: CGFloat = 0
var animatableData: CGFloat {
get { height }
set { height = newValue }
}
func body(content: Content) -> some View {
content.frame(height: height)
}
}