Show hint view in LazyVGrid in SwiftUI - swiftui

I have lots of button in a LazyVGrid in a ScrollView. I am trying to show a hint view just top of the button I clicked (as like keyboard popup). I don't know how do I catch the position of a ScrollView button. Besides need help to select suitable gesture to complete the task.
Graphical representation...
Here is my code:
struct ShowHint: View {
#State var isPressed: Bool = false
var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 5)
var body: some View {
ZStack{
if isPressed {
ShowOnTopOfButton().zIndex(1)
}
ScrollView(showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 30) {
ForEach(0..<500) { i in
Text("\(i)")
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.background(Color.red.opacity( isPressed ? 0.5 : 0.9))
.gesture(TapGesture()
//.onStart { _ in isPressed = true } //but there is no property like this!
.onEnded { _ in isPressed = !isPressed }
)
}
}
}
.padding(.top, 50)
.padding(.horizontal, 10)
}
}
}
struct ShowOnTopOfButton: View {
var theS: String = "A"
var body: some View {
VStack {
Text("\(theS)")
.padding(20)
.background(Color.blue)
}
}
}

Here is possible solution - the idea is to show hint view as overlay of tapped element.
Tested with Xcode 12 / iOS 14
struct ShowHint: View {
#State var pressed: Int = -1
var columns: [GridItem] = Array(repeating: .init(.flexible()), count: 5)
var body: some View {
ZStack{
ScrollView(showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 30) {
ForEach(0..<500) { i in
Text("\(i)")
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.background(Color.red.opacity( pressed == i ? 0.5 : 0.9))
.gesture(TapGesture()
.onEnded { _ in pressed = pressed == i ? -1 : i }
)
.overlay(Group {
if pressed == i {
ShowOnTopOfButton()
.allowsHitTesting(false)
}}
)
}
}
}
.padding(.top, 50)
.padding(.horizontal, 10)
}
}
}

Related

How to move up SwiftUI `ScrollView` content when keyboard is appeared?

I'm having a chat view with messages. When message composer gets a focus and keyboard is appeared the height of ScrollView decreases. Now I want all messages to move up a little so users can see the same bottom message she saw before. Is there anyway to achieve this with a pure SwiftUI?
ScrollViewReader { scrollReader in
ScrollView {
LazyVStack(spacing: 24) {
ForEach(messages, id: \.id) {
MessageContainer(message: $0)
.id($0.id)
}
}
.padding(.horizontal, 16)
}
}
Here is an example that uses ScrollViewReader to scroll to the tapped message for answering it:
struct ContentView: View {
let messages = Message.dummyData
#State private var tappedMessage: Message?
#State private var newMessage = ""
#FocusState private var focus: Bool
var body: some View {
ScrollViewReader { scrollReader in
ScrollView {
LazyVStack(alignment: .leading, spacing: 24) {
ForEach(messages, id: \.id) { message in
MessageContainer(message: message)
.id(message.id)
.onTapGesture {
tappedMessage = message
focus = true
}
}
}
.padding(.horizontal, 16)
}
if let tappedMessage {
VStack {
TextEditor(text: $newMessage)
.frame(height: 80)
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.gray, lineWidth: 1)
)
.padding(.horizontal, 16)
.focused($focus)
Button("Send") { self.tappedMessage = nil }
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now()+0.5) {
withAnimation {
scrollReader.scrollTo(tappedMessage.id)
}
}
}
}
}
}
}
Then just try it with .offset:
struct ContentView: View {
let messages = Message.dummyData
#State private var showNewMessage = false
#State private var newMessage = ""
#FocusState private var focus: Bool
var body: some View {
VStack {
ScrollView {
LazyVStack(alignment: .leading, spacing: 24) {
ForEach(messages, id: \.id) { message in
MessageContainer(message: message)
.offset(y: showNewMessage ? -300 : 0)
}
}
.padding(.horizontal, 16)
}
if showNewMessage == false {
Button("New Message") {
withAnimation {
showNewMessage = true
focus = true
}
}
} else {
Button("Send") {
showNewMessage = false
}
TextEditor(text: $newMessage)
.frame(height: 80)
.padding()
.background(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.gray, lineWidth: 1)
)
.padding(.horizontal, 16)
.focused($focus)
}
}
}
}

