Is the SwiftUI tapGesture super greedy? - swiftui

Swift 5, iOS 13
This code works if I change the second Gesture to say a LONG Press, but leave them both as tap and it never shows the red box? Am I going mad?
import SwiftUI
struct SwiftUIViewQ: View {
#State var swap: Bool = false
var body: some View {
VStack {
if swap {
SquareView(fillColor: Color.red)
.onTapGesture {
self.swap = false
}
} else {
SquareView(fillColor: Color.blue)
.onTapGesture {
self.swap = true
}
}
}
}
}
struct SquareView: View {
#State var fillColor: Color
var body: some View {
Rectangle()
.fill(fillColor)
.frame(width: 128, height: 128)
.onAppear {
print("fillColor \(self.fillColor)")
}
}
}
Oddly if I add an onAppear to the first view, it works... if I than add an onAppear to the second it breaks it again..

var fillColor doesn't need #State, just remove that and it'll work fine
struct SquareView: View {
var fillColor: Color
var body: some View {
Rectangle()
.fill(fillColor)
.frame(width: 128, height: 128)
.onAppear {
print("fillColor \(self.fillColor)")
}
}
}

Related

LazyGridView how to detect and act on item overflowing screen?

I have a grid of items. Each item can expand height. I want to autoscroll when the item is expanded so it doesn't overflow the screen.
I was successful with the following code but I had to revert to a hack.
The idea was to detect when the item is overflowing using a Geometry reader on the item's background. Works wonders.
The issue is that when the view is expanded , the geo reader will update after the condition to check if autoscroll should execute is ran by the dispatcher. Hence my ugly hack.
Wonder what is the proper way ?
import SwiftUI
struct BlocksGridView: View {
private var gridItemLayout = [GridItem(.adaptive(minimum: 300, maximum: .infinity), spacing: 20)]
var body: some View {
ZStack{
ScrollView {
ScrollViewReader { value in
LazyVGrid(columns: gridItemLayout, spacing: 20) {
ForEach((0..<20), id: \.self) {
BlockView(cardID: $0,scrollReader: value).id($0)
}
}
}
.padding(20)
}
}
}
}
struct BlockView : View {
var cardID : Int
var scrollReader : ScrollViewProxy
#State private var isOverflowingScreen = false
#State private var expand = false
var body: some View {
ZStack{
Rectangle()
.foregroundColor(isOverflowingScreen ? Color.blue : Color.green)
.frame(height: expand ? 300 : 135)
.clipShape(Rectangle()).cornerRadius(14)
.overlay(Text(cardID.description))
.background(GeometryReader { geo -> Color in
DispatchQueue.main.async {
if geo.frame(in: .global).maxY > UIScreen.main.bounds.maxY {
isOverflowingScreen = true
} else {
isOverflowingScreen = false
}
}
return Color.clear
})
.onTapGesture {
expand.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // <-- Hack :(
if isOverflowingScreen {
withAnimation{
scrollReader.scrollTo(cardID)
}
}
}
}
}
}
}
struct BlocksGridView_Previews: PreviewProvider {
static var previews: some View {
BlocksGridView()
}
}
Blue items are overflowing ...

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: Insertion transition animation not executed

In the following simple app, I expect the red rectangle to appear with scale animation and disappear with the slide animation but only the disappear animation is executed. Why is that?
struct ContentView: View {
#State private var showDetails = false
var body: some View {
VStack {
Button(action: {
withAnimation {
self.showDetails.toggle()
}
}) {
Text("Tap to show details")
}
if showDetails {
Color.red
.frame(width: 100, height: 100, alignment: .center)
.transition(.asymmetric(insertion: .scale, removal: .slide))
}
}
}
}

Button not changing view in SwiftUI

