ScrollView PreferenceKey Offset changes from iOS14 -> iOS16 - swiftui

I am currently trying to implement a spreadsheet like view (I found this, but it's not in SwiftUI https://github.com/bannzai/SpreadsheetView) and am following this SO (SwiftUI: Pin headers in scrollview which has vertical and horizontal scroll in excel like view) which is working on iOS14, but it breaks on iOS16.
when executed on ios14 simulator, the Initial Offset on Console is (0.0,0.0) and the scrollview would be at the correct location (Row1, Co1). However, the same code, when executed on iOS16, the offset changes to another number eg: (-300.22,0.0) and the resultant view will show at say (Row1,Col16)
Q: How can I get the same behaviour back?
I have tried to use onAppear and putting the CGPoint.Zero coordinate to push it back to the origin but it's not working..
Tx
The code is per below:
struct ContentView: View {
let columns = 20
let rows = 30
#State private var offset = CGPoint.zero
var body: some View {
HStack(alignment: .top, spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
// empty corner
Color.clear.frame(width: 70, height: 50)
ScrollView([.vertical]) {
rowsHeader
.offset(y: offset.y)
}
.disabled(true)
}
VStack(alignment: .leading, spacing: 0) {
ScrollView([.horizontal]) {
colsHeader
.offset(x: offset.x)
}
.disabled(true)
table
.coordinateSpace(name: "scroll")
}
}
.padding()
}
var colsHeader: some View {
HStack(alignment: .top, spacing: 0) {
ForEach(0..<columns) { col in
Text("COL \(col)")
.foregroundColor(.secondary)
.font(.caption)
.frame(width: 70, height: 50)
.border(Color.blue)
}
}
}
var rowsHeader: some View {
VStack(alignment: .leading, spacing: 0) {
ForEach(0..<rows) { row in
Text("ROW \(row)")
.foregroundColor(.secondary)
.font(.caption)
.frame(width: 70, height: 50)
.border(Color.blue)
}
}
}
var table: some View {
ScrollView([.vertical, .horizontal]) {
VStack(alignment: .leading, spacing: 0) {
ForEach(0..<rows) { row in
HStack(alignment: .top, spacing: 0) {
ForEach(0..<columns) { col in
// Cell
Text("(\(row), \(col))")
.frame(width: 70, height: 50)
.border(Color.blue)
.id("\(row)_\(col)")
}
}
}
}
.background( GeometryReader { geo in
Color.clear
.preference(key: ViewOffsetKey.self, value: geo.frame(in: .named("scroll")).origin)
})
.onPreferenceChange(ViewOffsetKey.self) { value in
print("offset >> \(value)")
offset = value
}
}
}
}
struct ViewOffsetKey: PreferenceKey {
typealias Value = CGPoint
static var defaultValue = CGPoint.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value.x += nextValue().x
value.y += nextValue().y
}
}

Related

SwiftUI: Double picker wheels with system behavioral