Conditionally apply overlay in SwiftUI

I have an overlay button I would like to appear on the condition that we aren't on the first view!
On the first page, I would like the user to click this button to add users.
After that I would like users to navigate the form using this overlay
However, I cannot get the overlay to conditionally format so it does it if 'views > 1' and so it looks like this.
'''
//
// ContentView.swift
// PartyUp
//
// Created by Aarya Chandak on 3/9/22.
//
import SwiftUI
struct PartyPage: View {
#State private var viewModel = User.userList
#State var views = 0
#State private var cur = 0;
private var pages = 3;
var body: some View {
if(viewModel.isEmpty) {
VStack {
RSVPView()
}
} else {
ZStack {
VStack{
Text("Lets plan something!").padding()
Button(action: {views += 1}, label: { Image(systemName: "person.badge.plus")
})
}
if(views == 1) {
InviteScreen()
}
if(views == 2) {
PlanningScreen()
}
if(views == 3) {
ReviewScreen()
}
}
.overlay(
Button(action: {
withAnimation(.easeInOut) {
if(views <= totalPages){
views += 1;
}
else {
views = 0
}
}
}, label: {
Image(systemName: "chevron.right")
.font(.system(size:20, weight: .semibold))
.frame(width: 33, height: 33)
.background(.white)
.clipShape(Circle())
// Circuclar Slide
.overlay(
ZStack{
Circle()
.stroke(Color.black.opacity(0.04), lineWidth: 4)
.padding(-3)
Circle()
.trim(from: 0.0, to: CGFloat(views/pages))
.stroke(Color.white, lineWidth: 4)
.rotationEffect(.init(degrees: -90))
}
.padding(-3)
)
}
),alignment: .bottom).foregroundColor(.primary)
}
}
'''
Almost all modifiers accept a nil value for no change.
So basically you can write
.overlay(views > 1 ? Button(action: { ... }, label: { ... }) : nil)
It becomes more legible if you extract the button to an extra view struct.

Touch/drag motion to select multiple cells in a lazyvgrid?

I'm trying to use a LazyVGrid in SwiftUI where you can touch and drag your finger to select multiple adjacent cells in a specific order. This is not a drag and drop and I don't want to move the cells (maybe drag isn't the right term here, but couldn't think of another term to describe it). Also, you would be able to reverse the selection (ie: each cell can only be selected once and reversing direction would un-select the cell). How can I accomplish this? Thanks!
For example:
struct ContentView: View {
#EnvironmentObject private var cellsArray: CellsArray
var body: some View {
VStack {
LazyVGrid(columns: gridItems, spacing: spacing) {
ForEach(0..<(rows * columns), id: \.self){index in
VStack(spacing: 0) {
CellsView(index: index)
}
}
}
}
}
}
struct CellsView: View {
#State var index: Int
#EnvironmentObject var cellsArray: CellsArray
var body: some View {
ZStack {
Text("\(self.cellsArray[index].cellValue)") //cellValue is a string
.foregroundColor(Color.yellow)
.frame(width: getWidth(), height: getWidth())
.background(Color.gray)
}
//.onTapGesture ???
}
func getWidth()->CGFloat{
let width = UIScreen.main.bounds.width - 10
return width / CGFloat(columns)
}
}
Something like this might help, the only issue here is the coordinate space. Overlay is not drawing the rectangle in the correct coordinate space.
struct ImagesView: View {
var columnSize: CGFloat
#Binding var projectImages: [ProjectImage]
#State var selectedImages: [ImageSelection] = []
#State var dragWidth: CGFloat = 1.0
#State var dragHeight: CGFloat = 1.0
#State var dragStart: CGPoint = CGPoint(x:0, y:0)
let columns = [
GridItem(.adaptive(minimum: 200), spacing: 0)
]
var body: some View {
GeometryReader() { geometry in
ZStack{
ScrollView{
LazyVGrid(columns: [
GridItem(.adaptive(minimum: columnSize), spacing: 2)
], spacing: 2){
ForEach(projectImages, id: \.imageUUID){ image in
Image(nsImage: NSImage(data: image.imageData)!)
.resizable()
.scaledToFit()
.border(selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) ? Color.blue : .primary, width: selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) ? 5.0 : 1.0)
.gesture(TapGesture(count: 2).onEnded{
print("Double tap finished")
})
.gesture(TapGesture(count: 1).onEnded{
if selectedImages.contains(where: {imageSelection in imageSelection.uuid == image.imageUUID}) {
print("Image is already selected")
if let index = selectedImages.firstIndex(where: {imageSelection in imageSelection.uuid == image.imageUUID}){
selectedImages.remove(at: index)
}
} else {
selectedImages.append(ImageSelection(imageUUID: image.imageUUID))
print("Image has been selected")
}
})
}
}
.frame(minHeight: 50)
.padding(.horizontal, 1)
}
.simultaneousGesture(
DragGesture(minimumDistance: 2)
.onChanged({ value in
print("Drag Start: \(value.startLocation.x)")
self.dragStart = value.startLocation
self.dragWidth = value.translation.width
self.dragHeight = value.translation.height
})
)
.overlay{
Rectangle()
.frame(width: dragWidth, height: dragHeight)
.offset(x:dragStart.x, y:dragStart.y)
}
}
}
}
}

