i want to toggle the height of a view when I press a button. I tried several things:
Button with transition (but I think this solution isn't good)
A Slider which one changes the height (like I want it, but I want to toggle between two values)
Here my current Swift File:
import SwiftUI
struct TextView2: View {
#State private var ChangeFrame2 = false
#State private var height: Double = 200
var body: some View {
if ChangeFrame2 {
Text("Hello, world!")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 400)
.font(.largeTitle)
.foregroundColor(.white)
.background(Color.red)
.transition(.move(edge: .top))
} else {
Text("Hello, world!")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: height)
.font(.largeTitle)
.foregroundColor(.white)
.background(Color.green)
.transition(.move(edge: .top))
}
Button("Press to show details") {
withAnimation(.easeInOut(duration: 1.00)) {
ChangeFrame2.toggle()
}
}
Slider(value: $height, in: 10...500)
Text("\(height, specifier: "%.0f") Height")
}
}
I also tried the following:
.background(ChangeFrame2 ? (maxHeight: 400) : (maxHeight: 600))
but it doesn't work. Thank you :)
You can just have one Text view, using the ternary operator to decide the height and background color.
The problem is that when you use if, the two views now have different identities which breaks animations. You also have an unwanted move transition.
Code:
struct TextView: View {
#State private var changeFrame = false
#State private var height: Double = 200
var body: some View {
VStack {
Text("Hello, world!")
.frame(height: changeFrame ? 400 : height)
.frame(maxWidth: .infinity)
.font(.largeTitle)
.foregroundColor(.white)
.background(changeFrame ? Color.red : Color.green)
Button("Press to show details") {
withAnimation(.easeInOut(duration: 1)) {
changeFrame.toggle()
}
}
Slider(value: $height, in: 10...500)
Text("\(height, specifier: "%.0f") Height")
}
}
}
Result:
Related
I have a view in a ScrollView and base on an offset value. There is another view that needs to animate out.
This view is under the gray box but it places its base on a hardcoded value. How to pin the green box under the gray area without a hardcoded value? Using the hard coded value will not be consistent with other devices.
struct ContentView: View {
#State private var contentOffset = CGFloat(0)
var body: some View {
NavigationView {
ZStack {
VStack(spacing: 0) {
TrackableScrollView { offset in
contentOffset = offset.y
} content: {
VStack(spacing: 0) {
Text("Hello World")
.padding()
.frame(maxWidth: .infinity)
.background(Color.gray)
}
}
}
Text("HELLO")
.frame(maxWidth: .infinity)
.background(Color.green)
.opacity(contentOffset < -16 ? 0 : 1)
.animation(.easeIn(duration: 0.2), value: contentOffset)
.offset(y: -280)
}
.ignoresSafeArea()
.frame(maxHeight: .infinity, alignment: .top)
.background(AccountBackground())
.navigationBarHidden(true)
}
}
}
What it looks like:
When you scroll up the green box will disappear.
To do this you need to an overlay to your ZStack. If you have a navigation bar the offset that you need to give it is 0 as it will be placed automatically below it. The TrackableScrollView is a preference key that is commonly found online to track the scrolling position of the scroll view.
struct ContentView: View {
#State private var contentOffset = CGFloat(0)
#State private var offsetPositionValue: CGFloat = 0
#State private var heightOfFrame: CGFloat = 0
#State private var isShyHeaderVisible = false
var body: some View {
NavigationView {
ZStack {
TrackableScrollView { offset in
withAnimation {
contentOffset = offset.y
}
} content: {
Text("Hello World")
}
.overlay(
ZStack {
HStack {
Text("Total number of points")
.foregroundColor(.white)
.lineLimit(1)
Spacer()
Text("20,000 pts")
.foregroundColor(.white)
.padding(.leading, 50)
}
.padding(.horizontal)
.padding(.vertical, 8)
.frame(width: UIScreen.main.bounds.width)
.background(Color.green)
.offset(y: contentOffset < -16 ? 0 : heightOfFrame - 5)
.opacity(contentOffset < -16 ? 1 : 0)
.transition(.move(edge: .top))
}
.frame(maxHeight: .infinity, alignment: .top)
)
}
.navigationTitle("Hello")
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarTitleDisplayMode(.inline)
.frame(maxHeight: .infinity, alignment: .top)
.background(AccountBackground())
}
}
}
I am trying to make a SwiftUI TextEditor with a Divider that adapts its position to stay under the bottom-most line of text inside of a edit-bio section of the app.
Note: I have a frame on my TextEditor so that it doesn't take up the whole-screen
Right now the Divider is static and stays in one place. Is there a built-in way to make the divider stay under the bottom most line of text?
I would think the Spacer would have given me this behavior?
Thank you!
struct EditBio: View {
#ObservedObject var editProfileVM: EditProfileViewModel
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM.bio)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
Divider().frame(height: 1).background(.white)
Spacer()
}
}
}
It is doing exactly what you told it to do. But a background color on your TextEditor. You will see that it has a height of 200 + a spacing of 10 from the VStack.
I changed your code to make it obvious:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.background(Color.gray)
Divider().frame(height: 1).background(.red)
Spacer()
}
}
}
to produce this:
You can see the TextEditor naturally wants to be taller than 200, but that is limiting it. Therefore, the Spacer() is not going to cause the TextEditor to be any smaller.
The other problem that setting a fixed frame causes will be that your text will end up off screen at some point. I am presuming what you really want is a self sizing TextEditor that is no larger than it's contents.
That can be simply done with the following code:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
SelfSizingTextEditor(text: $editProfileVM)
// Frame removed for the image below.
// .frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.foregroundColor(.white)
// made the .top padding to be .vertical
.padding(.vertical, 70)
.padding([.leading, .trailing], 50)
.background(Color.gray)
Divider().frame(height: 5).background(.red)
Spacer()
}
}
}
struct SelfSizingTextEditor: View {
#Binding var text: String
#State var textEditorSize = CGSize.zero
var body: some View {
ZStack {
Text(text)
.foregroundColor(.clear)
.copySize(to: $textEditorSize)
TextEditor(text: $text)
.frame(height: textEditorSize.height)
}
}
}
extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
func copySize(to binding: Binding<CGSize>) -> some View {
self.readSize { size in
binding.wrappedValue = size
}
}
}
producing this view:
I have an app with 3 views. they are connected by a NavigationView and have NavigationLinks that link to the next view. however, all views are created with an extra NavigationBar and with lots of blank white space on the bottom that travels up the screen every time you click through the views and go back to the home page. I remember a few days ago having this issue when I went from the HomeView() to the TimerView() but I'm not sure how I got rid of it. and also I definitely did not get rid of it because I'm still having the same problem. and also I don't remember how I worked around this the first time. I have seen other posts which say I should set the navigationBar color to clear but that does nothing. not sure what is going on. most other posts on this topic are about removing the space up top and very few talk about the blank space on the bottom so I'm not really sure what to do.
I go into the capture view hierarchy and I see that white footer bar is created when the view is created but then just seems to stack on top of every other view until the application is unusable. using Xcode 13.1 and running in simulator on iPhone 12 with iOS 15.0.
here's the video as well as pictures from the capture view hierarchy thing https://imgur.com/a/jBprYbN
and here's the code
struct ContentView: View {
#State private var selectedTab = 0
let numTabs = 2
let minDragTranslationForSwipe: CGFloat = 50
#State var secondScreenShown = true
#State private var tabSelection = 0
#State private var tappedTwice:Bool = false
#State private var homeID = UUID()
var handler: Binding<Int> { Binding (
get: {self.tabSelection},
set: {
if $0 == self.tabSelection {
tappedTwice = true
print("tappedTwice = true")
}
self.tabSelection = $0
}
)}
init() {
UITabBar.appearance().isTranslucent = false
}
var body: some View {
TabView(selection:handler) {
NavigationView {
HomeView()
.id(homeID)
.tabItem {
Label("Home", systemImage: "house.fill")
}.tag(0)
.onChange(of: tappedTwice, perform: { tappedTwice in
guard tappedTwice else { return }
homeID = UUID()
self.tappedTwice = false
})
}
}
.edgesIgnoringSafeArea(.all)
.frame(
minWidth: UIScreen.main.bounds.width,
maxWidth: .infinity,
minHeight: UIScreen.main.bounds.height,
maxHeight: .infinity,
alignment: .center
)
}
}
here's the HomeView()
struct HomeView: View {
#State var secondScreenShown = false
#State var timerVal = 1
#State private var chosenSound = "None"
var times = [1, 2, 3, 4, 5, 10, 15, 20, 30, 45, 60]
var sounds = ["None", "Creepy Kids", "Conjuring The Dark Ones", "The Forbidden Forest" ]
init() {
UITableView.appearance().tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
UITableView.appearance().tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
}
var body: some View {
// NavigationView {
VStack {
VStack {
VStack {
Group {
VStack {
NavigationLink(
destination: TimerView(timerScreenShown: $secondScreenShown, timerVal: timerVal, chosenSound: chosenSound, initialTime: timerVal),
isActive: $secondScreenShown,
label: {Text("Go")
.font(.title2)
.padding()
.frame(minWidth: 250)
.overlay(
Capsule(style: .continuous)
.stroke(Color.black, lineWidth: 3)
)
})
.frame(minWidth: /*#START_MENU_TOKEN#*/0/*#END_MENU_TOKEN#*/, maxWidth: 100, alignment: .bottom)
.padding(.top, 10)
} // third group
}
} // VStack just outside rectangle
.frame(
maxWidth: .infinity,
minHeight: UIScreen.main.bounds.height,
maxHeight: .infinity,
alignment: .center
)
.padding()
// ) // rectangle.overlay()
} // VStack
.edgesIgnoringSafeArea([.top, .bottom])
.background(Color.pink)
.frame(
minWidth: UIScreen.main.bounds.width,
maxWidth: .infinity,
minHeight: UIScreen.main.bounds.height,
maxHeight: .infinity,
alignment: .center
)
// } // NavigationView
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarTitle("main level")
}
}
}
And here's the TimerView()
struct TimerView: View {
#Binding var timerScreenShown:Bool
#State var timerVal:Int
#State var chosenSound:String
#State private var showNextView = false
#State private var paused = false
var initialTime:Int
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
VStack {
VStack {
NavigationLink(
destination: HomeView(),
label: {Text("Home Page")})
.font(.title2)
.frame(minWidth: 0,maxWidth: 200, alignment: .center)
.overlay(
Capsule(style: .continuous)
.stroke(Color.black, lineWidth: 3)
)
.padding(.top, 35)
// } // else
} // innermost VStack
.frame(
alignment: .center
)
.padding()
// )
} // VStack 2
.frame(
minWidth: UIScreen.main.bounds.width,
maxWidth: .infinity,
minHeight: UIScreen.main.bounds.height,
maxHeight: .infinity,
alignment: .center
)
} // VStack
.edgesIgnoringSafeArea([.top, .bottom])
.background(Color.purple
// Image("HomePageBackground")
// .resizable()
// .ignoresSafeArea()
)
.frame(
minWidth: UIScreen.main.bounds.width,
maxWidth: .infinity,
minHeight: UIScreen.main.bounds.height,
maxHeight: .infinity,
alignment: .center
)
.onAppear(){
UITableView.appearance().tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
UITableView.appearance().tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
}
.navigationBarTitle("timer view")
} // View
}
so yeah it's obvious there's extra navigation views being created somewhere but I'm not sure where. it's also obvious that blank space in the footer is created when the view is loaded but I don't know where. so yeah any help would be appreciated.
if there are any brackets/parenthesis missing it's because I accidentally deleted it while removing commented out extraneous code.
The issue is caused by the UITabBar.appearance().isTranslucent = false line -- without that, as you confirmed in the comments, it behaves as expected.
Here's a working version with a note about that line:
import SwiftUI
struct ContentView: View {
init() {
UITabBar.appearance().isTranslucent = false //REMOVE THIS and functionality will be as-expected
}
var body: some View {
TabView {
NavigationView {
HomeView()
}.tabItem {
Label("Home", systemImage: "house.fill")
}
.navigationViewStyle(StackNavigationViewStyle())
.tag(0)
}
}
}
struct TimerView: View {
var body: some View {
VStack {
NavigationLink(
destination: HomeView(),
label: {Text("Home Page")})
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.purple)
.edgesIgnoringSafeArea([.top, .bottom])
.navigationBarTitle("timer view")
}
}
struct HomeView: View {
var body: some View {
VStack {
NavigationLink(
destination: TimerView(),
label: {
Text("Go")
})
.frame(minWidth: 0, maxWidth: 100, alignment: .bottom)
.padding(.top, 10)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.pink)
.edgesIgnoringSafeArea([.top, .bottom])
.navigationBarTitle("main level")
}
}
I want to lay a View over another when I tap a button.
The following is my code:
import SwiftUI
struct SheetView: View {
#State private var showSheet: Bool = false
var body: some View {
NavigationView {
VStack {
ZStack {
Rectangle()
.fill(Color.orange)
.frame(height: 32.0)
Button("Please select a mailing address") {
showSheet.toggle()
}.foregroundColor(Color.black)
}
Spacer()
}
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(StackNavigationViewStyle())
}
.overlay(popOver)
}
var popOver: some View {
Group {
if showSheet {
ZStack {
Color.black.opacity(0.4).ignoresSafeArea()
ZStack {
Rectangle()
.fill(Color.white)
//.frame(width: UIScreen.main.bounds.width, height: 400)
.frame(maxWidth: .infinity, maxHeight: 320.0, alignment: .bottom)
//.position(x: UIScreen.main.bounds.width / 2.0, y: 600)
}
}.onTapGesture {
showSheet.toggle()
}
}
}
}
}
And it looks like the following picture. I get pretty much what I need except that the overlaid View will appear at the center. How can I make it appear, aligning to the bottom View?
Here is a fix - use alignment for internal ZStack (tested with Xcode 13.2 / iOS 15.2):
ZStack(alignment: .bottom) {
Color.black.opacity(0.4).ignoresSafeArea()
ZStack {
Rectangle()
.fill(Color.white)
.frame(maxWidth: .infinity, maxHeight: 320.0, alignment: .bottom)
}
}
//.ignoresSafeArea() // << probably you also need this
.onTapGesture {
showSheet.toggle()
}
I have a ScrollView with multiple Buttons. A Button contains a Image and a Text underneath.
As the images are pretty large I am using .scaledToFill and .clipped. And it seems that the 'clipped' part of the image is still clickable even if it's not shown.
In the video you see I am clicking on button 1 but button 2 is triggered.
This is my Coding. The Image is inside the View Card.
struct ContentView: View {
#State var useWebImage = false
#State var isSheetShowing = false
#State var selectedIndex = 0
private let images = [
"https://images.unsplash.com/photo-1478368499690-1316c519df07?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2706&q=80",
"https://images.unsplash.com/photo-1507154258-c81e5cca5931?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2600&q=80",
"https://images.unsplash.com/photo-1513310719763-d43889d6fc95?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2734&q=80",
"https://images.unsplash.com/photo-1585766765962-28aa4c7d719c?ixlib=rb-1.2.1&auto=format&fit=crop&w=2734&q=80",
"https://images.unsplash.com/photo-1485970671356-ff9156bd4a98?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2734&q=80",
"https://images.unsplash.com/photo-1585607666104-4d5b201d6d8c?ixlib=rb-1.2.1&auto=format&fit=crop&w=2700&q=80",
"https://images.unsplash.com/photo-1577702066866-6c8897d06443?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2177&q=80",
"https://images.unsplash.com/photo-1513809491260-0e192158ae44?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2736&q=80",
"https://images.unsplash.com/photo-1582092723055-ad941d1db0d4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2700&q=80",
"https://images.unsplash.com/photo-1478264635837-66efba4b74ba?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjF9&auto=format&fit=crop&w=2682&q=80"
]
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 40) {
Text(useWebImage ? "WebImage is used." : "SwiftUI Image is used")
.font(.system(size: 18))
.bold()
.kerning(0.5)
.padding(.top, 20)
Toggle(isOn: $useWebImage) {
Text("Use WebImage")
.font(.system(size: 18))
.bold()
.kerning(0.5)
.padding(.top, 20)
}
ForEach(0..<images.count) { index in
Button(action: {
self.selectedIndex = index
self.isSheetShowing.toggle()
}) {
Card(imageUrl: self.images[index], index: index, useWebImage: self.$useWebImage)
}
.buttonStyle(PlainButtonStyle())
}
}
.padding(.horizontal, 20)
.sheet(isPresented: self.$isSheetShowing) {
DestinationView(imageUrl: self.images[self.selectedIndex], index: self.selectedIndex, useWebImage: self.$useWebImage)
}
}
.navigationBarTitle("Images")
}
}
}
struct Card: View {
let imageUrl: String
let index: Int
#Binding var useWebImage: Bool
var body: some View {
VStack {
if useWebImage {
WebImage(url: URL(string: imageUrl))
.resizable()
.indicator(.activity)
.animation(.easeInOut(duration: 0.5))
.transition(.fade)
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
.cornerRadius(12)
.clipped()
} else {
Image("image\(index)")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
.cornerRadius(12)
.clipped()
}
HStack {
Text("Image #\(index + 1) (\(useWebImage ? "WebImage" : "SwiftUI Image"))")
.font(.system(size: 18))
.bold()
.kerning(0.5)
Spacer()
}
}
.padding(2)
.border(Color(.systemRed), width: 2)
}
}
Do you have an idea how to fix this issue?
I already tried to use .resizable(resizingMode: .tile) but I need to shrink the image before I could use just a tile.
For detailed information you can also find the project on GitHub GitHub Project
I would appreciate your help a lot.
The .clipped affects only drawing, and by-default Button has all content clickable not depending what it is.
So if you want make your button clickable only in image area, you have to limit hit testing only to its rect explicitly and disable everything else.
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4.
Demo code (simplified variant of your snapshot):
struct ButtonCard: View {
var body: some View {
VStack {
Image("sea")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
.cornerRadius(12)
.contentShape(Rectangle()) // << define clickable rect !!
.clipped()
HStack {
Text("Image #1")
.font(.system(size: 18))
.bold()
.kerning(0.5)
Spacer()
}.allowsHitTesting(false) // << disable label area !!
}
.padding(2)
.border(Color(.systemRed), width: 2)
}
}
struct TestClippedButton: View {
var body: some View {
Button(action: { print(">> tapped") }) {
ButtonCard()
}.buttonStyle(PlainButtonStyle())
}
}