Can someone, please, explain me why MyButton's label stays in I'm pressed state (should be clicked once) and never reverts to its initial state Press Me when I switch views in a list by clicking the yellow rectangle.
Here is the example:
import SwiftUI
struct MyButton: View {
#State private var label: String
init() {
label = "Press Me"
}
var body: some View {
Button() {
label = "I'm Pressed"
} label: {
Text(label)
}
}
}
struct ContentView: View {
enum Mode {
case text
case button
}
#State private var mode: Mode
init() {
mode = .button
}
var body: some View {
VStack {
List{
switch mode {
case .button:
MyButton()
case .text:
Text("No button here for sure!")
}
}
Spacer()
Button() {
mode = mode == .text ? .button : .text
} label: {
Color(.yellow)
.frame(width: 100, height: 100)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I understand that this behaviour is because MyButton is in the List. If the List is removed from the sources, then switching the views by tapping on the yellow rectangle does give me the result I expect, e.g. MyButton gets initial state Press Me. Why is it so different with the List then?
I'm aware I can "reset" state by providing different .id() to MyButton, but are there any other options? I also tried .onDisappear to reset the state, seems to work as well, but first of all I would really love to know why List is so special that it remembers state of a non-rendered MyButton?
The List caches cell types. It is like UITableView's dequeueReusableCell(withIdentifier. And state inside view persist until anything changes, but in question code view is just removed and then inserted, ie. no data changed - it remains the same.
Related
Using iOS 16.0 and Xcode 14.2
I am super new to SwiftUI and it honestly still hasn't really clicked yet, so this code probably is very inefficient. But basically I just want a button that when you press it, you go to another view. But when I try it like this, the preview crashes. Is there a better way to do this? And also what is causing the preview to crash? I've tried a bunch of different things and it either causes the preview to crash or the button just doesn't do anything.
Homescreen (had other buttons that were working that I redacted for clarity)
import SwiftUI
struct WordAndArrows: View {
#State private var showLibrary = false
var body: some View {
LibraryButton (action: {
self.navigateToLibraryScreen()
})
}
VStack {
if showLibrary {
LibraryView()
}
}
}
func navigateToLibraryScreen() {
self.showLibrary = true
}
}
struct WordAndArrows_Previews: PreviewProvider {
static var previews: some View {
ZStack {
GradientBackground()
WordAndArrows()
}
}
}
Library Button View
import SwiftUI
struct LibraryButton: View {
var action: () -> Void
var body: some View {
//Button("Check out my library") {
Button(action: action) {
Text("Library")
}
.padding()
.background(Color("Lime"))
.clipShape(Capsule())
.buttonStyle(SquishButtonStyle(fadeOnPress: false))
.font(Font.custom("Quicksand-Bold", size: 15))
.foregroundColor(Color("Indigo"))
.shadow(color: .gray, radius: 2.5, x: 0, y: 5)
}
}
I tried:
Making a navigatetolibraryscreen function that would be triggered when the buttn was hit
Using a navigationLink
3 Wrapping the navigation link in a Vstack
And it either caused the button to not do anything or the preview to crash
My app has a SwiftUI List with cells that can be swiped (leading, trailing).
Problem:
Say, a cell is swiped so that the buttons are visible. Then the app is de-activated in this state, e.g. by a switch to another app, by locking the screen, etc.
When the app later is re-activated, the earlier swiped cell is still swiped, although the user might no longer be aware of the reason.
It would thus be better to undo the swipe by code, when the app is deactivated.
Question:
Is this possible?
It is a but of a brute-force approach but redrawing with this approach works.
import SwiftUI
#available(iOS 15.0, *)
struct ResetSwipeView: View {
#Environment(\.scenePhase) var scenePhase
#State var id: UUID = .init()
var body: some View {
List(1...10){n in
Text(n, format: .number)
.swipeActions {
Button {
print("Button :: \(n)")
} label: {
Text("print")
}
}
}.id(id)
.onChange(of: scenePhase, perform: { newValue in
if newValue == .inactive{
id = .init()
}
})
}
}
#available(iOS 15.0, *)
struct ResetSwipeView_Previews: PreviewProvider {
static var previews: some View {
ResetSwipeView()
}
}
Good day.
Is there a way to make a child view that is being called from the parent view disappear if, let's say, a bool condition is true? I want to make it completely disappear, not hidden via opacity, isHidden, etc.
I want to know if this is possible, thank you!
In SwiftUI you can conditionally show a view like...
if someCondition {
YourConditionalView()
}
This will only show that view if the someCondition is true.
From your comment...
struct YourView: View {
#State var showView = true
var body: some View {
VStack {
if showView {
Text("Hello there!")
}
Button {
self.showView.toggle()
} label: {
Text("Press me")
}
}
}
}
This view will start with a label "Hello there!" and a button. When you tap the button the showView boolean is toggled. This will then cause the label to be added/removed from the view based on the value of showView.
I wanna create a button with SwiftUI that fires the moment my finger touches it (like UIKit's touch down instead of touch up inside). I also want the opacity of the button to become 0.7 when my finger is pressing the button. And I want the opacity of the button to change back to 1 ONLY when my finger is no longer touching the button.
I've tried 2 different types of button styles to create such a button but both of them failed:
struct ContentView: View {
var body: some View {
Button(action: {
print("action triggered")
}){
Text("Button").padding()
}
.buttonStyle(SomeButtonStyle())
}
}
struct SomeButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(Color.green)
.opacity(configuration.isPressed ? 0.7 : 1)
.onLongPressGesture(
minimumDuration: 0,
perform: configuration.trigger//Value of type 'SomeButtonStyle.Configuration' (aka 'ButtonStyleConfiguration') has no member 'trigger'
)
}
}
struct SomePrimativeButtonStyle: PrimitiveButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(Color.green)
.opacity(configuration.isPressed ? 0.7 : 1)//Value of type 'SomePrimativeButtonStyle.Configuration' (aka 'PrimitiveButtonStyleConfiguration') has no member 'isPressed'
.onLongPressGesture(
minimumDuration: 0,
perform: configuration.trigger
)
}
}
Apparently none of the button styles above worked because ButtonStyle and PrimitiveButtonStyle don't share the same methods and properties so I can't use both the isPressed property (which belongs to ButtonStyle) AND the trigger method (which belongs to PrimitiveButtonStyle) in the same button style.
How should I configure my button style to make this work?
Ok, I understand that author wants to see solution only with Button, so I dig a little more. And found something interesting at Swift UI Lab. The idea is the same as in my first answer: use #GestureState and create LongPressGesture which .updating($...) this state. But in PrimitiveButtonStyle you don't need to compose a few gestures together. So, I simplified code a little and tested it at simulator. And I think now it just what author need:
struct ComposingGestures: View {
var body: some View {
Button(action: {
print("action triggered")
}){
Text("Button")
.padding()
}
.buttonStyle(MyPrimitiveButtonStyle())
}
}
struct MyPrimitiveButtonStyle: PrimitiveButtonStyle {
func makeBody(configuration: PrimitiveButtonStyle.Configuration) -> some View {
MyButton(configuration: configuration)
}
struct MyButton: View {
#GestureState private var pressed = false
let configuration: PrimitiveButtonStyle.Configuration
let color: Color = .green
#State private var didTriggered = false
var body: some View {
// you can set minimumDuration to Double.greatestFiniteMagnitude if you think that
// user can hold button for such a long time
let longPress = LongPressGesture(minimumDuration: 300, maximumDistance: 300.0)
.updating($pressed) { value, state, _ in
state = value
self.configuration.trigger()
}
return configuration.label
.background(Color.green)
.opacity(pressed ? 0.5 : 1.0)
.gesture(longPress)
}
}
}
I didn't work with ButtonStyle, but tried to solve it with Composing SwiftUI Gestures. I compose TapGesture and LongPressGesture and playing with #GestureState to control .opacity of "button" (which is just Text). The result is just as you asked:
struct ComposingGestures: View {
enum TapAndLongPress {
case inactive
case pressing
var isPressing: Bool {
return self == .pressing
}
}
#GestureState var gestureState = TapAndLongPress.inactive
#State private var didPress = false
var body: some View {
let tapAndLongPressGesture = LongPressGesture(minimumDuration: 2) // if minimumDuration <= 1 gesture state returns to inactive in 1 second
.sequenced(before: TapGesture())
.updating($gestureState) { value, state, transaction in
switch value {
case .first(true), .second(true, nil):
self.didPress = true // simulation of firing action
state = .pressing
default:
state = .pressing
}
}
return VStack {
Text("action was fired!")
.opacity(didPress ? 1 : 0)
Text("Hello world!")
.gesture(tapAndLongPressGesture)
.background(Color.green)
.opacity(gestureState.isPressing ? 0.7 : 1)
}
}
}
P.S. I played only with #State var didPress to show, how to fire action. Maybe it's better to fire it only in the first case, like this:
// ...
.updating($gestureState) { value, state, transaction in
switch value {
case .first(true):
self.didPress = true // simulation of firing action
state = .pressing
case .second(true, nil):
state = .pressing
default:
state = .pressing
}
UPDATE
tried code at simulator and fixed two mistakes:
// ...
let tapAndLongPressGesture = LongPressGesture(minimumDuration: 300, maximumDistance: 300) // now button don't return opacity to 1 even if you move your finger
// ...
case .first(true), .second(true, nil):
DispatchQueue.main.async { // now there are no purple mistakes
self.didPress = true // simulation of firing action
}
state = .pressing
// ...
I have a view BugSplitView which works fine alone but causes a
precondition failure: attribute failed to set an initial value
error when navigated to in either preview or the simulator.
The view has an upper part (Color) and a lower part (Color) separated by a horizontal button bar and laid out using the GeometeryReader and a split state. When it is the destination of NavigationButton it doesn't show properly in the Preview and reports the assertion above when run in the simulator. Remove the BugButtonBar and it works. Got me stumped! Help.
import SwiftUI
struct BugSplitView: View {
#State var split : CGFloat = 0.75
var buttons : [BugButtonBar.Info]{
[BugButtonBar.Info(title: "title", imageName: "text.insert"){}]
}
var body: some View {
GeometryReader{ g in
VStack(spacing: 0){
Color.gray
.frame(width: g.size.width, height: (g.size.height) * self.split)
VStack{
BugButtonBar(infos: self.buttons)
Color(white: 0.3)
}
.frame(height: (g.size.height) * (1 - self.split))
}
}.edgesIgnoringSafeArea(.all)
}
}
struct BugButtonBar : View{
struct Info : Identifiable {
var id = UUID()
var title : String
var imageName : String
var action: () -> Void
}
var infos : [Info]
func color() -> Color{
Color.black
}
var body: some View {
HStack(){
Spacer()
ForEach(self.infos){ info in
Button(action: info.action){
Text(info.title)
}
Spacer()
}
}
}
}
struct ShowBugView : View{
var body : some View{
NavigationView {
NavigationLink(destination: BugSplitView()){
Text("Show Bug")
}
}
}
}
struct BugSplitView_Previews: PreviewProvider {
static var previews: some View {
Group{
BugSplitView()
ShowBugView()
}
}
}
The problem is that your buttons are declared as computed property. To solve the crash declare them like this:
var buttons = [BugButtonBar.Info(title: "title", imageName: "text.insert"){}]
Turns out the id property of struct Info was the problem. Changed it to a computed property as follows:
var id : String {
title + imageName
}
Great example of why I love/hate SwiftUI.
For me it was displayMode inline in navigation bar title. Removing it fixes this problem.
Crash
.navigationBarTitle("Title", displayMode: .inline)
No crash
.navigationBarTitle("Title")
Since it seems that this error - which can't be directly debugged - can be caused by so many different issues, I figured I'd throw my case up here too.
In my case, the error I was getting was:
precondition failure: attribute failed to set an initial value - 128
The issue was that I was attempting to present a sheet on a VStack that contained a NavigationView inside of it, like the below:
var body: some View {
VStack(alignment: .center) {
if /* some condition */ {
/* some other content */
} else {
NavigationView {
/* view content */
}
}
}.sheet(isPresented: /* Binding value */) {
/* sheet content */
}
}
The fix was to make sure that the sheet was being presented on the NavigationView instead:
var body: some View {
NavigationView {
VStack(alignment: .center) {
if /* some condition */ {
/* some other content */
} else {
/* view content */
}
}
}.sheet(isPresented: /* Binding value */) {
/* sheet content */
}
}
Seems obvious in hindsight, but it would have been nice to get a bit more information when the crash occurred in the first place.
I had this error. In my case, it was caused by having a NavigationView inside both blocks of an if-else statement.
// bad
if someBool {
NavigationView {
Text("some content")
}
} else {
NavigationView {
Text("different content")
}
}
// good
NavigationView {
if someBool {
Text("some content")
} else {
Text("different content")
}
}
In my case it was setting a value to a binding property when view disappears, a property that changes a view like this:
.onDisappear(perform: {
withAnimation(.easeInOut) {
self.action.collageWidthSize = 2.0 /* modifies next view border */
}
})
Setting this in the next view's onAppear fixed it:
.onAppear {
withAnimation(.easeInOut) {
self.action.collageWidthSize = 2.0
}
}
Ok, I was bit by this. Xcode 11.6.
My views are probably a bit convoluted, but what i'm doing is if a user puts the view into an 'edit' mode all of the cells change their presentation. I was getting this error seemingly at random when i switched back. I fixed it (fingers still crossed) by removing an unnecessary binding. I was passing a boolean binding into some of the subviews so that they know what state things are in, and how to be presented. Thing is, they don't need to respond to the boolean change, because the parent is being redrawn anyway, and it can just recreate the children. They don't need to be notified that they should change state, they are simply destroyed and recreated.
I used NavigationView as the root of TabView:
NavigationView {
TabView {
}
}
Then in the TabView I used NavigationView too, so due to this error. The solution is only use one NavigationView.