Reputation: 51
I have a target page like this. The entire page contains four parts.
Sticky header: A navigation bar with fixed position, width and height on the page, including an icon for return.
Some info view: A view used to display certain information and pictures.
Tabs: A tab bar with two tabs for toggling a scrolling list of cards.
Post cards in ScrollView: A scrollable card list view, where cards are displayed in the form of a waterfall flow. (waterfall already implemented by https://github.com/paololeonardi/WaterfallGrid)
First Problem
The firs problem that bothers me is that when the user is in the initial state of the page, the list at the bottom will slide directly at the bottom instead of pushing the parent scroll view to scroll collaboratively.
Second Problem
Here is the demo code. I use a horizontal scrollview and scrollTargetBehavior api to switch tabs. Each tab is an inner vertical scrollview which contains a card list in VStack.
import SwiftUI
struct ContentView: View {
@State private var currentTab: String? = "Posted"
@State private var tabProgress: CGFloat = 0
var body: some View {
let screenWidth: CGFloat = UIScreen.main.bounds.width
let bw: CGFloat = UIScreen.main.bounds.width / 375
let screenHeight: CGFloat = UIScreen.main.bounds.height
GeometryReader { geometry in
ZStack {
// Sticky top bar, on the top of any other views
VStack {
VStack {
NavRow()
.padding(.top, geometry.safeAreaInsets.top)
}
.frame(alignment: .top)
.background(.green)
Spacer()
}
.zIndex(999)
// Outer Scroll View
ScrollView (.vertical, showsIndicators: false) {
VStack (spacing: 0) {
// Same height as the top nav row
Spacer().frame(height: 64 * bw + geometry.safeAreaInsets.top)
// Info View
InfoView()
// Tab Row
TabRow(currentTab: $currentTab)
}
// Scroll Views
// Use horizontal paging scroll to implement tab switching
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 0) {
ScrollView (.vertical, showsIndicators: false) {
PostedList()
}
.frame(width: screenWidth, height: screenHeight - 112 * bw - geometry.safeAreaInsets.top)
.id("Posted")
ScrollView (.vertical, showsIndicators: false) {
LikedList()
}
.frame(width: screenWidth, height: screenHeight - 112 * bw - geometry.safeAreaInsets.top)
.id("Liked")
}
.scrollTargetLayout()
.offsetX { value in
/// Converting Offset into Progress
let progress = -value / (screenWidth * CGFloat(1))
/// Capping Progress BTW 0-1
tabProgress = max(min(progress, 1), 0)
}
}
.scrollPosition(id: $currentTab)
.scrollTargetBehavior(.paging)
.scrollClipDisabled()
}
.ignoresSafeArea()
.frame(width: screenWidth, height: screenHeight)
}
.ignoresSafeArea()
.frame(width: screenWidth, height: screenHeight)
}
}
}
// Top nav row
struct NavRow: View {
var body: some View {
let screenWidth: CGFloat = UIScreen.main.bounds.width
let bw: CGFloat = UIScreen.main.bounds.width / 375
Rectangle().fill(.clear)
.frame(width: screenWidth, height: 64 * bw)
.overlay(alignment: .center, content: {
Text("Nav Row")
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.white)
})
}
}
struct InfoView: View {
var body: some View {
let bw: CGFloat = UIScreen.main.bounds.width / 375
Rectangle().fill(.red.opacity(0.6))
.frame(width: 343 * bw, height: 212 * bw)
.cornerRadius(12 * bw)
.overlay {
Text("Info View")
.foregroundColor(.white)
.font(.system(size: 24, weight: .semibold))
}
}
}
struct TabRow: View {
@Binding var currentTab: String?
@ViewBuilder
func SingleTab(tab: String) -> some View {
let isCurrent = currentTab == tab
Text(tab)
.font(.system(size: isCurrent ? 16 : 15, weight: isCurrent ? .semibold : .regular))
.onTapGesture {
if (!isCurrent) {
withAnimation(.snappy) {
currentTab = tab
}
}
}
}
var body: some View {
let screenWidth: CGFloat = UIScreen.main.bounds.width
let bw: CGFloat = UIScreen.main.bounds.width / 375
Rectangle().fill(.yellow.opacity(0.5))
.frame(width: screenWidth, height: 64 * bw)
.overlay(alignment: .leading, content: {
HStack {
SingleTab(tab: "Posted")
Spacer().frame(width: 24 * bw)
SingleTab(tab: "Liked")
}
.padding(.leading, 16 * bw)
})
}
}
@ViewBuilder
func sampleCard(cardText: String, cardColor: Color) -> some View {
let bw: CGFloat = UIScreen.main.bounds.width / 375
Rectangle().fill(cardColor)
.frame(width: 343 * bw, height: 166 * bw)
.overlay {
Text(cardText)
.font(.system(size: 18, weight: .semibold))
}
.padding(.bottom, 32 * bw)
}
struct PostedList: View {
// 11 colors
private let colorList: [Color] = [.gray, .red, .purple, .orange, .blue, .yellow, .pink, .brown, .cyan, .indigo, .teal]
var body: some View {
let screenWidth: CGFloat = UIScreen.main.bounds.width
VStack (spacing: 0) {
ForEach(0...colorList.count - 1, id: \.self) { i in
sampleCard(cardText: "Post \(i)", cardColor: colorList[i])
}
}
.frame(width: screenWidth)
}
}
struct LikedList: View {
var body: some View {
let screenWidth: CGFloat = UIScreen.main.bounds.width
VStack (spacing: 0) {
ForEach(0...19, id: \.self) { i in
sampleCard(cardText: "Liked \(i)", cardColor: .gray.opacity(0.2))
}
}
.frame(width: screenWidth)
}
}
/// Offset Key
struct OffsetKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
extension View {
@ViewBuilder
func offsetX(completion: @escaping (CGFloat) -> ()) -> some View {
self
.overlay {
GeometryReader {
let minX = $0.frame(in: .scrollView(axis: .horizontal)).minX
Color.clear
.preference(key: OffsetKey.self, value: minX)
.onPreferenceChange(OffsetKey.self, perform: completion)
}
}
}
}
I've been stuck with this problem for a few days and would really appreciate it if anyone could offer some help.
Upvotes: 1
Views: 115