SwiftUI Crash when calling scrollTo method when view is disappearing

I try to make my ScrollView fixed in a specific place, but the scrollTo method will cause the application to crash.
How to make the ScrollView stay in a fixed place?
I want to control the switching of views by MagnificationGesture to switch from one view to another.
But when Scroll() disappdars, the app crashs.
struct ContentView: View {
#State var tabCount:Int = 1
#State var current:CGFloat = 1
#State var final:CGFloat = 1
var body: some View {
let magni = MagnificationGesture()
.onChanged(){ value in
current = value
}
.onEnded { value in
if current > 2 {
self.tabCount += 1
}
final = current + final
current = 0
}
VStack {
VStack {
Button("ChangeView"){
self.tabCount += 1
}
if tabCount%2 == 0 {
Text("some text")
}else {
Scroll(current: $current)
}
}
Spacer()
HStack {
Color.blue
}
.frame(width: 600, height: 100, alignment: .bottomLeading)
}
.frame(width: 600, height: 400)
.gesture(magni)
}
}
This is ScrollView, I want it can appear and disappear. When MagnificationGesture is changing, scrollview can keep somewhere.
struct Scroll:View {
#Binding var current:CGFloat
let intRandom = Int.random(in: 1..<18)
var body: some View {
ScrollViewReader { proxy in
HStack {
Button("Foreword"){
proxy.scrollTo(9, anchor: .center)
}
}
ScrollView(.horizontal) {
HStack {
ForEach(0..<20) { item in
RoundedRectangle(cornerRadius: 25.0)
.frame(width: 100, height: 40)
.overlay(Text("\(item)").foregroundColor(.white))
.id(item)
}
}
.onChange(of: current, perform: { value in
proxy.scrollTo(13, anchor: .center)
})
}
}
}
}

SwiftUI: Have a button change itself?

I am trying to make a struct that shows a menu of radio buttons.
The issue I have is the following: when I press a button, I want the Text(item) View to change color. I'm not sure how to do that, since the Text(item) is encompassed by the button.
import SwiftUI
struct RadioMenu: View {
var items = [String]()
#State var isChecked: Bool = false
#State var selection: String? = nil
var textSize: Int = 20
init(items: [String], textSize: Int) {
self.items = items
self.textSize = textSize
}
var body: some View {
VStack {
ForEach(items, id:\.self) { item in
Button (action: {
self.isChecked = true
self.selection = item
}) {
Text(item)
.font(.system(size: CGFloat(self.textSize), weight: .medium, design: .rounded))
.padding()
.overlay(
RoundedRectangle(cornerRadius: 15)
.stroke(lineWidth: 2)
)}
.padding(.bottom, 10)
}
}
}
}
You can apply this modifier on Text :
.foregroundColor(item == self.selection ? Color.red : Color.black)