Swiftui Button to behave like a checkbox doesn't preview but works - swiftui

Here's the code for the Checkbox I created. It works great but in the Xcode preview canvas it doesn't work. As in, in the preview when you click it, it doesn't change. When I use the checkbox in my app and run it in the simulator it works great. Any ideas why this is?
//
// CheckboxToggle.swift
//
//
//
import SwiftUI
enum CheckboxShape : String {
case circle = "circle"
case square = "square"
}
struct Checkbox: View {
#Binding var isChecked:Bool
var style:CheckboxShape
#State var action:(Bool)->() = {_ in }
var body: some View {
Button {
isChecked.toggle()
action(isChecked)
} label: {
let shape = style.rawValue
Image(systemName: isChecked ? "checkmark.\(shape).fill" : "\(shape)")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(isChecked ? .green : .gray)
.font(.system(size: 20, weight: .regular, design: .default))
}
.buttonStyle(PlainButtonStyle())
}
}
struct Checkbox_Previews: PreviewProvider {
#State static var isChecked:Bool = false
// Not sure why this preview doens't work but the view works in other views fine.
static var previews: some View {
HStack {
Checkbox(isChecked: $isChecked, style: .circle)
}
}
}

SwiftUI previews don't work this way.
To make Checkbox interactive declare isChecked as #State
#State var isChecked = false
and replace the PreviewProvider with
struct Checkbox_Previews: PreviewProvider {
static var previews: some View {
Checkbox(style: .circle)
}
}
And if you want to see both states (and also the square appearance) and keep the #Binding add more Checkbox views and specify constants
struct Checkbox_Previews: PreviewProvider {
static var previews: some View {
HStack {
Checkbox(isChecked: .constant(true), style: .circle)
Checkbox(isChecked: .constant(false), style: .circle)
Checkbox(isChecked: .constant(true), style: .square)
Checkbox(isChecked: .constant(false), style: .square)
}
}
}
But this preview is not interactive.

Related

SwiftUI View doesn't change when using button and #State / #Binding

