I have a list of Texts in a LazyVStack that is embedded within a GeometryReader like so
#State var profilePictureEnlarge: Bool = false
#State var showLiveTutorial: Bool = false
#State var textInput: String = ""
#Namespace var namespace
// State variables from LegacyPostView
#State var user: User = exampleUsers[0]
#State var boujees: [LivePost] = []
#State var addBoujee: Bool = false
#State private var listener: ListenerRegistration?
#State private var scrollProxy: ScrollViewProxy? = nil
var textContent: some View {
ScrollView {
ScrollViewReader { proxy in
LazyVStack {
ForEach(boujees, id: \.self) { boujee in
GeometryReader { geo in
let midY = geo.frame(in: .named("scrollView")).midY
ZStack {
let height = (boujee.text.count / 40)
Text("**\(boujee.anonymous ? "anon".uppercased() : boujee.authorUsername.lowercased()):** \(boujee.text)")
.font(.title2)
.fontWeight(.heavy)
.blur(radius: 30 * (1 - (midY / 130)))
}
.padding(.horizontal, 20)
.opacity(midY / 130 > 1 ? 1 : midY / 130)
}
.frame(maxWidth: .infinity, minHeight: 150, maxHeight: 400)
}
}
.onAppear {
scrollProxy = proxy
// In the beginning scroll to bottom without animation.
scrollProxy?.scrollTo(boujees.last?.id)
}
.onTapGesture {
if profilePictureEnlarge {
withAnimation(.spring()) {
profilePictureEnlarge.toggle()
}
}
hideKeyboardOnTap()
}
}
}
.safeAreaInset(edge: .bottom, content: {
inputField
.offset(x: 0, y: profilePictureEnlarge ? 150 : 0)
})
.coordinateSpace(name: "scrollView")
}
I need the geometry reader to blur the text as it disappears out of the screen during scrolling. This messes up the height of each Text view like so
As you can see, there's unnecessary spacing in between text views that aren't as long as other views. SwiftUI handles this automatically but the GeometryReader messes up this auto-adjustment. This is because I set a minHeight value for the GeometryReader frame. I'd like to have text views adjust accoring to the line count, but I'm not sure how.
Related
Here's a main ContentView and DetailedCardView and when open DetailedCardView there's a ScrollView with more content. It also have closeGesture which allow to close the card when swiping from left to right and vice versa. So, at that point problems start to appear, like this ones inside DetailedCardView:
When scrolling in ScrollView then safeAreaInsets .top and .bottom start to be visible and as a result it's shrink the whole view vertically.
The closeGesture and standard ScrollView gesture while combined is not smooth and I'd like to add closing card not only when swiping horizontally but vertically too.
As a result I'd like to have the same gesture behaviour like opening and closing detailed card in App Store App Today section. Would love to get your help 🙌
ContentView DetailedCardView closeGesture in Action safeAreaInsets
struct ContentView: View {
#State private var showCardView = false
let colors: [Color] = [.red, .purple, .yellow, .green, .blue, .mint, .orange]
#State private var selectedCardNumber = 0
var body: some View {
ZStack {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(1..<101) { number in
colors[number % colors.count]
.overlay(Text("\(number)").font(.largeTitle.bold()).foregroundColor(.white))
.frame(height: 300)
.cornerRadius(30)
.padding(10)
.onTapGesture {
showCardView.toggle()
selectedCardNumber = number
}
}
}
}
.opacity(showCardView ? 0 : 1)
.animation(.spring(), value: showCardView)
if showCardView {
CardDetailView(showCardView: $showCardView, number: selectedCardNumber)
}
}
}
}
struct CardDetailView: View {
#Binding var showCardView: Bool
#State private var cardPosition: CGSize = .zero
let number: Int
var body: some View {
ScrollView {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.fill(.green)
.overlay(Text("\(number)").font(.system(size: 100).bold()).foregroundColor(.white))
.frame(height: 500)
Color.red.opacity(0.8)
.frame(height: 200)
Color.gray.opacity(0.8)
.frame(height: 200)
Color.blue
.frame(height: 200)
Color.yellow
.frame(height: 200)
}
.scaleEffect(gestureScale())
.gesture(closeGesture)
}
private var closeGesture: some Gesture {
DragGesture()
.onChanged { progress in
withAnimation(.spring()) {
cardPosition = progress.translation
}
}
.onEnded { _ in
withAnimation(.spring()) {
let positionInTwoDirections = abs(cardPosition.width)
if positionInTwoDirections > 100 {
showCardView = false
}
cardPosition = .zero
}
}
}
private func gestureScale() -> CGFloat {
let max = UIScreen.main.bounds.width / 2
let currentAmount = abs(cardPosition.width)
let percentage = currentAmount / max
return 1 - min(percentage, 0.5)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I am working with SwiftUI 2 and using a TabView with PageTabViewStyle.
Now, I am searching for a way to "tease" the pages adjacent to the current page like so:
Is it possible to achieve this effect with TabView and PageTabViewStyle?
I already tried to reduce the width of my TabView to be windowWidth-50. However, this did not lead to the adjacent pages being visible at the sides. Instead, this change introduced a hard vertical edge 50px left of the right window border, where new pages would slide in.
Here is a simple implementation. You can use the struct with the AnyView array or use the logic directly in your own implementation.
struct ContentView: View {
#State private var selected = 4
var body: some View {
// the trailing closure takes an Array of AnyView type erased views
TeasingTabView(selectedTab: $selected, spacing: 20) {
[
AnyView(TabContentView(title: "First", color: .yellow)),
AnyView(TabContentView(title: "Second", color: .orange)),
AnyView(TabContentView(title: "Fourth", color: .green)),
AnyView(TabContentView(title: "Fifth", color: .blue)),
AnyView(
Image(systemName: "lizard")
.resizable().scaledToFit()
.padding()
.frame(maxHeight: .infinity)
.border(.red)
)
]
}
}
}
struct TeasingTabView: View {
#Binding var selectedTab: Int
let spacing: CGFloat
let views: () -> [AnyView]
#State private var offset = CGFloat.zero
var viewCount: Int { views().count }
var body: some View {
VStack(spacing: spacing) {
GeometryReader { geo in
let width = geo.size.width * 0.7
LazyHStack(spacing: spacing) {
Color.clear
.frame(width: geo.size.width * 0.15 - spacing)
ForEach(0..<viewCount, id: \.self) { idx in
views()[idx]
.frame(width: width)
.padding(.vertical)
}
}
.offset(x: CGFloat(-selectedTab) * (width + spacing) + offset)
.animation(.easeOut, value: selectedTab)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation.width
}
.onEnded { value in
withAnimation(.easeOut) {
offset = value.predictedEndTranslation.width
selectedTab -= Int((offset / width).rounded())
selectedTab = max(0, min(selectedTab, viewCount-1))
offset = 0
}
}
)
}
//
HStack {
ForEach(0..<viewCount, id: \.self) { idx in
Circle().frame(width: 8)
.foregroundColor(idx == selectedTab ? .primary : .secondary.opacity(0.5))
.onTapGesture {
selectedTab = idx
}
}
}
}
}
}
struct TabContentView: View {
let title: String
let color: Color
var body: some View {
Text(title).font(.title)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(color.opacity(0.4), ignoresSafeAreaEdges: .all)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
}
I'd like to make this bar's height (part of a larger bar graph) animate on appear from 0 to its given height, but I can't quite figure it out in SwiftUI? I found this code that was helpful, but with this only the first bar (of 7) animates:
struct BarChartItem: View {
var value: Int
var dayText: String
var topOfSCaleColorIsRed: Bool
#State var growMore: CGFloat = 0
#State var showMore: Double = 0
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(getBarColor())
.frame(width: 40, height: growMore)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(.linear(duration: 7.0)) {
growMore = CGFloat(value)
showMore = 1
}
}
}.opacity(showMore)
Text(dayText.uppercased())
.font(.caption)
}
}
And this is the chart:
HStack(alignment: .bottom) {
ForEach(pastRecoveryScoreObjects) { pastRecoveryScoreObject in
BarChartItem(value: pastRecoveryScoreObject.recoveryScore, dayText: "\(pastRecoveryScoreObject.date.weekdayName)", topOfSCaleColorIsRed: false)
}
}
This works: you can get the index of the ForEach loop and delay each bar's animation by some time * that index. All the bars are connected to the same state variable via a binding, and the state is set immediately on appear. You can play with the parameters to tune it to your own preferences.
struct TestChartAnimationView: View {
let values = [200, 120, 120, 90, 10, 80]
#State var open = false
var animationTime: Double = 0.25
var body: some View {
VStack {
Spacer()
HStack(alignment: .bottom) {
ForEach(values.indices, id: \.self) { index in
BarChartItem(open: $open, value: values[index])
.animation(
Animation.linear(duration: animationTime).delay(animationTime * Double(index))
)
}
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
open = true
}
}
}
}
struct BarChartItem: View {
#Binding var open: Bool
var value: Int
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(Color.blue)
.frame(width: 20, height: open ? CGFloat(value) : 0)
Text("Week".uppercased())
.font(.caption)
}
}
}
Edit: ForEach animations are kind of weird. I'm not sure why, but this works:
struct ContentView: View {
let values = [200, 120, 120, 90, 10, 80]
var body: some View {
HStack(alignment: .bottom) {
ForEach(values, id: \.self) { value in
BarChartItem(totalValue: value)
.animation(.linear(duration: 3))
}
}
}
}
struct BarChartItem: View {
#State var value: Int = 0
var totalValue = 0
var body: some View {
VStack {
Text("\(value)%")
.font(Font.footnote.monospacedDigit())
RoundedRectangle(cornerRadius: 5.0)
.fill(Color.blue)
.frame(width: 20, height: CGFloat(value))
Text("Week".uppercased())
.font(.caption)
}.onAppear {
value = totalValue
}
}
}
Instead of applying the animation in the onAppear, I applied an implicit animation inside the ForEach.
Result:
I'm struggling to find a way in SwiftUI to get the x,y coordinates of the origin of a
TextField (or any view). I can certainly provide a position or offset to move the view
but I can't seem to find a way to get a reference to the TextField to get its
coordinates.
In UIKit, I believe I would use myTextField.frame.origin.x
This is a very simple example:
struct ContentView: View {
#State private var someNumber1 = "1000"
#State private var someNumber2 = "2000"
//bunch more
#State private var enteredNumber = "Some Text at the Top"
#State var value: CGFloat = 0
var body: some View {
ScrollView {
VStack {
Spacer()
Text("\(enteredNumber)")
Spacer()
Group { //1
TextField("Placeholder", text: $someNumber1)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
//this does not work
.onTapGesture {
let aY = (textfieldreferencehere).frame.origin.y
}
TextField("Placeholder", text: $someNumber2)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
}//group 1
//bunch more
Button(action: {
self.enteredNumber = self.someNumber1
self.someNumber1 = ""
UIApplication.shared.endEditing()
}) {
Text("Submit")
}
.padding(.bottom, 50)
}//outer v
.padding(.horizontal, 16)
.padding(.top, 44)
}//Scrollview or Form
.modifier(AdaptsToSoftwareKeyboard())
}
}
Any Guidance would be appreciated. Xcode 11.4.1
Here is a demo of how specific view coordinates can be read (see helpful comments inline)
struct DemoReadViewOrigin: View {
#State private var someNumber1 = "1000"
#State private var someNumber2 = "2000"
//bunch more
#State private var enteredNumber = "Some Text at the Top"
#State var value: CGFloat = 0
var body: some View {
ScrollView {
VStack {
Spacer()
Text("\(enteredNumber)")
Spacer()
Group { //1
TextField("Placeholder", text: $someNumber1)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
.background(GeometryReader { gp -> Color in
let rect = gp.frame(in: .named("OuterV")) // < in specific container
// let rect = gp.frame(in: .global) // < in window
// let rect = gp.frame(in: .local) // < own bounds
print("Origin: \(rect.origin)")
return Color.clear
})
//this does not work
.onTapGesture {
}
TextField("Placeholder", text: $someNumber2)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
}//group 1
//bunch more
Button(action: {
self.enteredNumber = self.someNumber1
self.someNumber1 = ""
// UIApplication.shared.endEditing()
}) {
Text("Submit")
}
.padding(.bottom, 50)
}//outer v
.coordinateSpace(name: "OuterV") // << declare coord space
.padding(.horizontal, 16)
.padding(.top, 44)
}//Scrollview or Form
// .modifier(AdaptsToSoftwareKeyboard())
}
}
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)
}
}
}
}