SwiftUI extend or recalculate VStack height when children views extend - swiftui

I have a scrollview that holds "cards" with weather details of locations and the cards extend to show more information when tapped.
I have to use a LegacyScrollView so that the bottomsheet that encompasses the scrollview gets dragged down when the the scroll is at the top.
I can't figure out how to extend the VStack when one of the cards extend. Is there a way to make a GeometryReader or VStack recalculate the needed height to hold all of the views?
I start off with the minHeight set to the window size so when the 1st card is extended, it all shows, but 2 or more extend past the LazyVStack window and get cut off.
If the minHeight on the LaxyVStack isn't set to 0, the cells expand from the middle and their tops get cut off. When it's set to 0, the cells expand downward as desired.
GeometryReader { proxy in
LegacyScrollView(.vertical, showsIndicators: false) {
Spacer(minLength: 40)
LazyVStack(spacing: 0) {
ForEach(mapsViewModel.placedMarkers, id: \.id) { _placeObject1 in
InfoCell(placeObject: _placeObject1)
.environmentObject(mapsViewModel)
.frame(alignment: .top)
Divider().foregroundColor(.white)
}
}
.cornerRadius(25)
.frame(minHeight: 0, alignment: .top)
}
.onGestureShouldBegin{ pan, scrollView in
scrollView.contentOffset.y > 0 || pan.translation(in: scrollView).y < 0
}
.onScroll{ test in
print(test.contentOffset.y)
}
.padding(.top, 30) //padding for top of list
}
and the cell's code is :
struct InfoCell: View {
#StateObject var placeObject: PlaceInfo
#State var expandCell = false
var body: some View {
VStack(alignment: .center, spacing: 0) {
//blahblah
if expandCell {
CellExpanded(placeObject: placeObject, isFirstInList: true)
}
}
.frame(maxWidth: .infinity)
.onTapGesture {
withAnimation { expandCell.toggle() }
}
}
}
and the expand code:
struct InfoCellExpanded: View {
#State var forecast = "Daily"
var placeObject: PlaceInfo
var isFirstInList: Bool
var body: some View {
VStack {
//blah
}
.padding(.horizontal, 10)
.padding(.bottom, 20)
}
}

Related

How do i implement .tag when jumping to a posiiton in a scrollview?