So in my ContentView.swift, I have this code:
import SwiftUI
extension UIScreen{
static let screenWidth = UIScreen.main.bounds.size.width
static let screenHeight = UIScreen.main.bounds.size.height
static let screenSize = UIScreen.main.bounds.size
}
struct ContentView: View {
#State var Direction: Int = 0
#State var ButtonsShowing: Bool = true
#State var View: Int = 0
func MoveLeft() {
if ButtonsShowing == true {
ButtonsShowing = false
}
}
func MoveRight() {
if ButtonsShowing == true {
ButtonsShowing = false
}
Direction = 1
}
var body: some View {
ZStack {
Image("Space").resizable()
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight + 50)
.edgesIgnoringSafeArea(.all)
VStack {
HStack {
Button(action: MoveLeft) {
if ButtonsShowing == true {
NavigationLink(destination: Playing()) {
Image("LeftClick")
.frame(width: UIScreen.screenWidth / 2, height: UIScreen.screenHeight)
}
}
}
Spacer()
Button(action: MoveRight) {
if ButtonsShowing == true {
Image("RightClick")
.frame(width: UIScreen.screenWidth / 2, height: UIScreen.screenHeight)
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I get a preview of this:
Preview of button with NavigationLink
If you look towards my LeftClick button, I want it to take me to a different view, but don't know how. I have a NavigationLink in it, with my destination being the different view (Playing.swift, if needed to know that, I call it using Playing())
Obviously I'm doing something wrong, and I [[hopefully]] know for sure that it's from the button, not from the other view:
NavigationLink(destination: Playing()) {
Image("LeftClick")
.frame(width: UIScreen.screenWidth / 2, height: UIScreen.screenHeight)
}
Thanks in advance.
Add a NavigationView before your ZStack, NavigationLink won't work without a NavigationView.
var body: some View {
NavigationView {
ZStack {
...
}
}
}

SwiftUI: popover to persist (not be dismissed when tapped outside)

I created this popover:
import SwiftUI
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: $showingPopover){
Rectangle()
.frame(width: 500, height: 500)
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
Default behaviour is that is dismisses, once tapped outside.
Question:
How can I set the popover to:
- Persist (not be dismissed when tapped outside)?
- Not block screen when active?
My solution to this problem doesn't involve spinning your own popover lookalike. Simply apply the .interactiveDismissDisabled() modifier to the parent content of the popover, as illustrated in the example below:
import SwiftUI
struct ContentView: View {
#State private var presentingPopover = false
#State private var count = 0
var body: some View {
VStack {
Button {
presentingPopover.toggle()
} label: {
Text("This view pops!")
}.popover(isPresented: $presentingPopover) {
Text("Surprise!")
.padding()
.interactiveDismissDisabled()
}.buttonStyle(.borderedProminent)
Text("Count: \(count)")
Button {
count += 1
} label: {
Text("Doesn't block other buttons too!")
}.buttonStyle(.borderedProminent)
}
.padding()
}
}
Tested on iPadOS 16 (Xcode 14.1), demo video included below:
Note: Although it looks like the buttons have lost focus, they are still interact-able, and might be a bug as such behaviour doesn't exist when running on macOS.
I tried to play with .popover and .sheet but didn't found even close solution. .sheet can present you modal view, but it blocks parent view. So I can offer you to use ZStack and make similar behavior (for user):
import SwiftUI
struct Popover: View {
#State var showingPopover = false
var body: some View {
ZStack {
// rectangles only for color control
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.white)
.opacity(showingPopover ? 0.75 : 1)
Button(action: {
withAnimation {
self.showingPopover.toggle()
}
}) {
Image(systemName: "square.stack.3d.up")
}
ModalView()
.opacity(showingPopover ? 1: 0)
.offset(y: self.showingPopover ? 0 : 3000)
}
}
}
// it can be whatever you need, but for arrow you should use Path() and draw it, for example
struct ModalView: View {
var body: some View {
VStack {
Spacer()
ZStack {
Rectangle()
.frame(width: 520, height: 520)
.foregroundColor(.white)
.cornerRadius(10)
Rectangle()
.frame(width: 500, height: 500)
.foregroundColor(.black)
}
}
}
}
struct Popover_Previews: PreviewProvider {
static var previews: some View {
Popover()
.colorScheme(.dark)
.previewDevice("iPad Pro (12.9-inch) (3rd generation)")
}
}
here ModalView pops up from below and the background makes a little darker. but you still can touch everything on your "parent" view
update: forget to show the result:
P.S.: from here you can go further. For example you can put everything into GeometryReader for counting ModalView position, add for the last .gesture(DragGesture()...) to offset the view under the bottom again and so on.
You just use .constant(showingPopover) instead of $showingPopover. When you use $ it uses binding and updates your #State variable when you press outside the popover and closes your popover. If you use .constant(), it will just read the value from you #State variable, and will not close the popover.
Your code should look like this:
struct Popover : View {
#State var showingPopover = false
var body: some View {
Button(action: {
self.showingPopover = true
}) {
Image(systemName: "square.stack.3d.up")
}
.popover(isPresented: .constant(showingPopover)) {
Rectangle()
.frame(width: 500, height: 500)
}
}
}