I want to recreate system picker behavioral with two options in wheels with SwiftUI and faced ton of problem. Some of this I solved but some still unsolved. I have pop-ups with different views inside. One of the view it's a DatePicker with displayedComponents: .hourAndMinute. And other one is two Pickers inside HStack. My question is how to make Pickers make look like in system: without white spacing between?
struct MultyPicker: View {
#State var value = 1
#State var value2 = 1
var body: some View {
ZStack(alignment: .bottom) {
Color.black.opacity(0.5)
ZStack {
VStack {
Text("Header")
.font(.title3)
.fontWeight(.bold)
HStack(spacing: 0) {
Picker(selection: $value, label: Text("")) {
ForEach(1..<26) { number in
Text("\(number)")
.tag("\(number)")
}
}
.pickerStyle(WheelPickerStyle())
.compositingGroup()
.clipped(antialiased: true)
Picker(selection: $value2, label: Text("")) {
ForEach(25..<76) { number in
Text("\(number)")
.tag("\(number)")
}
}
.pickerStyle(WheelPickerStyle())
.compositingGroup()
.clipped(antialiased: true)
}
}
.padding()
.background(
RoundedRectangle(cornerRadius: 34)
.foregroundColor(.white)
)
}
.padding(.horizontal)
.padding(.bottom, 50)
}
.edgesIgnoringSafeArea([.top, .horizontal])
}
}
// This extension for correct touching area
extension UIPickerView {
open override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: super.intrinsicContentSize.height)
}
}
Want to achive looks like that with one grey line in selected value
//
// Test2.swift
// Test
//
// Created by Serdar Onur KARADAĞ on 26.08.2022.
//
import SwiftUI
struct Test2: View {
#State var choice1 = 0
#State var choice2 = 0
var body: some View {
ZStack {
Rectangle()
.fill(.gray.opacity(0.2))
.cornerRadius(30)
.frame(width: 350, height: 400)
Rectangle()
.fill(.white.opacity(1))
.cornerRadius(30)
.frame(width: 300, height: 350)
VStack {
Text("HEADER")
HStack(spacing: 0) {
Picker(selection: $choice1, label: Text("C1")) {
ForEach(0..<10) { n in
Text("\(n)").tag(n)
}
}
.pickerStyle(.wheel)
.frame(minWidth: 0)
.clipped()
Picker(selection: $choice2, label: Text("C1")) {
ForEach(0..<10) { n in
Text("\(n)").tag(n)
}
}
.pickerStyle(.wheel)
.frame(minWidth: 0)
.clipped()
}
}
}
}
}
struct Test2_Previews: PreviewProvider {
static var previews: some View {
Test2()
}
}
SwiftUI multi-component Picker basically consists of several individual Picker views arranged horizontally. Therefore, we start by creating an ordinary Picker view for our first component. I am using Xcode version 13.4.1(iOS 15.0).
import SwiftUI
struct ContentView: View {
#State var hourSelect = 0
#State var minuteSelect = 0
var hours = [Int](0..<24)
var minutes = [Int](0..<60)
var body: some View {
ZStack {
Color.black
.opacity(0.5)
.ignoresSafeArea()
.preferredColorScheme(.light)
Rectangle()
.fill(.white.opacity(1))
.cornerRadius(30)
.frame(width: 300, height: 350)
VStack {
Text("Header")
HStack(spacing: 0) {
Picker(selection: $hourSelect, label: Text("")) {
ForEach(0..<self.hours.count) { index in
Text("\(self.hours[index])").tag(index)
}
}
.pickerStyle(.wheel)
.frame(minWidth: 0)
.compositingGroup()
.clipped()
Picker(selection: $minuteSelect, label: Text("")) {
ForEach(0..<self.minutes.count) { index in
Text("\(self.minutes[index])").tag(index)
}
}
.pickerStyle(.wheel)
.frame(minWidth: 0)
.compositingGroup()
.clipped()
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Output :

resizable header in SwiftUI

I was trying to make resizable header but it breaks.
I am trying to clone twitter profile,
I think logic is right but can I know why this one is not working?
I made HStack and try to hide it but when I scroll back it can't come back.
Tried with GeometryReader
Please help me, thanks
import SwiftUI
struct ProfileView: View {
// change views
#State var isHide = false
#State private var selectionFilter: TweetFilterViewModel = .tweets
#Namespace var animation
var body: some View {
VStack(alignment: .leading) {
//hiding
if isHide == false {
headerView
actionButtons
userInfoDetails
}
tweetFilterBar
.padding(0)
ScrollView(showsIndicators: false) {
LazyVStack {
GeometryReader { reader -> AnyView in
let yAxis = reader.frame(in: .global).minY
let height = UIScreen.main.bounds.height / 2
if yAxis < height && !isHide {
DispatchQueue.main.async {
withAnimation {
isHide = true
}
}
}
if yAxis > 0 && isHide {
DispatchQueue.main.async {
withAnimation {
isHide = false
}
}
}
return AnyView(
Text("")
.frame(width: 0, height: 0)
)
}
.frame(width: 0, height: 0)
ForEach(0...9, id: \.self) { _ in
TweetRowView()
}
}
}
Spacer()
}
}
}
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
ProfileView()
}
}
extension ProfileView {
var headerView: some View {
ZStack(alignment: .bottomLeading) {
Color(.systemBlue)
.ignoresSafeArea()
VStack {
Button {
} label: {
Image(systemName: "arrow.left")
.resizable()
.frame(width: 20, height: 16)
.foregroundColor(.white)
.position(x: 30, y: 12)
}
}
Circle()
.frame(width: 72, height: 72)
.offset(x: 16, y: 24)
}.frame(height: 96)
}
var actionButtons: some View {
HStack(spacing: 12){
Spacer()
Image(systemName: "bell.badge")
.font(.title3)
.padding(6)
.overlay(Circle().stroke(Color.gray, lineWidth: 0.75))
Button {
} label: {
Text("Edit Profile")
.font(.subheadline).bold()
.accentColor(.black)
.padding(10)
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.gray, lineWidth: 0.75))
}
}
.padding(.trailing)
}
var userInfoDetails: some View {
VStack(alignment: .leading) {
HStack(spacing: 4) {
Text("Heath Legdet")
.font(.title2).bold()
Image(systemName: "checkmark.seal.fill")
.foregroundColor(Color(.systemBlue))
}
.padding(.bottom, 2)
Text("#joker")
.font(.subheadline)
.foregroundColor(.gray)
Text("Your mom`s favorite villain")
.font(.subheadline)
.padding(.vertical)
HStack(spacing: 24) {
Label("Gothem.NY", systemImage: "mappin.and.ellipse")
Label("www.thejoker.com", systemImage: "link")
}
.font(.caption)
.foregroundColor(.gray)
HStack(spacing: 24) {
HStack {
Text("807")
.font(.subheadline)
.bold()
Text("following")
.font(.caption)
.foregroundColor(.gray)
}
HStack {
Text("200")
.font(.subheadline)
.bold()
Text("followers")
.font(.caption)
.foregroundColor(.gray)
}
}
.padding(.vertical)
}
.padding(.horizontal)
}
var tweetFilterBar: some View {
HStack {
ForEach(TweetFilterViewModel.allCases, id: \.rawValue) { item in
VStack {
Text(item.title)
.font(.subheadline)
.fontWeight(selectionFilter == item ? .semibold : .regular)
.foregroundColor(selectionFilter == item ? .black : .gray)
ZStack {
Capsule()
.fill(Color(.clear))
.frame(height: 3)
if selectionFilter == item {
Capsule()
.fill(Color(.systemBlue))
.frame(height: 3)
.matchedGeometryEffect(id: "filter", in: animation)
}
}
}
.onTapGesture {
withAnimation(.easeInOut) {
self.selectionFilter = item
}
}
}
}
}
}
While the animation between show and hide is running, the GeometryReader is still calculating values – which lets the view jump between show and hide – and gridlock.
You can introduce a new #State var isInTransition = false that checks if a show/hide animation is in progress and check for that. You set it to true at the beginning of the animation and to false 0.5 secs later.
Also I believe the switch height is not exactly 1/2 of the screen size.
So add a new state var:
#State var isInTransition = false
and in GeometryReader add:
GeometryReader { reader -> AnyView in
let yAxis = reader.frame(in: .global).minY
let height = UIScreen.main.bounds.height / 2
if yAxis < 350 && !isHide && !isInTransition {
DispatchQueue.main.async {
isInTransition = true
withAnimation {
isHide = true
}
}
// wait for animation to finish
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isInTransition = false
}
} else if yAxis > 0 && isHide && !isInTransition {
DispatchQueue.main.async {
isInTransition = true
withAnimation {
isHide = false
}
}
// wait for animation to finish
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isInTransition = false
}
}
return AnyView(
// displaying values for test purpose
Text("\(yAxis) - \(isInTransition ? "true" : "false")").foregroundColor(.red)
// .frame(width: 0, height: 0)
)
}

