I’m working on a major update for my app and I’m aiming to improve the design to match premium standards, similar to Apple’s design approach. The app I’m working on is SignDict, which displays both ASL and JSL. To elevate the UI, I was getting bored with using Apple's original TableView or Scroll view, with new design inspired by VisionOS style.
While brainstorming, I came across the Apple Weather app show great scroll view UI, which features a smooth scrolling experience where it's good for under the AIUEO collection view on my app. However, I’m encountering an issue where the scroll view seems to lag when I swipe quickly like this 2 screenshots...
, In the last screenshot, I noticed that the slide with slowly down or up make prefect on top without nagged, but super little lag. This is exactly what I wanted, but I’m not sure how to prevent it from happening. Can you help me with that?
A friend of mine suggested that the issue might be related to using GeometryReader, but I’m still learning how to work with it effectively. I’ve followed tutorials and tried various solutions, but the problem persists.
If anyone has experience with this or could point me in the right direction, I would greatly appreciate your help. Here’s the full code I’m currently using:
struct CustomCorner: Shape {
var corners: UIRectCorner
var radius: CGFloat
func path( in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
struct CustomStackView < Title: View, Content: View>: View {
@State var topOffset: CGFloat = 0
@State var bottomOffset: CGFloat = 0
var titleView: Title
var contentView: Content
init(@ViewBuilder titleView: @escaping ()->Title, @ViewBuilder contentView: @escaping () -> Content) {
self.titleView = titleView()
self.contentView = contentView()
}
var body: some View {
VStack(spacing: 0) {
titleView
.font(.callout)
.lineLimit(1)
.frame(height: 38)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading)
.background(.ultraThinMaterial, in: CustomCorner(corners: bottomOffset < 37 ? .allCorners : [.topLeft, .topRight], radius: 12))
.zIndex(1)
VStack {
Divider()
contentView
.padding()
}
.background(.ultraThinMaterial, in: CustomCorner(corners: [.bottomLeft, .bottomRight], radius: 12))
// Moving Content upward
.offset(y: topOffset >= 165 ? 0 : -(-topOffset + 165))
.zIndex(0)
.clipped()
.opacity(getOpacity())
}
.offset(y: topOffset >= 165 ? 0 : -topOffset + 165)
.opacity(getOpacity())
.background(
GeometryReader{proxy -> Color in
let minY = proxy.frame(in: .global).minY
let maxY = proxy.frame(in: .global).maxY
DispatchQueue.main.async {
self.topOffset = minY
self.bottomOffset = maxY - 165
}
return Color.clear
}
)
.modifier(CornerModifer(bottomOffset: $bottomOffset))
}
func getOpacity()->CGFloat {
if bottomOffset < 38 {
let progress = bottomOffset / 28
return progress
}
return 1
}
}
struct CornerModifer: ViewModifier {
@Binding var bottomOffset: CGFloat
func body(content: Content) -> some View {
if bottomOffset < 37 {
content
} else {
content.cornerRadius(12)
}
}
}
this code to custom scroll view like Weather Scroll View... now I put a last one code for to display it right here:
struct RedesignList: View {
@State private var scrollOffset: CGFloat = 0
@State private var searchText = ""
@State private var selectedHiragana: String = "あ" // Default to "あ" on app launch
// color here
let headerView: [HeaderData] = [
// MARK: Vowels
HeaderData(title: "あ", color: Color(uiColor: UIColor.systemMint)),
HeaderData(title: "い", color: Color(uiColor: UIColor.systemMint)),
].sorted { $0.title < $1.title }
@State private var isJapaneseSheetPresented = false
@State private var isEnglishSheetPresented = false
@State private var selectedItem: (HeaderTitle: String, HiraganaText: String, KanjiText: String, japImage: String, engTitle: String, engImage: String)?
var listOfLanguage: [(HeaderTitle: String, HiraganaText: String, KanjiText: String, japImage: String, engTitle: String, engImage: String)] = [
(HeaderTitle: "あ", HiraganaText: "あか", KanjiText: "", japImage: "あか", engTitle: "Red (Color)", engImage: "Red"),
(HeaderTitle: "あ", HiraganaText: "あかちやん", KanjiText: "赤ちやん", japImage: "あかちやん", engTitle: "Baby", engImage: "Baby"),
(HeaderTitle: "あ", HiraganaText: "あか", KanjiText: "", japImage: "あか", engTitle: "Red (Color)", engImage: "Red"),
(HeaderTitle: "あ", HiraganaText: "あかちやん", KanjiText: "赤ちやん", japImage: "あかちやん", engTitle: "Baby", engImage: "Baby"),
(HeaderTitle: "あ", HiraganaText: "あか", KanjiText: "", japImage: "あか", engTitle: "Red (Color)", engImage: "Red"),
(HeaderTitle: "あ", HiraganaText: "あかちやん", KanjiText: "赤ちやん", japImage: "あかちやん", engTitle: "Baby", engImage: "Baby"),
(HeaderTitle: "あ", HiraganaText: "あか", KanjiText: "", japImage: "あか", engTitle: "Red (Color)", engImage: "Red"),
(HeaderTitle: "あ", HiraganaText: "あかちやん", KanjiText: "赤ちやん", japImage: "あかちやん", engTitle: "Baby", engImage: "Baby"),
]
var filteredList: [(HeaderTitle: String, HiraganaText: String, KanjiText: String, japImage: String, engTitle: String, engImage: String)] {
if searchText.isEmpty {
return listOfLanguage.filter { $0.HeaderTitle == selectedHiragana }
} else {
return listOfLanguage.filter {
$0.engTitle.localizedCaseInsensitiveContains(searchText) ||
$0.HiraganaText.localizedCaseInsensitiveContains(searchText) ||
$0.KanjiText.localizedCaseInsensitiveContains(searchText)
}
}
}
@State private var scrolloffset: CGFloat = 0
@State private var topInset: CGFloat = 0
var body: some View {
NavigationStack {
ZStack {
VStack {
// Fetch the selected HeaderData color dynamically
let selectedColor = headerView.first { $0.title == selectedHiragana }?.color ?? .clear
// blur here...
LinearGradient(gradient: Gradient(stops: [
.init(color: selectedColor, location: 0.005), // Use selected color
.init(color: .clear, location: 0.20),
]), startPoint: .top, endPoint: .bottom)
.ignoresSafeArea()
}
.blur(radius: 50)
ScrollView(.vertical, showsIndicators: true) {
VStack {
// Collection あいうえお
VStack {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(headerView, id: \.title) { header in
if listOfLanguage.contains(where: { $0.HeaderTitle == header.title }) { // Changed filter logic
Text(header.title)
.font(Font.system(size: 20, weight: .medium))
.frame(width: 50, height: 50)
.background(header.color.opacity(selectedHiragana == header.title ? 0.7 : 0.3))
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.5), lineWidth: 1)
)
.onTapGesture {
withAnimation {
selectedHiragana = header.title
}
}
}
}
}
.padding(.horizontal)
.frame(height: 52)
}
.padding(.top, 6)
.offset(y: scrolloffset > 0 ? scrolloffset : 0)
}
// Table View
VStack {
ForEach(filteredList, id: \.HiraganaText) { item in
VStack(spacing: 12) {
HStack {
CustomStackView {
Label {
HStack {
Text(item.HiraganaText)
.padding(.leading, -17)
Spacer()
Text(item.KanjiText)
.foregroundStyle(.gray)
}
.padding()
} icon: {}
} contentView: {
Image(item.japImage)
.resizable()
.scaledToFit()
.clipped()
.aspectRatio(contentMode: .fill)
.frame(width: 150, height: 80)
.padding(.top, -8)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
selectedItem = item
isJapaneseSheetPresented.toggle()
}
}
CustomStackView {
Label {
let splitTitle = extractTitleParts(from: item.engTitle)
HStack {
Text(splitTitle.0)
.font(.system(size: 16))
.padding(.leading, -10)
Spacer()
if !splitTitle.1.isEmpty {
Text(splitTitle.1)
.foregroundColor(.secondary)
.padding(.trailing, 3)
}
}
.frame(maxWidth: .infinity)
.padding(.horizontal, 15)
} icon: {}
} contentView: {
VStack {
Image(item.engImage)
.resizable()
.scaledToFit()
.clipped()
.aspectRatio(contentMode: .fill)
.frame(width: 150, height: 80)
.padding(.top, -8)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
selectedItem = item
isEnglishSheetPresented.toggle()
}
}
}
}
}
.padding(.horizontal, 15)
}
}
}
}
.onScrollGeometryChange(for: CGFloat.self, of: {
$0.contentOffset.y + $0.contentInsets.top
}, action: { _, newValue in
scrolloffset = newValue
})
.onScrollGeometryChange(for: CGFloat.self, of: {
$0.contentInsets.top
}, action: { _, newValue in
topInset = newValue
})
.background(GeometryReader { geometry in
Color.clear
.onAppear {
scrollOffset = geometry.frame(in: .global).minY
}
})
.navigationTitle("Dictionary")
.navigationBarTitleDisplayMode(.automatic)
.searchable(text: $searchText)
.toolbarBackground(.clear, for: .navigationBar) // Makes the nav bar background transparent
.toolbarBackground(.hidden, for: .navigationBar)
}
}
}
// English
private func extractTitleParts(from title: String) -> (String, String) {
if let range = title.range(of: #"\((.*?)\)"#, options: .regularExpression) {
let mainTitle = title[..<range.lowerBound].trimmingCharacters(in: .whitespaces)
let insideParentheses = title[range].trimmingCharacters(in: ["(", ")"])
return (mainTitle, insideParentheses)
}
return (title, "") // No parentheses found
}
}
private let leftOffset: CGFloat = 0.1
DONE!
Please give me any issues you found I can make my app to improved before launch V2.0! Thanks!