TapGesture is not working in Xcode 11.0 Beta - swift

Playing around with SwiftUI and this TapGesture() (or any other gesture) doesn't seem to work for me in a Cocoa app for MacOS, despite #available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) when I View Definition on TapGesture.
import SwiftUI
struct CircleView : View {
var body: some View {
Circle()
.fill(Color.blue)
.frame(width: 400, height: 400, alignment: .center)
.gesture(
TapGesture()
.onEnded { _ in
print("View tapped!")
}
)
}
}
#if DEBUG
struct CircleView_Previews : PreviewProvider {
static var previews: some View {
CircleView()
}
}
#endif
The Build succeeded, I'm viewing the Circle in Preview and I have the console open, but nothing seems to print.
Am I doing something wrong? Is this a 10.15 Beta bug? Is there another framework I need to import other than SwiftUI? New to Swift here.

The tap is working fine, but when you’re previewing the app, you won’t see your print statements in the console. Actually run the app and you’ll see the print statements show up in your console.
Or change your app to present something in the UI confirming the tap gesture, e.g., an Alert:
struct CircleView : View {
#State var showAlert = false
var body: some View {
Circle()
.fill(Color.blue)
.tapAction {
self.showAlert = true
}
.presentation($showAlert) {
Alert(title: Text("View Tapped!"),
primaryButton: .default(Text("OK")),
secondaryButton: .cancel())
}
}
}
Or perhaps you would want to animate the change of color of the shape:
struct CircleView: View {
#State var isBlue = true
var body: some View {
Circle()
.fill(isBlue ? Color.blue : Color.red)
.tapAction {
withAnimation {
self.isBlue.toggle()
}
}
}
}

Related

Full size AccessoryWidgetBackground() in accessoryRectangular family