Background color of view and list SwiftUI

I have a Cell View that the user is able to select the background color when they save the trip. I am trying to figure out how to give the detail view and the list with all the information the same custom color.
struct TripView: View {
let tripViewModel: TripViewModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
}
.foregroundColor(.white)
.padding()
.background(RoundedRectangle(cornerRadius: 10).fill(tripViewModel.color))
}
}
struct TripDetailView: View {
#ObservedObject var viewModel: TripDetailViewModel
init(tripDetailViewModel: TripDetailViewModel) {
self.viewModel = tripDetailViewModel
}
var body: some View {
ZStack {
List {
Section(header: Text("Dates:")
.fontWeight(.bold)
.font(.system(size: 20))) {
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 5) {
Text("Start Date:").fontWeight(.bold)
Text("End Date:").fontWeight(.bold)
}
.frame(width: 150, alignment: .leading)
VStack(alignment: .leading, spacing: 5) {
Text(viewModel.trip.startDate)
Text(viewModel.trip.endDate)
}
}.padding(.bottom)
}
}
}
}
}

ScrollViewReader is not working and building is pretty slow

I couldn't figure out why it is not working my ScrollViewReader below my code. Please help me figure out the issue. I want to pass my zolyric.number into scrollToIndex and that scrollToIndex will set the number that I want to scroll in my HymnLyrics(). Also, although I couldn't find the error, the building is pretty slow.
Here ZolaiTitles() is the same list just sorting the song titles algebraically and HymnLyrics() is the one to display in the canvas.
struct ZolaiTitles: View {
#AppStorage("scrollToIndex") var scrollToIndex: Int?
#EnvironmentObject var tappingSwitches: TapToggle
let zoLyrics: [Lyric] = LyricList.hymnLa.sorted { lhs, rhs in
return lhs.zoTitle < rhs.zoTitle
}
var body: some View {
ScrollView {
ForEach(zoLyrics, id: \.id) { zoLyric in
VStack {
Button(action: {
//if let lyricNum = String(zoLyric) {
scrollToIndex = zoLyric.number // zolyric.number is already an interger.
//}
self.tappingSwitches.isHymnTapped.toggle()
}, label: {
HStack {
Text(zoLyric.zoTitle)
.foregroundColor(Color("bTextColor"))
.lineLimit(1)
.minimumScaleFactor(0.5)
Spacer()
Text("\(zoLyric.number)")
.foregroundColor(Color("bTextColor"))
}
})
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding([.leading, .bottom, .trailing])
}
}
}
}
struct HymnLyrics: View {
#AppStorage("scrollToIndex") var scrollToIndex: Int = 1
var lyrics: [Lyric] = LyricList.hymnLa
#AppStorage("fontSizeIndex") var fontSizeIndex = Int("Medium") ?? 18
#AppStorage("fontIndex") var fontIndex: String = ""
#AppStorage("showHVNumbers") var showHVNumbers: Bool = true
var body: some View {
ScrollViewReader { proxy in
List(lyrics, id: \.id) { lyric in
VStack(alignment: .center, spacing: 0) {
Text("\(lyric.number)")
.padding()
.multilineTextAlignment(/*#START_MENU_TOKEN#*/.leading/*#END_MENU_TOKEN#*/)
.id(lyric.number)
VStack {
VStack {
Text(lyric.zoTitle)
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.primary)
.autocapitalization(.allCharacters)
Text(lyric.engTitle)
.font(.title3)
.fontWeight(.medium)
.foregroundColor(.secondary)
.italic()
}
// Don't use Padding here!
}
.multilineTextAlignment(.center)
HStack {
Text(lyric.key)
.italic()
Spacer()
Text(lyric.musicStyle)
}
.foregroundColor(.blue)
.padding(.vertical)
}
//.id(lyric.number)
}
.onChange(of: scrollToIndex, perform: { value in
proxy.scrollTo(value, anchor: .top)
})
}
}
}