I have added the .id(1) to the positions in the scrollview and can get it to work as expected if i add a button inside the scrollview but i want to use a picker to jump to the .id and outside the scrollview.
Im new to this.
I have this code:
if i use this button it works as expected although its placed inside the scrollview...
Button("Jump to position") {
value.scrollTo(1)
}
This is my picker...
// Main Picker
Picker("MainTab", selection: $mainTab) {
Text("iP1").tag(1)
Text("iP2").tag(2)
Text("Logo").tag(3)
Text("Canvas").tag(4)
}
.frame(width: 400)
.pickerStyle(.segmented)
ScrollViewReader { value in
ScrollView (.vertical, showsIndicators: false) {
ZStack {
RoundedRectangle(cornerRadius: 20)
// .backgroundStyle(.ultraThinMaterial)
.foregroundColor(.black)
.opacity(0.2)
.frame(width: 350, height:185)
// .foregroundColor(.secondary)
.id(1)
There are 2 things you are mixing here:
The tag modifier is to differentiate elements among certain selectable views. i.e. Picker TabView
You can't access the proxy reader from outside unless you make it available. In other words the tag in the Picker and the ScrollViewReader does not have a direct relationship, you have to create that yourself:
import SwiftUI
struct ScrollTest: View {
#State private var mainTab = 1
#State private var scrollReader: ScrollViewProxy?
var body: some View {
// Main Picker
Picker("MainTab", selection: $mainTab) {
Text("iP1").tag(1)
Text("iP2").tag(2)
Text("Logo").tag(3)
Text("Canvas").tag(4)
}
.frame(width: 400)
.pickerStyle(.segmented)
.onChange(of: mainTab) { mainTab in
withAnimation(.linear) {
scrollReader?.scrollTo(mainTab, anchor: .top)
}
}
ScrollViewReader { value in
ScrollView (.vertical, showsIndicators: false) {
ForEach(1...4, id: \.self) { index in
ZStack {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(.black)
.opacity(0.2)
.frame(width: 350, height: 500)
Text("index: \(index)")
}
.id(index)
}
}
.onAppear {
scrollReader = value
}
}
}
}

Sticky section in SwiftUI scrollview

I'm trying to simulate sticky section header that PlainListStyle has in scrollView scenario:
struct MyScreen: View {
#State private var scrollViewOffset: CGFloat = .zero
#State var headerScrollView: CGRect = .zero
#State var headerTop: CGRect = .zero
var body: some View {
VStack(spacing: 0) {
HStack {
Text("Static top content")
.frame(width: UIScreen.main.bounds.width, height: 50)
.background(Color.red)
Spacer()
}
ZStack(alignment: .top) {
scrollable
tabHeader
.background(Color.white)
.opacity(shouldSwap ? 1 : 0)
.rectReader($headerTop, in: .global)
}
}
.background(Color(hex: "#F7F7F7"))
}
var tabHeader: some View {
Text("My Header")
.frame(width: UIScreen.main.bounds.width, height: 50)
.background(Color.blue)
}
var topContent: some View {
Text("My scrollable content")
.frame(width: UIScreen.main.bounds.width, height: 200)
}
var scrollable: some View {
TrackableScrollView(contentOffset: $scrollViewOffset) {
VStack {
VStack {
topContent
tabHeader
.opacity(shouldSwap ? 0 : 1)
.rectReader($headerScrollView, in: .global)
}
HStack() {
// ... content
}
}
}
}
var shouldSwap: Bool {
return headerTop.origin.y >= headerScrollView.origin.y
}
}
I'm trying to achieve this by defining my tabHeader view at two places - inside scrollview and at top of scrollView (in ZStack with scrollView) - once headers overlap, I just show top one and hide one from inside the scrollView and it actually looks pretty good, just like list sticky section.
My question is: can I somehow reuse same view to be animated/translated/updated in those two locations (inside scrollView -> swipeUp -> static above scrollView -> swipe down -> inside scrollView), because right now, if there is any internal state in tabHeader, it won't work because those are 2 different views so all the state they have must be in their parent and passed as binding.
If that's not possible, can I somehow restrict view from having internal state by implementing some protocol or something?

Size a SwiftUI view to be safeAreaInsets.top + 'x'

I am trying to create a full bleed SwiftUI 'header' view in my scene. This header will sit within a List or a scrollable VStack.
In order to do this, I'd like to have my text in the header positioned below the safe area, but the full view should extend from the top of the screen (and thus, overlap the safe area). Here is visual representation:
V:[(safe-area-spacing)-(padding)-(text)]
here is my attempt:
struct HeaderView: View {
#State var spacing: CGFloat = 100
var body: some View {
HStack {
VStack(alignment: .leading) {
Rectangle()
.frame(height: spacing)
.opacity(0.5)
Text("this!").font(.largeTitle)
Text("this!").font(.headline)
Text("that!").font(.subheadline)
}
Spacer()
}
.frame(maxWidth: .infinity)
.background(Color.red)
.background(
GeometryReader { proxy in
Color.clear
.preference(
key: SafeAreaSpacingKey.self,
value: proxy.safeAreaInsets.top
)
}
)
.onPreferenceChange(SafeAreaSpacingKey.self) { value in
self.spacing = value
}
}
}
This however, does not seem to correctly size 'Rectangle'. How can I size a view according to the safe area?
Is this what you're looking for? I try to avoid using GeometryReader unless you really need it... I created a MainView, which has a background and a foreground layer. The background layer will ignore the safe areas (full bleed) but the foreground will stay within the safe area by default.
struct HeaderView: View {
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("this!").font(.largeTitle)
Text("this!").font(.headline)
Text("that!").font(.subheadline)
}
Spacer(minLength: 0)
}
}
}
struct MainView: View {
var body: some View {
ZStack {
// Background
ZStack {
}
.frame(maxWidth:. infinity, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
// Foreground
VStack {
HeaderView()
Spacer()
}
}
}
}
add an state to store desired height
#State desiredHeight : CGFloat = 0
then on views body :
.onAppear(perform: {
if let window = UIApplication.shared.windows.first{
let phoneSafeAreaTopnInset = window.safeAreaInsets.top
desiredHeight = phoneSafeAreaTopnInset + x
}
})
set the desiredHeight for your view .
.frame(height : desiredHeight)

SwiftUI: List is messing up animation for its subviews inside an HStack

I'm making a WatchOS app that displays a bunch of real-time arrival times. I want to place a view, a real-time indicator I designed, on the trailing end of each cell of a List that will be continuously animated.
The real-time indicator view just has two image whose opacity I'm continuously animating. This View by itself seems to work fine:
animated view by itself
However, when embedded inside a List then inside an HStack the animation seems to be affecting the position of my animated view not only its opacity.
animated view inside a cell
The distance this view travels seems to only be affected by the height of the HStack.
Animated view code:
struct RTIndicator: View {
#State var isAnimating = true
private var repeatingAnimation: Animation {
Animation
.spring()
.repeatForever()
}
private var delayedRepeatingAnimation: Animation {
Animation
.spring()
.repeatForever()
.delay(0.2)
}
var body: some View {
ZStack {
Image("rt-inner")
.opacity(isAnimating ? 0.2 : 1)
.animation(repeatingAnimation)
Image("rt-outer")
.opacity(isAnimating ? 0.2 : 1)
.animation(delayedRepeatingAnimation)
}
.frame(width: 16, height: 16, alignment: .center)
.colorMultiply(.red)
.padding(.top, -6)
.padding(.trailing, -12)
.onAppear {
self.isAnimating.toggle()
}
}
}
All code:
struct SwiftUIView: View {
var body: some View {
List {
HStack {
Text("Cell")
.frame(height: 100)
Spacer()
RTIndicator()
}.padding(8)
}
}
}
Here is found workaround. Tested with Xcode 12.
var body: some View {
List {
HStack {
Text("Cell")
.frame(height: 100)
Spacer()
}
.overlay(RTIndicator(), alignment: .trailing) // << here !!
.padding(8)
}
}
Although it's pretty hacky I have found a temporary solution to this problem. It's based on the answer from Asperi.
I have create a separate View called ClearView which has an animation but does not render anything visual and used it as a second overall in the same HStack.
struct ClearView: View {
#State var isAnimating = false
var body: some View {
Rectangle()
.foregroundColor(.clear)
.onAppear {
withAnimation(Animation.linear(duration: 0)) {
self.isAnimating = true
}
}
}
}
var body: some View {
List {
HStack {
Text("Cell")
.frame(height: 100)
Spacer()
}
.overlay(RTIndicator(), alignment: .trailing)
.overlay(ClearView(), alignment: .trailing)
.padding(8)
}
}

How to make SwiftUI to forget internal state

I have a view like following
struct A: View {
var content: AnyView
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
// Common Elements
content
// More Common Elements
}
}
}
}
When I call this from another view like
A(nextInnerView())
two things happen. Firstly, as the size of the content element changes ScrollView animates the transition. Secondly, if you scroll down and then change the content the scrolling position does not reset.
Here is a demo of possible solution. Tested with Xcode 11.4 / iOS 13.4
The origin of this behaviour is in SwiftUI rendering optimisation, that tries to re-render only changed part, so approach is to identify view A (to mark it as completely changed) based on condition that originated in interview changes, alternatively it can be identified just by UUID().
struct TestInnerViewReplacement: View {
#State private var counter = 0
var body: some View {
VStack {
Button("Next") { self.counter += 1 }
Divider()
A(content: nextInnerView())
.id(counter) // << here !!
}
}
private func nextInnerView() -> AnyView {
AnyView(Group {
if counter % 2 == 0 {
Text("Text Demo")
} else {
Image(systemName: "star")
.resizable()
.frame(width: 100, height: 100)
}
})
}
}
struct A: View {
var content: AnyView
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ForEach(0..<5) { _ in // upper content demo
Rectangle().fill(Color.yellow)
.frame(height: 40)
.frame(maxWidth: .infinity)
.padding()
}
content
ForEach(0..<10) { _ in // lower content demo
Rectangle().fill(Color.blue)
.frame(height: 40)
.frame(maxWidth: .infinity)
.padding()
}
}
}
}
}