Long story short this is an onboarding view after a user goes through auth. My main navigation of the app uses navigationview but I can't use that for onboarding. I've put a fullscreencover over the main screen for this onboarding stuff.
However, when trying to simply navigate between the views from different files between the onboarding screens, the button doesn't show the other view. I've tried everything under the son from #Appstorage and #Environment stuff but can't get it to work.
Also please note I cut off the bottom of the rest of the file as that had nothing to do with this logic.
import SwiftUI
struct OnboardingTestView: View {
#State var shouldShowOnboarding = true
var body: some View {
if shouldShowOnboarding {
OffsetChoicesView(shouldShowOnboarding: $shouldShowOnboarding)
}
if shouldShowOnboarding == false {
PersonalInfoView()
}
}
}
struct OnboardingTestView_Previews: PreviewProvider {
static var previews: some View {
OnboardingTestView()
}
}
//*********************************************************************
//OffsetChoicesView
struct OffsetChoicesView: View {
#Binding var shouldShowOnboarding: Bool
var body: some View {
ZStack {
Color(#colorLiteral(red: 0.9803921569, green: 0.9568627451, blue: 0.9568627451, alpha: 1)).edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
VStack {
Progress1View()
.padding(.bottom, 40)
.padding(.top)
Spacer()
Button(action: {
shouldShowOnboarding = false
}) {
NextButtonView()
.padding(.top)
.padding(.bottom)
}
}
You can try something like this - utilizes #AppStorage
I was not quite sure what the OffsetChoiceView() parameters are supposed to be.. so I just removed them in the example. This should be whatever your onboarding view is called.
import SwiftUI
struct OnboardingTestView: View {
//#State var shouldShowOnboarding = true
#AppStorage("showOnboarding") var showOnboarding: Bool = true
var body: some View {
if (showOnboarding == true) {
OffsetChoicesView()
} else if (showOnboarding == false) {
PersonalInfoView()
}
}
}
struct OnboardingTestView_Previews: PreviewProvider {
static var previews: some View {
OnboardingTestView()
}
}
//*********************************************************************
//OffsetChoicesView
struct OffsetChoicesView: View {
//#Binding var shouldShowOnboarding: Bool
#AppStorage("showOnboarding") var showOnboarding: Bool = false
var body: some View {
ZStack {
Color(#colorLiteral(red: 0.9803921569, green: 0.9568627451, blue: 0.9568627451, alpha: 1)).edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
VStack {
Progress1View()
.padding(.bottom, 40)
.padding(.top)
Spacer()
Button(action: {
showOnboarding = false
}) {
NextButtonView()
.padding(.top)
.padding(.bottom)
}
}

Fade-in/out animation with a boolean flag

I am trying to implement a simple "tap to toggle the visibility of the UI" in SwiftUI with fade in/out animation. The following code animates the fade-in effect of the Text element as I expected, but it immediately hides the Text element when isVisible become false.
I'd like to understand why this code does not work, and how to fix it in the most natural way.
import SwiftUI
struct ContentView: View {
#State var isVisible = true
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.blue)
.gesture(TapGesture(count: 1).onEnded {
withAnimation(.easeInOut(duration: 1.0)) {
isVisible.toggle()
}
})
if isVisible {
Text("Tap me!")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm using Xcode 12.5 on Big Sur, and my iPhone is running iOS 14.5.1.
Thanks to Erik Philips, here is the answer.
import SwiftUI
struct ContentView: View {
#State var isVisible = true
var body: some View {
ZStack {
Rectangle()
.zIndex(1)
.foregroundColor(.blue)
.gesture(TapGesture(count: 1).onEnded {
withAnimation(.easeInOut(duration: 1.0)) {
isVisible.toggle()
}
})
if isVisible {
Text("Tap me!")
.zIndex(2)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How can I preview a SwiftUI button that depends on PresentationMode?

How can I preview this button who needs a PresentationMode to get constructed?
The button works well the main view that contains it creates it with an environment PresentationMode object declared as:
#Environment(\.presentationMode) var presentationMode:Binding<PresentationMode>
struct BackButton: View {
#Binding var presentationMode: PresentationMode
var color: Color
var body: some View {
Button(action: {
self.$presentationMode.wrappedValue.dismiss()
}, label: { Image(systemName: "chevron.left")
.scaleEffect(1.3)
.foregroundColor(color)
.offset(x: -17)
.frame(width: 43, height: 43)
}
)
}
}
struct BackButton_Previews: PreviewProvider {
static var previews: some View {
let pres = PresentationMode()
return BackButton(presentationMode: pres, color: .black) // Compiler Error: PresentationMode cannot be constructed because it has no accessible initializers
}
}
I think PresentationMode should be declared as Environment Variable.
So declare it like this..
#Environment(\.presentationMode) var presentationMode
and then change it on action like that, as it is not a Binding anymore.
Button(action: {
self.presentationMode.wrappedValue.dismiss()
Edit:
Here is a working example/ with preview for BackButton View and how to use PresentationMode.
struct MainView: View {
var body: some View {
NavigationView
{
VStack()
{
Text("Hello World")
NavigationLink("Go to Detail View", destination: BackButton(color: .black))
}.navigationBarTitle(Text("Main View"))
}
}
}
struct BackButton : View
{
//Environment variable here
#Environment(\.presentationMode) var presentationMode
var color: Color
var body: some View
{
Button(action: {
//Dismiss the View
self.presentationMode.wrappedValue.dismiss()
}, label: { Image(systemName: "chevron.left")
.scaleEffect(1.3)
.foregroundColor(color)
.offset(x: -17)
.frame(width: 43, height: 43)
})
}
}
struct BackButton_Previews: PreviewProvider {
static var previews: some View {
//Preview here is working, no need to pass environment variable
//Going back from this view in Preview won't work
BackButton(color: .black)
}
}

SwiftUI - Animation in view stops when scrolling the list

Considering the following code, why the animations in the views that are initialized without the n property stops when you scroll the list?
Tested on Xcode 11.3 (11C29) with a new default project on device and simulator.
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
List(1...50, id: \.self) { n in
HStack {
KeepRolling()
Spacer()
KeepRolling(n: n)
}
}
}
}
}
struct KeepRolling: View {
#State var isAnimating = false
var n: Int? = nil
var body: some View {
Rectangle()
.frame(width: 50, height: 50)
.rotationEffect(Angle(degrees: self.isAnimating ? 360 : 0))
.onAppear {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
self.isAnimating = true
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
IMO it is due to caching/reuse in List. For List all the values of KeepRolling() is the same, so .onAppear is not always re-called.
If to make every such view unique, say using .id as below, all works (tested with Xcode 11.2)
KeepRolling().id(UUID().uuidString)

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)
}
}
}