How to ensure view appears above other views when iterating with ForEach in SwiftUI?

I have a SwiftUI view that is a circular view which when tapped opens up and is supposed to extend over the UI to its right. How can I make sure that it will appear atop the other ui? The other UI elements were created using a ForEach loop. I tried zindex but it doesn't do the trick. What am I missing?
ZStack {
VStack(alignment: .leading) {
Text("ALL WORKSTATIONS")
ZStack {
ChartBackground()
HStack(alignment: .bottom, spacing: 15.0) {
ForEach(Array(zip(1..., dataPoints)), id: \.1.id) { number, point in
VStack(alignment: .center, spacing: 5) {
DataCircle().zIndex(10)
ChartBar(percentage: point.percentage).zIndex(-1)
Text(point.month)
.font(.caption)
}
.frame(width: 25.0, height: 200.0, alignment: .bottom)
.animation(.default)
}
}
.offset(x: 30, y: 20)
}
.frame(width: 500, height: 300, alignment: .center)
}
}
}
}
.zIndex have effect for views within one container. So to solve your case, as I assume expanded DataCircle on click, you need to increase zIndex of entire bar VStack per that click by introducing some kind of handling selection.
Here is simplified replicated demo to show the effect
struct TestBarZIndex: View {
#State private var selection: Int? = nil
var body: some View {
ZStack {
VStack(alignment: .leading) {
Text("ALL WORKSTATIONS")
ZStack {
Rectangle().fill(Color.yellow)//ChartBackground()
HStack(alignment: .bottom, spacing: 15.0) {
ForEach(1...10) { number in
VStack(spacing: 5) {
Spacer()
ZStack() { // DataCircle()
Circle().fill(Color.pink).frame(width: 20, height: 20)
.onTapGesture { self.selection = number }
if number == self.selection {
Text("Top Description").fixedSize()
}
}
Rectangle().fill(Color.green) // ChartBar()
.frame(width: 20, height: CGFloat(Int.random(in: 40...150)))
Text("Jun")
.font(.caption)
}.zIndex(number == self.selection ? 1 : 0) // << here !!
.frame(width: 25.0, height: 200.0, alignment: .bottom)
.animation(.default)
}
}
}
.frame(height: 300)
}
}
}
}