I'd like to add a full size AccessoryWidgetBackground() to an accessoryRectangular widget family.
I made a brand new project and added a brand new widget. Then I changed the view to:
struct Some_WidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
AccessoryWidgetBackground()
.frame(width: .infinity, height: .infinity)
.ignoresSafeArea()
}
}
Here's what I get:
Is there a way to take up the whole view with the background blur?
Here is a possible solution. Tested with Xcode 11.4 / iOS 13.4
struct Some_WidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
ZStack {
Color.clear.edgesIgnoringSafeArea(.all) // << here !!
AccessoryWidgetBackground()
.edgesIgnoringSafeArea(.all)
}
}
}
The answer to the question as posed is: this is not possible in iOS 16.0, the view is cropped as can be seen from the inner box in the image in the question.
The workaround I settled for is to create a rounded background view and add it in a ZStack:
var body: some View {
ZStack {
OptionalBlurView(showBlur: true)
// etc
Here's the background view, it works in previews too.
#available(iOSApplicationExtension 16.0, *)
struct OptionalBlurView: View {
var showBlur: Bool
#Environment(\.widgetFamily) var family
var body: some View {
if showBlur {
blurView
} else {
EmptyView()
}
}
var blurView: some View {
#if targetEnvironment(simulator)
// at the time of coding, AccessoryWidgetBackground does not show in previews, so this is an aproximation
switch family {
case .accessoryCircular:
return Circle()
.opacity(0.3)
.eraseToAnyView()
default:
return Rectangle()
.clipShape(RoundedRectangle(cornerSize: CGSize(width: 10, height: 10), style: .continuous))
.opacity(0.3)
.eraseToAnyView()
}
#else
AccessoryWidgetBackground()
.clipShape(RoundedRectangle(cornerSize: CGSize(width: 10, height: 10), style: .continuous))
.opacity(0.7)
#endif
}
}

Show bottom sheet after button press swiftui

I'm trying to add to my app bottom sheet with responsive height which I can set programmatically. For this purpose I'm trying to use this video. Here is code of my view controller:
struct SecondView: View {
#State var cardShown = false
#State var cardDismissal = false
var body: some View {
Button {
cardShown.toggle()
cardDismissal.toggle()
} label: {
Text("Show card")
.bold()
.foregroundColor(Color.white)
.background(Color.red)
.frame(width: 200, height: 50)
}
BottomCard(cardShown: $cardShown, cardDismissal: $cardDismissal) {
CardContent()
}
}
}
struct CardContent:View{
var body: some View{
Text("some text")
}
}
struct BottomCard<Content:View>:View{
#Binding var cardShown:Bool
#Binding var cardDismissal:Bool
let content:Content
init(cardShown:Binding<Bool> , cardDismissal:Binding<Bool>, #ViewBuilder content: () -> Content){
_cardShown = cardShown
_cardDismissal = cardDismissal
self.content = content()
}
var body: some View{
ZStack{
//Dimmed
GeometryReader{ _ in
EmptyView()
}
.background(Color.red.opacity(0.2))
.opacity(cardShown ? 1 : 0)
.animation(.easeIn)
.onTapGesture {
cardShown.toggle()
}
// Card
VStack{
Spacer()
VStack{
content
}
}
.edgesIgnoringSafeArea(.all)
}
}
}
but after pressing the button I don't see any pushed bottom menu. I checked and it seems that I have similar code to this video but on the video bottom sheet appears. Maybe I missed something important for menu showing. The main purpose is to show bottom menu with responsive height which will wrap elements and will be able to change menu height. I tried to use .sheet() but this element has stable height as I see. I know that from the ios 15+ we will have some solutions for this problem but I would like to create something more stable and convenient :)
iOS 16
We can have native SwiftUI resizable sheet (like UIKit). This is possible with the new .presentationDetents() modifier.
.sheet(isPresented: $showBudget) {
BudgetView()
.presentationDetents([.height(250), .medium])
.presentationDragIndicator(.visible)
}
Demo:
This is what I got when running your code
I got this after some adjustments to bottom card
struct BottomCard<Content:View>:View{
#Binding var cardShown:Bool
#Binding var cardDismissal:Bool
let content:Content
init(cardShown:Binding<Bool> , cardDismissal:Binding<Bool>, #ViewBuilder content: () -> Content){
_cardShown = cardShown
_cardDismissal = cardDismissal
self.content = content()
}
var body: some View{
ZStack{
//Dimmed
GeometryReader{ _ in
EmptyView()
}
.background(Color.red.opacity(0.2))
.animation(.easeIn)
.onTapGesture {
cardShown.toggle()
}
// Card
VStack{
Spacer()
VStack{
content
}
Spacer()
}
}.edgesIgnoringSafeArea(.all)
.opacity(cardShown ? 1 : 0)
}
}
So you just need to set the height!
what you want to do is to have a card that only exists when there is a certain standard met.
If you want to push up a card from the bottom then you can make a view of a card and put it at the bottom of a Zstack view using a geometry reader and then make a button that only allows for that card to exist when the button is pressed INSTEAD of trying to hire it by changing its opacity. Also, make sure you move the dismissal button to the inside of the cad you have.
Heres an example you can try :
struct SecondView: View {
#State var cardShown = false
var body: some View {
GeometryReader{
ZStack {
ZStack{
// I would also suggest getting used to physically making your
//button and then giving them functionality using a "Gesture"
Text("Show Button")
.background(Rectangle())
.onTapGesture{
let animation = Animation.spring()
withAnimation(animation){
self.cardShown.toggle
}
}
}
ZStack {
if cardShown == true{
BottomCard(cardShown: $cardShown) {
CardContent()
}
}
// here you can change how far up the card comes after the button
//is pushed by changing the "0"
.offset(cardShown == false ? geometry.size.height : 0)
}
}
}
}
}
Also, you don't need to have a variable for the card being shown and a variable for the card being dismissed. Just have one "cardShown" variable and make it so that when it is TRUE the card is shown and when it is FALSE (after hitting the button on the card or hitting the initial button again.) the card goes away.
iOS 16.0+
iPadOS 16.0+
macOS 13.0+
Mac Catalyst 16.0+
tvOS 16.0+
watchOS 9.0+
Use presentationDetents(_:)
struct ContentView: View {
#State private var isBottomSheetVisible = false
var body: some View {
Button("View Settings") {
isBottomSheetVisible = true
}
.sheet(isPresented: $isBottomSheetVisible) {
Text("Bottom Sheet")
.presentationDetents([.height(250), .medium])
.presentationDragIndicator(.visible)
}
}
}

SwiftUI how do I temporarily animate a view color's foregroundColor?

When a View is pressed I know through a model button.isSelected. How do I animate the view's foreground color, similar to the IOS calculators button press animation?
Something like:
White -> Grey -> White
struct ButtonView: View {
let button: ViewModel.Button
var body: some View {
let shape = Rectangle()
ZStack {
shape.fill().foregroundColor(button.isSelected ? Color.gray : Color.white)
.animation(Animation.linear(duration: 0.01))
.border(Color.black, width: 0.33)
Text(button.content)
.font(Font.system(size:32))
}
}
}
I think there are many ways to do this.
Among them, I will write an example using DispatchQueue.main.asyncAfter()
struct ContentView: View {
#State private var isSelected: Bool = false
var body: some View {
VStack {
Button {
isSelected = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2 ) {
// To change the time, change 0.2 seconds above
isSelected = false
}
} label: {
Text("Button")
.foregroundColor(isSelected ? Color.red : Color.blue)
}
}
}
}
While DispatchQueue.main.asyncAfter() will work as Taeeun answered, note how the calculator app doesn't use a set delay. Instead, it changes color when the finger presses down, then reverts back upon release.
So, you probably want something like ButtonStyle.
struct ContentView: View {
var body: some View {
ButtonView()
}
}
struct CalculatorButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding() /// no need to use `shape` + `ZStack`, normal padding is ok
.background(configuration.isPressed ? Color.gray : Color.white) /// use `isPressed` to determine if button is currently pressed or not
.animation(Animation.linear(duration: 0.01))
.cornerRadius(10)
}
}
struct ButtonView: View {
var body: some View {
ZStack {
Color.black /// for testing purposes (see the button better)
Button {} label: {
Text("Button")
.font(.system(size: 32))
}
.buttonStyle(CalculatorButtonStyle()) /// apply the style
}
}
}
Result:

SwiftUI sheet not animating dismissal on macOS Big Sur

I would like the sheet dismissal animated just like the appearance but reversed. I think this is also the standard behavior. You can see it in Xcode for example when you create a new file.
But as you can see it just disappears without animation
Here's my code:
struct ContentView: View {
#State var isAnotherViewPresented: Bool = false
var body: some View {
HStack {
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
}).sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
}
.frame(width: 500, height: 300, alignment: .center)
}
}
struct AnotherView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Close")
})
}.padding()
}
}
I'm on
Mac mini (M1, 2020)
macOS Big Sur 11.1 (20C69)
Xcode 12.3 (12C33)
But I can reproduce this on a
Mac mini (2018)
macOS Big Sur 11.0.1 (20B29)
Xcode 12.2 (12B45b)
I finally figured out how to do it, in my SwiftUI app it works if I do this while closing the sheet:
isSheetVisible = false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!)
Example:
struct SheetView: View {
#Binding var isSheetVisible: Bool
var body: some View {
Button("Close") {
isSheetVisible = false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!)
}
}
}
Disclaimer: I had/have the same problem where if I try to dismiss a sheet through a binding, it just disappears instead of having an animation. The below solution worked for me but I am unclear as to why its working.
Solution
Apparently the view you "attach" a modal to has an impact on how it transitions from being presented to not. For instance, in your code the sheet is attached to the button view:
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
// sheet is attached here
}).sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
When you call presentationMode.wrappedValue.dismiss() in the second view the modal jolts and disappears instead of sliding away. However, if you attach the sheet to the outer HStack view, then it works and it slides away as expected:
var body: some View {
HStack {
Button(action: {
isAnotherViewPresented.toggle()
}, label: {
Text("Button")
})
}
.frame(width: 500, height: 300, alignment: .center)
.sheet(isPresented: $isAnotherViewPresented, content: {
AnotherView()
})
// sheet is now here
}
For me as long as the sheet wasn't attached to the button the animation worked. I don't know why this works but it did for me and hopefully it will for you as well.
I like this mix:
assuming you have:
#Environment(\.presentationMode) var presentationMode
then:
presentationMode.wrappedValue.dismiss() // this updates the binding from .sheet(isPresented: ...) to false
NSApp.mainWindow?.endSheet(NSApp.keyWindow!) // this runs the animation

SwiftUI onDrag does not show preview image when running on device

I noticed an issue with the drag and drop features in SwiftUI that are available since iOS 13.4. The drag and drop operation with the .onDrag and .onDrop modifiers works fine in the simulator, but on a real device (iPhone and iPad) you just see a transparent rect, instead of the view while dragging the view.
Does anyone have a solution to get the correct preview image while the view is dragged?
struct MainView: View {
#State var isDropTarget = false
var body: some View {
VStack{
Image(systemName: "doc.text")
.font(.system(size: 40))
.frame(width: 150, height: 150)
.onDrag { return NSItemProvider(object: "TestString" as NSString) }
Color.orange
.opacity(isDropTarget ? 0.5 : 1)
.onDrop(of: ["public.text"], isTargeted: $isDropTarget) { items in
for item in items {
if item.canLoadObject(ofClass: NSString.self) {
item.loadObject(ofClass: String.self) { str, _ in
print(str ?? "nil")
}
}
}
return true
}
}
}
While iOS 15 did not fix this bug in my testing, there is a new API that allows you to specify the preview View to display: onDrag(_:preview:). You can recreate the view being dragged, in this case your Image, for the preview.